diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index 2f58ffa8c..9b6306615 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -39,3 +39,4 @@ export * from './plugin-captcha/index.js' export * from './plugin-xinnet/index.js' export * from './plugin-xinnetconnet/index.js' export * from './plugin-oauth/index.js' +export * from './plugin-cmcc/index.js' \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-cmcc/access.ts b/packages/ui/certd-server/src/plugins/plugin-cmcc/access.ts new file mode 100644 index 000000000..874e66744 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-cmcc/access.ts @@ -0,0 +1,66 @@ +import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline"; +import { CmccClient } from "./cmcc-client.js"; + +/** + * + * tenantId: string; + tenantKey: string; + endpoint?: string; + */ +@IsAccess({ + name: "cmcc", + title: "中国移动CND授权", + desc: "", + icon: "clarity:plugin-line" +}) +export class CmccAccess extends BaseAccess { + + @AccessInput({ + title: 'TenantID', + component: { + placeholder: 'TenantID', + }, + required: true, + }) + tenantId = ''; + + + @AccessInput({ + title: 'TenantKey', + component: { + placeholder: 'TenantKey', + }, + required: true, + encrypt: true, + }) + tenantKey = ''; + + + @AccessInput({ + title: "测试", + component: { + name: "api-test", + action: "TestRequest" + }, + helper: "点击测试接口是否正常" + }) + testRequest = true; + + async onTestRequest() { + const client = await this.getCmccClient() + await client.getDomainList({}) + return "ok" + } + + async getCmccClient() { + return new CmccClient({ + tenantId: this.tenantId, + tenantKey: this.tenantKey, + http: this.ctx.http, + logger: this.ctx.logger, + }) + } +} + + +new CmccAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-cmcc/cmcc-client.ts b/packages/ui/certd-server/src/plugins/plugin-cmcc/cmcc-client.ts new file mode 100644 index 000000000..dcf72031c --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-cmcc/cmcc-client.ts @@ -0,0 +1,405 @@ +import { HttpClient, ILogger } from '@certd/basic'; +import { CertInfo, CertReader } from '@certd/plugin-cert'; +import * as crypto from 'crypto'; +export interface CmcdnConfig { + tenantId: string; + tenantKey: string; + endpoint?: string; + + http: HttpClient; + logger: ILogger; +} +/** + * 移动CDN平台SDK + */ +export class CmccClient { + private config: Required; + private token: string | null = null; + private tokenExpiresAt: number | null = null; + private http: HttpClient; + private logger: ILogger; + + /** + * 构造函数 + * @param config 配置信息 + */ + constructor(config: CmcdnConfig) { + this.config = { + endpoint: 'https://p.cdn.10086.cn/', + ...config, + }; + this.http = config.http + this.logger = config.logger; + + if (!this.config.tenantId) { + throw new Error('tenantId is required'); + } + + if (!this.config.tenantKey) { + throw new Error('tenantKey is required'); + } + } + + /** + * 生成SHA256哈希 + * @param data 输入数据 + * @returns SHA256哈希值 + */ + private sha256Hex(data: string): string { + return crypto.createHash('sha256').update(data).digest('hex'); + } + + /** + * 获取当前ISO8601格式时间 + * @returns ISO8601时间字符串 + */ + private getCurrentIsoTime(): string { + return new Date().toISOString(); + } + + /** + * 生成认证请求签名 + * @param datetime 请求时间 + * @returns 签名 + */ + private generateAuthSign(datetime: string): string { + const signData = `${this.config.tenantId}${datetime}${this.config.tenantKey}`; + return this.sha256Hex(signData); + } + + /** + * 生成API请求签名 + * @param body 请求体 + * @param token 认证token + * @returns 签名 + */ + private generateApiSign(body: any, token: string): string { + const bodyStr = body ? JSON.stringify(body) : ''; + return this.sha256Hex(bodyStr + token); + } + + /** + * 检查token是否有效 + * @returns token是否有效 + */ + private isTokenValid(): boolean { + if (!this.token || !this.tokenExpiresAt) { + return false; + } + return Date.now() < this.tokenExpiresAt; + } + + + + /** + * 获取认证token + * @returns 认证token + */ + async getToken(): Promise { + // 检查是否有有效的token + if (this.isTokenValid()) { + return this.token!; + } + + const datetime = this.getCurrentIsoTime(); + const sign = this.generateAuthSign(datetime); + + const authRequest = { + datetime, + authorization: { + tenant_id: this.config.tenantId, + sign, + }, + }; + + const response = await this.http.request({ + baseURL: this.config.endpoint, + url: '/api/authentication', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + data: authRequest, + skipSslVerify: true, + logParams: false, + logRes: false, + logData: false + }); + + this.token = response.token; + // Token有效期为12小时 + this.tokenExpiresAt = Date.now() + 12 * 60 * 60 * 1000; + return this.token; + } + + /** + * 调用API + * @param req 请求选项 + * @returns API响应 + */ + async doRequest(req: any): Promise { + // 获取有效的token + const token = await this.getToken(); + + // 设置默认headers + const defaultHeaders: Record = { + 'Content-Type': 'application/json', + 'Accept': 'application/vnd.cmcdn+json', + 'CMCDN-Auth-Token': token, + }; + + // 生成签名 + if (req.method === 'POST' || req.method === 'PUT') { + const signature = this.generateApiSign(req.data, token); + defaultHeaders['HTTP-X-CMCDN-Signature'] = signature; + } else { + const signature = this.sha256Hex(token); + defaultHeaders['HTTP-X-CMCDN-Signature'] = signature; + } + + // 合并自定义headers + const headers = { ...defaultHeaders, ...req.headers }; + + // 发送请求 + try { + const response = await this.http.request({ + baseURL: this.config.endpoint, + url: req.url, + method: req.method, + headers: headers, + data: req.data, + skipSslVerify: true, + logParams: false, + logRes: false, + logData: false + }); + if (response.error_code != 0) { + this.logger.error(`接口请求失败,${JSON.stringify(response)}`); + throw new Error(response.error_msg || "接口请求失败"); + } + + return response.data; + } catch (error) { + this.logger.error(`接口请求失败,${error.response?.data?.error_msg || error.message}`); + throw new Error(error.response?.data?.error_msg || error.message); + } + } + /** + * 清除token + */ + clearToken(): void { + this.token = null; + this.tokenExpiresAt = null; + } + + /** + * 获取当前token + * @returns 当前token + */ + getCurrentToken(): string | null { + return this.token; + } + + /** + * + 域名列表查询 + 本接口由 CDN 运营平台提供 ,所有外部 EC 客户使用。该接口为客户提供该客户各状态域名列表查询。本接口仅支持 JSON 结构。 + + 7.1 目录信息 + + + /api/domain_list?domainName =${domainName}&domainStatus =${domainStatus} + + + + 7.2 请求方法 + + GET + + + + 7.3 响应状态码 + + 请求接收成功 :201 , body 内容详见下一节; + 授权错误 :403; + 请求错误 :400; + 其他 :见 1.2.5 状态码。 + + + 7.4 JSON 结构规范 + 7.4.1 请求 URI 参数 + + 序号 父元素 元素名称 约束 类型 长度 描述 + 1 domainName ? String 域名模糊匹配过滤 + + + + 2 domainStatus ? String 域名状态过滤online:启用 + offline:停用 + configuring:配置中 + configure_failed :配置失败 + + + 7.4.2 请求 URI 示例 + GET: http://xxx.com/api/domain_list?domainName=www.test.com&domainStatus=online + + + + 7.4.3 响应数据体 + + + + 序号 父元 元素名称 约束 类型 长度 描述 + 1 data 1 array 查询结果 + 2 data domainName 1 String 加速域名名称 + 3 data createTime 1 datetime 加速域名创建时间(2017-07-25 17:45:52) + 4 data cname * String 加速域名对应的 CNAME 域名 + 5 data type * String 域名产品类型: + demand:点播产品(视音频/网页、下载); + live:直播产品 + 6 data status * String 域名状态:启用 + 停用 + 配置中 + 配置失败待分发 + 已生效启用中 + 删除中 + + + 7.4.4 响应报文示例 + + + { + "data": [{ + "createTime": "2017-07-25 17:45:52", + "domainName": "www.ponshine.com", + "cname": "www.ponshine.com.cmcdn.cdn.10086.cn", "type ": "demand" + }, + { + "createTime": "2018-11-07 22:09:41", + "domainName": "www.testcustom.com", + "cname": "www.testcustom.com.cmcdn.cdn.10086.cn", "type ": "live" + } + ] + } + */ + async getDomainList(req: { domainName?: string, domainStatus?: string }) { + + const res = await this.doRequest({ + url: "/api/domain_list", + method: "GET", + params: { + domainName: req.domainName, + domainStatus: req.domainStatus, + } + }) + + this.logger.info("getDomainList", res); + + return res.data; + } + + /** + * /api/config/action?commandType =saveCrt&version =1 + 12.1.2 请求方法 + 新增 POST + 修改 PUT + 12.1.3 响应状态码 + 请求接收成功: 200/201 , body内容详见下一节; + 授权错误 :403; + 请求错误 :400; + 其他 :见1.2.5状态码。 + 12.1.4 JSON 结构规范 + 12.1.4.1请求数据体 + + 参数 说明 类型 约束 + + + + certificate 证书 ,仅支持 PEM 格式, + 证书内的换行符使用字符串“\n”代替 + 内容如需加密传输可使用 + PBEWith MD5And DES 加密 ,秘钥将私下 + 提供 + + + string + + + 必选 + + + + private_key 私钥 ,仅支持 PEM 格式, + 私钥内的换行符使用字符串“\n”代替 + 内容如需加密传输可使用 + PBEWith MD5And DES 加密 ,秘钥将私下 + 提供 + + + string + + + 必选 + + crt_name 证书名称 ,不支持修改 ,有传 unique_id + 时不需要 + (仅支持英文字母、数字、下划线 ,最大长 + string + 必选 + + + + 度为 32 个字符) + + unique_id 证书唯一 id ,修改证书时该项必选; + 修改证书时 ,如历史证书已绑定域名, + 修改后证书也需支持对应域名 + string + 修改必选 + contact_name 证书联系人 string 可选 + contact_mobile 证书联系人手机号 string 可选 + contact_email 证书联系人邮箱 string 可选 + + 12.1.4.2请求报文示例 + */ + async uploadCert(req: { cert: CertInfo }) { + + const certReader = new CertReader(req.cert); + const res = await this.doRequest({ + url: "/api/config/action?commandType=saveCrt&version=1", + method: "POST", + data: { + certificate: req.cert.crt, + private_key: req.cert.key, + crt_name: certReader.buildCertName(), + } + }) + + this.logger.info("uploadCert", res); + + return res; + } + + /** + * + * @param req + */ + async deployCertToCdn(req: { domainNames: string[], certId: string }) { + // /api/config/action?commandType = manageDomainBaseConfig&version = 1 + const res = await this.doRequest({ + url: "/api/config/action?commandType=manageDomainBaseConfig&version=1", + method: "PUT", + data: { + modify_type: 0, + domains: req.domainNames, + https_enable: true, + unique_id: req.certId, + } + }) + this.logger.info("deployCertToCdn", res); + + return res.data; + } + +} diff --git a/packages/ui/certd-server/src/plugins/plugin-cmcc/index.ts b/packages/ui/certd-server/src/plugins/plugin-cmcc/index.ts new file mode 100644 index 000000000..8572082ac --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-cmcc/index.ts @@ -0,0 +1,2 @@ +export * from './access.js' +export * from './plugin-deploy-to-cdn.js' \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-cmcc/plugin-deploy-to-cdn.ts b/packages/ui/certd-server/src/plugins/plugin-cmcc/plugin-deploy-to-cdn.ts new file mode 100644 index 000000000..109405461 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-cmcc/plugin-deploy-to-cdn.ts @@ -0,0 +1,126 @@ +import { + IsTaskPlugin, + PageSearch, + pluginGroups, + RunStrategy, + TaskInput +} from "@certd/pipeline"; +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; +import { AbstractPlusTaskPlugin } from "@certd/plugin-plus"; +import { CmccAccess } from "./access.js"; + +@IsTaskPlugin({ + //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 + name: "CmccDeployCertToCdn", + title: "中国移动-部署证书到CDN", + desc: "中国移动自动部署证书到CDN", + icon: "svg:icon-lucky", + //插件分组 + group: pluginGroups.cdn.key, + needPlus: true, + default: { + //默认值配置照抄即可 + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed + } + } +}) +//类名规范,跟上面插件名称(name)一致 +export class CmccDeployCertToCdn extends AbstractPlusTaskPlugin { + //证书选择,此项必须要有 + @TaskInput({ + title: "域名证书", + helper: "请选择前置任务输出的域名证书", + component: { + name: "output-selector", + from: [...CertApplyPluginNames] + } + // required: true, // 必填 + }) + cert!: CertInfo; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + //授权选择框 + @TaskInput({ + title: "中国移动-授权", + component: { + name: "access-selector", + type: "cmcc" //固定授权类型 + }, + required: true //必填 + }) + accessId!: string; + // + + @TaskInput( + createRemoteSelectInputDefine({ + title: "加速域名", + helper: "要更新的中国移动CDN域名", + action: CmccDeployCertToCdn.prototype.onGetDomainList.name, + pager: false, + search: false + }) + ) + domainList!: string[]; + + //插件实例化时执行的方法 + async onInstance() { + } + + //插件执行方法 + async execute(): Promise { + const access = await this.getAccess(this.accessId); + + const client = await access.getCmccClient(); + this.logger.info(`----------- 开始更新证书:${this.domainList}`); + + + const newCert = await client.uploadCert({ + cert: this.cert + }) + + const certId = newCert.unique_id + this.logger.info(`----------- 上传证书成功,证书ID:${certId}`); + + await client.deployCertToCdn({ + certId: certId, + domainNames: this.domainList + }); + this.logger.info(`----------- 更新证书${this.domainList}成功,等待10s`); + await this.ctx.utils.sleep(10000); + this.logger.info("部署完成"); + } + + async onGetDomainList(data: PageSearch = {}) { + const access = await this.getAccess(this.accessId); + const client= await access.getCmccClient(); + const res = await client.getDomainList({}) + const list = res || [] + if (!list || list.length === 0) { + throw new Error("没有找到加速域名"); + } + + + /** + * certificate-id + * name + * dns-names + */ + const options = list.map((item: any) => { + return { + label: `${item.domainName}`, + value: item.domainName, + domain: item.domainName + }; + }); + return { + list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains), + }; + } +} + +//实例化一下,注册插件 +new CmccDeployCertToCdn();