From cbe0b1c5a6538f232e9a63f1693d20d5acf0a306 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Thu, 7 Aug 2025 11:04:25 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=94=AF=E6=8C=81webhook=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=AF=81=E4=B9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugins/plugin-lib/src/ssh/ssh-access.ts | 4 +- .../certd-client/src/components/pem-input.vue | 4 +- .../src/plugins/plugin-other/plugins/index.ts | 1 + .../plugin-other/plugins/plugin-webhook.ts | 214 ++++++++++++++++++ 4 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-webhook.ts diff --git a/packages/plugins/plugin-lib/src/ssh/ssh-access.ts b/packages/plugins/plugin-lib/src/ssh/ssh-access.ts index 2247b9bf..22f6956f 100644 --- a/packages/plugins/plugin-lib/src/ssh/ssh-access.ts +++ b/packages/plugins/plugin-lib/src/ssh/ssh-access.ts @@ -45,8 +45,8 @@ export class SshAccess extends BaseAccess { title: "私钥登录", helper: "私钥或密码必填一项", component: { - name: "a-textarea", - vModel: "value", + name: "pem-input", + vModel: "modelValue", }, encrypt: true, }) diff --git a/packages/ui/certd-client/src/components/pem-input.vue b/packages/ui/certd-client/src/components/pem-input.vue index ec0ac685..0d1437f0 100644 --- a/packages/ui/certd-client/src/components/pem-input.vue +++ b/packages/ui/certd-client/src/components/pem-input.vue @@ -1,7 +1,7 @@ @@ -27,7 +27,7 @@ function onChange(e: any) { const size = file.size; if (size > 100 * 1024) { notification.error({ - message: "文件超过100k,请选择正确的证书文件", + message: "文件超过100k,请选择正确的文件", }); return; } diff --git a/packages/ui/certd-server/src/plugins/plugin-other/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-other/plugins/index.ts index 83597a4e..e84abfc7 100644 --- a/packages/ui/certd-server/src/plugins/plugin-other/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-other/plugins/index.ts @@ -1,2 +1,3 @@ export * from './plugin-wait.js'; export * from './plugin-deploy-to-mail.js'; +export * from './plugin-webhook.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-webhook.ts b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-webhook.ts new file mode 100644 index 00000000..add059d8 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-other/plugins/plugin-webhook.ts @@ -0,0 +1,214 @@ +import qs from 'qs'; +import { + AbstractTaskPlugin, + IsTaskPlugin, + NotificationBody, + pluginGroups, + RunStrategy, + TaskInput +} from '@certd/pipeline'; +import {CertApplyPluginNames, CertInfo, CertReader} from "@certd/plugin-cert"; + +@IsTaskPlugin({ + name: 'WebhookDeployCert', + title: 'webhook方式部署证书', + icon: 'ion:send-sharp', + desc: '调用webhook部署证书', + group: pluginGroups.other.key, + showRunStrategy:true, + default: { + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed, + }, + }, +}) +export class WebhookDeployCert extends AbstractTaskPlugin { + + @TaskInput({ + title: "域名证书", + helper: "请选择前置任务输出的域名证书", + component: { + name: "output-selector", + from: [...CertApplyPluginNames] + }, + required: true + }) + cert!: CertInfo ; + + @TaskInput({ + title: 'webhook地址', + component: { + placeholder: 'https://xxxxx.com/xxxx', + }, + col: { + span: 24, + }, + required: true, + }) + webhook = ''; + + @TaskInput({ + title: '请求方式', + value: 'POST', + component: { + name: 'a-select', + placeholder: 'post/put/get', + options: [ + { value: 'POST', label: 'POST' }, + { value: 'PUT', label: 'PUT' }, + { value: 'GET', label: 'GET' }, + ], + }, + required: true, + }) + method = ''; + + @TaskInput({ + title: 'ContentType', + value: 'application/json', + component: { + name: 'a-auto-complete', + options: [ + { value: 'application/json', label: 'application/json' }, + { value: 'application/x-www-form-urlencoded', label: 'application/x-www-form-urlencoded' }, + ], + }, + helper: '也可以自定义填写', + required: true, + }) + contentType = ''; + + @TaskInput({ + title: 'Headers', + component: { + name: 'a-textarea', + vModel: 'value', + rows: 2, + }, + col: { + span: 24, + }, + helper: '一行一个,格式为key=value', + required: false, + }) + headers = ''; + + @TaskInput({ + title: '消息body模版', + value: `{ + "id":"123", + "crt":"{crt}", + "key":"{key}" +}`, + component: { + name: 'a-textarea', + rows: 4, + }, + col: { + span: 24, + }, + helper: `根据对应的webhook接口文档,构建一个json对象作为参数(默认值只是一个示例,一般不是正确的参数) +变量用\${}包裹\n字符串需要双引号,使用\\n换行 +如果是get方式,将作为query参数拼接到url上 +变量列表:\${domain} 主域名、\${domains} 全部域名、\${crt} 证书、\${key} 私钥、\${ic} 中间证书、\${one} 一体证书、\${der} der证书(base64)、\${pfx} pfx证书(base64)、\${jks} jks证书(base64)、`, + required: true, + }) + template = ''; + + @TaskInput({ + title: '忽略证书校验', + value: false, + component: { + name: 'a-switch', + vModel: 'checked', + }, + required: false, + }) + skipSslVerify: boolean; + + replaceTemplate(target: string, body: any, urlEncode = false) { + let bodyStr = target; + const keys = Object.keys(body); + for (const key of keys) { + let value = urlEncode ? encodeURIComponent(body[key]) : body[key]; + value = value.replaceAll(`\n`, "\\n"); + bodyStr = bodyStr.replaceAll(`\${${key}}`, value); + + } + return bodyStr; + } + + async send() { + if (!this.template) { + throw new Error('模版不能为空'); + } + if (!this.webhook) { + throw new Error('webhook不能为空'); + } + + + const certReader = new CertReader(this.cert) + + const replaceBody = { + domain: certReader.getMainDomain(), + domains: certReader.getAllDomains().join(","), + ...this.cert + }; + const bodyStr = this.replaceTemplate(this.template, replaceBody); + let data = JSON.parse(bodyStr); + + let url = this.webhook; + if (this.method.toLowerCase() === 'get') { + const query = qs.stringify(data); + if (url.includes('?')) { + url = `${url}&${query}`; + } else { + url = `${url}?${query}`; + } + data = null; + } + + const headers: any = {}; + if (this.headers && this.headers.trim()) { + this.headers.split('\n').forEach(item => { + item = item.trim(); + if (item) { + const eqIndex = item.indexOf('='); + if (eqIndex <= 0) { + this.logger.warn('header格式错误,请使用=号分割', item); + return; + } + const key = item.substring(0, eqIndex); + headers[key] = item.substring(eqIndex + 1); + } + }); + } + + try { + const res = await this.http.request({ + url: url, + method: this.method, + headers: { + 'Content-Type': `${this.contentType}; charset=UTF-8`, + ...headers, + }, + data: data, + skipSslVerify: this.skipSslVerify, + }); + return res + } catch (e) { + if (e.response?.data) { + throw new Error(e.message + ',' + JSON.stringify(e.response.data)); + } + throw e; + } + } + + async onInstance() {} + async execute(): Promise { + this.logger.info(`通过webhook部署开始`); + await this.send(); + this.logger.info('部署成功'); + } +} +new WebhookDeployCert();