From 385757b54b2103cb7521465952a59baae4f90899 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Wed, 30 Oct 2024 01:44:02 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E8=AF=81=E4=B9=A6=E6=94=AF=E6=8C=81jk?= =?UTF-8?q?s=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide/use/host/windows.md | 5 +- docs/index.md | 2 +- packages/core/basic/src/utils/util.sp.ts | 52 +++++++++++++------ .../src/plugin/cert-plugin/acme.ts | 2 +- .../src/plugin/cert-plugin/base.ts | 17 +++--- .../src/plugin/cert-plugin/cert-reader.ts | 8 ++- .../src/plugin/cert-plugin/convert.ts | 46 +++++++++------- packages/ui/Dockerfile | 1 + .../plugin-host/plugin/copy-to-local/index.ts | 26 +++++----- .../plugin/upload-to-host/index.ts | 24 ++++----- 10 files changed, 111 insertions(+), 72 deletions(-) diff --git a/docs/guide/use/host/windows.md b/docs/guide/use/host/windows.md index 301fb2c5..4f2dc71c 100644 --- a/docs/guide/use/host/windows.md +++ b/docs/guide/use/host/windows.md @@ -4,7 +4,10 @@ ## windows开启OpenSSH Server ### 1. 安装OpenSSH Server -请前往Microsoft官方文档查看如何开启openSSH + +* 下载安装包安装: https://github.com/PowerShell/Win32-OpenSSH/releases OpenSSH-Win64-vxx.xx.x.msi + +* 前往Microsoft官方文档查看如何开启openSSH,以及其他设置 https://learn.microsoft.com/zh-cn/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui#install-openssh-for-windows ### 2. 启动OpenSSH Server服务 diff --git a/docs/index.md b/docs/index.md index e3afc152..f5d42b18 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,7 +28,7 @@ features: - title: 多域名、泛域名打到一个证书上 details: 支持通配符域名/泛域名,支持多个域名打到一个证书上 - title: 多证书格式支持 - details: 支持pem、pfx、der、p12等多种证书格式,支持Google、Letsencrypt、ZeroSSL证书颁发机构 + details: 支持pem、pfx、der、jks等多种证书格式,支持Google、Letsencrypt、ZeroSSL证书颁发机构 - title: 支持私有化部署 details: 保障数据安全 - title: 多数据库支持 diff --git a/packages/core/basic/src/utils/util.sp.ts b/packages/core/basic/src/utils/util.sp.ts index 09cef230..1ff9cfc5 100644 --- a/packages/core/basic/src/utils/util.sp.ts +++ b/packages/core/basic/src/utils/util.sp.ts @@ -1,8 +1,8 @@ //转换为import -import childProcess from "child_process"; -import { safePromise } from "./util.promise.js"; -import { ILogger, logger } from "./util.log.js"; - +import childProcess from 'child_process'; +import { safePromise } from './util.promise.js'; +import { ILogger, logger } from './util.log.js'; +import iconv from 'iconv-lite'; export type ExecOption = { cmd: string | string[]; env: any; @@ -11,12 +11,12 @@ export type ExecOption = { }; async function exec(opts: ExecOption): Promise { - let cmd = ""; + let cmd = ''; const log = opts.logger || logger; if (opts.cmd instanceof Array) { for (const item of opts.cmd) { if (cmd) { - cmd += " && " + item; + cmd += ' && ' + item; } else { cmd = item; } @@ -38,7 +38,7 @@ async function exec(opts: ExecOption): Promise { log.error(`exec error: ${error}`); reject(error); } else { - const res = stdout.toString("utf-8"); + const res = stdout.toString('utf-8'); log.info(`stdout: ${res}`); resolve(res); } @@ -55,13 +55,31 @@ export type SpawnOption = { logger?: ILogger; options?: any; }; + +function isWindows() { + return process.platform === 'win32'; +} +function convert(buffer: any) { + if (isWindows()) { + const decoded = iconv.decode(buffer, 'GBK'); + // 检查是否有有效字符 + return decoded && decoded.trim().length > 0 ? decoded : buffer.toString(); + } else { + return buffer; + } +} + +// function convert(buffer: any) { +// return buffer; +// } + async function spawn(opts: SpawnOption): Promise { - let cmd = ""; + let cmd = ''; const log = opts.logger || logger; if (opts.cmd instanceof Array) { for (const item of opts.cmd) { if (cmd) { - cmd += " && " + item; + cmd += ' && ' + item; } else { cmd = item; } @@ -70,8 +88,8 @@ async function spawn(opts: SpawnOption): Promise { cmd = opts.cmd; } log.info(`执行命令: ${cmd}`); - let stdout = ""; - let stderr = ""; + let stdout = ''; + let stderr = ''; return safePromise((resolve, reject) => { const ls = childProcess.spawn(cmd, { shell: true, @@ -81,21 +99,23 @@ async function spawn(opts: SpawnOption): Promise { }, ...opts.options, }); - ls.stdout.on("data", (data) => { + ls.stdout.on('data', data => { + data = convert(data); log.info(`stdout: ${data}`); stdout += data; }); - ls.stderr.on("data", (data) => { - log.error(`stderr: ${data}`); + ls.stderr.on('data', data => { + data = convert(data); + log.warn(`stderr: ${data}`); stderr += data; }); - ls.on("error", (error) => { + ls.on('error', error => { log.error(`child process error: ${error}`); reject(error); }); - ls.on("close", (code: number) => { + ls.on('close', (code: number) => { if (code !== 0) { log.error(`child process exited with code ${code}`); reject(new Error(stderr)); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts index cbdcf0d5..a1e87ff2 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -30,7 +30,7 @@ export type CertInfo = { ic?: string; pfx?: string; der?: string; - p12?: string; + jks?: string; }; export type SSLProvider = "letsencrypt" | "google" | "zerossl"; export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521"; diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts index 8c6cd213..ece87a32 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts @@ -55,7 +55,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { }, required: false, order: 100, - helper: "PFX、P12格式证书是否需要加密", + helper: "PFX、jks格式证书是否加密;jks必须设置密码,不传则默认123456", }) pfxPassword!: string; @@ -150,7 +150,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { } this._result.pipelinePrivateVars.cert = cert; - if (cert.pfx == null || cert.der == null || cert.p12 == null) { + if (cert.pfx == null || cert.der == null || cert.jks == null) { try { const converter = new CertConverter({ logger: this.logger }); const res = await converter.convert({ @@ -160,16 +160,19 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { if (res.pfxPath) { const pfxBuffer = fs.readFileSync(res.pfxPath); cert.pfx = pfxBuffer.toString("base64"); + fs.unlinkSync(res.pfxPath); } if (res.derPath) { const derBuffer = fs.readFileSync(res.derPath); cert.der = derBuffer.toString("base64"); + fs.unlinkSync(res.derPath); } - if (res.p12Path) { - const p12Buffer = fs.readFileSync(res.p12Path); - cert.p12 = p12Buffer.toString("base64"); + if (res.jksPath) { + const jksBuffer = fs.readFileSync(res.jksPath); + cert.jks = jksBuffer.toString("base64"); + fs.unlinkSync(res.jksPath); } this.logger.info("转换证书格式成功"); @@ -202,8 +205,8 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { if (cert.der) { zip.file("cert.der", Buffer.from(cert.der, "base64")); } - if (cert.p12) { - zip.file("cert.p12", Buffer.from(cert.p12, "base64")); + if (cert.jks) { + zip.file("cert.jks", Buffer.from(cert.jks, "base64")); } const content = await zip.generateAsync({ type: "nodebuffer" }); this.saveFile(filename, content); 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 d16f6550..af9ee4cf 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 @@ -13,6 +13,7 @@ export type CertReaderHandleContext = { tmpPfxPath?: string; tmpDerPath?: string; tmpIcPath?: string; + tmpJksPath?: string; }; export type CertReaderHandle = (ctx: CertReaderHandleContext) => Promise; export type HandleOpts = { logger: ILogger; handle: CertReaderHandle }; @@ -72,14 +73,14 @@ export class CertReader { return domains; } - saveToFile(type: "crt" | "key" | "pfx" | "der" | "ic", filepath?: string) { + saveToFile(type: "crt" | "key" | "pfx" | "der" | "ic" | "jks", filepath?: string) { if (!this.cert[type]) { return; } if (filepath == null) { //写入临时目录 - filepath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.${type}`); + filepath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.${type}`); } const dir = path.dirname(filepath); @@ -103,6 +104,7 @@ export class CertReader { const tmpIcPath = this.saveToFile("ic"); logger.info("本地文件写入成功"); const tmpDerPath = this.saveToFile("der"); + const tmpJksPath = this.saveToFile("jks"); try { return await opts.handle({ reader: this, @@ -111,6 +113,7 @@ export class CertReader { tmpPfxPath: tmpPfxPath, tmpDerPath: tmpDerPath, tmpIcPath: tmpIcPath, + tmpJksPath: tmpJksPath, }); } catch (err) { throw err; @@ -127,6 +130,7 @@ export class CertReader { removeFile(tmpPfxPath); removeFile(tmpDerPath); removeFile(tmpIcPath); + removeFile(tmpJksPath); } } diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/convert.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/convert.ts index 77fb1944..79d62cfe 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/convert.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/convert.ts @@ -17,12 +17,12 @@ export class CertConverter { async convert(opts: { cert: CertInfo; pfxPassword: string }): Promise<{ pfxPath: string; derPath: string; - p12Path: string; + jksPath: string; }> { const certReader = new CertReader(opts.cert); let pfxPath: string; let derPath: string; - let p12Path: string; + let jksPath: string; const handle = async (ctx: CertReaderHandleContext) => { // 调用openssl 转pfx pfxPath = await this.convertPfx(ctx, opts.pfxPassword); @@ -30,7 +30,7 @@ export class CertConverter { // 转der derPath = await this.convertDer(ctx); - p12Path = await this.convertP12(ctx, opts.pfxPassword); + jksPath = await this.convertJks(ctx, pfxPath, opts.pfxPassword); }; await certReader.readCertFile({ logger: this.logger, handle }); @@ -38,11 +38,12 @@ export class CertConverter { return { pfxPath, derPath, - p12Path, + jksPath, }; } async exec(cmd: string) { + process.env.LANG = "zh_CN.GBK"; await sp.spawn({ cmd: cmd, logger: this.logger, @@ -52,7 +53,7 @@ export class CertConverter { private async convertPfx(opts: CertReaderHandleContext, pfxPassword: string) { const { tmpCrtPath, tmpKeyPath } = opts; - const pfxPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", "cert.pfx"); + const pfxPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "_cert.pfx"); const dir = path.dirname(pfxPath); if (!fs.existsSync(dir)) { @@ -75,7 +76,7 @@ export class CertConverter { private async convertDer(opts: CertReaderHandleContext) { const { tmpCrtPath } = opts; - const derPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.der`); + const derPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + `_cert.der`); const dir = path.dirname(derPath); if (!fs.existsSync(dir)) { @@ -94,21 +95,28 @@ export class CertConverter { // this.saveFile(filename, fileBuffer); } - async convertP12(opts: CertReaderHandleContext, pfxPassword: string) { - const { tmpCrtPath, tmpKeyPath } = opts; - const p12Path = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "", `cert.p12`); - - const dir = path.dirname(p12Path); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } + async convertJks(opts: CertReaderHandleContext, pfxPath: string, pfxPassword = "") { + const jksPassword = pfxPassword || "123456"; try { - let passwordArg = "-passout pass:"; - if (pfxPassword) { - passwordArg = `-password pass:${pfxPassword}`; + const randomStr = Math.floor(Math.random() * 1000000) + ""; + + // const p12Path = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.p12`); + // const { tmpCrtPath, tmpKeyPath } = opts; + // let passwordArg = "-passout pass:"; + // if (pfxPassword) { + // passwordArg = `-password pass:${pfxPassword}`; + // } + // await this.exec(`openssl pkcs12 -export -in ${tmpCrtPath} -inkey ${tmpKeyPath} -out ${p12Path} -name certd ${passwordArg}`); + + const jksPath = path.join(os.tmpdir(), "/certd/tmp/", randomStr + `_cert.jks`); + const dir = path.dirname(jksPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); } - await this.exec(`openssl pkcs12 -export -in ${tmpCrtPath} -inkey ${tmpKeyPath} -out ${p12Path} -name certd ${passwordArg}`); - return p12Path; + await this.exec( + `keytool -importkeystore -srckeystore ${pfxPath} -srcstoretype PKCS12 -srcstorepass "${pfxPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" ` + ); + return jksPath; } catch (e) { this.logger.error("转换jks失败", e); return; diff --git a/packages/ui/Dockerfile b/packages/ui/Dockerfile index 8271f4f5..78240fee 100644 --- a/packages/ui/Dockerfile +++ b/packages/ui/Dockerfile @@ -13,6 +13,7 @@ RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker FROM node:18-alpine RUN apk add --no-cache openssl +RUN apk add --no-cache openjdk WORKDIR /app/ COPY --from=builder /workspace/certd-server/ /app/ RUN chmod +x /app/tools/linux/* diff --git a/packages/ui/certd-server/src/plugins/plugin-host/plugin/copy-to-local/index.ts b/packages/ui/certd-server/src/plugins/plugin-host/plugin/copy-to-local/index.ts index ff29c05d..7a8bb3b1 100644 --- a/packages/ui/certd-server/src/plugins/plugin-host/plugin/copy-to-local/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-host/plugin/copy-to-local/index.ts @@ -68,14 +68,14 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin { derPath!: string; @TaskInput({ - title: 'p12证书保存路径', - helper: '用于java,路径要包含文件名,例如:tmp/cert.p12', + title: 'jks证书保存路径', + helper: '用于java,路径要包含文件名,例如:tmp/cert.jks', component: { - placeholder: 'tmp/cert.p12', + placeholder: 'tmp/cert.jks', }, rules: [{ type: 'filepath' }], }) - p12Path!: string; + jksPath!: string; @TaskInput({ title: '域名证书', @@ -119,10 +119,10 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin { hostDerPath!: string; @TaskOutput({ - title: 'P12保存路径', - type: 'HostP12Path', + title: 'jks保存路径', + type: 'HostJksPath', }) - hostP12Path!: string; + hostJksPath!: string; async onInstance() {} @@ -139,10 +139,10 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin { throw new Error('只有管理员才能运行此任务'); } - let { crtPath, keyPath, icPath, pfxPath, derPath, p12Path } = this; + let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath } = this; const certReader = new CertReader(this.cert); - const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath, tmpIcPath, tmpP12Path }) => { + const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath, tmpIcPath, tmpJksPath }) => { this.logger.info('复制到目标路径'); if (crtPath) { crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath); @@ -169,10 +169,10 @@ export class CopyCertToLocalPlugin extends AbstractTaskPlugin { this.copyFile(tmpDerPath, derPath); this.hostDerPath = derPath; } - if (p12Path) { - p12Path = p12Path.startsWith('/') ? p12Path : path.join(Constants.dataDir, p12Path); - this.copyFile(tmpP12Path, p12Path); - this.hostP12Path = p12Path; + if (jksPath) { + jksPath = jksPath.startsWith('/') ? jksPath : path.join(Constants.dataDir, jksPath); + this.copyFile(tmpJksPath, jksPath); + this.hostJksPath = jksPath; } this.logger.info('请注意,如果使用的是相对路径,那么文件就在你的数据库同级目录下,默认是/data/certd/下面'); this.logger.info( 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 784bb02d..0f9e8b0f 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 @@ -68,14 +68,14 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin { derPath!: string; @TaskInput({ - title: 'p12证书保存路径', - helper: '需要有写入权限,路径要包含证书文件名,例如:/tmp/cert.p12', + title: 'jks证书保存路径', + helper: '需要有写入权限,路径要包含证书文件名,例如:/tmp/cert.jks', component: { - placeholder: '/root/deploy/nginx/cert.p12', + placeholder: '/root/deploy/nginx/cert.jks', }, rules: [{ type: 'filepath' }], }) - p12Path!: string; + jksPath!: string; @TaskInput({ title: '域名证书', @@ -158,9 +158,9 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin { }) hostDerPath!: string; @TaskOutput({ - title: 'P12保存路径', + title: 'jks保存路径', }) - hostP12Path!: string; + hostJksPath!: string; async onInstance() {} @@ -181,7 +181,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin { const certReader = new CertReader(cert); const handle = async (opts: CertReaderHandleContext) => { - const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpP12Path, tmpPfxPath, tmpIcPath } = opts; + const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpJksPath, tmpPfxPath, tmpIcPath } = opts; // if (this.copyToThisHost) { // this.logger.info('复制到目标路径'); // this.copyFile(tmpCrtPath, crtPath); @@ -241,12 +241,12 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin { }); this.logger.info(`上传DER证书到主机:${this.derPath}`); } - if (this.p12Path) { + if (this.jksPath) { transports.push({ - localPath: tmpP12Path, - remotePath: this.p12Path, + localPath: tmpJksPath, + remotePath: this.jksPath, }); - this.logger.info(`上传p12证书到主机:${this.p12Path}`); + this.logger.info(`上传jks证书到主机:${this.jksPath}`); } this.logger.info('开始上传文件到服务器'); await sshClient.uploadFiles({ @@ -261,7 +261,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin { this.hostIcPath = this.icPath; this.hostPfxPath = this.pfxPath; this.hostDerPath = this.derPath; - this.hostP12Path = this.p12Path; + this.hostJksPath = this.jksPath; }; await certReader.readCertFile({