2025-03-18 00:52:50 +08:00
|
|
|
import { IsTaskPlugin, pluginGroups, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
|
2025-03-17 00:19:01 +08:00
|
|
|
import type { CertInfo } from "../acme.js";
|
|
|
|
|
import { CertReader } from "../cert-reader.js";
|
2025-03-18 00:52:50 +08:00
|
|
|
import { CertApplyBaseConvertPlugin } from "../base-convert.js";
|
2025-03-21 12:23:59 +08:00
|
|
|
import dayjs from "dayjs";
|
2025-03-22 02:06:02 +08:00
|
|
|
|
2025-03-17 00:19:01 +08:00
|
|
|
export { CertReader };
|
|
|
|
|
export type { CertInfo };
|
2025-09-24 14:14:19 +08:00
|
|
|
|
2025-03-17 00:19:01 +08:00
|
|
|
@IsTaskPlugin({
|
2025-03-18 00:52:50 +08:00
|
|
|
name: "CertApplyUpload",
|
2025-03-17 00:19:01 +08:00
|
|
|
icon: "ph:certificate",
|
2025-04-10 10:34:10 +08:00
|
|
|
title: "商用证书托管",
|
2025-03-17 00:19:01 +08:00
|
|
|
group: pluginGroups.cert.key,
|
2025-04-10 10:34:10 +08:00
|
|
|
desc: "手动上传自定义证书后,自动部署(每次证书有更新,都需要手动上传一次)",
|
2025-03-17 00:19:01 +08:00
|
|
|
default: {
|
|
|
|
|
strategy: {
|
|
|
|
|
runStrategy: RunStrategy.AlwaysRun,
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-03-21 01:02:57 +08:00
|
|
|
shortcut: {
|
|
|
|
|
certUpdate: {
|
2025-03-21 12:23:59 +08:00
|
|
|
title: "更新证书",
|
2025-03-21 23:11:58 +08:00
|
|
|
icon: "ion:upload",
|
2025-03-21 01:02:57 +08:00
|
|
|
action: "onCertUpdate",
|
|
|
|
|
form: {
|
|
|
|
|
columns: {
|
|
|
|
|
crt: {
|
|
|
|
|
title: "证书",
|
2025-03-21 12:23:59 +08:00
|
|
|
type: "text",
|
2025-03-21 01:02:57 +08:00
|
|
|
form: {
|
|
|
|
|
component: {
|
|
|
|
|
name: "pem-input",
|
|
|
|
|
vModel: "modelValue",
|
|
|
|
|
textarea: {
|
2025-03-21 12:23:59 +08:00
|
|
|
rows: 4,
|
|
|
|
|
placeholder: "-----BEGIN CERTIFICATE-----\n...\n...\n-----END CERTIFICATE-----",
|
2025-03-21 01:02:57 +08:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
rules: [{ required: true, message: "此项必填" }],
|
|
|
|
|
col: { span: 24 },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
key: {
|
|
|
|
|
title: "私钥",
|
2025-03-21 12:23:59 +08:00
|
|
|
type: "text",
|
2025-03-21 01:02:57 +08:00
|
|
|
form: {
|
|
|
|
|
component: {
|
|
|
|
|
name: "pem-input",
|
|
|
|
|
vModel: "modelValue",
|
|
|
|
|
textarea: {
|
2025-03-21 12:23:59 +08:00
|
|
|
rows: 4,
|
|
|
|
|
placeholder: "-----BEGIN PRIVATE KEY-----\n...\n...\n-----END PRIVATE KEY----- ",
|
2025-03-21 01:02:57 +08:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
rules: [{ required: true, message: "此项必填" }],
|
|
|
|
|
col: { span: 24 },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-03-17 00:19:01 +08:00
|
|
|
})
|
2025-03-18 00:52:50 +08:00
|
|
|
export class CertApplyUploadPlugin extends CertApplyBaseConvertPlugin {
|
2025-09-24 14:14:19 +08:00
|
|
|
@TaskInput({
|
|
|
|
|
title: "过期前提醒",
|
|
|
|
|
value: 10,
|
|
|
|
|
component: {
|
|
|
|
|
name: "a-input-number",
|
|
|
|
|
vModel: "value",
|
|
|
|
|
},
|
|
|
|
|
required: true,
|
|
|
|
|
order: 100,
|
|
|
|
|
helper: "到期前多少天提醒",
|
|
|
|
|
})
|
|
|
|
|
renewDays!: number;
|
|
|
|
|
|
2025-03-17 00:19:01 +08:00
|
|
|
@TaskInput({
|
2025-03-21 23:11:58 +08:00
|
|
|
title: "手动上传证书",
|
2025-03-17 00:19:01 +08:00
|
|
|
component: {
|
2025-03-21 23:11:58 +08:00
|
|
|
name: "cert-info-updater",
|
2025-03-18 00:52:50 +08:00
|
|
|
vModel: "modelValue",
|
2025-03-17 00:19:01 +08:00
|
|
|
},
|
2025-03-21 23:11:58 +08:00
|
|
|
helper: "手动上传证书",
|
2025-03-18 00:52:50 +08:00
|
|
|
order: -9999,
|
2025-03-17 00:19:01 +08:00
|
|
|
required: true,
|
2025-03-18 00:52:50 +08:00
|
|
|
mergeScript: `
|
|
|
|
|
return {
|
|
|
|
|
component:{
|
|
|
|
|
on:{
|
2025-03-21 23:11:58 +08:00
|
|
|
updated(scope){
|
2025-03-18 00:52:50 +08:00
|
|
|
scope.form.input.domains = scope.$event?.domains
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
2025-03-17 00:19:01 +08:00
|
|
|
})
|
2025-03-22 02:06:02 +08:00
|
|
|
uploadCert!: CertInfo;
|
2025-03-17 00:19:01 +08:00
|
|
|
|
2025-03-18 00:52:50 +08:00
|
|
|
@TaskOutput({
|
|
|
|
|
title: "证书MD5",
|
2025-07-09 14:34:24 +08:00
|
|
|
type: "certMd5",
|
2025-03-18 00:52:50 +08:00
|
|
|
})
|
|
|
|
|
certMd5?: string;
|
|
|
|
|
|
2025-03-17 00:19:01 +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;
|
|
|
|
|
}
|
2025-09-24 14:14:19 +08:00
|
|
|
|
2025-03-17 00:19:01 +08:00
|
|
|
async onInit(): Promise<void> {}
|
|
|
|
|
|
2025-03-18 00:52:50 +08:00
|
|
|
async getCertFromStore() {
|
2025-07-28 16:18:49 +08:00
|
|
|
let certReader = null;
|
|
|
|
|
try {
|
|
|
|
|
this.logger.info("读取上次证书");
|
|
|
|
|
certReader = await this.readLastCert();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.logger.warn("读取cert失败:", e);
|
|
|
|
|
}
|
2025-03-17 00:19:01 +08:00
|
|
|
return certReader;
|
|
|
|
|
}
|
2025-03-18 00:52:50 +08:00
|
|
|
|
2025-09-24 14:14:19 +08:00
|
|
|
private checkExpires(certReader: CertReader) {
|
|
|
|
|
const renewDays = (this.renewDays ?? 10) * 24 * 60 * 60 * 1000;
|
|
|
|
|
if (certReader.expires) {
|
|
|
|
|
if (certReader.expires < new Date().getTime()) {
|
|
|
|
|
throw new Error("证书已过期,停止部署,请尽快上传新证书");
|
|
|
|
|
}
|
|
|
|
|
if (certReader.expires < new Date().getTime() + renewDays) {
|
|
|
|
|
throw new Error("证书即将已过期,停止部署,请尽快上传新证书");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-22 02:06:02 +08:00
|
|
|
|
2025-09-24 14:14:19 +08:00
|
|
|
async execute(): Promise<string | void> {
|
|
|
|
|
const oldCertReader = await this.getCertFromStore();
|
|
|
|
|
if (oldCertReader) {
|
|
|
|
|
const leftDays = dayjs(oldCertReader.expires).diff(dayjs(), "day");
|
|
|
|
|
this.logger.info(`证书过期时间${dayjs(oldCertReader.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${leftDays}天`);
|
|
|
|
|
this.checkExpires(oldCertReader);
|
|
|
|
|
if (!this.ctx.inputChanged) {
|
|
|
|
|
this.logger.info("输入参数无变化");
|
|
|
|
|
const lastCrtMd5 = this.lastStatus?.status?.output?.certMd5;
|
|
|
|
|
const newCrtMd5 = this.ctx.utils.hash.md5(this.uploadCert.crt);
|
|
|
|
|
this.logger.info("证书MD5", newCrtMd5);
|
|
|
|
|
this.logger.info("上次证书MD5", lastCrtMd5);
|
|
|
|
|
if (lastCrtMd5 === newCrtMd5) {
|
|
|
|
|
this.logger.info("证书无变化,跳过");
|
|
|
|
|
//输出证书MD5
|
|
|
|
|
this.certMd5 = newCrtMd5;
|
|
|
|
|
await this.output(oldCertReader, false);
|
|
|
|
|
return "skip";
|
|
|
|
|
}
|
|
|
|
|
this.logger.info("证书有变化,重新部署");
|
|
|
|
|
} else {
|
|
|
|
|
this.logger.info("输入参数有变化,重新部署");
|
2025-03-22 02:06:02 +08:00
|
|
|
}
|
2025-03-18 00:52:50 +08:00
|
|
|
}
|
2025-03-22 02:06:02 +08:00
|
|
|
|
2025-09-24 14:14:19 +08:00
|
|
|
const newCertReader = new CertReader(this.uploadCert);
|
2025-03-18 00:52:50 +08:00
|
|
|
this.clearLastStatus();
|
|
|
|
|
//输出证书MD5
|
2025-09-24 14:14:19 +08:00
|
|
|
this.certMd5 = this.ctx.utils.hash.md5(newCertReader.cert.crt);
|
|
|
|
|
const newLeftDays = dayjs(newCertReader.expires).diff(dayjs(), "day");
|
|
|
|
|
this.logger.info(`新证书过期时间${dayjs(newCertReader.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${newLeftDays}天`);
|
|
|
|
|
this.checkExpires(newCertReader);
|
|
|
|
|
await this.output(newCertReader, true);
|
2025-03-22 02:06:02 +08:00
|
|
|
|
|
|
|
|
//必须output之后执行
|
|
|
|
|
await this.emitCertApplySuccess();
|
2025-03-18 00:52:50 +08:00
|
|
|
return;
|
|
|
|
|
}
|
2025-03-21 01:02:57 +08:00
|
|
|
|
2025-03-21 12:23:59 +08:00
|
|
|
async onCertUpdate(data: any) {
|
2025-03-22 02:06:02 +08:00
|
|
|
const certReader = new CertReader(data);
|
2025-03-21 23:11:58 +08:00
|
|
|
return {
|
|
|
|
|
input: {
|
2025-03-22 02:06:02 +08:00
|
|
|
uploadCert: {
|
|
|
|
|
crt: data.crt,
|
|
|
|
|
key: data.key,
|
|
|
|
|
},
|
|
|
|
|
domains: certReader.getAllDomains(),
|
2025-03-21 23:11:58 +08:00
|
|
|
},
|
|
|
|
|
};
|
2025-03-21 12:23:59 +08:00
|
|
|
}
|
2025-03-17 00:19:01 +08:00
|
|
|
}
|
|
|
|
|
|
2025-03-18 00:52:50 +08:00
|
|
|
new CertApplyUploadPlugin();
|