mirror of
https://github.com/certd/certd.git
synced 2026-04-14 20:40:53 +08:00
feat: 支持lego,海量DNS提供商
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
import { AbstractTaskPlugin, Decorator, HttpClient, IAccessService, IContext, IsTaskPlugin, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import dayjs from "dayjs";
|
||||
import { AcmeService } from "./acme.js";
|
||||
import { Decorator, IsTaskPlugin, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import type { CertInfo, SSLProvider } from "./acme.js";
|
||||
import { AcmeService } from "./acme.js";
|
||||
import _ from "lodash-es";
|
||||
import { Logger } from "log4js";
|
||||
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js";
|
||||
import { CertReader } from "./cert-reader.js";
|
||||
import JSZip from "jszip";
|
||||
import { CertApplyBasePlugin } from "./base.js";
|
||||
|
||||
export { CertReader };
|
||||
export type { CertInfo };
|
||||
@@ -25,38 +23,7 @@ export type { CertInfo };
|
||||
},
|
||||
},
|
||||
})
|
||||
export class CertApplyPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
},
|
||||
required: true,
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
helper:
|
||||
"1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
|
||||
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
||||
"3、多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com)\n" +
|
||||
"4、输入一个回车之后,再输入下一个",
|
||||
})
|
||||
domains!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "邮箱",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "请输入邮箱",
|
||||
})
|
||||
email!: string;
|
||||
|
||||
export class CertApplyPlugin extends CertApplyBasePlugin {
|
||||
@TaskInput({
|
||||
title: "证书提供商",
|
||||
default: "letsencrypt",
|
||||
@@ -121,69 +88,9 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
|
||||
})
|
||||
skipLocalVerify = false;
|
||||
|
||||
@TaskInput({
|
||||
title: "更新天数",
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "到期前多少天后更新证书,注意:流水线默认不会自动运行,请设置定时器,每天定时运行本流水线",
|
||||
})
|
||||
renewDays!: number;
|
||||
|
||||
@TaskInput({
|
||||
title: "强制更新",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "是否强制重新申请证书",
|
||||
})
|
||||
forceUpdate!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "成功后邮件通知",
|
||||
value: true,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "申请成功后是否发送邮件通知",
|
||||
})
|
||||
successNotify = true;
|
||||
|
||||
// @TaskInput({
|
||||
// title: "CsrInfo",
|
||||
// helper: "暂时没有用",
|
||||
// })
|
||||
csrInfo!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "配置说明",
|
||||
helper: "运行策略请选择总是运行,其他证书部署任务请选择成功后跳过;当证书快到期前将会自动重新申请证书,然后会清空后续任务的成功状态,部署任务将会重新运行",
|
||||
})
|
||||
intro!: string;
|
||||
|
||||
acme!: AcmeService;
|
||||
logger!: Logger;
|
||||
userContext!: IContext;
|
||||
accessService!: IAccessService;
|
||||
http!: HttpClient;
|
||||
lastStatus!: Step;
|
||||
|
||||
@TaskOutput({
|
||||
title: "域名证书",
|
||||
})
|
||||
cert?: CertInfo;
|
||||
|
||||
async onInstance() {
|
||||
this.accessService = this.ctx.accessService;
|
||||
this.logger = this.ctx.logger;
|
||||
this.userContext = this.ctx.userContext;
|
||||
this.http = this.ctx.http;
|
||||
this.lastStatus = this.ctx.lastStatus as Step;
|
||||
|
||||
async onInit() {
|
||||
let eab: any = null;
|
||||
if (this.eabAccessId) {
|
||||
eab = await this.ctx.accessService.getById(this.eabAccessId);
|
||||
@@ -197,90 +104,6 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
|
||||
});
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const oldCert = await this.condition();
|
||||
if (oldCert != null) {
|
||||
return await this.output(oldCert, false);
|
||||
}
|
||||
const cert = await this.doCertApply();
|
||||
if (cert != null) {
|
||||
await this.output(cert, true);
|
||||
//清空后续任务的状态,让后续任务能够重新执行
|
||||
this.clearLastStatus();
|
||||
|
||||
if (this.successNotify) {
|
||||
await this.sendSuccessEmail();
|
||||
}
|
||||
} else {
|
||||
throw new Error("申请证书失败");
|
||||
}
|
||||
}
|
||||
|
||||
async output(certReader: CertReader, isNew: boolean) {
|
||||
const cert: CertInfo = certReader.toCertInfo();
|
||||
this.cert = cert;
|
||||
|
||||
if (isNew) {
|
||||
const applyTime = dayjs(certReader.detail.validity.notBefore).format("YYYYMMDD_HHmmss");
|
||||
await this.zipCert(cert, applyTime);
|
||||
} else {
|
||||
this.extendsFiles();
|
||||
}
|
||||
// thi
|
||||
// s.logger.info(JSON.stringify(certReader.detail));
|
||||
}
|
||||
|
||||
async zipCert(cert: CertInfo, applyTime: string) {
|
||||
const zip = new JSZip();
|
||||
zip.file("cert.crt", cert.crt);
|
||||
zip.file("cert.key", cert.key);
|
||||
const domain_name = this.domains[0].replace(".", "_").replace("*", "_");
|
||||
const filename = `cert_${domain_name}_${applyTime}.zip`;
|
||||
const content = await zip.generateAsync({ type: "nodebuffer" });
|
||||
this.saveFile(filename, content);
|
||||
this.logger.info(`已保存文件:${filename}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否更新证书
|
||||
*/
|
||||
async condition() {
|
||||
if (this.forceUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let inputChanged = false;
|
||||
const oldInput = JSON.stringify(this.lastStatus?.input?.domains);
|
||||
const thisInput = JSON.stringify(this.domains);
|
||||
if (oldInput !== thisInput) {
|
||||
inputChanged = true;
|
||||
}
|
||||
|
||||
let oldCert: CertReader | undefined = undefined;
|
||||
try {
|
||||
oldCert = await this.readLastCert();
|
||||
} catch (e) {
|
||||
this.logger.warn("读取cert失败:", e);
|
||||
}
|
||||
if (oldCert == null) {
|
||||
this.logger.info("还未申请过,准备申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inputChanged) {
|
||||
this.logger.info("输入参数变更,申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
const ret = this.isWillExpire(oldCert.expires, this.renewDays);
|
||||
if (!ret.isWillExpire) {
|
||||
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}天`);
|
||||
return oldCert;
|
||||
}
|
||||
this.logger.info("即将过期,开始更新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
async doCertApply() {
|
||||
const email = this["email"];
|
||||
const domains = this["domains"];
|
||||
@@ -331,63 +154,6 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
formatCert(pem: string) {
|
||||
pem = pem.replace(/\r/g, "");
|
||||
pem = pem.replace(/\n\n/g, "\n");
|
||||
pem = pem.replace(/\n$/g, "");
|
||||
return pem;
|
||||
}
|
||||
|
||||
formatCerts(cert: { crt: string; key: string; csr: string }) {
|
||||
const newCert: CertInfo = {
|
||||
crt: this.formatCert(cert.crt),
|
||||
key: this.formatCert(cert.key),
|
||||
csr: this.formatCert(cert.csr),
|
||||
};
|
||||
return newCert;
|
||||
}
|
||||
|
||||
async readLastCert(): Promise<CertReader | undefined> {
|
||||
const cert = this.lastStatus?.status?.output?.cert;
|
||||
if (cert == null) {
|
||||
return undefined;
|
||||
}
|
||||
return new CertReader(cert);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期,默认提前20天
|
||||
* @param expires
|
||||
* @param maxDays
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isWillExpire(expires: number, maxDays = 20) {
|
||||
if (expires == null) {
|
||||
throw new Error("过期时间不能为空");
|
||||
}
|
||||
// 检查有效期
|
||||
const leftDays = dayjs(expires).diff(dayjs(), "day");
|
||||
return {
|
||||
isWillExpire: leftDays < maxDays,
|
||||
leftDays,
|
||||
};
|
||||
}
|
||||
|
||||
private async sendSuccessEmail() {
|
||||
try {
|
||||
this.logger.info("发送成功邮件通知:" + this.email);
|
||||
const subject = `【CertD】证书申请成功【${this.domains[0]}】`;
|
||||
await this.ctx.emailService.send({
|
||||
userId: this.ctx.pipeline.userId,
|
||||
receivers: [this.email],
|
||||
subject: subject,
|
||||
content: `证书申请成功,域名:${this.domains.join(",")}`,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error("send email error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new CertApplyPlugin();
|
||||
|
||||
Reference in New Issue
Block a user