chore: zenlayer 证书刷新插件完成

This commit is contained in:
xiaojunnuo
2026-01-06 00:29:41 +08:00
parent e500af1ed4
commit 1d6a8bd851
2 changed files with 103 additions and 255 deletions

View File

@@ -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;
}

View File

@@ -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<void> {
// const access = await this.getAccess<ZenlayerAccess>(this.accessId);
const access = await this.getAccess<ZenlayerAccess>(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<ZenlayerAccess>(this.accessId);
const access = await this.getAccess<ZenlayerAccess>(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
};
}
}