From ddfd0fb81d6638352920261065f1ab8e27bdd564 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Tue, 3 Jun 2025 23:52:43 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E5=88=B0=E9=A3=9E=E7=89=9BOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/basic/src/utils/util.log.ts | 11 +- packages/core/pipeline/src/plugin/api.ts | 18 ++ packages/plugins/plugin-lib/src/ssh/ssh.ts | 25 ++- packages/ui/certd-server/src/plugins/index.ts | 1 + .../plugins/plugin-refresh-cert.ts | 57 ++---- .../src/plugins/plugin-fnos/index.ts | 170 ++++++++++++++++++ 6 files changed, 236 insertions(+), 46 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-fnos/index.ts diff --git a/packages/core/basic/src/utils/util.log.ts b/packages/core/basic/src/utils/util.log.ts index 171da309..6f6709aa 100644 --- a/packages/core/basic/src/utils/util.log.ts +++ b/packages/core/basic/src/utils/util.log.ts @@ -56,8 +56,15 @@ export function buildLogger(write: (text: string) => void) { if (item == null) { continue; } - //换成同长度的*号, item可能有多行 - text = text.replaceAll(item, "*".repeat(item.length)); + if (item.includes(text)) { + //整个包含 + text = "*".repeat(text.length); + continue; + } + if (text.includes(item)) { + //换成同长度的*号, item可能有多行 + text = text.replaceAll(item, "*".repeat(item.length)); + } } write(text); }, diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index e47d6675..1159569c 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -152,6 +152,16 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin { this.logger = ctx.logger; this.accessService = ctx.accessService; this.http = ctx.http; + // 将证书加入secret + // @ts-ignore + if (this.cert && this.cert.crt && this.cert.key) { + //有证书 + // @ts-ignore + const cert: any = this.cert; + this.registerSecret(cert.crt); + this.registerSecret(cert.key); + this.registerSecret(cert.one); + } } async getAccess(accessId: string | number, isCommon = false) { @@ -186,6 +196,14 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin { return res as T; } + registerSecret(value: string) { + // @ts-ignore + if (this.logger?.addSecret) { + // @ts-ignore + this.logger.addSecret(value); + } + } + randomFileId() { return Math.random().toString(36).substring(2, 9); } diff --git a/packages/plugins/plugin-lib/src/ssh/ssh.ts b/packages/plugins/plugin-lib/src/ssh/ssh.ts index c026022e..4a105779 100644 --- a/packages/plugins/plugin-lib/src/ssh/ssh.ts +++ b/packages/plugins/plugin-lib/src/ssh/ssh.ts @@ -165,10 +165,16 @@ export class AsyncSsh2Client { }); } + /** + * + * @param script + * @param opts {withStdErr 返回{stdOut,stdErr}} + */ async exec( script: string, opts: { throwOnStdErr?: boolean; + withStdErr?: boolean; env?: any; } = {} ): Promise { @@ -193,6 +199,7 @@ export class AsyncSsh2Client { return; } let data = ""; + let stdErr = ""; let hasErrorLog = false; stream .on("close", (code: any, signal: any) => { @@ -205,7 +212,15 @@ export class AsyncSsh2Client { } if (code === 0) { - resolve(data); + if (opts.withStdErr === true) { + //@ts-ignore + resolve({ + stdErr, + stdOut: data, + }); + } else { + resolve(data); + } } else { reject(new Error(data)); } @@ -221,7 +236,7 @@ export class AsyncSsh2Client { }) .stderr.on("data", (ret: Buffer) => { const err = this.convert(iconv, ret); - data += err; + stdErr += err; hasErrorLog = true; this.logger.error(`[${this.connConf.host}][error]: ` + err.trimEnd()); }); @@ -323,9 +338,6 @@ export class AsyncSsh2Client { export class SshClient { logger: ILogger; - constructor(logger: ILogger) { - this.logger = logger; - } /** * * @param connectConf @@ -382,6 +394,9 @@ export class SshClient { }, }); } + constructor(logger: ILogger) { + this.logger = logger; + } async scpUpload(options: { conn: any; localPath: string; remotePath: string; opts?: { mode?: string } }) { const { conn, localPath, remotePath } = options; diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index 91c6961d..7fa95664 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -22,3 +22,4 @@ export * from './plugin-51dns/index.js' export * from './plugin-notification/index.js' export * from './plugin-flex/index.js' export * from './plugin-farcdn/index.js' +export * from './plugin-fnos/index.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-flex/plugins/plugin-refresh-cert.ts b/packages/ui/certd-server/src/plugins/plugin-flex/plugins/plugin-refresh-cert.ts index 8544a8e8..4a33961a 100644 --- a/packages/ui/certd-server/src/plugins/plugin-flex/plugins/plugin-refresh-cert.ts +++ b/packages/ui/certd-server/src/plugins/plugin-flex/plugins/plugin-refresh-cert.ts @@ -1,9 +1,8 @@ import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; -import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { CertApplyPluginNames, CertInfo,CertReader } from "@certd/plugin-cert"; import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; import { FlexCDNAccess } from "../access.js"; import { FlexCDNClient } from "../client.js"; -import crypto from 'crypto' @IsTaskPlugin({ //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 @@ -62,41 +61,6 @@ export class FlexCDNRefreshCert extends AbstractTaskPlugin { async onInstance() { } - static parseCertInfo(certPem: string) { - const certificateArray = certPem - .trim() - .split('-----END CERTIFICATE-----') - .filter(cert => cert.trim() !== '') - .map(cert => (cert + '-----END CERTIFICATE-----').trim()); - - const currentInfo = new crypto.X509Certificate(certificateArray[0]) - - const dnsNames = currentInfo.subjectAltName.split(',') - .map(it => it.trim()) - .filter(it => it.startsWith('DNS:')) - .map(it => it.substring(4)) - - const commonNames = certificateArray.map(it => { - const info = new crypto.X509Certificate(it) - - const subjectCN = info.issuer.trim() - .split('\n') - .map(it => it.trim()) - .find((part) => part.trim().startsWith('CN=')) - ?.split('=')[1] - ?.trim(); - - return subjectCN - }) - - return { - commonNames: commonNames, - dnsNames: dnsNames, - timeBeginAt: Math.floor((new Date(currentInfo.validFrom)).getTime() / 1000), - timeEndAt: Math.floor((new Date(currentInfo.validTo)).getTime() / 1000), - } - } - //插件执行方法 async execute(): Promise { const access: FlexCDNAccess = await this.getAccess(this.accessId); @@ -119,9 +83,24 @@ export class FlexCDNRefreshCert extends AbstractTaskPlugin { const sslCert = JSON.parse(this.ctx.utils.hash.base64Decode(res.sslCertJSON)) this.logger.info(`证书信息:${sslCert.name},${sslCert.dnsNames}`); + const certReader = new CertReader(this.cert) + /** + * commonNames: commonNames, + * dnsNames: dnsNames, + * timeBeginAt: Math.floor((new Date(currentInfo.validFrom)).getTime() / 1000), + * timeEndAt: Math.floor((new Date(currentInfo.validTo)).getTime() / 1000), + * + */ + const commonNames =[ certReader.getMainDomain()] + const dnsNames = certReader.getAltNames() + const timeBeginAt = Math.floor(certReader.detail.notBefore.getTime() / 1000); + const timeEndAt = Math.floor(certReader.detail.notAfter.getTime() / 1000); const body = { ...sslCert, // inherit old cert info like name and description - ...FlexCDNRefreshCert.parseCertInfo(this.cert.crt), + commonNames, + dnsNames, + timeBeginAt, + timeEndAt, name: sslCert.name, sslCertId: item, certData: this.ctx.utils.hash.base64(this.cert.crt), @@ -160,7 +139,7 @@ export class FlexCDNRefreshCert extends AbstractTaskPlugin { const options = list.map((item: any) => { return { - label: `${item.name}<${item.id}-${item.dnsNames[0]}>`, + label: `${item.name}<${item.id}-${item.dnsNames?.[0]}>`, value: item.id, domain: item.dnsNames }; diff --git a/packages/ui/certd-server/src/plugins/plugin-fnos/index.ts b/packages/ui/certd-server/src/plugins/plugin-fnos/index.ts new file mode 100644 index 00000000..bd4b864b --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-fnos/index.ts @@ -0,0 +1,170 @@ +import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { + createCertDomainGetterInputDefine, + createRemoteSelectInputDefine, + SshAccess, + SshClient +} from "@certd/plugin-lib"; + +@IsTaskPlugin({ + //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 + name: "FnOSDeployToNAS", + title: "飞牛NAS-部署证书", + icon: "svg:icon-lucky", + //插件分组 + group: pluginGroups.panel.key, + needPlus: false, + default: { + //默认值配置照抄即可 + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed + } + } +}) +//类名规范,跟上面插件名称(name)一致 +export class FnOSDeployToNAS extends AbstractTaskPlugin { + //证书选择,此项必须要有 + @TaskInput({ + title: "域名证书", + helper: "请选择前置任务输出的域名证书", + component: { + name: "output-selector", + from: [...CertApplyPluginNames] + } + // required: true, // 必填 + }) + cert!: CertInfo; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + //授权选择框 + @TaskInput({ + title: "飞牛SSH授权", + component: { + name: "access-selector", + type: "ssh" //固定授权类型 + }, + helper:"请先配置sudo免密:\nsudo visudo\n#在文件最后一行增加以下内容,需要将username替换成自己的用户名\nusername ALL=(ALL) NOPASSWD: NOPASSWD: ALL\nctrl+x 保存退出", + required: true //必填 + }) + accessId!: string; + + + + @TaskInput( + createRemoteSelectInputDefine({ + title: "证书Id", + helper: "要更新的证书id", + action: FnOSDeployToNAS.prototype.onGetCertList.name + }) + ) + certList!: number[]; + + //插件实例化时执行的方法 + async onInstance() { + } + + //插件执行方法 + async execute(): Promise { + const access: SshAccess = await this.getAccess(this.accessId); + + const client = new SshClient(this.logger); + + //复制证书 + const list = await this.doGetCertList() + + for (const target of this.certList) { + this.logger.info(`----------- 准备部署:${target}`); + let found = false + for (const item of list) { + if (item.sum === target) { + this.logger.info(`----------- 找到证书,开始部署:${item.sum},${item.domain}`) + const certPath = item.certificate; + const keyPath = item.privateKey; + const cmd = ` +sudo tee ${certPath} > /dev/null <<'EOF' +${this.cert.crt} +EOF +sudo tee ${keyPath} > /dev/null <<'EOF' +${this.cert.key} +EOF +` + const res = await client.exec({ + connectConf: access, + script: cmd + }); + if (res.indexOf("Permission denied") > -1){ + this.logger.error("权限不足,请先配置 sudo 免密") + } + found = true + break + } + } + if (!found) { + throw new Error(`没有找到证书:${target},请修改任务重新选择证书id`); + } + } + + + this.logger.info("证书已上传,准备重启..."); + + + const restartCmd= ` +echo "正在重启相关服务..." +systemctl restart webdav.service +systemctl restart smbftpd.service +systemctl restart trim_nginx.service +echo "服务重启完成!" +` + await client.exec({ + connectConf: access, + script: restartCmd + }); + + this.logger.info("部署完成"); + } + + async doGetCertList(){ + const access: SshAccess = await this.getAccess(this.accessId); + const client = new SshClient(this.logger); + + /** + * :/usr/trim/etc$ cat network_cert_all.conf | jq . + */ + const sslListCmd = "cat /usr/trim/etc/network_cert_all.conf | jq ." + + const res:string = await client.exec({ + connectConf: access, + script: sslListCmd + }); + let list = [] + try{ + list = JSON.parse(res.trim()) + }catch (e){ + throw new Error(`证书列表解析失败:${res}`) + } + + if (!list || list.length === 0) { + throw new Error("没有找到证书,请先在证书管理也没上传一次证书"); + } + return list + } + + async onGetCertList() { + + const list = await this.doGetCertList() + + const options = list.map((item: any) => { + return { + label: `${item.domain}<${item.used?'已使用':"未使用"}-${item.sum}>`, + value: item.sum, + domain: item.san + }; + }); + return this.ctx.utils.options.buildGroupOptions(options, this.certDomains); + } +} + +new FnOSDeployToNAS();