perf: 支持pfx、der

pull/189/head
xiaojunnuo 2024-09-05 15:36:35 +08:00
parent ecad7f58c1
commit fbeaed2035
7 changed files with 169 additions and 26 deletions

View File

@ -17,6 +17,7 @@ export enum ContextScope {
export type TaskOutputDefine = {
title: string;
value?: any;
type?: string;
};
export type TaskInputDefine = FormItemProps;

View File

@ -51,7 +51,7 @@ export type SpawnOption = {
cmd: string | string[];
onStdout?: (data: string) => void;
onStderr?: (data: string) => void;
env: any;
env?: any;
logger?: ILogger;
options?: any;
};

View File

@ -0,0 +1,115 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, sp, TaskInput, TaskOutput } from "@certd/pipeline";
import type { CertInfo } from "../cert-plugin/acme.js";
import { CertReader, CertReaderHandleContext } from "../cert-plugin/cert-reader.js";
import path from "path";
import os from "os";
import fs from "fs";
export { CertReader };
export type { CertInfo };
@IsTaskPlugin({
name: "CertConvert",
title: "证书转换器",
group: pluginGroups.cert.key,
desc: "转换为pfx、der等证书格式",
default: {
input: {
renewDays: 20,
forceUpdate: false,
},
strategy: {
runStrategy: RunStrategy.AlwaysRun,
},
},
})
export class CertConvertPlugin extends AbstractTaskPlugin {
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "pi-output-selector",
from: "CertApply",
},
required: true,
})
cert!: CertInfo;
@TaskInput({
title: "PFX证书密码",
helper: "不填则没有密码",
component: {
name: "a-input-password",
vModel: "value",
},
required: true,
})
pfxPassword!: string;
@TaskOutput({
title: "pfx格式证书",
type: "PfxCert",
})
pfxCert?: string;
@TaskOutput({
title: "der格式证书",
type: "DerCert",
})
derCert?: string;
async onInit() {}
async execute(): Promise<void> {
const certReader = new CertReader(this.cert);
const handle = async (opts: CertReaderHandleContext) => {
// 调用openssl 转pfx
await this.convertPfx(opts);
// 转der
await this.convertDer(opts);
};
await certReader.readCertFile({ logger: this.logger, handle });
}
async exec(cmd: string) {
await sp.spawn({
cmd: cmd,
logger: this.logger,
});
}
private async convertPfx(opts: CertReaderHandleContext) {
const { reader, tmpCrtPath, tmpKeyPath } = opts;
const pfxPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", "cert.pfx");
let passwordArg = "-passout pass:";
if (this.pfxPassword) {
passwordArg = `-password pass:${this.pfxPassword}`;
}
await this.exec(`openssl pkcs12 -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`);
this.pfxCert = pfxPath;
const applyTime = new Date().getTime();
const filename = reader.buildCertFileName("pfx", applyTime);
const fileBuffer = fs.readFileSync(pfxPath);
this.saveFile(filename, fileBuffer);
}
private async convertDer(opts: CertReaderHandleContext) {
const { reader, tmpCrtPath } = opts;
const derPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.der`);
await this.exec(`openssl x509 -outform der -in ${tmpCrtPath} -out ${derPath}`);
this.derCert = derPath;
const applyTime = new Date().getTime();
const filename = reader.buildCertFileName("der", applyTime);
const fileBuffer = fs.readFileSync(derPath);
this.saveFile(filename, fileBuffer);
}
}
new CertConvertPlugin();

View File

@ -3,6 +3,11 @@ import fs from "fs";
import os from "os";
import path from "path";
import { crypto } from "@certd/acme-client";
import { ILogger } from "@certd/pipeline";
export type CertReaderHandleContext = { reader: CertReader; tmpCrtPath: string; tmpKeyPath: string };
export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise<void>;
export type HandleOpts = { logger: ILogger; handle: CertReaderHandle };
export class CertReader implements CertInfo {
crt: string;
key: string;
@ -28,7 +33,7 @@ export class CertReader implements CertInfo {
};
}
getCrtDetail(crt: string) {
getCrtDetail(crt: string = this.crt) {
const detail = crypto.readCertificateInfo(crt.toString());
const expires = detail.notAfter;
return { detail, expires };
@ -48,4 +53,31 @@ export class CertReader implements CertInfo {
fs.writeFileSync(filepath, this[type]);
return filepath;
}
async readCertFile(opts: HandleOpts) {
const logger = opts.logger;
logger.info("将证书写入本地缓存文件");
const tmpCrtPath = this.saveToFile("crt");
const tmpKeyPath = this.saveToFile("key");
logger.info("本地文件写入成功");
try {
await opts.handle({
reader: this,
tmpCrtPath: tmpCrtPath,
tmpKeyPath: tmpKeyPath,
});
} finally {
//删除临时文件
logger.info("删除临时文件");
fs.unlinkSync(tmpCrtPath);
fs.unlinkSync(tmpKeyPath);
}
}
buildCertFileName(suffix: string, applyTime: number, prefix = "cert") {
const detail = this.getCrtDetail();
let domain = detail.detail.domains.commonName;
domain = domain.replace(".", "_").replace("*", "_");
return `${prefix}_${domain}_${applyTime}.${suffix}`;
}
}

View File

@ -6,8 +6,8 @@ import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../.
import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
export { CertReader };
export type { CertInfo };
export * from "./cert-reader.js";
@IsTaskPlugin({
name: "CertApply",

View File

@ -1,2 +1,3 @@
export * from "./cert-plugin/index.js";
export * from "./cert-plugin/lego/index.js";
export * from "./cert-convert/index.js";

View File

@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { SshClient } from '../../lib/ssh.js';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import { CertInfo, CertReader, CertReaderHandleContext } from '@certd/plugin-cert';
import * as fs from 'fs';
import { SshAccess } from '../../access/index.js';
@ -99,15 +99,13 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
async execute(): Promise<void> {
const { crtPath, keyPath, cert, accessId } = this;
const certReader = new CertReader(cert);
this.logger.info('将证书写入本地缓存文件');
const saveCrtPath = certReader.saveToFile('crt');
const saveKeyPath = certReader.saveToFile('key');
this.logger.info('本地文件写入成功');
try {
const handle = async (opts: CertReaderHandleContext) => {
const { tmpCrtPath, tmpKeyPath } = opts;
if (this.copyToThisHost) {
this.logger.info('复制到目标路径');
this.copyFile(saveCrtPath, crtPath);
this.copyFile(saveKeyPath, keyPath);
this.copyFile(tmpCrtPath, crtPath);
this.copyFile(tmpKeyPath, keyPath);
this.logger.info('证书复制成功crtPath=', crtPath, ',keyPath=', keyPath);
} else {
if (!accessId) {
@ -120,31 +118,27 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
connectConf,
transports: [
{
localPath: saveCrtPath,
localPath: tmpCrtPath,
remotePath: crtPath,
},
{
localPath: saveKeyPath,
localPath: tmpKeyPath,
remotePath: keyPath,
},
],
mkdirs: this.mkdirs,
});
this.logger.info('证书上传成功crtPath=', crtPath, ',keyPath=', keyPath);
//输出
this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath;
}
} catch (e) {
this.logger.error(`上传失败:${e.message}`);
throw e;
} finally {
//删除临时文件
this.logger.info('删除临时文件');
fs.unlinkSync(saveCrtPath);
fs.unlinkSync(saveKeyPath);
}
this.logger.info('执行完成');
//输出
this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath;
};
await certReader.readCertFile({
logger: this.logger,
handle,
});
}
}