Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev

This commit is contained in:
xiaojunnuo
2026-01-06 09:24:26 +08:00
30 changed files with 1266 additions and 168 deletions

View File

@@ -15,19 +15,21 @@ input:
required: true
encrypt: false
publicKey:
title: 公钥
title: 证书公钥
component:
name: a-textarea
rows: 3
placeholder: MIIBIjANBg...
placeholder: '-----BEGIN CERTIFICATE-----'
helper: 微信商户平台—>账户设置—>API安全—>验证商户身份—>商户API证书—>管理证书—>apiclient_cert.pem
required: true
encrypt: true
privateKey:
title: 私钥
component:
placeholder: MIIEvQIBADANB...
placeholder: '-----BEGIN PRIVATE KEY-----'
name: a-textarea
rows: 3
helper: 证书私钥 apiclient_key.pem
required: true
encrypt: true
key:

View File

@@ -0,0 +1,27 @@
name: zenlayer
title: Zenlayer授权
icon: svg:icon-lucky
desc: Zenlayer授权
input:
accessKeyId:
title: AccessKeyId
component:
placeholder: 访问密钥ID
helper: '[访问密钥管理](https://console.zenlayer.com/accessKey)获取'
required: true
encrypt: false
accessKeyPassword:
title: AccessKey Password
component:
placeholder: 访问密钥密码
required: true
encrypt: true
testRequest:
title: 测试
component:
name: api-test
action: TestRequest
helper: 点击测试接口是否正常
pluginType: access
type: builtIn
scriptFilePath: /plugins/plugin-zenlayer/access.js

View File

@@ -0,0 +1,77 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: 1PanelDeployToPanel
title: 1Panel-部署面板证书
icon: svg:icon-onepanel
desc: 更新1Panel的面板证书
group: panel
needPlus: false
input:
cert:
title: 域名证书
helper: 请选择前置任务输出的域名证书
component:
name: output-selector
from:
- ':cert:'
required: true
order: 0
certDomains:
title: 当前证书域名
component:
name: cert-domains-getter
mergeScript: |2-
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
template: false
required: true
order: 0
accessId:
title: 1Panel授权
helper: 1Panel授权
component:
name: access-selector
type: 1panel
required: true
order: 0
currentNode:
title: 1Panel节点
component:
name: remote-select
vModel: value
mode: tags
type: plugin
typeName: OnePanelDeployToPanelPlugin
action: onGetNodes
search: false
pager: false
watches:
- certDomains
- accessId
value: local
required: true
mergeScript: |2-
return {
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: 要更新的1Panel证书的节点信息目前只有v2存在此概念
order: 0
output: {}
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-plus/1panel/plugins/deploy-to-panel.js

View File

@@ -0,0 +1,73 @@
showRunStrategy: false
default:
strategy:
runStrategy: 1
name: ZenlayerRefreshCert
title: Zenlayer-刷新证书
desc: 刷新Zenlayer CDN证书
icon: svg:icon-lucky
group: cdn
needPlus: false
input:
cert:
title: 域名证书
helper: 请选择前置任务输出的域名证书
component:
name: output-selector
from:
- ':cert:'
order: 0
certDomains:
title: 当前证书域名
component:
name: cert-domains-getter
mergeScript: |2-
return {
component:{
inputKey: ctx.compute(({form})=>{
return form.cert
}),
}
}
template: false
required: false
order: 0
accessId:
title: Zenlayer授权
component:
name: access-selector
type: zenlayer
required: true
order: 0
certList:
title: 证书ID列表
component:
name: remote-select
vModel: value
mode: tags
type: plugin
action: onGetCertList
search: false
pager: false
watches:
- certDomains
- accessId
required: true
mergeScript: |2-
return {
component:{
form: ctx.compute(({form})=>{
return form
})
},
}
helper: 要更新的Zenlayer证书ID列表
order: 0
output: {}
pluginType: deploy
type: builtIn
scriptFilePath: /plugins/plugin-zenlayer/plugins/plugin-refresh-cert.js

View File

@@ -130,7 +130,9 @@
"@google-cloud/publicca": "^1.3.0",
"jsrsasign": "^11.1.0",
"ssh2": "^1.17.0",
"@alicloud/openapi-util": "^0.3.2"
"@alicloud/openapi-util": "^0.3.2",
"wechatpay-node-v3": "^2.2.1",
"alipay-sdk": "^4.13.0"
},
"devDependencies": {
"@midwayjs/mock": "3.20.11",

View File

@@ -1,6 +1,7 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { PluginService } from "../plugin/service/plugin-service.js";
import { registerPaymentProviders } from "../suite/payments/index.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -19,6 +20,8 @@ export class AutoBLoadPlugins {
}
// await import("../../plugins/index.js")
await this.pluginService.registerFromDb()
await registerPaymentProviders();
logger.info(`加载插件完成,加载模式:${process.env.certd_plugin_loadmode}`);
}

View File

@@ -0,0 +1,148 @@
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
import dayjs from "dayjs";
import { logger, utils } from "@certd/basic";
import { AlipayAccess } from "../../../plugins/plugin-plus/alipay/access.js";
export class PaymentAlipay implements IPaymentProvider {
access: AlipayAccess;
constructor(access: AlipayAccess) {
this.access = access;
}
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
const alipaySdk = await this.createAlipaySdk();
const result: any = await alipaySdk.curl("POST", "/v3/alipay/trade/query", {
body: {
out_trade_no: tradeNo,
},
});
logger.info("获取支付宝订单详情", JSON.stringify(result));
if (result.responseHttpStatus !== 200) {
throw new Error("请求支付宝失败:" + result.responseHttpStatus);
}
const data = result.data;
let status: string = undefined;
let payTime: number = undefined;
if (data.trade_status === "TRADE_SUCCESS") {
status = "paid";
payTime = dayjs(data.send_pay_date).valueOf();
} else if (data.trade_status === "TRADE_CLOSED") {
status = "closed";
} else {
logger.info("支付宝订单状态为:" + data.trade_status);
}
return {
tradeNo,
status: status,
amount: utils.amount.toCent(parseFloat(data.total_amount)),
payNo: data.trade_no,
payTime: payTime,
};
}
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
const return_url = `${opts.bindUrl}/#/certd/payment/return/alipay`;
const notify_url = `${opts.bindUrl}/api/payment/notify/alipay`;
const alipaySdk = await this.createAlipaySdk();
const url = alipaySdk.pageExec("alipay.trade.page.pay", "GET", {
return_url,
notify_url,
bizContent: {
out_trade_no: trade.tradeNo,
total_amount: utils.amount.toYuan(trade.amount),
subject: trade.title,
product_code: "FAST_INSTANT_TRADE_PAY",
// qr_pay_mode: "1",
// qrcode_width: "100",
// time_expire: "2016-12-31+10:05:01",
// sub_merchant: {
// merchant_id: "2088000603999128",
// merchant_type: "alipay",
// },
// extend_params: {
// sys_service_provider_id: "2088511833207846",
// hb_fq_seller_percent: "100",
// hb_fq_num: "3",
// industry_reflux_info: '{\\"scene_code\\":\\"metro_tradeorder\\",\\"channel\\":\\"xxxx\\",\\"scene_data\\":{\\"asset_name\\":\\"ALIPAY\\"}}',
// specified_seller_name: "XXX的跨境小铺",
// royalty_freeze: "true",
// card_type: "S0JP0000",
// },
// business_params: '{"mc_create_trade_ip":"127.0.0.1"}',
// promo_params: '{"storeIdType":"1"}',
// integration_type: "PCWEB",
// request_from_url: "https://",
// store_id: "NJ_001",
// merchant_order_no: "20161008001",
// ext_user_info: {
// cert_type: "IDENTITY_CARD",
// cert_no: "362334768769238881",
// name: "李明",
// mobile: "16587658765",
// min_age: "18",
// need_check_info: "F",
// identity_hash: "27bfcd1dee4f22c8fe8a2374af9b660419d1361b1c207e9b41a754a113f38fcc",
// },
// invoice_info: {
// key_info: {
// tax_num: "1464888883494",
// is_support_invoice: "true",
// invoice_merchant_name: "ABC|003",
// },
// details: '[{"code":"100294400","name":"服饰","num":"2","sumPrice":"200.00","taxRate":"6%"}]',
// },
},
});
return {
url,
body: {},
};
}
private async createAlipaySdk() {
const AlipaySdk = await import("alipay-sdk");
const alipaySdk = new AlipaySdk.AlipaySdk({
appId: this.access.appId,
privateKey: this.access.privateKey,
alipayPublicKey: this.access.alipayPublicKey,
gateway: "https://openapi.alipay.com/gateway.do",
});
return alipaySdk;
}
async onNotify(data: any, updateTrade: UpdateTrade) {
const alipaySdk = await this.createAlipaySdk();
logger.info(`支付宝notify${JSON.stringify(data)}`);
// true | false
let success = alipaySdk.checkNotifySign(data);
if (!success) {
success = alipaySdk.checkNotifySignV2(data);
if (!success) {
throw new Error("签名验证失败");
}
}
if (data.trade_status === "TRADE_SUCCESS") {
await updateTrade({
tradeNo: data.out_trade_no,
status: "paid",
amount: utils.amount.toCent(parseFloat(data.total_amount)),
payNo: data.trade_no,
payTime: dayjs().valueOf(),
});
} else if (data.trade_status === "TRADE_CLOSED") {
await updateTrade({
tradeNo: data.out_trade_no,
status: "closed",
payNo: data.trade_no,
});
return "success";
}
}
}

View File

@@ -0,0 +1,7 @@
import {paymentProviderFactory} from "@certd/commercial-core"
export function registerPaymentProviders() {
paymentProviderFactory.registerProvider("alipay", async () => (await import("./alipay.js")).PaymentAlipay);
paymentProviderFactory.registerProvider("wxpay", async () => (await import("./wxpay.js")).PaymentWxpay);
paymentProviderFactory.registerProvider("yizhifu", async () => (await import("./yizhifu.js")).PaymentYizhifu);
}

View File

@@ -0,0 +1,136 @@
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
import WxPay from "wechatpay-node-v3";
import dayjs from "dayjs";
import { logger } from "@certd/basic"; // 支持使用require
import { WxpayAccess } from "../../../plugins/plugin-plus/wxpay/access.js";
export class PaymentWxpay implements IPaymentProvider {
access: WxpayAccess;
constructor(access: WxpayAccess) {
this.access = access;
}
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
/**
* const result = await pay.query({out_trade_no: '1609914303237'});
* # 或者 const result = await pay.query({transaction_id: ''});
* console.log(result);
* {
* status: 200,
* appid: 'appid',
* attach: '',
* mchid: '商户号',
* out_trade_no: '1609899981750',
* payer: {},
* promotion_detail: [],
* trade_state: 'CLOSED',
* trade_state_desc: '订单已关闭'
* }
*/
const pay = this.createSdk();
const result: any = await pay.query({ out_trade_no: tradeNo });
logger.info(`微信支付查询订单返回:${JSON.stringify(result)}`);
if (result.status !== 200) {
throw new Error("查询微信支付订单失败:" + result.status);
}
const data = result.data;
let status: string = undefined;
let payTime: number = undefined;
let amount: number = undefined;
if (data.trade_state === "SUCCESS") {
status = "paid";
payTime = dayjs(data.success_time).valueOf();
amount = data.amount.total;
} else if (data.trade_state === "CLOSED") {
status = "closed";
} else {
logger.info("微信支付订单状态为:" + data.trade_state);
}
return {
tradeNo: data.out_trade_no,
status,
amount,
payNo: data.transaction_id,
payTime,
};
}
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
const notify_url = `${opts.bindUrl}/api/payment/notify/wxpay`;
const pay = this.createSdk();
const params = {
description: trade.title,
out_trade_no: trade.tradeNo,
notify_url,
amount: {
total: trade.amount,
},
scene_info: {
payer_client_ip: "ip",
},
};
logger.info(`微信支付下单请求:${JSON.stringify(params)}`);
const result: any = await pay.transactions_native(params);
logger.info(`微信支付下单返回:${JSON.stringify(result)}`);
if (result.status !== 200) {
throw new Error("请求微信支付失败:" + result.status);
}
return {
qrcode: result.data.code_url,
};
}
private createSdk() {
const pay = new WxPay({
appid: this.access.appId,
mchid: this.access.mchid,
publicKey: Buffer.from(this.access.publicKey), // 公钥
privateKey: Buffer.from(this.access.privateKey), // 秘钥
});
return pay;
}
async onNotify(notifyData: any, updateTrade: UpdateTrade) {
const pay = this.createSdk();
const { ciphertext, associated_data, nonce } = notifyData.resource;
logger.info(`微信支付notify${JSON.stringify(notifyData)}`);
const key = this.access.key;
const result: any = pay.decipher_gcm(ciphertext, associated_data, nonce, key);
logger.info(`微信支付解析结果:${JSON.stringify(result)}`);
/**
* mchid: '商户号',
* # appid: 'appid',
* # out_trade_no: '1610419296553',
* # transaction_id: '4200000848202101120290526543',
* # trade_type: 'NATIVE',
* # trade_state: 'SUCCESS',
* # trade_state_desc: '支付成功',
* # bank_type: 'OTHERS',
* # attach: '',
* # success_time: '2021-01-12T10:43:43+08:00',
* # payer: { openid: '' },
* # amount: { total: 1, payer_total: 1, currency: 'CNY', payer_currency: 'CNY' }
*/
const data: any = result;
if (data.trade_state === "SUCCESS") {
await updateTrade({
tradeNo: data.out_trade_no,
status: "paid",
amount: data.amount.total,
payNo: data.transaction_id,
payTime: dayjs(data.success_time).valueOf(),
});
} else if (data.trade_state === "CLOSED") {
await updateTrade({
tradeNo: data.out_trade_no,
status: "closed",
payNo: data.transaction_id,
});
}
return "success";
}
}

View File

@@ -0,0 +1,184 @@
import { logger, utils } from "@certd/basic";
import { IPaymentProvider, TradeEntity, UpdateTrade, UpdateTradeInfo } from "@certd/commercial-core";
import dayjs from "dayjs";
import { YizhifuAccess } from "../../../plugins/plugin-plus/yizhifu/access.js";
export class PaymentYizhifu implements IPaymentProvider {
access: YizhifuAccess;
constructor(access: YizhifuAccess) {
this.access = access;
}
async getDetail(tradeNo: string): Promise<UpdateTradeInfo> {
/**
* http://pay.docmirror.cn/api.php?act=order&pid={商户ID}&key={商户密钥}&out_trade_no={商户订单号}
*
* 请求参数说明:
*
* 字段名 变量名 必填 类型 示例值 描述
* 操作类型 act 是 String order 此API固定值
* 商户ID pid 是 Int 1001
* 商户密钥 key 是 String 89unJUB8HZ54Hj7x4nUj56HN4nUzUJ8i
* 系统订单号 trade_no 选择 String 20160806151343312
* 商户订单号 out_trade_no 选择 String 20160806151343349
*/
const paymentReq = {
pid: this.access.pid,
act: "order",
key: this.access.key,
out_trade_no: tradeNo,
};
let url = this.access.url;
if (url.endsWith("/")) {
url = url.substring(0, url.length - 1);
}
const res = await utils.http.request({
url: url + "/api.php",
method: "get",
params: paymentReq,
});
if (res.code !== 1) {
throw new Error(res.msg);
}
if (res.status !== "1") {
throw new Error("该订单还未支付");
}
/**
* 易支付订单号 trade_no String 2016080622555342651 袖手科技聚合支付平台订单号
* 商户订单号 out_trade_no String 20160806151343349 商户系统内部的订单号
* 第三方订单号 api_trade_no String 20160806151343349 支付宝微信等接口方订单号
* 支付方式 type String alipay 支付方式列表
* 商户ID pid Int 1001 发起支付的商户ID
* 创建订单时间 addtime String 2016-08-06 22:55:52
* 完成交易时间 endtime String 2016-08-06 22:55:52
* 商品名称 name String VIP会员
* 商品金额 money String 1.00
* 支付状态 status Int 0 1为支付成功0为未支付
* 业务扩展参数 param String 默认留空
* 支付者账号 buyer String 默认留空
*/
let status: string = null;
let payTime: number = null;
if (res.status === "1") {
status = "paid";
payTime = dayjs(res.endtime).valueOf();
} else {
throw new Error("订单未支付");
}
return {
tradeNo: res.out_trade_no,
payNo: res.trade_no,
remark: "支付类型:" + res.type,
amount: utils.amount.toCent(parseFloat(res.money)),
status: status,
payTime: payTime,
};
}
sign(paymentReq: any): any {
const keys = Object.keys(paymentReq);
if (paymentReq.pid && paymentReq.pid + "" !== this.access.pid + "") {
throw new Error("pid not match");
}
paymentReq.pid = this.access.pid;
const params: any[] = [];
for (const key of keys) {
const value = paymentReq[key];
if (value != null && value !== "" && key !== "sign" && key !== "sign_type") {
params.push({ name: key, value: value });
}
}
/**
* 1、将发送或接收到的所有参数按照参数名ASCII码从小到大排序a-zsign、sign_type、和空值不参与签名
* 2、将排序后的参数拼接成URL键值对的格式例如 a=b&c=d&e=f参数值不要进行url编码。
* 3、再将拼接好的字符串与商户密钥KEY进行MD5加密得出sign签名参数sign = md5 ( a=b&c=d&e=f + KEY ) (注意:+ 为各语言的拼接符不是字符md5结果为小写。
*/
//sort
const sortedParams = params.sort((a, b) => a.name.localeCompare(b.name));
//join
const signStr = sortedParams.map(p => `${p.name}=${p.value}`).join("&");
//md5
const signType = this.access.signType;
let sign = "";
if (signType === "MD5") {
sign = utils.hash.md5(signStr + this.access.key);
} else if (signType === "SHA256") {
sign = utils.hash.sha256(signStr + this.access.key);
} else {
throw new Error("不支持的签名方式");
}
sign = sign.toLowerCase();
params.push({ name: "sign", value: sign });
params.push({ name: "sign_type", value: signType });
const body = {};
params.forEach(p => {
body[p.name] = p.value;
});
return body;
}
async createOrder(trade: TradeEntity, opts: { bindUrl: string; clientIp: string }) {
const { bindUrl } = opts;
const paymentReq: any = {
pid: this.access.pid,
out_trade_no: trade.tradeNo,
return_url: `${bindUrl}/#/certd/payment/return/yizhifu`,
notify_url: `${bindUrl}/api/payment/notify/yizhifu`,
name: trade.title,
money: utils.amount.toYuan(trade.amount),
};
if (this.access.payType) {
paymentReq.type = this.access.payType;
}
const body = this.sign(paymentReq);
let url = this.access.url;
if (url.endsWith("/")) {
url = url.substring(0, url.length - 1);
}
return {
url: url + "/submit.php",
body,
};
}
checkSign(paymentRes: any) {
// const { pid, trade_no, out_trade_no, type, name, money, trade_status, param, sign, sign_type } = paymentRes;
const body = this.sign(paymentRes);
const pass = body.sign === paymentRes.sign;
if (!pass) {
throw new Error("签名校验失败");
}
return pass;
}
async onNotify(paymentRes: any, updateTrade: UpdateTrade) {
logger.info(`易支付notify${JSON.stringify(paymentRes)}`);
this.checkSign(paymentRes);
const success = paymentRes.trade_status === "TRADE_SUCCESS";
if (success) {
await updateTrade({
tradeNo: paymentRes.out_trade_no,
status: "paid",
amount: utils.amount.toCent(parseFloat(paymentRes.money)),
remark: "支付类型:" + paymentRes.type,
payNo: paymentRes.trade_no,
payTime: dayjs().valueOf(),
});
}
return "success";
}
}

View File

@@ -43,4 +43,5 @@ export * from './plugin-ucloud/index.js'
export * from './plugin-goedge/index.js'
export * from './plugin-lib/index.js'
export * from './plugin-plus/index.js'
export * from './plugin-cert/index.js'
export * from './plugin-cert/index.js'
export * from './plugin-zenlayer/index.js'

View File

@@ -0,0 +1,155 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { OnePanelAccess } from "../access.js";
import { CertReader, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
import { OnePanelClient } from "../client.js";
@IsTaskPlugin({
name: "1PanelDeployToPanel",
title: "1Panel-部署面板证书",
icon: "svg:icon-onepanel",
desc: "更新1Panel的面板证书",
group: pluginGroups.panel.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
needPlus: false,
})
export class OnePanelDeployToPanelPlugin extends AbstractTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames],
},
required: true,
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine())
certDomains!: string[];
//授权选择框
@TaskInput({
title: "1Panel授权",
helper: "1Panel授权",
component: {
name: "access-selector",
type: "1panel",
},
required: true,
})
accessId!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: "1Panel节点",
helper: "要更新的1Panel证书的节点信息目前只有v2存在此概念",
typeName: "OnePanelDeployToPanelPlugin",
action: OnePanelDeployToPanelPlugin.prototype.onGetNodes.name,
value: "local",
required: true,
})
)
currentNode!: string;
access: OnePanelAccess;
async onInstance() {
this.access = await this.getAccess(this.accessId);
}
//http://xxx:xxxx/1panel/swagger/index.html#/App/get_apps__key
async execute(): Promise<void> {
const client = new OnePanelClient({
access: this.access,
http: this.http,
logger: this.logger,
utils: this.ctx.utils,
});
const certReader = new CertReader(this.cert);
const domain = certReader.getMainDomain();
if (this.access.apiVersion === "v1") {
const uploadRes = await client.doRequest({
// api/v1/settings/ssl/update
url: `/api/v1/settings/ssl/update`,
method: "post",
data: {
cert: this.cert.crt,
key: this.cert.key,
domain: domain,
ssl: "enable",
sslID: null,
sslType: "import-paste",
},
currentNode: this.currentNode,
});
console.log("uploadRes", JSON.stringify(uploadRes));
} else {
const uploadRes = await client.doRequest({
// api/v2/core/settings/ssl/update
url: `/api/v2/core/settings/ssl/update`,
method: "post",
data: {
cert: this.cert.crt,
key: this.cert.key,
domain: domain,
ssl: "Enable",
sslID: null,
sslType: "import-paste",
},
currentNode: this.currentNode,
});
console.log("uploadRes", JSON.stringify(uploadRes));
}
await this.ctx.utils.sleep(10000);
this.logger.info(`证书更新完成`);
}
isNeedUpdate(certRes: any) {
if (certRes.pem === this.cert.crt && certRes.key === this.cert.key) {
this.logger.info(`证书(id:${certRes.id})已经是最新的了,不需要更新`);
return false;
}
return true;
}
async onGetNodes() {
const options = [{ label: "主节点", value: "local" }];
if (this.access.apiVersion === "v1") {
return options;
}
if (!this.access) {
throw new Error("请先选择授权");
}
const client = new OnePanelClient({
access: this.access,
http: this.http,
logger: this.logger,
utils: this.ctx.utils,
});
const resp = await client.doRequest({
url: `/api/${this.access.apiVersion}/core/nodes/list`,
method: "post",
data: {},
});
// console.log('resp', resp)
return [...options, ...(resp?.map(item => ({ label: `${item.addr}(${item.name})`, value: item.name })) || [])];
}
}
new OnePanelDeployToPanelPlugin();

View File

@@ -1 +1,2 @@
export * from "./deploy-to-website.js";
export * from "./deploy-to-panel.js";

View File

@@ -0,0 +1,194 @@
import { HttpRequestConfig } from '@certd/basic';
import { IsAccess, AccessInput, BaseAccess, PageSearch } from '@certd/pipeline';
import qs from 'qs';
export type ZenlayerRequest = HttpRequestConfig & {
action: string;
version?: string;
}
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: 'zenlayer',
title: 'Zenlayer授权',
icon: 'svg:icon-lucky',
desc: 'Zenlayer授权',
})
export class ZenlayerAccess extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: 'AccessKeyId',
component: {
placeholder: '访问密钥ID',
},
helper: "[访问密钥管理](https://console.zenlayer.com/accessKey)获取",
required: true,
encrypt: false,
})
accessKeyId = '';
/**
* 授权属性配置
*/
@AccessInput({
title: 'AccessKey Password',
component: {
placeholder: '访问密钥密码',
},
required: true,
encrypt: true,
})
accessKeyPassword = '';
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
client: any;
async onTestRequest() {
const res = await this.getCertList({ pageSize: 1 });
this.ctx.logger.info(res);
return "ok";
}
async getCertList(req: PageSearch = {}):Promise<{totalCount:number,dataSet:{sans:string[],certificateId:string,certificateLabel:string,common:string}[]}> {
const pageNo = req.pageNo ?? 1;
const pageSize = req.pageSize ?? 100;
const res = await this.doRequest({
url: "/api/v2/cdn",
action: "DescribeCertificates",
data: {
PageNum: pageNo,
PageSize: pageSize
}
});
return res;
}
async getAuthorizationHeaders(req: ZenlayerRequest) {
/**
* CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
HexEncode(Hash(RequestPayload))
*/
if (!req.headers) {
req.headers = {};
}
if (!req.headers['content-type']) {
req.headers['content-type'] = "application/json; charset=utf-8";
}
if (!req.headers['host']) {
req.headers['host'] = "console.zenlayer.com";
}
if (!req.method) {
req.method = "POST";
}
// this.accessKeyPassword="Gu5t9xGARNpq86cd98joQYCN3"
// req.data = {"pageSize":10,"pageNum":1,"zoneId":"HKG-A"}
const CanonicalQueryString = req.method === 'POST' ? '' : qs.stringify(req.params);
const SignedHeaders = "content-type;host";
const CanonicalHeaders = `content-type:${req.headers['content-type']}\nhost:${req.headers['host']}\n`;
const HashedRequestPayload = this.ctx.utils.hash.sha256(JSON.stringify(req.data || {}), "hex");
const CanonicalRequest = `${req.method}\n/\n${CanonicalQueryString}\n${CanonicalHeaders}\n${SignedHeaders}\n${HashedRequestPayload}`;
let HashedCanonicalRequest = this.ctx.utils.hash.sha256(CanonicalRequest, "hex");
// HashedCanonicalRequest = "29396f9dfa0f03820b931e8aa06e20cda197e73285ebd76aceb83f7dede493ee"
const timestamp = Math.floor(Date.now() / 1000);
// const timestamp= 1673361177
const signMethod = "ZC2-HMAC-SHA256";
const StringToSign = `${signMethod}\n${timestamp}\n${HashedCanonicalRequest}`;
const signature = this.ctx.utils.hash.hmacSha256WithKey(this.accessKeyPassword, StringToSign, "hex");
const authorization = `${signMethod} Credential=${this.accessKeyId}, SignedHeaders=${SignedHeaders}, Signature=${signature}`;
/**
* X-ZC-Timestamp
请求的时间戳,精确到秒
1673361177
X-ZC-Version
请求的API版本
2022-11-20
X-ZC-Action
请求的动作
DescribeInstances
X-ZC-Signature-Method
签名方法
ZC2-HMAC-SHA256
Authorization
签名认证
*/
return {
...req.headers,
'X-ZC-Timestamp': timestamp.toString(),
'X-ZC-Action': req.action,
'X-ZC-Version': req.version || "2022-11-20",
'X-ZC-Signature-Method': signMethod,
'Authorization': authorization,
};
}
async doRequest(req: ZenlayerRequest) {
const headers = await this.getAuthorizationHeaders(req);
req.headers = headers
let res :any = undefined;
try{
res = await this.ctx.http.request({
baseURL: req.baseURL || "https://console.zenlayer.com",
...req
});
} catch (error) {
const resData = error.response?.data;
if (resData){
let desc = ""
if (resData.code === "CERTIFICATE_NOT_COVER_ALL_DOMAIN"){
desc = `证书未覆盖所有域名`;
}
throw new Error(`[code=${resData.code}] ${desc} ${resData.message} [requestId:${resData.requestId}]`);
}
throw error
}
if (res.code) {
throw new Error(`[${res.code}]:${res.message} [requestId:${res.requestId}]`);
}
return res.response;
}
}
new ZenlayerAccess();

View File

@@ -0,0 +1,2 @@
export * from './access.js';
export * from './plugins/index.js';

View File

@@ -0,0 +1 @@
export * from './plugin-refresh-cert.js';

View File

@@ -0,0 +1,139 @@
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 { ZenlayerAccess } from "../access.js";
@IsTaskPlugin({
//命名规范,插件类型+功能就是目录plugin-demo中的demo大写字母开头驼峰命名
name: "ZenlayerRefreshCert",
title: "Zenlayer-刷新证书",
desc: "刷新Zenlayer CDN证书",
icon: "svg:icon-lucky",
//插件分组
group: pluginGroups.cdn.key,
needPlus: false,
default: {
//默认值配置照抄即可
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed
}
}
})
//类名规范跟上面插件名称name一致
export class ZenlayerRefreshCert extends AbstractTaskPlugin {
//证书选择,此项必须要有
@TaskInput({
title: "域名证书",
helper: "请选择前置任务输出的域名证书",
component: {
name: "output-selector",
from: [...CertApplyPluginNames]
}
// required: true, // 必填
})
cert!: CertInfo;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
//授权选择框
@TaskInput({
title: "Zenlayer授权",
component: {
name: "access-selector",
type: "zenlayer" //固定授权类型
},
required: true //必填
})
accessId!: string;
//
@TaskInput(
createRemoteSelectInputDefine({
title: "证书ID列表",
helper: "要更新的Zenlayer证书ID列表",
action: ZenlayerRefreshCert.prototype.onGetCertList.name
})
)
certList!: string[];
//插件实例化时执行的方法
async onInstance() {
}
//插件执行方法
async execute(): Promise<void> {
const access = await this.getAccess<ZenlayerAccess>(this.accessId);
for (const certId of this.certList) {
await this.updateCert({
access: access,
certId: certId,
cert: this.cert
});
this.logger.info(`刷新证书${certId}成功`);
await this.ctx.utils.sleep(1000);
}
this.logger.info("部署完成");
}
async updateCert(req:{access:ZenlayerAccess,certId:string, cert: CertInfo}){
const {access,certId, cert} = req;
// ModifyCertificate
await access.doRequest({
url: "/api/v2/cdn",
action: "ModifyCertificate",
data: {
/**
* certificateId
certificateContent
certificateKey
*/
certificateId: certId,
certificateContent: cert.crt,
certificateKey: cert.key,
}
});
}
async onGetCertList(req: PageSearch = {}) {
const access = await this.getAccess<ZenlayerAccess>(this.accessId);
const pageNo = req.pageNo ?? 1;
const pageSize = req.pageSize ?? 100;
const res = await access.getCertList(
{
pageNo: pageNo,
pageSize: pageSize
}
);
const total = res.totalCount;
const list = res.dataSet || [];
if (!list || list.length === 0) {
throw new Error("没有找到Zenlayer证书请先在控制台CDN证书管理创建证书");
}
/**
* "Domain": "ucloud.certd.handsfree.work",
"DomainId": "ucdn-1kwdtph5ygbb"
*/
const options = list.map((item: any) => {
return {
label: `${item.certificateLabel}<${item.certificateId}-${item.common}>`,
value: `${item.certificateId}`,
domain: item.sans
};
});
return {
list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains),
total: total,
pageNo: pageNo,
pageSize: pageSize
};
}
}
//实例化一下,注册插件
new ZenlayerRefreshCert();