From fbeaed203519f59b6d9396c4e8953353ccb5e723 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Thu, 5 Sep 2024 15:36:35 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81pfx=E3=80=81der?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/pipeline/src/plugin/api.ts | 1 + packages/core/pipeline/src/utils/util.sp.ts | 2 +- .../src/plugin/cert-convert/index.ts | 115 ++++++++++++++++++ .../src/plugin/cert-plugin/cert-reader.ts | 34 +++++- .../src/plugin/cert-plugin/index.ts | 2 +- .../plugins/plugin-cert/src/plugin/index.ts | 1 + .../plugin/upload-to-host/index.ts | 40 +++--- 7 files changed, 169 insertions(+), 26 deletions(-) create mode 100644 packages/plugins/plugin-cert/src/plugin/cert-convert/index.ts diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index 71ff2b3b..559f0c54 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -17,6 +17,7 @@ export enum ContextScope { export type TaskOutputDefine = { title: string; value?: any; + type?: string; }; export type TaskInputDefine = FormItemProps; diff --git a/packages/core/pipeline/src/utils/util.sp.ts b/packages/core/pipeline/src/utils/util.sp.ts index 9d32eaa9..4a434a30 100644 --- a/packages/core/pipeline/src/utils/util.sp.ts +++ b/packages/core/pipeline/src/utils/util.sp.ts @@ -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; }; diff --git a/packages/plugins/plugin-cert/src/plugin/cert-convert/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-convert/index.ts new file mode 100644 index 00000000..5b848b2a --- /dev/null +++ b/packages/plugins/plugin-cert/src/plugin/cert-convert/index.ts @@ -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 { + 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(); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts index babe3af4..8a4271ab 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts @@ -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; +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}`; + } } diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts index c8eec49b..d2c0ebea 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -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", diff --git a/packages/plugins/plugin-cert/src/plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/index.ts index aaafb350..f6de18c9 100644 --- a/packages/plugins/plugin-cert/src/plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/index.ts @@ -1,2 +1,3 @@ export * from "./cert-plugin/index.js"; export * from "./cert-plugin/lego/index.js"; +export * from "./cert-convert/index.js"; diff --git a/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts b/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts index d0dc7761..c42bf5ee 100644 --- a/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-host/plugin/upload-to-host/index.ts @@ -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 { 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, + }); } }