feat: 【破坏性更新】插件改为metadata加载模式,plugin-cert、plugin-lib包部分代码转移到certd-server中,影响自定义插件,需要修改相关import引用

ssh、aliyun、tencent、qiniu、oss等 access和client需要转移import
This commit is contained in:
xiaojunnuo
2025-12-31 17:01:37 +08:00
parent 9c26598831
commit a3fb24993d
312 changed files with 14321 additions and 597 deletions
@@ -0,0 +1,61 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
import { BaiduYunCertClient } from "./client.js";
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
*/
@IsAccess({
name: "baidu",
title: "百度云授权",
desc: "",
icon: "ant-design:baidu-outlined",
order: 2,
})
export class BaiduAccess extends BaseAccess {
@AccessInput({
title: "AccessKey",
component: {
placeholder: "AccessKey",
},
helper: "[百度智能云->安全认证获取](https://console.bce.baidu.com/iam/#/iam/accesslist)",
required: true,
encrypt: false,
})
accessKey = "";
@AccessInput({
title: "SecretKey",
component: {
placeholder: "SecretKey",
},
helper: "",
required: true,
encrypt: true,
})
secretKey = "";
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "onTestRequest",
},
helper: "点击测试接口看是否正常",
})
testRequest = true;
async onTestRequest() {
const certClient = new BaiduYunCertClient({
access: this,
logger: this.ctx.logger,
http: this.ctx.http,
});
const res = await certClient.getCertList();
this.ctx.logger.info("测试接口返回", res);
return "ok";
}
}
new BaiduAccess();
@@ -0,0 +1,191 @@
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
import { BaiduAccess } from "./access.js";
import crypto from "crypto";
import { CertInfo } from "@certd/plugin-cert";
export type BaiduYunClientOptions = {
access: BaiduAccess;
logger: ILogger;
http: HttpClient;
};
export type BaiduYunReq = {
host: string;
uri: string;
body?: any;
headers?: any;
query?: any;
method: string;
};
export class BaiduYunClient {
opts: BaiduYunClientOptions;
constructor(opts: BaiduYunClientOptions) {
this.opts = opts;
}
// 调用百度云接口,传接口uri和json参数
async doRequest(req: BaiduYunReq, config?: HttpRequestConfig) {
const host = req.host;
const timestamp = this.getTimestampString();
const queryString = this.getQueryString(req.query);
const Authorization = this.getAuthString(host, req.method, req.uri, queryString, timestamp);
const ContentType = "application/json; charset=utf-8";
let url = "https://" + host + req.uri;
if (req.query) {
url += "?" + queryString;
}
const res = await this.opts.http.request({
url: url,
method: req.method,
data: req.body,
headers: {
Authorization: Authorization,
"Content-Type": ContentType,
Host: host,
"x-bce-date": timestamp,
...req.headers,
},
...config,
});
if (res.code) {
throw new Error(`请求失败:${res.message}`);
}
return res;
}
// 获取UTC时间
getTimestampString() {
return new Date().toISOString().replace(/\.\d*/, "");
}
// 获取参数拼接字符串
getQueryString(params) {
let queryString = "";
let paramKeyArray = [];
if (params) {
for (const key in params) {
paramKeyArray.push(key);
}
paramKeyArray = paramKeyArray.sort();
}
if (paramKeyArray && paramKeyArray.length > 0) {
for (const key of paramKeyArray) {
queryString += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]) + "&";
}
queryString = queryString.substring(0, queryString.length - 1);
}
return queryString;
}
uriEncode(input: string, encodeSlash = false) {
let result = "";
for (let i = 0; i < input.length; i++) {
const ch = input.charAt(i);
if ((ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_" || ch === "-" || ch === "~" || ch === ".") {
result += ch;
} else if (ch === "/") {
result += encodeSlash ? "%2F" : ch;
} else {
result += this.toHexUTF8(ch);
}
}
return result;
}
toHexUTF8(ch) {
// Convert character to UTF-8 bytes and return the hex representation
const utf8Bytes = new TextEncoder().encode(ch);
let hexString = "";
for (const byte of utf8Bytes) {
hexString += "%" + byte.toString(16).padStart(2, "0").toUpperCase();
}
return hexString;
}
// 签名
getAuthString(Host: string, Method: string, CanonicalURI: string, CanonicalQueryString: string, timestamp: string) {
// 1
const expirationPeriodInSeconds = 120;
const authStringPrefix = `bce-auth-v1/${this.opts.access.accessKey}/${timestamp}/${expirationPeriodInSeconds}`;
// 2
const signedHeaders = "host;x-bce-date";
const CanonicalHeaders = encodeURIComponent("host") + ":" + encodeURIComponent(Host) + "\n" + encodeURIComponent("x-bce-date") + ":" + encodeURIComponent(timestamp);
const CanonicalRequest = Method.toUpperCase() + "\n" + this.uriEncode(CanonicalURI, false) + "\n" + CanonicalQueryString + "\n" + CanonicalHeaders;
// 3
const SigningKey = crypto.createHmac("sha256", this.opts.access.secretKey).update(authStringPrefix).digest().toString("hex");
// 4
const Signature = crypto.createHmac("sha256", SigningKey).update(CanonicalRequest).digest().toString("hex");
// 5
return `${authStringPrefix}/${signedHeaders}/${Signature}`;
}
}
export class BaiduYunCertClient {
client: BaiduYunClient;
constructor(opts: BaiduYunClientOptions) {
this.client = new BaiduYunClient(opts);
}
async createCert(opts: { certName: string; cert: CertInfo }) {
// /v1/certificate
const res = await this.client.doRequest({
host: "certificate.baidubce.com",
uri: `/v1/certificate`,
method: "post",
body: {
/**
* certName String 必须 证书的名称。长度限制为1-65个字符,以字母开头,只允许包含字母、数字、’-‘、’/’、’.’、’’,Java正则表达式` ^[a-zA-Z]a-zA-Z0-9\-/\.]{2,64}$`
* certServerData String 必须 服务器证书的数据内容 (Base64编码)
* certPrivateData String 必须 证书的私钥数据内容 (Base64编码)
*/
certName: "certd_" + opts.certName, // 字母开头,且小于64长度
certServerData: opts.cert.crt,
certPrivateData: opts.cert.key,
},
});
return res;
}
async getCertList() {
/**
* GET /v1/certificate HTTP/1.1
* HOST: certificate.baidubce.com
* Authorization: {authorization}
* Content-Type: application/json; charset=utf-8
* x-bce-date: 2014-06-01T23:00:10Z
*/
return await this.client.doRequest({
host: "certificate.baidubce.com",
uri: `/v1/certificate`,
method: "get",
});
}
async updateCert(opts: { certId: string; certName: string; cert: CertInfo }) {
/**
* /v1/certificate/{certId}?certData
* certName String 必须 证书的名称。长度限制为1-65个字符,以字母开头,只允许包含字母、数字、’-‘、’/’、’.’、’’,Java正则表达式` ^[a-zA-Z]a-zA-Z0-9\-/\.]{2,64}$`
* certServerData String 必须 服务器证书的数据内容 (Base64编码)
* certPrivateData String 必须 证书的私钥数据内容 (Base64编码)
* certLinkData String 可选 证书链数据内容 (Base64编码)
* certType Integer 可选 证书类型,非必填,默认为1
*/
return await this.client.doRequest({
host: "certificate.baidubce.com",
uri: `/v1/certificate/${opts.certId}`,
method: "put",
body: {
certName: opts.certName,
certServerData: opts.cert.crt,
certPrivateData: opts.cert.key,
},
});
}
}
@@ -0,0 +1,3 @@
export * from "./plugins/index.js";
export * from "./access.js";
export * from "./client.js";
@@ -0,0 +1,3 @@
export * from "./plugin-deploy-to-cdn.js";
export * from "./plugin-deploy-to-blb.js";
export * from "./plugin-upload-to-baidu.js";
@@ -0,0 +1,323 @@
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine } from "@certd/plugin-lib";
import { BaiduYunCertClient, BaiduYunClient } from "../client.js";
@IsTaskPlugin({
name: "BaiduDeployToBLB",
title: "百度云-部署证书到负载均衡",
icon: "ant-design:baidu-outlined",
group: pluginGroups.baidu.key,
desc: "部署到百度云负载均衡,包括BLB、APPBLB",
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
needPlus: false,
})
export class BaiduDeployToBLBPlugin extends AbstractTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames, "BaiduUploadCert"],
},
required: true,
})
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine())
certDomains!: string[];
@TaskInput({
title: "区域",
component: {
name: "a-select",
vModel: "value",
options: [
/**
* 北京 blb.bj.baidubce.com HTTP/HTTPS
* 广州 blb.gz.baidubce.com HTTP/HTTPS
* 苏州 blb.su.baidubce.com HTTP/HTTPS
* 香港 blb.hkg.baidubce.com HTTP/HTTPS
* 武汉 blb.fwh.baidubce.com HTTP/HTTPS
* 保定 blb.bd.baidubce.com HTTP/HTTPS
* 上海 blb.fsh.baidubce.com HTTP/HTTPS
* 新加坡 blb.sin.baidubce.com HTTP/HTTPS
*/
{ value: "bj", label: "北京" },
{ value: "fsh", label: "上海" },
{ value: "gz", label: "广州" },
{ value: "fwh", label: "武汉" },
{ value: "su", label: "苏州" },
{ value: "bd", label: "保定" },
{ value: "hkg", label: "香港" },
{ value: "sin", label: "新加坡" },
],
},
required: true,
})
region!: string;
@TaskInput({
title: "负载均衡类型",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "blb", label: "普通负载均衡" },
{ value: "appblb", label: "应用负载均衡" },
],
},
required: true,
})
blbType!: string;
//授权选择框
@TaskInput({
title: "百度云授权",
helper: "百度云授权",
component: {
name: "access-selector",
type: "baidu",
},
required: true,
})
accessId!: string;
@TaskInput({
title: "负载均衡ID",
component: {
name: "remote-select",
vModel: "value",
mode: "tags",
action: "GetBLBList",
watches: ["certDomains", "blbType", "accessId"],
},
required: true,
})
blbIds!: string[];
@TaskInput({
title: "监听器ID",
component: {
name: "remote-select",
vModel: "value",
mode: "tags",
action: "GetListenerList",
watches: ["certDomains", "accessId", "blbIds"],
},
required: true,
})
listenerIds!: string[];
async onInstance() {}
async execute(): Promise<void> {
this.logger.info("开始更新百度云监听器证书");
const access = await this.getAccess(this.accessId);
const certClient = new BaiduYunCertClient({
access,
logger: this.logger,
http: this.ctx.http,
});
let certId = this.cert as string;
if (typeof this.cert !== "string") {
this.logger.info("上传证书到百度云");
const res = await certClient.createCert({
cert: this.cert,
certName: CertReader.buildCertName(this.cert),
});
certId = res.certId;
this.logger.info(`上传证书到百度云成功:${certId}`);
}
const baiduyunClient = new BaiduYunClient({
access,
logger: this.logger,
http: this.ctx.http,
});
for (const listenerId of this.listenerIds) {
const listenerParams = listenerId.split("_");
const blbId = listenerParams[0];
const listenerType = listenerParams[1];
const listenerPort = listenerParams[2];
let additionalCertHost = null;
if (listenerParams.length > 3) {
additionalCertHost = listenerParams[3];
}
this.logger.info(`更新监听器证书开始:${listenerId}`);
if (!additionalCertHost) {
await this.updateListenerCert({
client: baiduyunClient,
blbId,
listenerType,
listenerPort,
certId,
});
} else {
const listenerDomains = await this.getListeners(baiduyunClient, blbId, listenerType, listenerPort);
if (!listenerDomains || listenerDomains.length === 0) {
throw new Error(`未找到监听器:${listenerId}`);
}
const oldAdditionals = listenerDomains[0].additionalCertDomains;
for (const oldAddi of oldAdditionals) {
if (oldAddi.host === additionalCertHost) {
oldAddi.certId = certId;
}
}
await this.updateListenerCert({
client: baiduyunClient,
blbId,
listenerType,
listenerPort,
certId,
additionalCertDomains: oldAdditionals,
});
}
this.logger.info(`更新监听器证书成功:${listenerId}`);
await this.ctx.utils.sleep(3000);
}
this.logger.info(`更新百度云监听器证书完成`);
}
async onGetListenerList(data: PageSearch = {}) {
const access = await this.getAccess(this.accessId);
const client = new BaiduYunClient({
access,
logger: this.logger,
http: this.ctx.http,
});
const listeners = [];
for (const blbId of this.blbIds) {
/**
* GET /v{version}/appblb/{blbId}/TCPlistener?listenerPort={listenerPort}&marker={marker}&maxKeys={maxKeys} HTTP/1.1
* Host: blb.bj.baidubce.com
*/
const listenerTypes = ["HTTPSlistener", "SSLlistener"];
for (const listenerType of listenerTypes) {
const list = await this.getListeners(client, blbId, listenerType);
if (list && list.length > 0) {
for (const item of list) {
const key = `${blbId}_${listenerType}_${item.listenerPort}`;
listeners.push({
value: key,
label: key,
});
if (item.additionalCertDomains && item.additionalCertDomains.length > 0) {
for (const addi of item.additionalCertDomains) {
const addiKey = `${key}_${addi.host}`;
listeners.push({
value: addiKey,
label: `${addiKey}【扩展】`,
});
}
}
}
}
}
}
if (!listeners || listeners.length === 0) {
throw new Error("未找到https/SSL监听器");
}
return listeners;
}
private async getListeners(client: BaiduYunClient, blbId: string, listenerType: string, listenerPort?: number | string) {
const query: any = {
maxItems: 1000,
};
if (listenerPort) {
query.listenerPort = listenerPort;
}
const res = await client.doRequest({
host: `blb.${this.region}.baidubce.com`,
uri: `/v1/${this.blbType}/${blbId}/${listenerType}`,
method: "GET",
query,
});
return res.listenerList;
}
async onGetBLBList(data: PageSearch = {}) {
const access = await this.getAccess(this.accessId);
const client = new BaiduYunClient({
access,
logger: this.logger,
http: this.ctx.http,
});
/**
* GET /v{version}/appblb?address={address}&name={name}&blbId={blbId}&marker={marker}&maxKeys={maxKeys} HTTP/1.1
* Host: blb.bj.baidubce.com
*/
const res = await client.doRequest({
host: `blb.${this.region}.baidubce.com`,
uri: `/v1/${this.blbType}`,
method: "GET",
query: {
maxItems: 1000,
},
});
const list = res.blbList;
if (!list || list.length === 0) {
throw new Error("没有数据,你可以手动输入");
}
const options: any[] = [];
for (const item of list) {
options.push({
value: item.blbId,
label: item.name,
});
}
return options;
}
private async updateListenerCert(param: { client: BaiduYunClient; blbId: string; listenerType: string; listenerPort: string; certId?: any; additionalCertDomains?: any[] }) {
/**
* PUT /v{version}/appblb/{blbId}/SSLlistener?clientToken={clientToken}&listenerPort={listenerPort} HTTP/1.1
* Host: blb.bj.baidubce.com
* Authorization: authorization string
*
* {
* "scheduler":scheduler,
* "certIds":[certId],
* "encryptionType":encryptionType,
* "encryptionProtocols":[protocol1, protacol2],
* "dualAuth":false,
* "clientCertIds":[clientCertId],
* "description":description
* }
*/
const { client, blbId, listenerType, listenerPort, certId, additionalCertDomains } = param;
const body: any = {};
if (additionalCertDomains) {
body.additionalCertDomains = additionalCertDomains;
}
if (certId) {
body.certIds = [certId];
}
const res = await client.doRequest({
host: `blb.${this.region}.baidubce.com`,
uri: `/v1/${this.blbType}/${blbId}/${listenerType}`,
method: "PUT",
query: {
listenerPort,
},
body,
});
return res;
}
}
new BaiduDeployToBLBPlugin();
@@ -0,0 +1,145 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
import { BaiduYunCertClient, BaiduYunClient } from "../client.js";
import { createCertDomainGetterInputDefine } from "@certd/plugin-lib";
@IsTaskPlugin({
name: "BaiduDeployToCDN",
title: "百度云-部署证书到CDN",
icon: "ant-design:baidu-outlined",
group: pluginGroups.baidu.key,
desc: "部署到百度云CDN",
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
needPlus: false,
})
export class BaiduDeployToCDNPlugin extends AbstractTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames, "BaiduUploadCert"],
},
required: true,
})
cert!: CertInfo | string;
@TaskInput(createCertDomainGetterInputDefine())
certDomains!: string[];
//授权选择框
@TaskInput({
title: "百度云授权",
helper: "百度云授权",
component: {
name: "access-selector",
type: "baidu",
},
required: true,
})
accessId!: string;
//证书选择,此项必须要有
@TaskInput({
title: "CDN域名",
component: {
name: "remote-select",
vModel: "value",
mode: "tags",
action: "GetDomainList",
watches: ["certDomains", "accessId"],
},
required: true,
})
domains!: string[];
async onInstance() {}
async execute(): Promise<void> {
const access = await this.getAccess(this.accessId);
const client = new BaiduYunClient({
access,
logger: this.logger,
http: this.ctx.http,
});
const certClient = new BaiduYunCertClient({
access,
logger: this.logger,
http: this.ctx.http,
});
let certId = this.cert as string;
if (typeof this.cert !== "string") {
this.logger.info("上传证书到百度云");
const res = await certClient.createCert({
cert: this.cert,
certName: CertReader.buildCertName(this.cert),
});
certId = res.certId;
this.logger.info(`上传证书到百度云成功:${certId}`);
}
const body = {
https: {
enabled: true,
certId: certId,
},
};
for (const domain of this.domains) {
await client.doRequest({
host: "cdn.baidubce.com",
uri: `/v2/domain/${domain}/config`,
body,
query: {
https: "",
},
method: "put",
});
this.logger.info(`部署证书到${domain}成功`);
}
}
async onGetDomainList() {
// if (!isPlus()) {
// throw new Error("自动获取站点列表为专业版功能,您可以手动输入站点域名/站点名称进行部署");
// }
const access = await this.getAccess(this.accessId);
const client = new BaiduYunClient({
access,
logger: this.logger,
http: this.ctx.http,
});
const res = await client.doRequest({
host: "cdn.baidubce.com",
uri: `/v2/domain`,
method: "GET",
query: {
maxItems: 1000,
},
});
const list = res.domains;
if (!list || list.length === 0) {
throw new Error("未找到加速域名,你可以手动输入");
}
const options: any[] = [];
for (const item of list) {
options.push({
value: item.name,
label: item.name,
domain: item.name,
});
}
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
}
}
new BaiduDeployToCDNPlugin();
@@ -0,0 +1,68 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
import { BaiduAccess } from "../access.js";
import { BaiduYunCertClient } from "../client.js";
@IsTaskPlugin({
name: "BaiduUploadCert",
title: "百度云-上传到证书托管",
icon: "ant-design:baidu-outlined",
desc: "上传证书到百度云证书托管中心",
group: pluginGroups.baidu.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class BaiduUploadCert extends AbstractTaskPlugin {
// @TaskInput({ title: '证书名称' })
// name!: string;
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames],
},
required: true,
})
cert!: any;
@TaskInput({
title: "Access授权",
helper: "access授权",
component: {
name: "access-selector",
type: "baidu",
},
required: true,
})
accessId!: string;
@TaskOutput({
title: "百度云CertId",
})
baiduCertId?: string;
async execute(): Promise<void> {
const access = await this.getAccess<BaiduAccess>(this.accessId);
const certClient = new BaiduYunCertClient({
access,
logger: this.logger,
http: this.http,
});
const certItem = await certClient.createCert({
cert: this.cert,
certName: CertReader.buildCertName(this.cert),
});
this.baiduCertId = certItem.certId;
this.logger.info(`上传成功,证书ID${certItem.certId}`);
}
}
new BaiduUploadCert();