Files
certd/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts

251 lines
7.8 KiB
TypeScript
Raw Normal View History

2024-11-04 15:14:56 +08:00
import { IsTaskPlugin, pluginGroups, RunStrategy, Step, TaskInput } from "@certd/pipeline";
2024-07-24 02:11:38 +08:00
import type { CertInfo } from "../acme.js";
import { CertReader } from "../cert-reader.js";
import { CertApplyBasePlugin } from "../base.js";
2024-07-18 21:10:13 +08:00
import fs from "fs";
2024-07-24 02:11:38 +08:00
import { EabAccess } from "../../../access/index.js";
2024-07-18 21:10:13 +08:00
import path from "path";
2024-10-30 16:12:08 +08:00
import JSZip from "jszip";
2024-07-18 21:10:13 +08:00
export { CertReader };
export type { CertInfo };
2025-04-05 17:01:41 +08:00
export type PrivateKeyType = "rsa2048" | "rsa3072" | "rsa4096" | "rsa8192" | "ec256" | "ec384";
2024-07-18 21:10:13 +08:00
@IsTaskPlugin({
name: "CertApplyLego",
2024-09-19 17:38:51 +08:00
icon: "ph:certificate",
2024-07-18 21:10:13 +08:00
title: "证书申请Lego",
group: pluginGroups.cert.key,
desc: "支持海量DNS解析提供商推荐使用一样的免费通配符域名证书申请支持多个域名打到同一个证书上",
2024-07-18 21:10:13 +08:00
default: {
input: {
renewDays: 35,
2024-07-18 21:10:13 +08:00
forceUpdate: false,
},
strategy: {
runStrategy: RunStrategy.AlwaysRun,
},
},
})
export class CertApplyLegoPlugin extends CertApplyBasePlugin {
2024-07-24 02:11:38 +08:00
// @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;
2024-07-18 21:10:13 +08:00
@TaskInput({
title: "DNS类型",
component: {
name: "a-input",
vModel: "value",
placeholder: "alidns",
2024-07-18 21:10:13 +08:00
},
helper: "你的域名是通过哪家提供商进行解析的具体应该配置什么请参考lego文档https://go-acme.github.io/lego/dns/",
2024-07-18 21:10:13 +08:00
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",
2024-07-18 21:10:13 +08:00
},
required: true,
2024-07-20 10:09:18 +08:00
helper: "一行一条,例如 appKeyId=xxxxx具体配置请参考lego文档https://go-acme.github.io/lego/dns/",
2024-07-18 21:10:13 +08:00
})
environment!: string;
@TaskInput({
title: "EAB授权",
component: {
name: "access-selector",
2024-07-18 21:10:13 +08:00
type: "eab",
},
maybeNeed: true,
2024-07-18 21:10:13 +08:00
helper: "如果需要提供EAB授权",
})
legoEabAccessId!: number;
2024-07-18 21:10:13 +08:00
@TaskInput({
title: "自定义LEGO全局参数",
2024-07-18 21:10:13 +08:00
component: {
name: "a-input",
vModel: "value",
placeholder: "--dns-timeout 30",
2024-07-18 21:10:13 +08:00
},
helper: "额外的lego全局命令行参数参考文档https://go-acme.github.io/lego/usage/cli/options/",
maybeNeed: true,
2024-07-18 21:10:13 +08:00
})
customArgs = "";
@TaskInput({
title: "自定义LEGO签名参数",
component: {
name: "a-input",
vModel: "value",
placeholder: "--no-bundle",
},
helper: "额外的lego签名命令行参数参考文档https://go-acme.github.io/lego/usage/cli/options/",
maybeNeed: true,
})
customCommandOptions = "";
2025-04-05 17:01:41 +08:00
@TaskInput({
title: "加密算法",
value: "ec256",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "rsa2048", label: "RSA 2048" },
{ value: "rsa3072", label: "RSA 3072" },
{ value: "rsa4096", label: "RSA 4096" },
{ value: "rsa8192", label: "RSA 8192" },
{ value: "ec256", label: "EC 256" },
{ value: "ec384", label: "EC 384" },
// { value: "ec_521", label: "EC 521" },
],
},
helper: "如无特殊需求,默认即可",
required: true,
})
privateKeyType!: PrivateKeyType;
2024-07-18 21:10:13 +08:00
eab?: EabAccess;
getCheckChangeInputKeys() {
return ["domains", "privateKeyType", "dnsType"];
}
2024-07-18 21:10:13 +08:00
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) {
2024-08-14 15:10:55 +08:00
this.eab = await this.accessService.getById(this.legoEabAccessId);
2024-07-18 21:10:13 +08:00
}
}
async onInit(): Promise<void> {}
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}"`;
2024-07-18 21:10:13 +08:00
}
2025-04-05 17:01:41 +08:00
const keyType = `-k ${this.privateKeyType}`;
2024-07-18 21:10:13 +08:00
const saveDir = `./data/.lego/pipeline_${this.pipeline.id}/`;
const savePathArgs = `--path "${saveDir}"`;
const os_type = process.platform === "win32" ? "windows" : "linux";
2024-11-04 15:14:56 +08:00
const legoDir = "./tools/lego";
2024-11-04 16:39:02 +08:00
const legoPath = path.resolve(legoDir, os_type === "windows" ? "lego.exe" : "lego");
2024-10-30 16:12:08 +08:00
if (!fs.existsSync(legoPath)) {
//解压缩
2024-11-04 15:14:56 +08:00
const arch = process.arch;
let platform = "amd64";
if (arch === "arm64" || arch === "arm") {
platform = "arm64";
}
const LEGO_VERSION = process.env.LEGO_VERSION;
2024-11-04 16:39:02 +08:00
let legoZipFileName = `lego_v${LEGO_VERSION}_windows_${platform}.zip`;
2024-10-30 16:12:08 +08:00
if (os_type === "linux") {
2024-11-04 16:39:02 +08:00
legoZipFileName = `lego_v${LEGO_VERSION}_linux_${platform}.tar.gz`;
2024-11-04 15:14:56 +08:00
}
2024-11-04 16:39:02 +08:00
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(
2024-11-04 15:14:56 +08:00
{
url: downloadUrl,
method: "GET",
2024-11-04 16:39:02 +08:00
logRes: false,
2024-11-04 15:14:56 +08:00
},
2024-11-04 16:39:02 +08:00
legoZipFilePath
2024-11-04 15:14:56 +08:00
);
this.logger.info("下载lego成功");
}
if (os_type === "linux") {
//tar是否存在
await this.ctx.utils.sp.spawn({
2024-11-04 16:39:02 +08:00
cmd: `tar -zxvf ${legoZipFilePath} -C ${legoDir}/`,
2024-10-30 16:12:08 +08:00
});
2024-11-04 15:14:56 +08:00
await this.ctx.utils.sp.spawn({
cmd: `chmod +x ${legoDir}/*`,
});
2024-10-30 16:12:08 +08:00
this.logger.info("解压lego成功");
} else {
const zip = new JSZip();
2024-11-04 16:39:02 +08:00
const data = fs.readFileSync(legoZipFilePath);
2024-10-30 16:12:08 +08:00
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");
2024-11-04 16:39:02 +08:00
fs.writeFileSync(`${legoDir}/${file}`, content);
2024-10-30 16:12:08 +08:00
}
this.logger.info("解压lego成功");
}
}
2024-07-24 02:11:38 +08:00
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 ${this.customCommandOptions || ""}`];
2024-07-18 21:10:13 +08:00
2024-11-04 15:14:56 +08:00
await this.ctx.utils.sp.spawn({
2024-07-18 21:10:13 +08:00
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();