mirror of
https://github.com/certd/certd.git
synced 2026-04-23 19:57:27 +08:00
Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev
This commit is contained in:
@@ -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'
|
||||
@@ -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();
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./deploy-to-website.js";
|
||||
export * from "./deploy-to-panel.js";
|
||||
@@ -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();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './access.js';
|
||||
export * from './plugins/index.js';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './plugin-refresh-cert.js';
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user