perf: 支持webhook部署证书

v2
xiaojunnuo 2025-08-07 11:04:25 +08:00
parent 0af193c505
commit cbe0b1c5a6
4 changed files with 219 additions and 4 deletions

View File

@ -45,8 +45,8 @@ export class SshAccess extends BaseAccess {
title: "私钥登录",
helper: "私钥或密码必填一项",
component: {
name: "a-textarea",
vModel: "value",
name: "pem-input",
vModel: "modelValue",
},
encrypt: true,
})

View File

@ -1,7 +1,7 @@
<template>
<div class="pem-input">
<FileInput v-bind="fileInput" class="mb-5" type="primary" text="选择文件" @change="onChange" />
<a-textarea v-bind="textarea" :value="modelValue" @update:value="emitValue"></a-textarea>
<a-textarea placeholder="或直接粘贴" v-bind="textarea" :value="modelValue" @update:value="emitValue"></a-textarea>
</div>
</template>
@ -27,7 +27,7 @@ function onChange(e: any) {
const size = file.size;
if (size > 100 * 1024) {
notification.error({
message: "文件超过100k请选择正确的证书文件",
message: "文件超过100k请选择正确的文件",
});
return;
}

View File

@ -1,2 +1,3 @@
export * from './plugin-wait.js';
export * from './plugin-deploy-to-mail.js';
export * from './plugin-webhook.js';

View File

@ -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
getqueryurl
\${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<void> {
this.logger.info(`通过webhook部署开始`);
await this.send();
this.logger.info('部署成功');
}
}
new WebhookDeployCert();