perf: 修复西数解析记录添加失败的bug,支持部署证书到西数虚拟主机

This commit is contained in:
xiaojunnuo
2025-11-18 01:04:47 +08:00
parent 5ad6cadcee
commit 1102952b47
4 changed files with 350 additions and 42 deletions

View File

@@ -1,5 +1,6 @@
import { HttpRequestConfig } from '@certd/basic';
import { IsAccess, AccessInput, BaseAccess } from '@certd/pipeline';
import qs from 'qs';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
@@ -55,7 +56,7 @@ export class WestAccess extends BaseAccess {
component: {
placeholder: '账户级别的key对整个账户都有管理权限',
},
helper: '账户级别的key对整个账户都有管理权限\n前往https://www.west.cn/manager/API/APIconfig.asp手动设置“api连接密码”',
helper: '账户级别的key对整个账户都有管理权限\n前往[API接口配置](https://www.west.cn/manager/API/APIconfig.asp)手动设置“api连接密码”',
encrypt: true,
required: false,
mergeScript: `
@@ -88,6 +89,100 @@ export class WestAccess extends BaseAccess {
`,
})
apidomainkey = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
await this.getDomainList();
return "ok";
}
async getDomainList() {
const res = await this.doRequest({
url: '/v2/domain',
method: 'GET',
data:{
act:'getdomains',
limit:1,
page:1
}
});
return res;
}
public async doRequest(req: HttpRequestConfig) {
let { url, method, data } = req;
if (data == null) {
data = {};
}
if (!method) {
method = 'POST';
}
if (this.scope === 'account') {
/**
* token text 身份验证字符串取值为md5(username+api_password+timestamp),其中:
username您在我司注册的用户名。
api_password您设置的API密码。您可登录官网管理中心在“代理商管理”-<API接口配置>""页面查看您的api密码。
timestamp当前时间的毫秒时间戳。
将字符串username与字符串api_password连接再与timestamp连接然后将生成的字符串进行md5求值md5算法要求为
32位16进制字符串小写格式。
身份验证串有效期10分钟。
比如您的用户名为zhangsan您的API密码为5dh232kfg!* ,当前毫秒时间戳为1554691950854
token=md5(zhangsan + 5dh232kfg!* + 1554691950854)=cfcd208495d565ef66e7dff9f98764da
*/
// data.apikey = this.ctx.utils.hash.md5(this.apikey);
data.username = this.username;
const timestamp = new Date().getTime();
const token = this.ctx.utils.hash.md5(`${this.username}${this.apikey}${timestamp}`).toLowerCase();
data.token = token;
data.time = timestamp;
} else {
data.apidomainkey = this.apidomainkey;
}
const headers = {}
const body: any = {}
if (method.toUpperCase() === 'POST') {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
body.data = data
} else if (method.toUpperCase() === 'GET') {
let queryString = '';
if (method.toUpperCase() === 'GET') {
queryString = qs.stringify(data);
}
url = `${url}?${queryString}`
}
const res = await this.ctx.http.request<any, any>({
baseURL: 'https://api.west.cn/api',
url,
method,
...body,
headers,
});
this.ctx.logger.info(`request ${url} ${method} res:${JSON.stringify(res)}`);
if (res.msg !== 'success' && res.result!= 200) {
throw new Error(`${JSON.stringify(res.msg)}`);
}
return res;
}
}
new WestAccess();

View File

@@ -4,10 +4,10 @@ import { WestAccess } from './access.js';
type westRecord = {
// 这里定义Record记录的数据结构跟对应云平台接口返回值一样即可一般是拿到id就行用于删除txt解析记录清理申请痕迹
code: number;
result: number;
msg: string;
body: {
record_id: number;
data: {
id: number;
};
};
@@ -31,27 +31,6 @@ export class WestDnsProvider extends AbstractDnsProvider<westRecord> {
//...
}
private async doRequestApi(url: string, data: any = null, method = 'post') {
if (this.access.scope === 'account') {
data.apikey = this.ctx.utils.hash.md5(this.access.apikey);
data.username = this.access.username;
} else {
data.apidomainkey = this.access.apidomainkey;
}
const res = await this.ctx.http.request<any, any>({
url,
method,
data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
if (res.msg !== 'success') {
throw new Error(`${JSON.stringify(res.msg)}`);
}
return res;
}
/**
* 创建dns解析记录用于验证域名所有权
*/
@@ -63,22 +42,26 @@ export class WestDnsProvider extends AbstractDnsProvider<westRecord> {
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
const { fullRecord, value, type, domain,hostRecord } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// 准备要发送到API的请求体
const requestBody = {
act: 'dnsrec.add', // API动作类型
act: 'adddnsrecord', // API动作类型
domain: domain, // 域名
record_type: 'TXT', // DNS记录类型
hostname: fullRecord, // 完整的记录名
record_value: value, // 记录的值
record_line: '', // 记录线路
record_ttl: 60, // TTL (生存时间)设置为60秒
type: 'TXT', // DNS记录类型
host: hostRecord, // 完整的记录名
value: value, // 记录的值
line: '', // 记录线路
ttl: 60, // TTL (生存时间)设置为60秒
};
const url = 'https://api.west.cn/API/v2/domain/dns/';
const res = await this.doRequestApi(url, requestBody);
const url = '/v2/domain/';
const res = await this.access.doRequest({
url,
method:'POST',
data: requestBody,
});
const record = res as westRecord;
this.logger.info(`添加域名解析成功:fullRecord=${fullRecord},value=${value}`);
this.logger.info(`dns解析记录:${JSON.stringify(record)}`);
@@ -90,6 +73,7 @@ export class WestDnsProvider extends AbstractDnsProvider<westRecord> {
/**
* 删除dns解析记录,清理申请痕迹
* https://console-docs.apipost.cn/preview/ab2c3103b22855ba/fac91d1e43fafb69?target_id=c4564349-6687-413d-a3d4-b0e8db5b34b2
* @param options
*/
async removeRecord(options: RemoveRecordOptions<westRecord>): Promise<void> {
@@ -104,16 +88,17 @@ export class WestDnsProvider extends AbstractDnsProvider<westRecord> {
// 准备要发送到API的请求体
const requestBody = {
act: 'dnsrec.remove', // API动作类型
act: 'deldnsrecord', // API动作类型
domain: domain, // 域名
record_id: record.body.record_id,
hostname: fullRecord, // 完整的记录名
record_type: 'TXT', // DNS记录类型
record_line: '', // 记录线路
id: record.data?.id,
};
const url = 'https://api.west.cn/API/v2/domain/dns/';
const res = await this.doRequestApi(url, requestBody);
const url = '/v2/domain/';
const res = await this.access.doRequest({
url,
method:'POST',
data: requestBody,
});
const result = res.result;
this.logger.info('删除域名解析成功:', fullRecord, value, JSON.stringify(result));
}

View File

@@ -1,2 +1,3 @@
export * from './dns-provider.js';
export * from './access.js';
export * from './plugins/deploy-to-vhost.js';

View File

@@ -0,0 +1,227 @@
import {
AbstractTaskPlugin,
IsTaskPlugin,
PageSearch,
pluginGroups,
RunStrategy,
TaskInput
} from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { WestAccess } from "../access.js";
@IsTaskPlugin({
//命名规范,插件类型+功能就是目录plugin-demo中的demo大写字母开头驼峰命名
name: "WestDeployToVhost",
title: "西数-部署到虚拟主机",
desc: "西部数码部署证书到虚拟主机",
icon: "svg:icon-lucky",
//插件分组
group: pluginGroups.cdn.key,
needPlus: false,
default: {
//默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
//类名规范跟上面插件名称name一致
export class WestDeployToVhost extends AbstractTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames]
}
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
//授权选择框
@TaskInput({
title: "西数授权",
component: {
name: "access-selector",
type: "west" //固定授权类型
},
required: true //必填
})
accessId!: string;
//
@TaskInput(
createRemoteSelectInputDefine({
title: "虚拟主机列表",
helper: "虚拟主机列表",
action: WestDeployToVhost.prototype.onGetVhostList.name,
pager: false,
search: false
})
)
vhostList!: string[];
// @TaskInput(
// createRemoteSelectInputDefine({
// title: "证书Id",
// helper: "要部署的西数证书id",
// action: WestDeployToVhost.prototype.onGetCertList.name,
// pager: false,
// search: false
// })
// )
// certList!: string[];
//插件实例化时执行的方法
async onInstance() {
}
//插件执行方法
async execute(): Promise<void> {
const access = await this.getAccess<WestAccess>(this.accessId);
for (const item of this.vhostList) {
this.logger.info(`----------- 开始更新证书到虚拟主机:${item}`);
const arr = item.split("_");
const sitename = arr[1];
await this.uploadCert({access,sitename});
await this.ctx.utils.sleep(2000);
const res = await this.getVhostSslInfo({access,sitename});
this.logger.info(`----------- 虚拟主机${sitename}证书信息:${JSON.stringify(res)}`);
this.logger.info(`----------- 更新证书${item}成功`);
}
this.logger.info("部署完成");
}
// async onGetCertList(data: PageSearch = {}) {
// const access = await this.getAccess<WestAccess>(this.accessId);
// const list = await access.getCertList({});
// if (!list || list.length === 0) {
// throw new Error("没有找到证书,请先在控制台上传一次证书且关联域名");
// }
// /**
// * certificate-id
// * name
// * dns-names
// */
// const options = list.map((item: any) => {
// const domains = item["dns-names"]
// const certId = item["certificate-id"];
// return {
// label: `${item.name}<${certId}-${domains[0]}>`,
// value: certId,
// domain: item["dns-names"]
// };
// });
// return {
// list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
// total: list.length,
// pageNo: 1,
// pageSize: list.length
// };
// }
async uploadCert(req:{access:any,sitename:string}){
const {access,sitename} = req;
const data = {
/**
* act
vhostssl
String
sitename
westly
String
cmd
import
String
openssl/closessl 部署/关闭
keycontent
String
私匙
certcontent
*/
act:"vhostssl",
sitename:sitename,
westly:"1",
cmd:"import",
opensslclosessl:"openssl",
keycontent:this.cert.key,
certcontent:this.cert.crt,
}
const res = await access.doRequest({
url: `/v2/vhost/`,
method:"POST",
data:data
});
return res;
}
async getVhostSslInfo(req:{access:any,sitename:string}){
const {access,sitename} = req;
const data = {
act:"vhostssl",
sitename:sitename,
cmd:"info",
}
const res = await access.doRequest({
url: `/v2/vhost/`,
method:"POST",
data:data
});
return res;
}
async onGetVhostList(data: PageSearch = {}) {
const access = await this.getAccess<WestAccess>(this.accessId);
const res = await access.doRequest({
url: `/v2/vhost/`,
method:"POST",
data:{
act:"sync",
westid: 1,
}
});
const list = res.data
if (!list || list.length === 0) {
throw new Error("没有找到虚拟主机");
}
/**
* certificate-id
* name
* dns-names
*/
const options = list.map((item: any) => {
return {
label: `${item.sitename}<${item.westid}-${item.bindings}>`,
value: `${item.westid}_${item.sitename}`,
domain: item.bindings.split(",")
};
});
return {
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
total: list.length,
pageNo: 1,
pageSize: list.length
};
}
}
//实例化一下,注册插件
new WestDeployToVhost();