import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import fs from "fs"; import path from "path"; import { tmpdir } from "node:os"; import { sp } from "@certd/basic"; import { AliyunAccess } from "../../../plugin-lib/aliyun/access/index.js"; @IsTaskPlugin({ name: "AliyunDeployCertToFC", title: "阿里云-部署至阿里云FC(3.0)", icon: "svg:icon-aliyun", group: pluginGroups.aliyun.key, desc: "部署证书到阿里云函数计算(FC3.0)", needPlus: false, default: { strategy: { runStrategy: RunStrategy.SkipWhenSucceed, }, }, }) export class AliyunDeployCertToFC extends AbstractTaskPlugin { @TaskInput({ title: "域名证书", helper: "请选择证书申请任务输出的域名证书", component: { name: "output-selector", from: [...CertApplyPluginNames], }, required: true, }) cert!: CertInfo; @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) certDomains!: string[]; @TaskInput({ title: "FC大区", value: "cn-hangzhou", component: { name: "a-auto-complete", vModel: "value", options: [ { value: "cn-qingdao", label: "华北1(青岛)" }, { value: "cn-beijing", label: "华北2(北京)" }, { value: "cn-zhangjiakou", label: "华北 3(张家口)" }, { value: "cn-huhehaote", label: "华北5(呼和浩特)" }, { value: "cn-hangzhou", label: "华东1(杭州)" }, { value: "cn-shanghai", label: "华东2(上海)" }, { value: "cn-shenzhen", label: "华南1(深圳)" }, { value: "ap-southeast-2", label: "澳大利亚(悉尼)" }, { value: "eu-central-1", label: "德国(法兰克福)" }, { value: "ap-southeast-3", label: "马来西亚(吉隆坡)" }, { value: "us-east-1", label: "美国(弗吉尼亚)" }, { value: "us-west-1", label: "美国(硅谷)" }, { value: "ap-northeast-1", label: "日本(东京)" }, { value: "ap-southeast-7", label: "泰国(曼谷)" }, { value: "cn-chengdu", label: "西南1(成都)" }, { value: "ap-southeast-1", label: "新加坡" }, { value: "ap-south-1", label: "印度(孟买)" }, { value: "ap-southeast-5", label: "印度尼西亚(雅加达)" }, { value: "eu-west-1", label: "英国(伦敦)" }, { value: "cn-hongkong", label: "中国香港" }, ], }, required: true, }) regionId!: string; @TaskInput({ title: "阿里云账号id", helper: "阿里云主账号ID,右上角头像下方获取", component: { name: "a-input", vModel: "value", }, required: true, }) accountId!: string; @TaskInput({ title: "Access授权", helper: "阿里云授权AccessKeyId、AccessKeySecret", component: { name: "access-selector", type: "aliyun", }, required: true, }) accessId!: string; @TaskInput( createRemoteSelectInputDefine({ title: "FC域名", helper: "请选择要部署证书的域名", typeName: "AliyunDeployCertToFC", action: AliyunDeployCertToFC.prototype.onGetDomainList.name, watches: ["accessId", "regionId"], }) ) fcDomains!: string[]; @TaskInput({ title: "域名支持的协议类型", value: "", component: { name: "a-select", vModel: "value", options: [ { value: "", label: "保持原样(适用于原来已经开启了HTTPS)" }, { value: "HTTPS", label: "仅HTTPS" }, { value: "HTTP,HTTPS", label: "HTTP与HTTPS同时支持" }, ], }, }) protocol!: string; @TaskInput({ title: "证书名称", helper: "上传后将以此名称作为前缀备注", }) certName!: string; async onInstance() {} async exec(cmd: string) { process.env.LANG = "zh_CN.GBK"; await sp.spawn({ cmd: cmd, logger: this.logger, }); } async execute(): Promise { this.logger.info("开始部署证书到阿里云"); const access = await this.getAccess(this.accessId); const client = await this.getClient(access); const $Util = await access.importRuntime("@alicloud/tea-util"); const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); let privateKey = this.cert.key; try { // openssl rsa -in private_key.pem -out private_key_pkcs1.pem const tempDir = path.join(tmpdir(), "certd"); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } const keyFileName = this.ctx.utils.id.randomNumber(10); const tempPem = `${tempDir}/${keyFileName}.pem`; const tempPkcs1Pem = `${tempDir}/${keyFileName}_pkcs1.pem`; fs.writeFileSync(tempPem, this.cert.key); const oldPfxCmd = `openssl rsa -in ${tempPem} -traditional -out ${tempPkcs1Pem}`; await this.exec(oldPfxCmd); const fileBuffer = fs.readFileSync(tempPkcs1Pem); privateKey = fileBuffer.toString(); fs.unlinkSync(tempPem); fs.unlinkSync(tempPkcs1Pem); } catch (e) { this.logger.warn("私钥转换为PKCS#1格式失败", e); } for (const domainName of this.fcDomains) { const params = new $OpenApi.Params({ // 接口名称 action: "UpdateCustomDomain", // 接口版本 version: "2023-03-30", // 接口协议 protocol: "HTTPS", // 接口 HTTP 方法 method: "PUT", authType: "AK", style: "FC", // 接口 PATH pathname: `/2023-03-30/custom-domains/${domainName}`, // 接口请求体内容格式 reqBodyType: "json", // 接口响应体内容格式 bodyType: "json", }); // body params const certName = this.buildCertName(CertReader.getMainDomain(this.cert.crt), this.certName ?? ""); const body: { [key: string]: any } = { certConfig: { certName: certName, certificate: this.cert.crt, privateKey: privateKey, }, }; if (this.protocol) { body.protocol = this.protocol; } const runtime = new $Util.RuntimeOptions({}); const request = new $OpenApi.OpenApiRequest({ body }); // 复制代码运行请自行打印 API 的返回值 // 返回值实际为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。 await client.callApi(params, request, runtime); this.logger.info(`部署[${domainName}]成功`); } } async getClient(access: AliyunAccess) { const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); const config = new $OpenApi.Config({ accessKeyId: access.accessKeyId, accessKeySecret: access.accessKeySecret, }); // Endpoint 请参考 https://api.aliyun.com/product/FC config.endpoint = `${this.accountId}.${this.regionId}.fc.aliyuncs.com`; return new $OpenApi.default.default(config); } async onGetDomainList(data: any) { if (!this.accessId) { throw new Error("请选择Access授权"); } const access = await this.getAccess(this.accessId); const client = await this.getClient(access); const $OpenApi = await access.importRuntime("@alicloud/openapi-client"); const $Util = await access.importRuntime("@alicloud/tea-util"); const params = new $OpenApi.Params({ // 接口名称 action: "ListCustomDomains", // 接口版本 version: "2023-03-30", // 接口协议 protocol: "HTTPS", // 接口 HTTP 方法 method: "GET", authType: "AK", style: "FC", // 接口 PATH pathname: `/2023-03-30/custom-domains`, // 接口请求体内容格式 reqBodyType: "json", // 接口响应体内容格式 bodyType: "json", }); const runtime = new $Util.RuntimeOptions({}); const request = new $OpenApi.OpenApiRequest({}); // 复制代码运行请自行打印 API 的返回值 // 返回值实际为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。 const res = await client.callApi(params, request, runtime); const list = res?.body?.customDomains; if (!list || list.length === 0) { throw new Error("没有找到FC域名,请先创建FC域名"); } const options = list.map((item: any) => { return { label: item.domainName, value: item.domainName, title: item.domainName, domain: item.domainName, }; }); return this.ctx.utils.options.buildGroupOptions(options, this.certDomains); } } new AliyunDeployCertToFC();