diff --git a/docs/guide/install/1panel/index.md b/docs/guide/install/1panel/index.md index 8472eb07..c35111cf 100644 --- a/docs/guide/install/1panel/index.md +++ b/docs/guide/install/1panel/index.md @@ -48,4 +48,4 @@ admin/123456 ## 五、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 +将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置,重启certd即可 diff --git a/docs/guide/install/baota/index.md b/docs/guide/install/baota/index.md index 3ee002c4..d1028c13 100644 --- a/docs/guide/install/baota/index.md +++ b/docs/guide/install/baota/index.md @@ -81,4 +81,4 @@ services: ## 五、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 +将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置,重启certd即可 diff --git a/docs/guide/install/docker/index.md b/docs/guide/install/docker/index.md index 8c17707d..bad32240 100644 --- a/docs/guide/install/docker/index.md +++ b/docs/guide/install/docker/index.md @@ -71,4 +71,4 @@ docker compose up -d ## 四、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 \ No newline at end of file +将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置,重启certd即可 \ No newline at end of file diff --git a/docs/guide/install/source/index.md b/docs/guide/install/source/index.md index 0114ee92..380a0d4c 100644 --- a/docs/guide/install/source/index.md +++ b/docs/guide/install/source/index.md @@ -1,6 +1,9 @@ # 源码部署 - +不推荐 ## 一、源码安装 + +### 环境要求 +- nodejs 20 及以上 ### 源码启动 ```shell # 克隆代码 @@ -42,4 +45,4 @@ kill -9 $(lsof -t -i:7001) ## 四、备份恢复 -将备份的`db.sqlite`覆盖到原来的位置,重启certd即可 +将备份的`db.sqlite`及同目录下的其他文件覆盖到原来的位置,重启certd即可 diff --git a/docs/guide/use/host/windows.md b/docs/guide/use/host/windows.md index 4f2dc71c..5a173ac1 100644 --- a/docs/guide/use/host/windows.md +++ b/docs/guide/use/host/windows.md @@ -25,3 +25,15 @@ win+R 弹出运行对话框,输入 services.msc 打开服务管理器 C:\Users\xxxxx> ↑↑↑↑---------这个就是windows ssh的登录用户名 ``` + +### 4. 切换默认shell终端 +安装openssh后,默认终端是cmd,建议切换成powershell +```shell +# powershell中执行如下命令切换 +# 设置默认shell为powershell 【推荐】 +New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force + +# 恢复默认shell为cmd 【不推荐】 +New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe" -PropertyType String -Force + +``` \ No newline at end of file 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 d367c660..670dda23 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts @@ -4,7 +4,6 @@ import type { CertInfo } from "./acme.js"; import { CertReader } from "./cert-reader.js"; import JSZip from "jszip"; import { CertConverter } from "./convert.js"; -import fs from "fs"; import { pick } from "lodash-es"; export { CertReader }; @@ -59,6 +58,19 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { }) pfxPassword!: string; + @TaskInput({ + title: "PFX证书转换参数", + value: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES", + component: { + name: "a-input", + vModel: "value", + }, + required: false, + order: 100, + helper: "兼容Server 2016,如果导入证书失败,请删除此参数", + }) + pfxArgs = "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES"; + @TaskInput({ title: "更新天数", value: 35, @@ -143,23 +155,18 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { const res = await converter.convert({ cert, pfxPassword: this.pfxPassword, + pfxArgs: this.pfxArgs, }); - if (cert.pfx == null && res.pfxPath) { - const pfxBuffer = fs.readFileSync(res.pfxPath); - cert.pfx = pfxBuffer.toString("base64"); - fs.unlinkSync(res.pfxPath); + if (cert.pfx == null && res.pfx) { + cert.pfx = res.pfx; } - if (cert.der == null && res.derPath) { - const derBuffer = fs.readFileSync(res.derPath); - cert.der = derBuffer.toString("base64"); - fs.unlinkSync(res.derPath); + if (cert.der == null && res.der) { + cert.der = res.der; } - if (cert.jks == null && res.jksPath) { - const jksBuffer = fs.readFileSync(res.jksPath); - cert.jks = jksBuffer.toString("base64"); - fs.unlinkSync(res.jksPath); + if (cert.jks == null && res.jks) { + cert.jks = res.jks; } this.logger.info("转换证书格式成功"); 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 9ba4b172..59fec30c 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/convert.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/convert.ts @@ -14,31 +14,31 @@ export class CertConverter { constructor(opts: { logger: ILogger }) { this.logger = opts.logger; } - async convert(opts: { cert: CertInfo; pfxPassword: string }): Promise<{ - pfxPath: string; - derPath: string; - jksPath: string; + async convert(opts: { cert: CertInfo; pfxPassword: string; pfxArgs: string }): Promise<{ + pfx: string; + der: string; + jks: string; }> { const certReader = new CertReader(opts.cert); - let pfxPath: string; - let derPath: string; - let jksPath: string; + let pfx: string; + let der: string; + let jks: string; const handle = async (ctx: CertReaderHandleContext) => { // 调用openssl 转pfx - pfxPath = await this.convertPfx(ctx, opts.pfxPassword); + pfx = await this.convertPfx(ctx, opts.pfxPassword, opts.pfxArgs); // 转der - derPath = await this.convertDer(ctx); + der = await this.convertDer(ctx); - jksPath = await this.convertJks(ctx, opts.pfxPassword); + jks = await this.convertJks(ctx, opts.pfxPassword); }; await certReader.readCertFile({ logger: this.logger, handle }); return { - pfxPath, - derPath, - jksPath, + pfx, + der, + jks, }; } @@ -50,7 +50,7 @@ export class CertConverter { }); } - private async convertPfx(opts: CertReaderHandleContext, pfxPassword: string) { + private async convertPfx(opts: CertReaderHandleContext, pfxPassword: string, pfxArgs: string) { const { tmpCrtPath, tmpKeyPath } = opts; const pfxPath = path.join(os.tmpdir(), "/certd/tmp/", Math.floor(Math.random() * 1000000) + "_cert.pfx"); @@ -65,12 +65,14 @@ export class CertConverter { passwordArg = `-password pass:${pfxPassword}`; } // 兼容server 2016,旧版本不能用sha256 - const oldPfxCmd = `openssl pkcs12 -macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`; + const oldPfxCmd = `openssl pkcs12 ${pfxArgs} -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`; // const newPfx = `openssl pkcs12 -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`; await this.exec(oldPfxCmd); - return pfxPath; - // const fileBuffer = fs.readFileSync(pfxPath); - // this.pfxCert = fileBuffer.toString("base64"); + const fileBuffer = fs.readFileSync(pfxPath); + const pfxCert = fileBuffer.toString("base64"); + fs.unlinkSync(pfxPath); + return pfxCert; + // // const applyTime = new Date().getTime(); // const filename = reader.buildCertFileName("pfx", applyTime); @@ -87,15 +89,10 @@ export class CertConverter { } await this.exec(`openssl x509 -outform der -in ${tmpCrtPath} -out ${derPath}`); - - return derPath; - - // const fileBuffer = fs.readFileSync(derPath); - // this.derCert = fileBuffer.toString("base64"); - // - // const applyTime = new Date().getTime(); - // const filename = reader.buildCertFileName("der", applyTime); - // this.saveFile(filename, fileBuffer); + const fileBuffer = fs.readFileSync(derPath); + const derCert = fileBuffer.toString("base64"); + fs.unlinkSync(derPath); + return derCert; } async convertJks(opts: CertReaderHandleContext, pfxPassword = "") { @@ -120,7 +117,11 @@ export class CertConverter { `keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" ` ); fs.unlinkSync(p12Path); - return jksPath; + + const fileBuffer = fs.readFileSync(jksPath); + const certBase64 = fileBuffer.toString("base64"); + fs.unlinkSync(jksPath); + return certBase64; } catch (e) { this.logger.error("转换jks失败", e); return; diff --git a/packages/plugins/plugin-lib/src/ssh/ssh.ts b/packages/plugins/plugin-lib/src/ssh/ssh.ts index 59cfba09..4a1ac081 100644 --- a/packages/plugins/plugin-lib/src/ssh/ssh.ts +++ b/packages/plugins/plugin-lib/src/ssh/ssh.ts @@ -25,7 +25,7 @@ export class AsyncSsh2Client { if (this.encoding) { return iconv.decode(buffer, this.encoding); } - return buffer.toString(); + return buffer.toString().replaceAll("\r\n", "\n"); } async connect() { @@ -95,7 +95,12 @@ export class AsyncSsh2Client { }); } - async exec(script: string) { + async exec( + script: string, + opts: { + throwOnStdErr?: boolean; + } = {} + ): Promise { if (!script) { this.logger.info("script 为空,取消执行"); return; @@ -114,9 +119,17 @@ export class AsyncSsh2Client { return; } let data = ""; + let hasErrorLog = false; stream .on("close", (code: any, signal: any) => { this.logger.info(`[${this.connConf.host}][close]:code:${code}`); + if (opts.throwOnStdErr == null && this.windows) { + opts.throwOnStdErr = true; + } + if (opts.throwOnStdErr && hasErrorLog) { + reject(new Error(data)); + } + if (code === 0) { resolve(data); } else { @@ -135,13 +148,14 @@ export class AsyncSsh2Client { .stderr.on("data", (ret: Buffer) => { const err = this.convert(iconv, ret); data += err; - this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd()); + hasErrorLog = true; + this.logger.error(`[${this.connConf.host}][error]: ` + err.trimEnd()); }); }); }); } - async shell(script: string | string[]): Promise { + async shell(script: string | string[]): Promise { return new Promise((resolve, reject) => { this.logger.info(`执行shell脚本:[${this.connConf.host}][shell]: ` + script); this.conn.shell((err: Error, stream: any) => { @@ -149,11 +163,11 @@ export class AsyncSsh2Client { reject(err); return; } - const output: string[] = []; + let output = ""; function ansiHandle(data: string) { - data = data.replace(/\[[0-9]+;1H/g, "\n"); + data = data.replace(/\[[0-9]+;1H/g, ""); data = stripAnsi(data); - return data; + return data.replaceAll("\r\n", "\n"); } stream .on("close", (code: any) => { @@ -163,7 +177,7 @@ export class AsyncSsh2Client { .on("data", (ret: Buffer) => { const data = ansiHandle(ret.toString()); this.logger.info(data); - output.push(data); + output += data; }) .on("error", (err: any) => { reject(err); @@ -171,8 +185,8 @@ export class AsyncSsh2Client { }) .stderr.on("data", (ret: Buffer) => { const data = ansiHandle(ret.toString()); - output.push(data); - this.logger.info(`[${this.connConf.host}][error]: ` + data); + output += data; + this.logger.error(`[${this.connConf.host}][error]: ` + data); }); //保证windows下正常退出 const exit = "\r\nexit\r\n"; @@ -269,7 +283,7 @@ export class SshClient { async getIsCmd(options: { connectConf: SshAccess }) { const { connectConf } = options; - return await this._call({ + return await this._call({ connectConf, callable: async (conn: AsyncSsh2Client) => { return await this.isCmd(conn); @@ -285,7 +299,7 @@ export class SshClient { * Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe" * @param options */ - async exec(options: { connectConf: SshAccess; script: string | Array; env?: any }): Promise { + async exec(options: { connectConf: SshAccess; script: string | Array; env?: any }): Promise { let { script } = options; const { connectConf } = options; @@ -337,7 +351,7 @@ export class SshClient { }); } - async shell(options: { connectConf: SshAccess; script: string | Array }): Promise { + async shell(options: { connectConf: SshAccess; script: string | Array }): Promise { let { script } = options; const { connectConf } = options; if (_.isArray(script)) { @@ -361,7 +375,7 @@ export class SshClient { }); } - async _call(options: { connectConf: SshAccess; callable: any }): Promise { + async _call(options: { connectConf: SshAccess; callable: (conn: AsyncSsh2Client) => Promise }): Promise { const { connectConf, callable } = options; const conn = new AsyncSsh2Client(connectConf, this.logger); try { diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue index 98facc70..302cfba1 100644 --- a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-view/index.vue @@ -8,7 +8,7 @@ -
+
@@ -84,11 +84,14 @@ export default { return node.logs.value.length; }, async () => { - let el = document.querySelector(`.pi-task-view-logs.${node.node.id}`); + let el = document.querySelector(`.pi-task-view-logs.id-${node.node.id}`); + if (!el) { + return; + } //判断当前是否在底部 const isBottom = el ? el.scrollHeight - el.scrollTop === el.clientHeight : true; await nextTick(); - el = document.querySelector(`.pi-task-view-logs.${node.node.id}`); + el = document.querySelector(`.pi-task-view-logs.id-${node.node.id}`); //如果在底部则滚动到底部 if (isBottom && el) { el?.scrollTo({ diff --git a/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-db-backup.ts b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-db-backup.ts index c231372b..76ba8d96 100644 --- a/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-db-backup.ts +++ b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-db-backup.ts @@ -79,6 +79,18 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin { }) filePrefix: string = defaultFilePrefix; + @TaskInput({ + title: '附加上传文件', + value: true, + component: { + name: 'a-switch', + vModel: 'checked', + placeholder: `是否备份上传的头像等文件`, + }, + required: false, + }) + withUpload = true; + @TaskInput({ title: '删除过期备份', component: { @@ -101,7 +113,6 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin { this.logger.error('数据库文件不存在:', dbPath); return; } - const dbTmpFilename = `${this.filePrefix}.${dayjs().format('YYYYMMDD.HHmmss')}.sqlite`; const dbZipFilename = `${dbTmpFilename}.zip`; const tempDir = path.resolve(os.tmpdir(), 'certd_backup'); @@ -118,6 +129,12 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin { const stream = fs.createReadStream(dbTmpPath); // 使用流的方式添加文件内容 zip.file(dbTmpFilename, stream, { binary: true, compression: 'DEFLATE' }); + + const uploadDir = path.resolve('data', 'upload'); + if (this.withUpload && fs.existsSync(uploadDir)) { + zip.folder(uploadDir); + } + const content = await zip.generateAsync({ type: 'nodebuffer' }); await fs.promises.writeFile(dbZipPath, content);