From 5d6f0d8546c74914535af6defbdb65b61c836549 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sun, 30 Mar 2025 00:30:42 +0800 Subject: [PATCH] =?UTF-8?q?pref(plugin-volcengine):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=81=AB=E5=B1=B1=E5=BC=95=E6=93=8E=20CDN=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/plugin-volcengine/cdn-client.ts | 60 ++++++ .../src/plugins/plugin-volcengine/client.ts | 182 ------------------ .../plugins/plugin-volcengine/dns-client.ts | 106 ++++++++++ .../plugin-volcengine/plugins/index.ts | 1 + .../plugins/plugin-deploy-to-cdn.ts | 144 ++++++++++++++ .../volcengine-dns-provider.ts | 6 +- 6 files changed, 314 insertions(+), 185 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts delete mode 100644 packages/ui/certd-server/src/plugins/plugin-volcengine/client.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-cdn.ts diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts new file mode 100644 index 00000000..90fcbe78 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/cdn-client.ts @@ -0,0 +1,60 @@ +import { VolcengineOpts } from "./dns-client.js"; +import { CertInfo } from "@certd/plugin-cert"; + +export class VolcengineCdnClient { + opts: VolcengineOpts; + + service: any; + + constructor(opts: VolcengineOpts) { + this.opts = opts; + } + + + async getCdnClient() { + if (this.service) { + return this.service; + } + const { cdn } = await import("@volcengine/openapi"); + const service = new cdn.CdnService(); + // 设置ak、sk + service.setAccessKeyId(this.opts.access.accessKeyId); + service.setSecretKey(this.opts.access.secretAccessKey); + + this.service = service; + return service; + } + + async uploadCert(cert: CertInfo, certName: string) { + const service = await this.getCdnClient(); + const res = await service.Generic("AddCertificate", { + Source: "volc_cert_center", + CertType: "server_cert", + Certificate: cert.crt, + PrivateKey: cert.key, + EncryType: "inter_cert", + Repeatable: false, + Desc: certName + }); + + if (res.ResponseMetadata?.Error) { + if (res.ResponseMetadata?.Error?.Code?.includes("Duplicated")) { + // 证书已存在,ID为 cert-16293a8524844a3e8e30ed62f8e5bc94。 + const message = res.ResponseMetadata?.Error?.Message + const reg = /ID为 (\S+)。/; + const certId = message.match(reg)?.[1] + if (certId) { + this.opts.logger.info(`证书已存在,ID为 ${certId}`); + return certId; + } + } + throw new Error(JSON.stringify(res.ResponseMetadata?.Error)); + } + + const certId = res.Result.CertId + this.opts.logger.info(`上传证书成功:${certId}`) + return certId; + + + } +} diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/client.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/client.ts deleted file mode 100644 index 8958ee0f..00000000 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/client.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { VolcengineAccess } from "./access.js"; -import { http, HttpClient, ILogger } from "@certd/basic"; -import querystring from "querystring"; - -export type VolcengineOpts = { - access: VolcengineAccess - logger: ILogger - http: HttpClient -} - -export type VolcengineReq = { - method?: string; - path?: string; - headers?: any; - body?: any; - query?: any; - service?: string, // 替换为实际服务名称 - region?: string, // 替换为实际区域名称 -} - -export class VolcengineClient { - opts: VolcengineOpts; - - constructor(opts: VolcengineOpts) { - this.opts = opts; - } - - // // 生成签名函数 - // async createSignedRequest(req: VolcengineReq) { - // if (!req.body) { - // req.body = {}; - // } - // const bodyStr = JSON.stringify(req.body); - // const { method, path, body, query } = req; - // const crypto = await import("crypto"); - // const config = { - // accessKeyId: this.opts.access.accessKeyId, - // secretKey: this.opts.access.secretAccessKey, - // service: req.service || "dns", // 默认服务名称为 dns - // region: req.region || "cn-beijing", // 默认区域名称为 cn-beijing - // endpoint: "https://open.volcengineapi.com" - // }; - // - // // 1. 生成时间戳 - // const now = new Date(); - // // 20201103T104027Z - // const timestamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); - // - // // 2. 处理查询参数 - // const sortedQuery = Object.keys(query || {}) - // .sort() - // .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(query[k])}`) - // .join("&"); - // - // // 3. 构造规范请求 - // const canonicalRequest = [ - // method.toUpperCase(), - // path || "/", - // sortedQuery, - // `content-type:application/json\nhost:${new URL(config.endpoint).host}`, - // "content-type;host", - // crypto.createHash("sha256").update(bodyStr).digest("hex") - // ].join("\n"); - // - // // 4. 生成签名字符串 - // const date = now.toISOString().substring(0, 10).replace(/-/g, ""); - // const credentialScope = `${date}/${config.region}/${config.service}/request`; - // - // const stringToSign = [ - // "HMAC-SHA256", - // timestamp, - // credentialScope, - // crypto.createHash("sha256").update(canonicalRequest).digest("hex") - // ].join("\n"); - // - // // 5. 计算签名 - // const sign = (key: Buffer, msg: string) => crypto.createHmac("sha256", key).update(msg).digest(); - // - // const kDate = sign(Buffer.from(`HMAC${config.secretKey}`, "utf8"), date); - // const kRegion = sign(kDate, config.region); - // const kService = sign(kRegion, config.service); - // const kSigning = sign(kService, "request"); - // const signature = crypto.createHmac("sha256", kSigning) - // .update(stringToSign) - // .digest("hex"); - // - // // 6. 构造请求头 - // const headers = { - // "Content-Type": "application/json", - // Host: new URL(config.endpoint).host, - // "X-Date": timestamp, - // Authorization: `HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=content-type;host, Signature=${signature}` - // }; - // - // return { - // method, - // url: `${config.endpoint}${path || ""}${sortedQuery ? `?${sortedQuery}` : ""}`, - // headers, - // data: body - // }; - // } - // - // async doRequest(req: VolcengineReq) { - // const requestConfig = await this.createSignedRequest(req); - // try { - // const res = await this.opts.http.request(requestConfig); - // if (res?.ResponseMetadata?.Error) { - // throw new Error(JSON.stringify(res.ResponseMetadata.Error)); - // } - // return res; - // } catch (e) { - // if (e?.response?.ResponseMetadata.Error) { - // throw new Error(JSON.stringify(e.response.ResponseMetadata.Error)); - // } - // throw e; - // } - // } - - - async doRequest(req: VolcengineReq) { - const {Signer} =await import('@volcengine/openapi') ; - -// http request data - const openApiRequestData: any = { - region: req.region, - method: req.method, - // [optional] http request url query - params: { - ...req.query, - }, - // http request headers - headers: { - "Content-Type": "application/json", - }, - // [optional] http request body - body: req.body, - } - - const signer = new Signer(openApiRequestData, req.service); - -// sign - signer.addAuthorization({accessKeyId:this.opts.access.accessKeyId, secretKey:this.opts.access.secretAccessKey}); - -// Print signed headers - console.log(openApiRequestData.headers); - - - const url = `https://open.volcengineapi.com/?${querystring.stringify(req.query)}` - const res = await http.request({ - url: url, - method: req.method, - headers: openApiRequestData.headers, - data:req.body - }); - - if (res?.ResponseMetadata?.Error) { - throw new Error(JSON.stringify(res.ResponseMetadata.Error)); - } - return res - } - - - // 列出域名解析记录 - async findDomain(domain: string) { - const req: VolcengineReq = { - method: "POST", - region: "cn-beijing", - service: "dns", - query: { - Action: "ListZones", - Version: "2018-08-01", - }, - body:{ - Key: domain, - SearchMode: "exact" - } - }; - - return this.doRequest(req); - } -} - diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts new file mode 100644 index 00000000..d4dbb215 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/dns-client.ts @@ -0,0 +1,106 @@ +import { VolcengineAccess } from "./access.js"; +import { http, HttpClient, ILogger } from "@certd/basic"; +import querystring from "querystring"; + +export type VolcengineOpts = { + access: VolcengineAccess + logger: ILogger + http: HttpClient +} + +export type VolcengineReq = { + method?: string; + path?: string; + headers?: any; + body?: any; + query?: any; + service?: string, // 替换为实际服务名称 + region?: string, // 替换为实际区域名称 +} + +export class VolcengineDnsClient { + opts: VolcengineOpts; + + constructor(opts: VolcengineOpts) { + this.opts = opts; + } + + + async doRequest(req: VolcengineReq) { + const {Signer} =await import('@volcengine/openapi') ; + +// http request data + const openApiRequestData: any = { + region: req.region, + method: req.method, + // [optional] http request url query + params: { + ...req.query, + }, + // http request headers + headers: { + "Content-Type": "application/json", + }, + // [optional] http request body + body: req.body, + } + + const signer = new Signer(openApiRequestData, req.service); + +// sign + signer.addAuthorization({accessKeyId:this.opts.access.accessKeyId, secretKey:this.opts.access.secretAccessKey}); + +// Print signed headers + console.log(openApiRequestData.headers); + + + const url = `https://open.volcengineapi.com/?${querystring.stringify(req.query)}` + + try{ + const res = await http.request({ + url: url, + method: req.method, + headers: openApiRequestData.headers, + data:req.body + }); + if (res?.ResponseMetadata?.Error) { + const err = new Error(JSON.stringify(res.ResponseMetadata.Error)); + // @ts-ignore + err.detail = res.ResponseMetadata.Error; + throw err + } + return res + }catch (e) { + if(e.response){ + const err = new Error(JSON.stringify(e.response.data.ResponseMetadata.Error)); + // @ts-ignore + err.detail = e.response.data.ResponseMetadata.Error; + throw err + } + } + + + + } + + + // 列出域名解析记录 + async findDomain(domain: string) { + const req: VolcengineReq = { + method: "POST", + region: "cn-beijing", + service: "dns", + query: { + Action: "ListZones", + Version: "2018-08-01", + }, + body:{ + Key: domain, + SearchMode: "exact" + } + }; + + return this.doRequest(req); + } +} + diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/index.ts index e69de29b..74f66650 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/index.ts @@ -0,0 +1 @@ +export * from './plugin-deploy-to-cdn.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-cdn.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-cdn.ts new file mode 100644 index 00000000..ab2e3c55 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/plugins/plugin-deploy-to-cdn.ts @@ -0,0 +1,144 @@ +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { optionsUtils } from "@certd/basic/dist/utils/util.options.js"; +import { VolcengineAccess } from "../access.js"; +import { VolcengineCdnClient } from "../cdn-client.js"; + +@IsTaskPlugin({ + name: 'VolcengineDeployToCDN', + title: '火山引擎-部署证书至CDN', + icon: 'svg:icon-volcengine', + group: pluginGroups.volcengine.key, + desc: '支持网页,文件下载,音视频点播', + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class VolcengineDeployToCDN extends AbstractTaskPlugin { + @TaskInput({ + title: '域名证书', + helper: '请选择前置任务输出的域名证书', + component: { + name: 'output-selector', + from: [...CertApplyPluginNames, 'VolcengineUploadCert'], + }, + required: true, + }) + cert!: CertInfo | string; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + + @TaskInput({ + title: 'Access授权', + helper: '火山引擎AccessKeyId、AccessKeySecret', + component: { + name: 'access-selector', + type: 'volcengine', + }, + required: true, + }) + accessId!: string; + + @TaskInput({ + title: '服务类型', + helper: '网页,文件下载,音视频点播', + component: { + name: 'a-select', + options:[ + { label: "网页", value: "web" }, + { label: "文件下载", value: "download" }, + { label: "音视频点播", value: 'video'}, + ] + }, + value: 'web', + required: true, + }) + serviceType:string = "web" + + + + + @TaskInput( + createRemoteSelectInputDefine({ + title: 'CDN加速域名', + helper: '你在火山引擎上配置的CDN加速域名,比如:certd.docmirror.cn', + action: VolcengineDeployToCDN.prototype.onGetDomainList.name, + watches: ['certDomains', 'accessId', 'serviceType'], + required: true, + }) + ) + domainName!: string | string[]; + + + async onInstance() {} + async execute(): Promise { + this.logger.info('开始部署证书到火山引擎CDN'); + const access = await this.accessService.getById(this.accessId); + + const client = await this.getClient(access) + const service = await client.getCdnClient() + let certId = this.cert + if (typeof certId !== 'string') { + const certInfo = this.cert as CertInfo + this.logger.info(`开始上传证书`) + certId = await client.uploadCert(certInfo, this.appendTimeSuffix('certd')) + } + + for (const domain of this.domainName) { + this.logger.info(`开始部署域名${domain}证书`) + await service.UpdateCdnConfig({ + Domain: domain, + HTTPS: { + CertInfo: { CertId: certId }, + } + }) + this.logger.info(`部署域名${domain}证书成功`); + await this.ctx.utils.sleep(1000) + } + + this.logger.info('部署完成'); + } + + + async getClient(access: VolcengineAccess) { + return new VolcengineCdnClient({ + logger: this.logger, + access, + http:this.http + }) + } + + async onGetDomainList(data: any) { + if (!this.accessId) { + throw new Error('请选择Access授权'); + } + const access = await this.accessService.getById(this.accessId); + + const client = await this.getClient(access); + const service = await client.getCdnClient() + const res = await service.ListCdnDomains({ + ServiceType: this.serviceType, + PageNum: 1, + PageSize: 100, + }) + // @ts-ignore + const list = res?.Result?.Data + if (!list || list.length === 0) { + throw new Error('找不到加速域名,您可以手动输入'); + } + const options = list.map((item: any) => { + return { + value: item.Domain, + label: item.Domain, + domain: item.Domain, + }; + }); + return optionsUtils.buildGroupOptions(options, this.certDomains); + } +} +new VolcengineDeployToCDN(); diff --git a/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts index ebf2acd7..b7ce4533 100644 --- a/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-volcengine/volcengine-dns-provider.ts @@ -1,7 +1,7 @@ import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; import { Autowire } from "@certd/pipeline"; -import { VolcengineClient } from "./client.js"; +import { VolcengineDnsClient } from "./dns-client.js"; import { VolcengineAccess } from "./access.js"; @IsDnsProvider({ @@ -12,13 +12,13 @@ import { VolcengineAccess } from "./access.js"; icon: "svg:icon-volcengine" }) export class VolcengineDnsProvider extends AbstractDnsProvider { - client: VolcengineClient; + client: VolcengineDnsClient; @Autowire() access!: VolcengineAccess; async onInstance() { - this.client = new VolcengineClient({ + this.client = new VolcengineDnsClient({ access: this.access, logger: this.logger, http: this.http