diff --git a/packages/ui/certd-server/src/plugins/plugin-zenlayer/access.ts b/packages/ui/certd-server/src/plugins/plugin-zenlayer/access.ts index 9bf2987c7..256d873a9 100644 --- a/packages/ui/certd-server/src/plugins/plugin-zenlayer/access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-zenlayer/access.ts @@ -13,7 +13,7 @@ export type ZenlayerRequest = HttpRequestConfig & { @IsAccess({ name: 'zenlayer', title: 'Zenlayer授权', - icon: 'svg:icon-zenlayer', + icon: 'svg:icon-lucky', desc: 'Zenlayer授权', }) export class ZenlayerAccess extends BaseAccess { @@ -62,17 +62,17 @@ export class ZenlayerAccess extends BaseAccess { client: any; async onTestRequest() { - await this.getCertList(); + const res = await this.getCertList({ pageSize: 1 }); + this.ctx.logger.info(res); return "ok"; } - async getCertList(req: PageSearch = {}) { + async getCertList(req: PageSearch = {}):Promise<{totalCount:number,dataSet:{sans:string[],certificateId:string,certificateLabel:string,common:string}[]}> { const pageNo = req.pageNo ?? 1; const pageSize = req.pageSize ?? 100; const res = await this.doRequest({ - url: "/", - baseURL: "https://console.zenlayer.com/api/v2/cdn", + url: "/api/v2/cdn", action: "DescribeCertificates", data: { PageNum: pageNo, @@ -82,209 +82,6 @@ export class ZenlayerAccess extends BaseAccess { return res; } - - /** - * 申请安全凭证 -本文使用的安全凭证为密钥,密钥包括 accessKeyId 和 accessKeyPassword。 - -AccessKeyId:用于标识 API 调用者身份,可以简单类比为用户名。 - -AccessKeyPassword:用于验证 API 调用者的身份,可以简单类比为密码。 - -用户必须严格保管安全凭证,避免泄露,否则将危及财产安全。如已泄漏,请立刻禁用该安全凭证。 - -你可以根据Zenlayer的用户指南文档来获取你的安全凭证。 - -签名过程v2 -Zenlayer Open API V2 支持 Post 请求,仅支持 Content-Type: application/json。 接口使用json格式进行调用。 - -下面以裸机云查询实例列表为例: - - -复制 -curl -X POST https://console.zenlayer.com/api/v2/bmc \ --H "Authorization: ZC2-HMAC-SHA256 Credential=0D9UtpyKYcHxms5v, SignedHeaders=content-type;host, Signature=efb356c32e55c781e10dc676da59462c22596d82e91c57803666243379555b2f" \ --H "Content-Type: application/json; charset=utf-8" \ --H "X-ZC-Action: DescribeInstances" \ --H "X-ZC-Timestamp: 1673361177" \ --H "X-ZC-Signature-Method: ZC2-HMAC-SHA256" \ --H "X-ZC-Version: 2022-11-20" \ --d '{"pageSize":10,"pageNum":1,"zoneId":"HKG-A"}' -Request Headers: - -Key -说明 -示例 -X-ZC-Timestamp - -请求的时间戳,精确到秒 - -1673361177 - -X-ZC-Version - -请求的API版本 - -2022-11-20 - -X-ZC-Action - -请求的动作 - -DescribeInstances - -X-ZC-Signature-Method - -签名方法 - -ZC2-HMAC-SHA256 - -Authorization - -签名认证 - -1. 拼接规范请求串 -按如下伪代码格式拼接规范请求串(CanonicalRequest): - - -复制 -CanonicalRequest = - HTTPRequestMethod + '\n' + - CanonicalURI + '\n' + - CanonicalQueryString + '\n' + - CanonicalHeaders + '\n' + - SignedHeaders + '\n' + - HexEncode(Hash(RequestPayload)) -字段名称 -解释 -HTTPRequestMethod - -HTTP 请求方法。 - -固定为POST。 - -CanonicalURI - -URI 参数。 - -API 固定为正斜杠(/)。 - -CanonicalQueryString - -发起 HTTP 请求 URL 中的查询字符串。 - -对于 POST 请求,固定为空字符串""。 - -CanonicalHeaders - -参与签名的头部信息,可加入自定义的头部参与签名以提高自身请求的唯一性和安全性。 - -拼接规则:头部 key 和 value 统一转成小写,并去掉首尾空格,按照 key:value\n 格式拼接(注意最后包含'\n');多个头部,按照头部 key(小写)的 ASCII 升序进行拼接。此示例计算结果是: - -content-type:application/json; charset=utf-8\nhost:console.zenlayer.com。 - -SignedHeaders - -参与签名的头部信息。 - -说明此次请求有哪些头部参与了签名,和 CanonicalHeaders 包含的头部内容是一一对应的。content-type 和 host 为必选头部。 拼接规则:头部 key 统一转成小写;多个头部 key(小写)按照 ASCII 升序进行拼接,并且以分号(;)分隔。此示例为content-type;host。 - -HashedRequestPayload - -请求正文(payload,即 body)。 - -此示例为{"pageSize":10,"pageNum":1,"zoneId":"HKG-A"})的哈希值,计算伪代码为 HexEncode(Hash(RequestPayload)),即对 HTTP 请求正文做 SHA256 哈希,然后十六进制编码。此示例的计算结果是 :5f714687ba91c606d503467766151206392474accd137ffea6dce2420b67c29a。 - -2. 拼接待签字符串 - -复制 -StringToSign = - Algorithm + \n + # 指定签名算法。对于 SHA256,算法为 ZC2-HMAC-SHA256。 - RequestDateTime + \n + # 指定请求时间戳。 - HashedCanonicalRequest -字段名称 -解释 -Algorithm - -签名算法。 - -目前固定为 ZC2-HMAC-SHA256。 - -RequestTimestamp - -请求时间戳。 - -即请求头部的公共参数 X-ZC-Timestamp 取值,取当前时间 UNIX 时间戳,精确到秒。此示例取值为1673361177。 - -HashedCanonicalRequest - -前述步骤拼接所得规范请求串的哈希值。 - -计算伪代码为 Lowercase(HexEncode(Hash.SHA256(CanonicalRequest)))。此示例计算结果是: 29396f9dfa0f03820b931e8aa06e20cda197e73285ebd76aceb83f7dede493ee。 - -根据以上规则,示例中得到的待签名字符串如下: - - -复制 -ZC2-HMAC-SHA256 -1673361177 -29396f9dfa0f03820b931e8aa06e20cda197e73285ebd76aceb83f7dede493ee -3. 基于 AK 和 StringToSign 计算出签名 -计算签名,伪代码如下: - - -复制 -Signature = HexEncode(HMAC_SHA256(AccessKeyPassword, StringToSign)) -字段名称 -解释 -AccessKeyPassword - -原始的 AccessKeyPassword。 - -如 Gu5t9xGARNpq86cd98joQYCN3。 - -StringToSign - -步骤二获得的结果。 - -4. 拼接 Authorization -按如下格式拼接 Authorization: - - -复制 -Authorization = - Algorithm + ' ' + - 'Credential=' + AccessKeyId + ', ' + - 'SignedHeaders=' + SignedHeaders + ', ' + - 'Signature=' + Signature -字段名称 -解释 -Algorithm - -签名算法。 - -目前为 ZC2-HMAC-SHA256。 - -AccessKeyId - -密钥对中的 AccessKeyId。 - -如 0D9UtpyKYcHxms5v。 - -SignedHeaders - -见上文,参与签名的头部信息。 - -此示例取值为 content-type;host。 - -Signature - -签名值。 - -根据以上方法,此示例计算结果是 efb356c32e55c781e10dc676da59462c22596d82e91c57803666243379555b2f。 - */ - - async getAuthorizationHeaders(req: ZenlayerRequest) { /** @@ -306,18 +103,23 @@ Signature req.headers['host'] = "console.zenlayer.com"; } + if (!req.method) { + req.method = "POST"; + } + // this.accessKeyPassword="Gu5t9xGARNpq86cd98joQYCN3" + // req.data = {"pageSize":10,"pageNum":1,"zoneId":"HKG-A"} const CanonicalQueryString = req.method === 'POST' ? '' : qs.stringify(req.params); const SignedHeaders = "content-type;host"; - const CanonicalHeaders = `${req.headers['content-type']}\n${req.headers['host']}`; + const CanonicalHeaders = `content-type:${req.headers['content-type']}\nhost:${req.headers['host']}\n`; const HashedRequestPayload = this.ctx.utils.hash.sha256(JSON.stringify(req.data || {}), "hex"); - const CanonicalRequest = `${req.method}\n${req.url}\n${CanonicalQueryString}\n${CanonicalHeaders}\n${SignedHeaders}\n${HashedRequestPayload}`; + const CanonicalRequest = `${req.method}\n/\n${CanonicalQueryString}\n${CanonicalHeaders}\n${SignedHeaders}\n${HashedRequestPayload}`; + let HashedCanonicalRequest = this.ctx.utils.hash.sha256(CanonicalRequest, "hex"); + // HashedCanonicalRequest = "29396f9dfa0f03820b931e8aa06e20cda197e73285ebd76aceb83f7dede493ee" const timestamp = Math.floor(Date.now() / 1000); + // const timestamp= 1673361177 const signMethod = "ZC2-HMAC-SHA256"; - - const StringToSign = `${signMethod}\n${timestamp}\n${this.accessKeyId}\n${CanonicalRequest}`; - + const StringToSign = `${signMethod}\n${timestamp}\n${HashedCanonicalRequest}`; const signature = this.ctx.utils.hash.hmacSha256WithKey(this.accessKeyPassword, StringToSign, "hex"); - const authorization = `${signMethod} Credential=${this.accessKeyId}, SignedHeaders=${SignedHeaders}, Signature=${signature}`; @@ -363,12 +165,27 @@ Authorization async doRequest(req: ZenlayerRequest) { const headers = await this.getAuthorizationHeaders(req); req.headers = headers - const res = await this.ctx.http.request({ - baseURL: req.baseURL || "https://console.zenlayer.com", - ...req - }); - this.ctx.logger.info(`doRequest ${req.url} ${res.statusCode} ${JSON.stringify(res.data)}`); - return res; + let res :any = undefined; + try{ + res = await this.ctx.http.request({ + baseURL: req.baseURL || "https://console.zenlayer.com", + ...req + }); + } catch (error) { + const resData = error.response?.data; + if (resData){ + let desc = "" + if (resData.code === "CERTIFICATE_NOT_COVER_ALL_DOMAIN"){ + desc = `证书未覆盖所有域名`; + } + throw new Error(`[code=${resData.code}] ${desc} ${resData.message} [requestId:${resData.requestId}]`); + } + throw error + } + if (res.code) { + throw new Error(`[${res.code}]:${res.message} [requestId:${res.requestId}]`); + } + return res.response; } diff --git a/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/plugin-refresh-cert.ts b/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/plugin-refresh-cert.ts index f63cca525..60ecb4ccb 100644 --- a/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/plugin-refresh-cert.ts +++ b/packages/ui/certd-server/src/plugins/plugin-zenlayer/plugins/plugin-refresh-cert.ts @@ -1,13 +1,14 @@ import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; +import { ZenlayerAccess } from "../access.js"; @IsTaskPlugin({ //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 name: "ZenlayerRefreshCert", title: "Zenlayer-刷新证书", - desc: "将证书部署到Zenlayer CDN", - icon: "svg:icon-zenlayer", + desc: "刷新Zenlayer CDN证书", + icon: "svg:icon-lucky", //插件分组 group: pluginGroups.cdn.key, needPlus: false, @@ -30,7 +31,7 @@ export class ZenlayerRefreshCert extends AbstractTaskPlugin { } // required: true, // 必填 }) - cert!: CertInfo | { type: string, id: number, name: string }; + cert!: CertInfo; @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) certDomains!: string[]; @@ -63,43 +64,73 @@ export class ZenlayerRefreshCert extends AbstractTaskPlugin { //插件执行方法 async execute(): Promise { - // const access = await this.getAccess(this.accessId); + const access = await this.getAccess(this.accessId); + + for (const certId of this.certList) { + await this.updateCert({ + access: access, + certId: certId, + cert: this.cert + }); + this.logger.info(`刷新证书${certId}成功`); + await this.ctx.utils.sleep(1000); + } + this.logger.info("部署完成"); } + + async updateCert(req:{access:ZenlayerAccess,certId:string, cert: CertInfo}){ + const {access,certId, cert} = req; + // ModifyCertificate + await access.doRequest({ + url: "/api/v2/cdn", + action: "ModifyCertificate", + data: { + /** + * certificateId +certificateContent +certificateKey + */ + certificateId: certId, + certificateContent: cert.crt, + certificateKey: cert.key, + } + }); + } async onGetCertList(req: PageSearch = {}) { - // const access = await this.getAccess(this.accessId); + const access = await this.getAccess(this.accessId); - // const pageNo = req.pageNo ?? 1; - // const pageSize = req.pageSize ?? 100; - // const res = await access.GetCertList( - // { - // PageNo: pageNo, - // PageSize: pageSize - // } - // ); - // const total = res.TotalCount; - // const list = res.DomainInfoList || []; - // if (!list || list.length === 0) { - // throw new Error("没有找到CDN域名,请先在控制台创建CDN域名"); - // } + const pageNo = req.pageNo ?? 1; + const pageSize = req.pageSize ?? 100; + const res = await access.getCertList( + { + pageNo: pageNo, + pageSize: pageSize + } + ); + const total = res.totalCount; + const list = res.dataSet || []; + if (!list || list.length === 0) { + throw new Error("没有找到Zenlayer证书,请先在控制台CDN证书管理创建证书"); + } - // /** - // * "Domain": "ucloud.certd.handsfree.work", - // "DomainId": "ucdn-1kwdtph5ygbb" - // */ - // const options = list.map((item: any) => { - // return { - // label: `${item.Domain}<${item.DomainId}>`, - // value: `${item.Domain}`, - // domain: item.Domain - // }; - // }); - // return { - // list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains), - // total: total, - // pageNo: pageNo, - // pageSize: pageSize - // }; + /** + * "Domain": "ucloud.certd.handsfree.work", + "DomainId": "ucdn-1kwdtph5ygbb" + */ + const options = list.map((item: any) => { + return { + label: `${item.certificateLabel}<${item.certificateId}-${item.common}>`, + value: `${item.certificateId}`, + domain: item.sans + }; + }); + return { + list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains), + total: total, + pageNo: pageNo, + pageSize: pageSize + }; } }