perf: 部署到IIS插件

pull/265/head
xiaojunnuo 2024-11-30 17:36:47 +08:00
parent aedc462135
commit 1534f45236
10 changed files with 121 additions and 64 deletions

View File

@ -48,4 +48,4 @@ admin/123456
## 五、备份恢复 ## 五、备份恢复
将备份的`db.sqlite`覆盖到原来的位置重启certd即可 将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置重启certd即可

View File

@ -81,4 +81,4 @@ services:
## 五、备份恢复 ## 五、备份恢复
将备份的`db.sqlite`覆盖到原来的位置重启certd即可 将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置重启certd即可

View File

@ -71,4 +71,4 @@ docker compose up -d
## 四、备份恢复 ## 四、备份恢复
将备份的`db.sqlite`覆盖到原来的位置重启certd即可 将备份的`db.sqlite`及同目录下的其他文件一起覆盖到原来的位置重启certd即可

View File

@ -1,6 +1,9 @@
# 源码部署 # 源码部署
不推荐
## 一、源码安装 ## 一、源码安装
### 环境要求
- nodejs 20 及以上
### 源码启动 ### 源码启动
```shell ```shell
# 克隆代码 # 克隆代码
@ -42,4 +45,4 @@ kill -9 $(lsof -t -i:7001)
## 四、备份恢复 ## 四、备份恢复
将备份的`db.sqlite`覆盖到原来的位置重启certd即可 将备份的`db.sqlite`及同目录下的其他文件覆盖到原来的位置重启certd即可

View File

@ -25,3 +25,15 @@ win+R 弹出运行对话框,输入 services.msc 打开服务管理器
C:\Users\xxxxx> C:\Users\xxxxx>
↑↑↑↑---------这个就是windows ssh的登录用户名 ↑↑↑↑---------这个就是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
```

View File

@ -4,7 +4,6 @@ import type { CertInfo } from "./acme.js";
import { CertReader } from "./cert-reader.js"; import { CertReader } from "./cert-reader.js";
import JSZip from "jszip"; import JSZip from "jszip";
import { CertConverter } from "./convert.js"; import { CertConverter } from "./convert.js";
import fs from "fs";
import { pick } from "lodash-es"; import { pick } from "lodash-es";
export { CertReader }; export { CertReader };
@ -59,6 +58,19 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
}) })
pfxPassword!: string; 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({ @TaskInput({
title: "更新天数", title: "更新天数",
value: 35, value: 35,
@ -143,23 +155,18 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
const res = await converter.convert({ const res = await converter.convert({
cert, cert,
pfxPassword: this.pfxPassword, pfxPassword: this.pfxPassword,
pfxArgs: this.pfxArgs,
}); });
if (cert.pfx == null && res.pfxPath) { if (cert.pfx == null && res.pfx) {
const pfxBuffer = fs.readFileSync(res.pfxPath); cert.pfx = res.pfx;
cert.pfx = pfxBuffer.toString("base64");
fs.unlinkSync(res.pfxPath);
} }
if (cert.der == null && res.derPath) { if (cert.der == null && res.der) {
const derBuffer = fs.readFileSync(res.derPath); cert.der = res.der;
cert.der = derBuffer.toString("base64");
fs.unlinkSync(res.derPath);
} }
if (cert.jks == null && res.jksPath) { if (cert.jks == null && res.jks) {
const jksBuffer = fs.readFileSync(res.jksPath); cert.jks = res.jks;
cert.jks = jksBuffer.toString("base64");
fs.unlinkSync(res.jksPath);
} }
this.logger.info("转换证书格式成功"); this.logger.info("转换证书格式成功");

View File

@ -14,31 +14,31 @@ export class CertConverter {
constructor(opts: { logger: ILogger }) { constructor(opts: { logger: ILogger }) {
this.logger = opts.logger; this.logger = opts.logger;
} }
async convert(opts: { cert: CertInfo; pfxPassword: string }): Promise<{ async convert(opts: { cert: CertInfo; pfxPassword: string; pfxArgs: string }): Promise<{
pfxPath: string; pfx: string;
derPath: string; der: string;
jksPath: string; jks: string;
}> { }> {
const certReader = new CertReader(opts.cert); const certReader = new CertReader(opts.cert);
let pfxPath: string; let pfx: string;
let derPath: string; let der: string;
let jksPath: string; let jks: string;
const handle = async (ctx: CertReaderHandleContext) => { const handle = async (ctx: CertReaderHandleContext) => {
// 调用openssl 转pfx // 调用openssl 转pfx
pfxPath = await this.convertPfx(ctx, opts.pfxPassword); pfx = await this.convertPfx(ctx, opts.pfxPassword, opts.pfxArgs);
// 转der // 转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 }); await certReader.readCertFile({ logger: this.logger, handle });
return { return {
pfxPath, pfx,
derPath, der,
jksPath, 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 { 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");
@ -65,12 +65,14 @@ export class CertConverter {
passwordArg = `-password pass:${pfxPassword}`; passwordArg = `-password pass:${pfxPassword}`;
} }
// 兼容server 2016旧版本不能用sha256 // 兼容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}`; // const newPfx = `openssl pkcs12 -export -out ${pfxPath} -inkey ${tmpKeyPath} -in ${tmpCrtPath} ${passwordArg}`;
await this.exec(oldPfxCmd); await this.exec(oldPfxCmd);
return pfxPath; const fileBuffer = fs.readFileSync(pfxPath);
// const fileBuffer = fs.readFileSync(pfxPath); const pfxCert = fileBuffer.toString("base64");
// this.pfxCert = fileBuffer.toString("base64"); fs.unlinkSync(pfxPath);
return pfxCert;
// //
// const applyTime = new Date().getTime(); // const applyTime = new Date().getTime();
// const filename = reader.buildCertFileName("pfx", applyTime); // const filename = reader.buildCertFileName("pfx", applyTime);
@ -87,15 +89,10 @@ export class CertConverter {
} }
await this.exec(`openssl x509 -outform der -in ${tmpCrtPath} -out ${derPath}`); await this.exec(`openssl x509 -outform der -in ${tmpCrtPath} -out ${derPath}`);
const fileBuffer = fs.readFileSync(derPath);
return derPath; const derCert = fileBuffer.toString("base64");
fs.unlinkSync(derPath);
// const fileBuffer = fs.readFileSync(derPath); return derCert;
// this.derCert = fileBuffer.toString("base64");
//
// const applyTime = new Date().getTime();
// const filename = reader.buildCertFileName("der", applyTime);
// this.saveFile(filename, fileBuffer);
} }
async convertJks(opts: CertReaderHandleContext, pfxPassword = "") { 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}" ` `keytool -importkeystore -srckeystore ${p12Path} -srcstoretype PKCS12 -srcstorepass "${jksPassword}" -destkeystore ${jksPath} -deststoretype PKCS12 -deststorepass "${jksPassword}" `
); );
fs.unlinkSync(p12Path); fs.unlinkSync(p12Path);
return jksPath;
const fileBuffer = fs.readFileSync(jksPath);
const certBase64 = fileBuffer.toString("base64");
fs.unlinkSync(jksPath);
return certBase64;
} catch (e) { } catch (e) {
this.logger.error("转换jks失败", e); this.logger.error("转换jks失败", e);
return; return;

View File

@ -25,7 +25,7 @@ export class AsyncSsh2Client {
if (this.encoding) { if (this.encoding) {
return iconv.decode(buffer, this.encoding); return iconv.decode(buffer, this.encoding);
} }
return buffer.toString(); return buffer.toString().replaceAll("\r\n", "\n");
} }
async connect() { async connect() {
@ -95,7 +95,12 @@ export class AsyncSsh2Client {
}); });
} }
async exec(script: string) { async exec(
script: string,
opts: {
throwOnStdErr?: boolean;
} = {}
): Promise<string> {
if (!script) { if (!script) {
this.logger.info("script 为空,取消执行"); this.logger.info("script 为空,取消执行");
return; return;
@ -114,9 +119,17 @@ export class AsyncSsh2Client {
return; return;
} }
let data = ""; let data = "";
let hasErrorLog = false;
stream stream
.on("close", (code: any, signal: any) => { .on("close", (code: any, signal: any) => {
this.logger.info(`[${this.connConf.host}][close]:code:${code}`); 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) { if (code === 0) {
resolve(data); resolve(data);
} else { } else {
@ -135,13 +148,14 @@ export class AsyncSsh2Client {
.stderr.on("data", (ret: Buffer) => { .stderr.on("data", (ret: Buffer) => {
const err = this.convert(iconv, ret); const err = this.convert(iconv, ret);
data += err; 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<string[]> { async shell(script: string | string[]): Promise<string> {
return new Promise<any>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
this.logger.info(`执行shell脚本[${this.connConf.host}][shell]: ` + script); this.logger.info(`执行shell脚本[${this.connConf.host}][shell]: ` + script);
this.conn.shell((err: Error, stream: any) => { this.conn.shell((err: Error, stream: any) => {
@ -149,11 +163,11 @@ export class AsyncSsh2Client {
reject(err); reject(err);
return; return;
} }
const output: string[] = []; let output = "";
function ansiHandle(data: string) { function ansiHandle(data: string) {
data = data.replace(/\[[0-9]+;1H/g, "\n"); data = data.replace(/\[[0-9]+;1H/g, "");
data = stripAnsi(data); data = stripAnsi(data);
return data; return data.replaceAll("\r\n", "\n");
} }
stream stream
.on("close", (code: any) => { .on("close", (code: any) => {
@ -163,7 +177,7 @@ export class AsyncSsh2Client {
.on("data", (ret: Buffer) => { .on("data", (ret: Buffer) => {
const data = ansiHandle(ret.toString()); const data = ansiHandle(ret.toString());
this.logger.info(data); this.logger.info(data);
output.push(data); output += data;
}) })
.on("error", (err: any) => { .on("error", (err: any) => {
reject(err); reject(err);
@ -171,8 +185,8 @@ export class AsyncSsh2Client {
}) })
.stderr.on("data", (ret: Buffer) => { .stderr.on("data", (ret: Buffer) => {
const data = ansiHandle(ret.toString()); const data = ansiHandle(ret.toString());
output.push(data); output += data;
this.logger.info(`[${this.connConf.host}][error]: ` + data); this.logger.error(`[${this.connConf.host}][error]: ` + data);
}); });
//保证windows下正常退出 //保证windows下正常退出
const exit = "\r\nexit\r\n"; const exit = "\r\nexit\r\n";
@ -269,7 +283,7 @@ export class SshClient {
async getIsCmd(options: { connectConf: SshAccess }) { async getIsCmd(options: { connectConf: SshAccess }) {
const { connectConf } = options; const { connectConf } = options;
return await this._call({ return await this._call<boolean>({
connectConf, connectConf,
callable: async (conn: AsyncSsh2Client) => { callable: async (conn: AsyncSsh2Client) => {
return await this.isCmd(conn); 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" * Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
* @param options * @param options
*/ */
async exec(options: { connectConf: SshAccess; script: string | Array<string>; env?: any }): Promise<string[]> { async exec(options: { connectConf: SshAccess; script: string | Array<string>; env?: any }): Promise<string> {
let { script } = options; let { script } = options;
const { connectConf } = options; const { connectConf } = options;
@ -337,7 +351,7 @@ export class SshClient {
}); });
} }
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string[]> { async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string> {
let { script } = options; let { script } = options;
const { connectConf } = options; const { connectConf } = options;
if (_.isArray(script)) { if (_.isArray(script)) {
@ -361,7 +375,7 @@ export class SshClient {
}); });
} }
async _call(options: { connectConf: SshAccess; callable: any }): Promise<string[]> { async _call<T = any>(options: { connectConf: SshAccess; callable: (conn: AsyncSsh2Client) => Promise<T> }): Promise<T> {
const { connectConf, callable } = options; const { connectConf, callable } = options;
const conn = new AsyncSsh2Client(connectConf, this.logger); const conn = new AsyncSsh2Client(connectConf, this.logger);
try { try {

View File

@ -8,7 +8,7 @@
<pi-status-show :status="item.node.status?.result" type="icon"></pi-status-show> <pi-status-show :status="item.node.status?.result" type="icon"></pi-status-show>
</div> </div>
</template> </template>
<div class="pi-task-view-logs" :class="item.node.id" style="overflow: auto"> <div class="pi-task-view-logs" :class="'id-' + item.node.id" style="overflow: auto">
<template v-for="(logItem, index) of item.logs" :key="index"> <template v-for="(logItem, index) of item.logs" :key="index">
<span :class="logItem.color"> {{ logItem.time }}</span> <span>{{ logItem.content }}</span> <span :class="logItem.color"> {{ logItem.time }}</span> <span>{{ logItem.content }}</span>
</template> </template>
@ -84,11 +84,14 @@ export default {
return node.logs.value.length; return node.logs.value.length;
}, },
async () => { 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; const isBottom = el ? el.scrollHeight - el.scrollTop === el.clientHeight : true;
await nextTick(); 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) { if (isBottom && el) {
el?.scrollTo({ el?.scrollTo({

View File

@ -79,6 +79,18 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
}) })
filePrefix: string = defaultFilePrefix; filePrefix: string = defaultFilePrefix;
@TaskInput({
title: '附加上传文件',
value: true,
component: {
name: 'a-switch',
vModel: 'checked',
placeholder: `是否备份上传的头像等文件`,
},
required: false,
})
withUpload = true;
@TaskInput({ @TaskInput({
title: '删除过期备份', title: '删除过期备份',
component: { component: {
@ -101,7 +113,6 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
this.logger.error('数据库文件不存在:', dbPath); this.logger.error('数据库文件不存在:', dbPath);
return; return;
} }
const dbTmpFilename = `${this.filePrefix}.${dayjs().format('YYYYMMDD.HHmmss')}.sqlite`; const dbTmpFilename = `${this.filePrefix}.${dayjs().format('YYYYMMDD.HHmmss')}.sqlite`;
const dbZipFilename = `${dbTmpFilename}.zip`; const dbZipFilename = `${dbTmpFilename}.zip`;
const tempDir = path.resolve(os.tmpdir(), 'certd_backup'); const tempDir = path.resolve(os.tmpdir(), 'certd_backup');
@ -118,6 +129,12 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
const stream = fs.createReadStream(dbTmpPath); const stream = fs.createReadStream(dbTmpPath);
// 使用流的方式添加文件内容 // 使用流的方式添加文件内容
zip.file(dbTmpFilename, stream, { binary: true, compression: 'DEFLATE' }); 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' }); const content = await zip.generateAsync({ type: 'nodebuffer' });
await fs.promises.writeFile(dbZipPath, content); await fs.promises.writeFile(dbZipPath, content);