import { IsTaskPlugin, pluginGroups, RunStrategy, Step, TaskInput } from "@certd/pipeline"; import type { CertInfo } from "../acme.js"; import { CertReader } from "../cert-reader.js"; import { CertApplyBasePlugin } from "../base.js"; import fs from "fs"; import { EabAccess } from "../../../access/index.js"; import path from "path"; import JSZip from "jszip"; export { CertReader }; export type { CertInfo }; @IsTaskPlugin({ name: "CertApplyLego", icon: "ph:certificate", title: "证书申请(Lego)", group: pluginGroups.cert.key, desc: "支持海量DNS解析提供商,推荐使用,一样的免费通配符域名证书申请,支持多个域名打到同一个证书上", default: { input: { renewDays: 35, forceUpdate: false, }, strategy: { runStrategy: RunStrategy.AlwaysRun, }, }, }) export class CertApplyLegoPlugin extends CertApplyBasePlugin { // @TaskInput({ // title: "ACME服务端点", // default: "https://acme-v02.api.letsencrypt.org/directory", // component: { // name: "a-select", // vModel: "value", // options: [ // { value: "https://acme-v02.api.letsencrypt.org/directory", label: "Let's Encrypt" }, // { value: "https://letsencrypt.proxy.handsfree.work/directory", label: "Let's Encrypt代理,letsencrypt.org无法访问时使用" }, // ], // }, // required: true, // }) acmeServer!: string; @TaskInput({ title: "DNS类型", component: { name: "a-input", vModel: "value", placeholder: "alidns", }, helper: "你的域名是通过哪家提供商进行解析的,具体应该配置什么请参考lego文档:https://go-acme.github.io/lego/dns/", required: true, }) dnsType!: string; @TaskInput({ title: "环境变量", component: { name: "a-textarea", vModel: "value", rows: 4, placeholder: "ALICLOUD_ACCESS_KEY=abcdefghijklmnopqrstuvwx\nALICLOUD_SECRET_KEY=your-secret-key", }, required: true, helper: "一行一条,例如 appKeyId=xxxxx,具体配置请参考lego文档:https://go-acme.github.io/lego/dns/", }) environment!: string; @TaskInput({ title: "EAB授权", component: { name: "access-selector", type: "eab", }, maybeNeed: true, helper: "如果需要提供EAB授权", }) legoEabAccessId!: number; @TaskInput({ title: "自定义LEGO参数", component: { name: "a-input", vModel: "value", placeholder: "--dns-timeout 30", }, helper: "额外的lego命令行参数,参考文档:https://go-acme.github.io/lego/usage/cli/options/", maybeNeed: true, }) customArgs = ""; eab?: EabAccess; async onInstance() { this.accessService = this.ctx.accessService; this.logger = this.ctx.logger; this.userContext = this.ctx.userContext; this.lastStatus = this.ctx.lastStatus as Step; if (this.legoEabAccessId) { this.eab = await this.accessService.getById(this.legoEabAccessId); } } async onInit(): Promise {} async doCertApply() { const env: any = {}; const env_lines = this.environment.split("\n"); for (const line of env_lines) { const [key, value] = line.trim().split("="); env[key] = value.trim(); } let domainArgs = ""; for (const domain of this.domains) { domainArgs += ` -d "${domain}"`; } this.logger.info(`环境变量:${JSON.stringify(env)}`); let eabArgs = ""; if (this.eab) { eabArgs = ` --eab --kid "${this.eab.kid}" --hmac "${this.eab.hmacKey}"`; } const keyType = "-k rsa2048"; const saveDir = `./data/.lego/pipeline_${this.pipeline.id}/`; const savePathArgs = `--path "${saveDir}"`; const os_type = process.platform === "win32" ? "windows" : "linux"; const legoDir = "./tools/lego"; const legoPath = path.resolve(legoDir, os_type === "windows" ? "lego.exe" : "lego"); if (!fs.existsSync(legoPath)) { //解压缩 const arch = process.arch; let platform = "amd64"; if (arch === "arm64" || arch === "arm") { platform = "arm64"; } const LEGO_VERSION = process.env.LEGO_VERSION; let legoZipFileName = `lego_v${LEGO_VERSION}_windows_${platform}.zip`; if (os_type === "linux") { legoZipFileName = `lego_v${LEGO_VERSION}_linux_${platform}.tar.gz`; } const legoZipFilePath = `${legoDir}/${legoZipFileName}`; if (!fs.existsSync(legoZipFilePath)) { this.logger.info(`lego文件不存在:${legoZipFilePath},准备下载`); const downloadUrl = `https://github.com/go-acme/lego/releases/download/v${LEGO_VERSION}/${legoZipFileName}`; await this.ctx.download( { url: downloadUrl, method: "GET", logRes: false, }, legoZipFilePath ); this.logger.info("下载lego成功"); } if (os_type === "linux") { //tar是否存在 await this.ctx.utils.sp.spawn({ cmd: `tar -zxvf ${legoZipFilePath} -C ${legoDir}/`, }); await this.ctx.utils.sp.spawn({ cmd: `chmod +x ${legoDir}/*`, }); this.logger.info("解压lego成功"); } else { const zip = new JSZip(); const data = fs.readFileSync(legoZipFilePath); const zipData = await zip.loadAsync(data); const files = Object.keys(zipData.files); for (const file of files) { const content = await zipData.files[file].async("nodebuffer"); fs.writeFileSync(`${legoDir}/${file}`, content); } this.logger.info("解压lego成功"); } } let serverArgs = ""; if (this.acmeServer) { serverArgs = ` --server ${this.acmeServer}`; } const cmds = [`${legoPath} -a --email "${this.email}" --dns ${this.dnsType} ${keyType} ${domainArgs} ${serverArgs} ${eabArgs} ${savePathArgs} ${this.customArgs || ""} run`]; await this.ctx.utils.sp.spawn({ cmd: cmds, logger: this.logger, env, }); //读取证书文件 // example.com.crt // example.com.issuer.crt // example.com.json // example.com.key let domain1 = this.domains[0]; domain1 = domain1.replaceAll("*", "_"); const crtPath = path.resolve(saveDir, "certificates", `${domain1}.crt`); if (fs.existsSync(crtPath) === false) { throw new Error(`证书文件不存在,证书申请失败:${crtPath}`); } const crt = fs.readFileSync(crtPath, "utf8"); const keyPath = path.resolve(saveDir, "certificates", `${domain1}.key`); const key = fs.readFileSync(keyPath, "utf8"); const csr = ""; const cert = { crt, key, csr }; const certInfo = this.formatCerts(cert); return new CertReader(certInfo); } } new CertApplyLegoPlugin();