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

295 lines
7.8 KiB
TypeScript
Raw Normal View History

import { AbstractTaskPlugin, Decorator, HttpClient, IAccessService, IContext, IsTaskPlugin, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
2022-11-08 22:10:42 +08:00
import dayjs from "dayjs";
import { AcmeService, CertInfo } from "./acme";
2022-11-08 22:10:42 +08:00
import _ from "lodash";
import { Logger } from "log4js";
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider";
import { CertReader } from "./cert-reader";
2023-06-25 23:45:13 +08:00
import JSZip from "jszip";
export { CertReader };
export type { CertInfo };
2023-01-11 20:39:48 +08:00
2022-12-29 23:52:51 +08:00
@IsTaskPlugin({
name: "CertApply",
title: "证书申请",
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
default: {
2022-11-08 22:10:42 +08:00
input: {
2022-12-29 23:52:51 +08:00
renewDays: 20,
forceUpdate: false,
2022-11-08 22:10:42 +08:00
},
2022-12-29 23:52:51 +08:00
strategy: {
runStrategy: RunStrategy.AlwaysRun,
2022-11-08 22:10:42 +08:00
},
2022-12-29 23:52:51 +08:00
},
2022-11-08 22:10:42 +08:00
})
2023-05-24 15:41:35 +08:00
export class CertApplyPlugin extends AbstractTaskPlugin {
2022-12-29 23:52:51 +08:00
@TaskInput({
title: "域名",
component: {
name: "a-select",
vModel: "value",
mode: "tags",
open: false,
},
required: true,
col: {
span: 24,
},
helper:
2024-03-08 17:42:47 +08:00
"支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
2022-12-29 23:52:51 +08:00
"支持多个域名、多个子域名、多个通配符域名打到一个证书上域名必须是在同一个DNS提供商解析\n" +
2024-03-08 17:44:03 +08:00
"多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com\n" +
2022-12-29 23:52:51 +08:00
"输入一个回车之后,再输入下一个",
})
domains!: string;
@TaskInput({
title: "邮箱",
component: {
name: "a-input",
vModel: "value",
},
required: true,
helper: "请输入邮箱",
})
email!: string;
@TaskInput({
title: "DNS提供商",
component: {
name: "pi-dns-provider-selector",
},
required: true,
helper: "请选择dns解析提供商",
})
dnsProviderType!: string;
@TaskInput({
title: "DNS解析授权",
component: {
name: "pi-access-selector",
},
required: true,
helper: "请选择dns解析提供商授权",
2023-06-25 16:25:23 +08:00
reference: [
{
src: "form.dnsProviderType",
dest: "component.type",
type: "computed",
},
],
2022-12-29 23:52:51 +08:00
})
dnsProviderAccess!: string;
@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: "CsrInfo",
})
csrInfo: any;
2023-06-25 23:25:56 +08:00
acme!: AcmeService;
2022-12-29 23:52:51 +08:00
logger!: Logger;
userContext!: IContext;
accessService!: IAccessService;
http!: HttpClient;
2023-05-24 15:41:35 +08:00
lastStatus!: Step;
2022-12-29 23:52:51 +08:00
@TaskOutput({
title: "域名证书",
})
cert?: CertInfo;
2023-05-09 10:16:49 +08:00
async onInstance() {
2023-06-25 23:25:56 +08:00
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;
2022-11-08 22:10:42 +08:00
this.acme = new AcmeService({ userContext: this.userContext, logger: this.logger });
}
2022-12-29 23:52:51 +08:00
async execute(): Promise<void> {
const oldCert = await this.condition();
2022-11-08 22:10:42 +08:00
if (oldCert != null) {
2024-05-30 10:12:48 +08:00
return await this.output(oldCert);
2022-11-08 22:10:42 +08:00
}
2022-12-29 23:52:51 +08:00
const cert = await this.doCertApply();
if (cert != null) {
2024-05-30 10:12:48 +08:00
await this.output(cert);
2023-05-24 15:41:35 +08:00
//清空后续任务的状态,让后续任务能够重新执行
this.clearLastStatus();
} else {
throw new Error("申请证书失败");
}
2022-12-29 23:52:51 +08:00
}
2024-05-30 10:12:48 +08:00
async output(certReader: CertReader) {
const cert: CertInfo = certReader.toCertInfo();
2022-12-29 23:52:51 +08:00
this.cert = cert;
2024-05-30 10:12:48 +08:00
// this.logger.info(JSON.stringify(certReader.detail));
const applyTime = dayjs(certReader.detail.validity.notBefore).format("YYYYMMDD_HHmmss");
2024-05-30 10:12:48 +08:00
await this.zipCert(cert, applyTime);
2023-06-25 23:45:13 +08:00
}
2024-05-30 10:12:48 +08:00
async zipCert(cert: CertInfo, applyTime: string) {
2023-06-25 23:45:13 +08:00
const zip = new JSZip();
zip.file("cert.crt", cert.crt);
zip.file("cert.key", cert.key);
2024-05-30 10:12:48 +08:00
const domain_name = this.domains[0].replace(".", "_").replace("*", "_");
const filename = `cert_${domain_name}_${applyTime}.zip`;
2023-06-25 23:45:13 +08:00
const content = await zip.generateAsync({ type: "nodebuffer" });
2024-05-30 10:12:48 +08:00
this.saveFile(filename, content);
this.logger.info(`已保存文件:${filename}`);
2022-11-08 22:10:42 +08:00
}
/**
*
* @param input
*/
2022-12-29 23:52:51 +08:00
async condition() {
if (this.forceUpdate) {
2022-11-08 22:10:42 +08:00
return null;
}
2023-05-09 13:52:25 +08:00
let inputChanged = false;
2023-05-24 15:41:35 +08:00
const oldInput = JSON.stringify(this.lastStatus?.input?.domains);
const thisInput = JSON.stringify(this.domains);
2023-05-09 13:52:25 +08:00
if (oldInput !== thisInput) {
inputChanged = true;
}
let oldCert: CertReader | undefined = undefined;
2022-11-08 22:10:42 +08:00
try {
2023-05-24 15:41:35 +08:00
oldCert = await this.readLastCert();
2022-11-08 22:10:42 +08:00
} catch (e) {
this.logger.warn("读取cert失败", e);
}
if (oldCert == null) {
this.logger.info("还未申请过,准备申请新证书");
return null;
}
2023-05-09 13:52:25 +08:00
if (inputChanged) {
this.logger.info("输入参数变更,申请新证书");
return null;
}
2022-12-29 23:52:51 +08:00
const ret = this.isWillExpire(oldCert.expires, this.renewDays);
2022-11-08 22:10:42 +08:00
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;
}
2022-12-29 23:52:51 +08:00
async doCertApply() {
const email = this["email"];
const domains = this["domains"];
const dnsProviderType = this["dnsProviderType"];
const dnsProviderAccessId = this["dnsProviderAccess"];
2022-11-08 22:10:42 +08:00
const csrInfo = _.merge(
{
country: "CN",
state: "GuangDong",
locality: "ShengZhen",
organization: "CertD Org.",
organizationUnit: "IT Department",
emailAddress: email,
},
2022-12-29 23:52:51 +08:00
this.csrInfo
2022-11-08 22:10:42 +08:00
);
this.logger.info("开始申请证书,", email, domains);
2023-01-11 20:39:48 +08:00
const dnsProviderPlugin = dnsProviderRegistry.get(dnsProviderType);
const DnsProviderClass = dnsProviderPlugin.target;
const dnsProviderDefine = dnsProviderPlugin.define as DnsProviderDefine;
2022-11-08 22:10:42 +08:00
const access = await this.accessService.getById(dnsProviderAccessId);
2023-01-11 20:39:48 +08:00
2022-11-08 22:10:42 +08:00
// @ts-ignore
2023-01-11 20:39:48 +08:00
const dnsProvider: IDnsProvider = new DnsProviderClass();
const context: DnsProviderContext = { access, logger: this.logger, http: this.http };
2023-01-11 20:39:48 +08:00
Decorator.inject(dnsProviderDefine.autowire, dnsProvider, context);
dnsProvider.setCtx(context);
2023-05-09 10:16:49 +08:00
await dnsProvider.onInstance();
2022-11-08 22:10:42 +08:00
const cert = await this.acme.order({
email,
domains,
dnsProvider,
csrInfo,
isTest: false,
});
2023-05-24 15:41:35 +08:00
const certInfo = this.formatCerts(cert);
return new CertReader(certInfo);
2022-11-08 22:10:42 +08:00
}
formatCert(pem: string) {
pem = pem.replace(/\r/g, "");
pem = pem.replace(/\n\n/g, "\n");
pem = pem.replace(/\n$/g, "");
return pem;
}
2023-05-24 15:41:35 +08:00
formatCerts(cert: { crt: string; key: string; csr: string }) {
const newCert: CertInfo = {
2022-11-08 22:10:42 +08:00
crt: this.formatCert(cert.crt),
key: this.formatCert(cert.key),
csr: this.formatCert(cert.csr),
};
2023-05-24 15:41:35 +08:00
return newCert;
2022-11-08 22:10:42 +08:00
}
2023-05-24 15:41:35 +08:00
async readLastCert(): Promise<CertReader | undefined> {
const cert = this.lastStatus?.status?.output?.cert;
2022-11-08 22:10:42 +08:00
if (cert == null) {
return undefined;
}
return new CertReader(cert);
2022-11-08 22:10:42 +08:00
}
/**
* 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,
};
}
}
2023-05-09 09:56:31 +08:00
new CertApplyPlugin();