mirror of
https://github.com/certd/certd.git
synced 2026-04-22 02:47:25 +08:00
feat: 【破坏性更新】插件改为metadata加载模式,plugin-cert、plugin-lib包部分代码转移到certd-server中,影响自定义插件,需要修改相关import引用
ssh、aliyun、tencent、qiniu、oss等 access和client需要转移import
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { HttpClient } from "@certd/basic";
|
||||
import { OnePanelClient } from "./client.js";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "1panel",
|
||||
title: "1panel授权",
|
||||
desc: "账号和密码",
|
||||
icon: "svg:icon-onepanel",
|
||||
})
|
||||
export class OnePanelAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "1Panel面板的url",
|
||||
component: {
|
||||
placeholder: "http://xxxx.com:1231",
|
||||
},
|
||||
helper: "不要带安全入口",
|
||||
required: true,
|
||||
})
|
||||
baseUrl = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "安全入口",
|
||||
component: {
|
||||
placeholder: "登录的安全入口",
|
||||
},
|
||||
encrypt: true,
|
||||
required: false,
|
||||
})
|
||||
safeEnter = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "授权方式",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ label: "模拟登录【不推荐】", value: "password" },
|
||||
{ label: "接口密钥【推荐】", value: "apikey" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
type = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "接口版本",
|
||||
value: "v1",
|
||||
component: {
|
||||
placeholder: "v1 / v2",
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ label: "v1", value: "v1" },
|
||||
{ label: "v2", value: "v2" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
apiVersion = "v1";
|
||||
|
||||
@AccessInput({
|
||||
title: "用户名",
|
||||
component: {
|
||||
placeholder: "username",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.type === 'password';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
})
|
||||
username = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "密码",
|
||||
component: {
|
||||
placeholder: "password",
|
||||
},
|
||||
helper: "",
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.type === 'password';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "接口密钥",
|
||||
component: {
|
||||
placeholder: "接口密钥",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.type === 'apikey';
|
||||
})
|
||||
}
|
||||
`,
|
||||
helper: "面板设置->API接口中获取",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiKey = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "忽略证书校验",
|
||||
value: true,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "如果面板的url是https,且使用的是自签名证书,则需要开启此选项,其他情况可以关闭",
|
||||
})
|
||||
skipSslVerify = true;
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "onTestRequest",
|
||||
},
|
||||
helper: "点击测试接口看是否正常\nIP需要加白名单,如果是同一台机器部署的,可以试试面板的url使用网卡docker0的ip,白名单使用172.16.0.0/12",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const http: HttpClient = this.ctx.http;
|
||||
const client = new OnePanelClient({
|
||||
logger: this.ctx.logger,
|
||||
http,
|
||||
access: this,
|
||||
utils: this.ctx.utils,
|
||||
});
|
||||
|
||||
await client.doRequest({
|
||||
url: `/api/${this.apiVersion}/websites/ssl/search`,
|
||||
method: "post",
|
||||
data: {
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
},
|
||||
});
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
new OnePanelAccess();
|
||||
@@ -0,0 +1,228 @@
|
||||
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
|
||||
import { OnePanelAccess } from "./access.js";
|
||||
|
||||
export class OnePanelClient {
|
||||
access: OnePanelAccess;
|
||||
http: HttpClient;
|
||||
logger: ILogger;
|
||||
utils: any;
|
||||
token: string;
|
||||
constructor(opts: { access: OnePanelAccess; http: HttpClient; logger: ILogger; utils: any }) {
|
||||
this.access = opts.access;
|
||||
this.http = opts.http;
|
||||
this.logger = opts.logger;
|
||||
this.utils = opts.utils;
|
||||
}
|
||||
|
||||
//
|
||||
// //http://xxx:xxxx/1panel/swagger/index.html#/App/get_apps__key
|
||||
// async execute(): Promise<void> {
|
||||
// //login 获取token
|
||||
// /**
|
||||
// * curl 'http://127.0.0.1:7001/api/v1/auth/login' --data-binary '{"name":"admin_test","password":"admin_test1234","ignoreCaptcha":true,"captcha":"","captchaID":"nY8Cqeut3TjZMfJMAz0k","authMethod":"jwt","language":"zh"}' -H 'EntranceCode: emhhbmd5eg=='
|
||||
// * curl 'http://127.0.0.1:7001/api/v1/dashboard/current/all/all' -H 'PanelAuthorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MCwiTmFtZSI6ImFkbWluX3Rlc3QiLCJCdWZmZXJUaW1lIjozNjAwLCJpc3MiOiIxUGFuZWwiLCJleHAiOjE3MDkyODg4MDl9.pdknJdjLY4Fp8wCE9Gvaiic2rLoSdvUSJB9ossyya_I'
|
||||
// */
|
||||
// const sslIds = this.sslIds;
|
||||
// for (const sslId of sslIds) {
|
||||
// try {
|
||||
// const certRes = await this.get1PanelCertInfo(sslId);
|
||||
// if (!this.isNeedUpdate(certRes)) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// const uploadRes = await this.doRequest({
|
||||
// url: "/api/v1/websites/ssl/upload",
|
||||
// method: "post",
|
||||
// data: {
|
||||
// sslIds,
|
||||
// certificate: this.cert.crt,
|
||||
// certificatePath: "",
|
||||
// description: certRes.description || this.appendTimeSuffix("certd"),
|
||||
// privateKey: this.cert.key,
|
||||
// privateKeyPath: "",
|
||||
// sslID: sslId,
|
||||
// type: "paste",
|
||||
// },
|
||||
// });
|
||||
// console.log("uploadRes", JSON.stringify(uploadRes));
|
||||
// } catch (e) {
|
||||
// this.logger.warn(`更新证书(id:${sslId})失败`, e);
|
||||
// this.logger.info("可能1Panel正在重启,等待10秒后检查证书是否更新成功");
|
||||
// await this.ctx.utils.sleep(10000);
|
||||
// const certRes = await this.get1PanelCertInfo(sslId);
|
||||
// if (!this.isNeedUpdate(certRes)) {
|
||||
// continue;
|
||||
// }
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
async get1PanelCertInfo(sslId: string) {
|
||||
const certRes = await this.doRequest({
|
||||
url: `/api/${this.access.apiVersion}/websites/ssl/${sslId}`,
|
||||
method: "get",
|
||||
});
|
||||
if (!certRes) {
|
||||
throw new Error(`没有找到证书(id:${sslId}),请先在1Panel中手动上传证书,后续才可以自动更新`);
|
||||
}
|
||||
return certRes;
|
||||
}
|
||||
|
||||
async doRequest(config: { currentNode?: string } & HttpRequestConfig<any>) {
|
||||
const tokenHeaders = await this.getAccessToken();
|
||||
config.headers = {
|
||||
...tokenHeaders,
|
||||
};
|
||||
if (config.currentNode) {
|
||||
config.headers.CurrentNode = this.getNodeValue(config.currentNode);
|
||||
delete config.currentNode;
|
||||
}
|
||||
return await this.doRequestWithoutAuth(config);
|
||||
}
|
||||
|
||||
async doRequestWithoutAuth(config: HttpRequestConfig<any>) {
|
||||
config.baseURL = this.access.baseUrl;
|
||||
config.skipSslVerify = this.access.skipSslVerify ?? false;
|
||||
config.logRes = false;
|
||||
config.logParams = false;
|
||||
const res = await this.http.request(config);
|
||||
if (config.returnOriginRes) {
|
||||
return res;
|
||||
}
|
||||
if (res.code === 200) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
async getCookie(name: string) {
|
||||
// https://www.docmirror.cn:20001/api/v1/auth/language
|
||||
const response = await this.doRequestWithoutAuth({
|
||||
url: `/api/${this.access.apiVersion}/auth/language`,
|
||||
method: "GET",
|
||||
returnOriginRes: true,
|
||||
});
|
||||
const cookies = response.headers["set-cookie"];
|
||||
//根据name 返回对应的cookie
|
||||
const found = cookies.find(cookie => cookie.includes(name));
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
const cookie = found.split(";")[0];
|
||||
return cookie.substring(cookie.indexOf("=") + 1);
|
||||
}
|
||||
|
||||
async encryptPassword(password: string) {
|
||||
const rsaPublicKeyText = await this.getCookie("panel_public_key");
|
||||
if (!rsaPublicKeyText) {
|
||||
return password;
|
||||
}
|
||||
// 使用rsa加密
|
||||
const { encryptPassword } = await import("./util.js");
|
||||
return encryptPassword(rsaPublicKeyText, password);
|
||||
}
|
||||
|
||||
async getAccessToken() {
|
||||
if (this.access.type === "apikey") {
|
||||
return this.getAccessTokenByApiKey();
|
||||
} else {
|
||||
return await this.getAccessTokenByPassword();
|
||||
}
|
||||
}
|
||||
|
||||
async getAccessTokenByApiKey() {
|
||||
/**
|
||||
* Token = md5('1panel' + API-Key + UnixTimestamp)
|
||||
* 组成部分:
|
||||
* 固定前缀: '1panel'
|
||||
* API-Key: 面板 API 接口密钥
|
||||
* UnixTimestamp: 当前的时间戳(秒级)
|
||||
* 请求 Header 设计¶
|
||||
* 每次请求必须携带以下两个 Header:
|
||||
*
|
||||
* Header 名称 说明
|
||||
* 1Panel-Token 自定义的 Token 值
|
||||
* 1Panel-Timestamp 当前时间戳
|
||||
* 示例请求头:¶
|
||||
*
|
||||
* curl -X GET "http://localhost:4004/api/v1/dashboard/current" \
|
||||
* -H "1Panel-Token: <1panel_token>" \
|
||||
* -H "1Panel-Timestamp: <current_unix_timestamp>"
|
||||
*/
|
||||
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const token = this.utils.hash.md5(`1panel${this.access.apiKey}${timestamp}`);
|
||||
return {
|
||||
"1Panel-Token": token,
|
||||
"1Panel-Timestamp": timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
async getAccessTokenByPassword() {
|
||||
// console.log("getAccessToken", this);
|
||||
// const tokenCacheKey = `1panel-token-${this.accessId}`;
|
||||
// let token = this.utils.cache.get(tokenCacheKey);
|
||||
// if (token) {
|
||||
// return token;
|
||||
// }
|
||||
if (this.token) {
|
||||
return {
|
||||
PanelAuthorization: this.token,
|
||||
};
|
||||
}
|
||||
let password = this.access.password;
|
||||
password = await this.encryptPassword(password);
|
||||
const loginRes = await this.doRequestWithoutAuth({
|
||||
url: `/api/${this.access.apiVersion}/auth/login`,
|
||||
method: "post",
|
||||
headers: {
|
||||
EntranceCode: Buffer.from(this.access.safeEnter).toString("base64"),
|
||||
},
|
||||
data: {
|
||||
name: this.access.username,
|
||||
password: password,
|
||||
ignoreCaptcha: true,
|
||||
captcha: "",
|
||||
captchaID: "",
|
||||
authMethod: "jwt",
|
||||
language: "zh",
|
||||
},
|
||||
});
|
||||
this.token = loginRes.token;
|
||||
|
||||
return {
|
||||
PanelAuthorization: this.token,
|
||||
};
|
||||
}
|
||||
|
||||
async onGetSSLIds() {
|
||||
// if (!isPlus()) {
|
||||
// throw new Error("自动获取站点列表为专业版功能,您可以手动输入证书id进行部署");
|
||||
// }
|
||||
const res = await this.doRequest({
|
||||
url: `/api/${this.access.apiVersion}/websites/ssl/search`,
|
||||
method: "post",
|
||||
data: {
|
||||
page: 1,
|
||||
pageSize: 99999,
|
||||
},
|
||||
});
|
||||
if (!res?.items) {
|
||||
throw new Error("没有找到证书,请先在1Panel中手动上传证书,并关联站点,后续才可以自动更新");
|
||||
}
|
||||
const options = res.items.map(item => {
|
||||
return {
|
||||
label: `${item.primaryDomain}<${item.id},${item.description || "无备注"}>`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
getNodeValue(node?: string) {
|
||||
const node_master_key = "local";
|
||||
const _value = node || node_master_key;
|
||||
return encodeURIComponent(_value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
export * from "./client.js";
|
||||
@@ -0,0 +1,212 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { OnePanelAccess } from "../access.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { OnePanelClient } from "../client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "1PanelDeployToWebsitePlugin",
|
||||
title: "1Panel-部署证书到1Panel",
|
||||
icon: "svg:icon-onepanel",
|
||||
desc: "更新1Panel的证书",
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class OnePanelDeployToWebsitePlugin 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: "OnePanelDeployToWebsitePlugin",
|
||||
action: OnePanelDeployToWebsitePlugin.prototype.onGetNodes.name,
|
||||
value: "local",
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
currentNode!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "1Panel证书ID",
|
||||
typeName: "1PanelDeployToWebsitePlugin",
|
||||
action: OnePanelDeployToWebsitePlugin.prototype.onGetSSLIds.name,
|
||||
watches: ["accessId"],
|
||||
helper: "要更新的1Panel证书id,选择授权之后,从下拉框中选择\nIP需要加白名单,如果是同一台机器部署的,可以试试172.16.0.0/12",
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
sslIds!: 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> {
|
||||
//login 获取token
|
||||
/**
|
||||
* curl 'http://127.0.0.1:7001/api/v1/auth/login' --data-binary '{"name":"admin_test","password":"admin_test1234","ignoreCaptcha":true,"captcha":"","captchaID":"nY8Cqeut3TjZMfJMAz0k","authMethod":"jwt","language":"zh"}' -H 'EntranceCode: emhhbmd5eg=='
|
||||
* curl 'http://127.0.0.1:7001/api/v1/dashboard/current/all/all' -H 'PanelAuthorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MCwiTmFtZSI6ImFkbWluX3Rlc3QiLCJCdWZmZXJUaW1lIjozNjAwLCJpc3MiOiIxUGFuZWwiLCJleHAiOjE3MDkyODg4MDl9.pdknJdjLY4Fp8wCE9Gvaiic2rLoSdvUSJB9ossyya_I'
|
||||
*/
|
||||
|
||||
const client = new OnePanelClient({
|
||||
access: this.access,
|
||||
http: this.http,
|
||||
logger: this.logger,
|
||||
utils: this.ctx.utils,
|
||||
});
|
||||
|
||||
const sslIds = this.sslIds;
|
||||
for (const sslId of sslIds) {
|
||||
try {
|
||||
const certRes = await this.get1PanelCertInfo(client, sslId);
|
||||
if (!this.isNeedUpdate(certRes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uploadRes = await client.doRequest({
|
||||
url: `/api/${this.access.apiVersion}/websites/ssl/upload`,
|
||||
method: "post",
|
||||
data: {
|
||||
sslIds,
|
||||
certificate: this.cert.crt,
|
||||
certificatePath: "",
|
||||
description: certRes.description || this.appendTimeSuffix("certd"),
|
||||
privateKey: this.cert.key,
|
||||
privateKeyPath: "",
|
||||
sslID: sslId,
|
||||
type: "paste",
|
||||
},
|
||||
currentNode: this.currentNode,
|
||||
});
|
||||
console.log("uploadRes", JSON.stringify(uploadRes));
|
||||
} catch (e) {
|
||||
this.logger.warn(`更新证书(id:${sslId})失败`, e);
|
||||
this.logger.info("可能1Panel正在重启,等待10秒后检查证书是否更新成功");
|
||||
await this.ctx.utils.sleep(10000);
|
||||
const certRes = await this.get1PanelCertInfo(client, sslId);
|
||||
if (!this.isNeedUpdate(certRes)) {
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 get1PanelCertInfo(client: OnePanelClient, sslId: string) {
|
||||
const certRes = await client.doRequest({
|
||||
url: `/api/${this.access.apiVersion}/websites/ssl/${sslId}`,
|
||||
method: "get",
|
||||
currentNode: this.currentNode,
|
||||
});
|
||||
if (!certRes) {
|
||||
throw new Error(`没有找到证书(id:${sslId}),请先在1Panel中手动上传证书,后续才可以自动更新`);
|
||||
}
|
||||
return certRes;
|
||||
}
|
||||
|
||||
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 })) || [])];
|
||||
}
|
||||
|
||||
// requestHandle
|
||||
async onGetSSLIds() {
|
||||
// if (!isPlus()) {
|
||||
// throw new Error("自动获取站点列表为专业版功能,您可以手动输入证书id进行部署");
|
||||
// }
|
||||
if (!this.access) {
|
||||
throw new Error("请先选择授权");
|
||||
}
|
||||
const client = new OnePanelClient({
|
||||
access: this.access,
|
||||
http: this.http,
|
||||
logger: this.logger,
|
||||
utils: this.ctx.utils,
|
||||
});
|
||||
const res = await client.doRequest({
|
||||
url: `api/${this.access.apiVersion}/websites/ssl/search`,
|
||||
method: "post",
|
||||
data: {
|
||||
page: 1,
|
||||
pageSize: 99999,
|
||||
},
|
||||
currentNode: this.currentNode,
|
||||
});
|
||||
if (!res?.items) {
|
||||
throw new Error("没有找到证书,请先在1Panel中手动上传证书,并关联站点,后续才可以自动更新");
|
||||
}
|
||||
const list = res.items.map(item => {
|
||||
const domains = item.domains ? [] : item.domains.split(",");
|
||||
const allDomains = [item.primaryDomain, ...domains];
|
||||
return {
|
||||
label: `${item.primaryDomain}<${item.id},${item.description || "无备注"}>`,
|
||||
value: item.id,
|
||||
domain: allDomains,
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(list, this.certDomains);
|
||||
}
|
||||
}
|
||||
new OnePanelDeployToWebsitePlugin();
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./deploy-to-website.js";
|
||||
@@ -0,0 +1,65 @@
|
||||
import CryptoJS from "crypto-js";
|
||||
import crypto from "crypto";
|
||||
function rsaEncrypt(data: string, publicKey: string) {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
// const jsEncrypt = new JSEncrypt();
|
||||
// jsEncrypt.setPublicKey(publicKey);
|
||||
// return jsEncrypt.encrypt(data);
|
||||
|
||||
// RSA encryption is not supported in browser
|
||||
//换一种nodejs的实现
|
||||
return crypto
|
||||
.publicEncrypt(
|
||||
{
|
||||
key: publicKey,
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING, // 明确指定填充方式
|
||||
},
|
||||
Buffer.from(data, "utf-8")
|
||||
)
|
||||
.toString("base64");
|
||||
}
|
||||
|
||||
function aesEncrypt(data: string, key: string) {
|
||||
const keyBytes = CryptoJS.enc.Utf8.parse(key);
|
||||
const iv = CryptoJS.lib.WordArray.random(16);
|
||||
const encrypted = CryptoJS.AES.encrypt(data, keyBytes, {
|
||||
iv: iv,
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return iv.toString(CryptoJS.enc.Base64) + ":" + encrypted.toString();
|
||||
}
|
||||
|
||||
function urlDecode(value: string): string {
|
||||
return decodeURIComponent(value.replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
function generateAESKey(): string {
|
||||
const keyLength = 16;
|
||||
const randomBytes = new Uint8Array(keyLength);
|
||||
crypto.getRandomValues(randomBytes);
|
||||
return Array.from(randomBytes)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
export const encryptPassword = (rsaPublicKeyText: string, password: string) => {
|
||||
if (!password) {
|
||||
return "";
|
||||
}
|
||||
// let rsaPublicKeyText = getCookie("panel_public_key");
|
||||
if (!rsaPublicKeyText) {
|
||||
console.log("RSA public key not found");
|
||||
return password;
|
||||
}
|
||||
rsaPublicKeyText = urlDecode(rsaPublicKeyText);
|
||||
|
||||
const aesKey = generateAESKey();
|
||||
rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', "");
|
||||
const rsaPublicKey = atob(rsaPublicKeyText);
|
||||
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey);
|
||||
const passwordCipher = aesEncrypt(password, aesKey);
|
||||
return `${keyCipher}:${passwordCipher}`;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
@IsAccess({
|
||||
name: "alipay",
|
||||
title: "支付宝",
|
||||
icon: "ion:logo-alipay",
|
||||
})
|
||||
export class AlipayAccess extends BaseAccess {
|
||||
/**
|
||||
* appId: "<-- 请填写您的AppId,例如:2019091767145019 -->",
|
||||
* privateKey: "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->",
|
||||
* alipayPublicKey: "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->",
|
||||
*/
|
||||
@AccessInput({
|
||||
title: "AppId",
|
||||
component: {
|
||||
placeholder: "201909176714xxxx",
|
||||
},
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
appId: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "应用私钥",
|
||||
component: {
|
||||
placeholder: "MIIEvQIBADANB...",
|
||||
name: "a-textarea",
|
||||
rows: 3,
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
privateKey: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "支付宝公钥",
|
||||
component: {
|
||||
name: "a-textarea",
|
||||
rows: 3,
|
||||
placeholder: "MIIBIjANBg...",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
alipayPublicKey: string;
|
||||
}
|
||||
|
||||
new AlipayAccess();
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1,61 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { BaiduYunCertClient } from "./client.js";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "baidu",
|
||||
title: "百度云授权",
|
||||
desc: "",
|
||||
icon: "ant-design:baidu-outlined",
|
||||
order: 2,
|
||||
})
|
||||
export class BaiduAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "AccessKey",
|
||||
component: {
|
||||
placeholder: "AccessKey",
|
||||
},
|
||||
helper: "[百度智能云->安全认证获取](https://console.bce.baidu.com/iam/#/iam/accesslist)",
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
accessKey = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "SecretKey",
|
||||
component: {
|
||||
placeholder: "SecretKey",
|
||||
},
|
||||
helper: "",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
secretKey = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "onTestRequest",
|
||||
},
|
||||
helper: "点击测试接口看是否正常",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const certClient = new BaiduYunCertClient({
|
||||
access: this,
|
||||
logger: this.ctx.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
|
||||
const res = await certClient.getCertList();
|
||||
this.ctx.logger.info("测试接口返回", res);
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
new BaiduAccess();
|
||||
@@ -0,0 +1,191 @@
|
||||
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
|
||||
import { BaiduAccess } from "./access.js";
|
||||
import crypto from "crypto";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
|
||||
export type BaiduYunClientOptions = {
|
||||
access: BaiduAccess;
|
||||
logger: ILogger;
|
||||
http: HttpClient;
|
||||
};
|
||||
export type BaiduYunReq = {
|
||||
host: string;
|
||||
uri: string;
|
||||
body?: any;
|
||||
headers?: any;
|
||||
query?: any;
|
||||
method: string;
|
||||
};
|
||||
export class BaiduYunClient {
|
||||
opts: BaiduYunClientOptions;
|
||||
constructor(opts: BaiduYunClientOptions) {
|
||||
this.opts = opts;
|
||||
}
|
||||
|
||||
// 调用百度云接口,传接口uri和json参数
|
||||
async doRequest(req: BaiduYunReq, config?: HttpRequestConfig) {
|
||||
const host = req.host;
|
||||
const timestamp = this.getTimestampString();
|
||||
const queryString = this.getQueryString(req.query);
|
||||
const Authorization = this.getAuthString(host, req.method, req.uri, queryString, timestamp);
|
||||
|
||||
const ContentType = "application/json; charset=utf-8";
|
||||
|
||||
let url = "https://" + host + req.uri;
|
||||
if (req.query) {
|
||||
url += "?" + queryString;
|
||||
}
|
||||
const res = await this.opts.http.request({
|
||||
url: url,
|
||||
method: req.method,
|
||||
data: req.body,
|
||||
headers: {
|
||||
Authorization: Authorization,
|
||||
"Content-Type": ContentType,
|
||||
Host: host,
|
||||
"x-bce-date": timestamp,
|
||||
...req.headers,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
if (res.code) {
|
||||
throw new Error(`请求失败:${res.message}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// 获取UTC时间
|
||||
getTimestampString() {
|
||||
return new Date().toISOString().replace(/\.\d*/, "");
|
||||
}
|
||||
|
||||
// 获取参数拼接字符串
|
||||
getQueryString(params) {
|
||||
let queryString = "";
|
||||
let paramKeyArray = [];
|
||||
if (params) {
|
||||
for (const key in params) {
|
||||
paramKeyArray.push(key);
|
||||
}
|
||||
paramKeyArray = paramKeyArray.sort();
|
||||
}
|
||||
if (paramKeyArray && paramKeyArray.length > 0) {
|
||||
for (const key of paramKeyArray) {
|
||||
queryString += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]) + "&";
|
||||
}
|
||||
queryString = queryString.substring(0, queryString.length - 1);
|
||||
}
|
||||
return queryString;
|
||||
}
|
||||
|
||||
uriEncode(input: string, encodeSlash = false) {
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const ch = input.charAt(i);
|
||||
|
||||
if ((ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_" || ch === "-" || ch === "~" || ch === ".") {
|
||||
result += ch;
|
||||
} else if (ch === "/") {
|
||||
result += encodeSlash ? "%2F" : ch;
|
||||
} else {
|
||||
result += this.toHexUTF8(ch);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
toHexUTF8(ch) {
|
||||
// Convert character to UTF-8 bytes and return the hex representation
|
||||
const utf8Bytes = new TextEncoder().encode(ch);
|
||||
let hexString = "";
|
||||
|
||||
for (const byte of utf8Bytes) {
|
||||
hexString += "%" + byte.toString(16).padStart(2, "0").toUpperCase();
|
||||
}
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
// 签名
|
||||
getAuthString(Host: string, Method: string, CanonicalURI: string, CanonicalQueryString: string, timestamp: string) {
|
||||
// 1
|
||||
const expirationPeriodInSeconds = 120;
|
||||
const authStringPrefix = `bce-auth-v1/${this.opts.access.accessKey}/${timestamp}/${expirationPeriodInSeconds}`;
|
||||
// 2
|
||||
const signedHeaders = "host;x-bce-date";
|
||||
const CanonicalHeaders = encodeURIComponent("host") + ":" + encodeURIComponent(Host) + "\n" + encodeURIComponent("x-bce-date") + ":" + encodeURIComponent(timestamp);
|
||||
const CanonicalRequest = Method.toUpperCase() + "\n" + this.uriEncode(CanonicalURI, false) + "\n" + CanonicalQueryString + "\n" + CanonicalHeaders;
|
||||
// 3
|
||||
const SigningKey = crypto.createHmac("sha256", this.opts.access.secretKey).update(authStringPrefix).digest().toString("hex");
|
||||
// 4
|
||||
const Signature = crypto.createHmac("sha256", SigningKey).update(CanonicalRequest).digest().toString("hex");
|
||||
// 5
|
||||
return `${authStringPrefix}/${signedHeaders}/${Signature}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class BaiduYunCertClient {
|
||||
client: BaiduYunClient;
|
||||
constructor(opts: BaiduYunClientOptions) {
|
||||
this.client = new BaiduYunClient(opts);
|
||||
}
|
||||
|
||||
async createCert(opts: { certName: string; cert: CertInfo }) {
|
||||
// /v1/certificate
|
||||
const res = await this.client.doRequest({
|
||||
host: "certificate.baidubce.com",
|
||||
uri: `/v1/certificate`,
|
||||
method: "post",
|
||||
body: {
|
||||
/**
|
||||
* certName String 必须 证书的名称。长度限制为1-65个字符,以字母开头,只允许包含字母、数字、’-‘、’/’、’.’、’’,Java正则表达式` ^[a-zA-Z]a-zA-Z0-9\-/\.]{2,64}$`
|
||||
* certServerData String 必须 服务器证书的数据内容 (Base64编码)
|
||||
* certPrivateData String 必须 证书的私钥数据内容 (Base64编码)
|
||||
*/
|
||||
certName: "certd_" + opts.certName, // 字母开头,且小于64长度
|
||||
certServerData: opts.cert.crt,
|
||||
certPrivateData: opts.cert.key,
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
async getCertList() {
|
||||
/**
|
||||
* GET /v1/certificate HTTP/1.1
|
||||
* HOST: certificate.baidubce.com
|
||||
* Authorization: {authorization}
|
||||
* Content-Type: application/json; charset=utf-8
|
||||
* x-bce-date: 2014-06-01T23:00:10Z
|
||||
*/
|
||||
return await this.client.doRequest({
|
||||
host: "certificate.baidubce.com",
|
||||
uri: `/v1/certificate`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
async updateCert(opts: { certId: string; certName: string; cert: CertInfo }) {
|
||||
/**
|
||||
* /v1/certificate/{certId}?certData
|
||||
* certName String 必须 证书的名称。长度限制为1-65个字符,以字母开头,只允许包含字母、数字、’-‘、’/’、’.’、’’,Java正则表达式` ^[a-zA-Z]a-zA-Z0-9\-/\.]{2,64}$`
|
||||
* certServerData String 必须 服务器证书的数据内容 (Base64编码)
|
||||
* certPrivateData String 必须 证书的私钥数据内容 (Base64编码)
|
||||
* certLinkData String 可选 证书链数据内容 (Base64编码)
|
||||
* certType Integer 可选 证书类型,非必填,默认为1
|
||||
*/
|
||||
|
||||
return await this.client.doRequest({
|
||||
host: "certificate.baidubce.com",
|
||||
uri: `/v1/certificate/${opts.certId}`,
|
||||
method: "put",
|
||||
body: {
|
||||
certName: opts.certName,
|
||||
certServerData: opts.cert.crt,
|
||||
certPrivateData: opts.cert.key,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
export * from "./client.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugin-deploy-to-cdn.js";
|
||||
export * from "./plugin-deploy-to-blb.js";
|
||||
export * from "./plugin-upload-to-baidu.js";
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine } from "@certd/plugin-lib";
|
||||
import { BaiduYunCertClient, BaiduYunClient } from "../client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "BaiduDeployToBLB",
|
||||
title: "百度云-部署证书到负载均衡",
|
||||
icon: "ant-design:baidu-outlined",
|
||||
group: pluginGroups.baidu.key,
|
||||
desc: "部署到百度云负载均衡,包括BLB、APPBLB",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class BaiduDeployToBLBPlugin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames, "BaiduUploadCert"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo | string;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine())
|
||||
certDomains!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "区域",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
/**
|
||||
* 北京 blb.bj.baidubce.com HTTP/HTTPS
|
||||
* 广州 blb.gz.baidubce.com HTTP/HTTPS
|
||||
* 苏州 blb.su.baidubce.com HTTP/HTTPS
|
||||
* 香港 blb.hkg.baidubce.com HTTP/HTTPS
|
||||
* 武汉 blb.fwh.baidubce.com HTTP/HTTPS
|
||||
* 保定 blb.bd.baidubce.com HTTP/HTTPS
|
||||
* 上海 blb.fsh.baidubce.com HTTP/HTTPS
|
||||
* 新加坡 blb.sin.baidubce.com HTTP/HTTPS
|
||||
*/
|
||||
{ value: "bj", label: "北京" },
|
||||
{ value: "fsh", label: "上海" },
|
||||
{ value: "gz", label: "广州" },
|
||||
{ value: "fwh", label: "武汉" },
|
||||
{ value: "su", label: "苏州" },
|
||||
{ value: "bd", label: "保定" },
|
||||
{ value: "hkg", label: "香港" },
|
||||
{ value: "sin", label: "新加坡" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
region!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "负载均衡类型",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "blb", label: "普通负载均衡" },
|
||||
{ value: "appblb", label: "应用负载均衡" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
blbType!: string;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "百度云授权",
|
||||
helper: "百度云授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baidu",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "负载均衡ID",
|
||||
component: {
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
action: "GetBLBList",
|
||||
watches: ["certDomains", "blbType", "accessId"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
blbIds!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "监听器ID",
|
||||
component: {
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
action: "GetListenerList",
|
||||
watches: ["certDomains", "accessId", "blbIds"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
listenerIds!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
this.logger.info("开始更新百度云监听器证书");
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const certClient = new BaiduYunCertClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
|
||||
let certId = this.cert as string;
|
||||
if (typeof this.cert !== "string") {
|
||||
this.logger.info("上传证书到百度云");
|
||||
const res = await certClient.createCert({
|
||||
cert: this.cert,
|
||||
certName: CertReader.buildCertName(this.cert),
|
||||
});
|
||||
certId = res.certId;
|
||||
this.logger.info(`上传证书到百度云成功:${certId}`);
|
||||
}
|
||||
|
||||
const baiduyunClient = new BaiduYunClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
for (const listenerId of this.listenerIds) {
|
||||
const listenerParams = listenerId.split("_");
|
||||
const blbId = listenerParams[0];
|
||||
const listenerType = listenerParams[1];
|
||||
const listenerPort = listenerParams[2];
|
||||
let additionalCertHost = null;
|
||||
if (listenerParams.length > 3) {
|
||||
additionalCertHost = listenerParams[3];
|
||||
}
|
||||
this.logger.info(`更新监听器证书开始:${listenerId}`);
|
||||
if (!additionalCertHost) {
|
||||
await this.updateListenerCert({
|
||||
client: baiduyunClient,
|
||||
blbId,
|
||||
listenerType,
|
||||
listenerPort,
|
||||
certId,
|
||||
});
|
||||
} else {
|
||||
const listenerDomains = await this.getListeners(baiduyunClient, blbId, listenerType, listenerPort);
|
||||
if (!listenerDomains || listenerDomains.length === 0) {
|
||||
throw new Error(`未找到监听器:${listenerId}`);
|
||||
}
|
||||
const oldAdditionals = listenerDomains[0].additionalCertDomains;
|
||||
for (const oldAddi of oldAdditionals) {
|
||||
if (oldAddi.host === additionalCertHost) {
|
||||
oldAddi.certId = certId;
|
||||
}
|
||||
}
|
||||
await this.updateListenerCert({
|
||||
client: baiduyunClient,
|
||||
blbId,
|
||||
listenerType,
|
||||
listenerPort,
|
||||
certId,
|
||||
additionalCertDomains: oldAdditionals,
|
||||
});
|
||||
}
|
||||
this.logger.info(`更新监听器证书成功:${listenerId}`);
|
||||
await this.ctx.utils.sleep(3000);
|
||||
}
|
||||
|
||||
this.logger.info(`更新百度云监听器证书完成`);
|
||||
}
|
||||
|
||||
async onGetListenerList(data: PageSearch = {}) {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const client = new BaiduYunClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
|
||||
const listeners = [];
|
||||
for (const blbId of this.blbIds) {
|
||||
/**
|
||||
* GET /v{version}/appblb/{blbId}/TCPlistener?listenerPort={listenerPort}&marker={marker}&maxKeys={maxKeys} HTTP/1.1
|
||||
* Host: blb.bj.baidubce.com
|
||||
*/
|
||||
const listenerTypes = ["HTTPSlistener", "SSLlistener"];
|
||||
for (const listenerType of listenerTypes) {
|
||||
const list = await this.getListeners(client, blbId, listenerType);
|
||||
if (list && list.length > 0) {
|
||||
for (const item of list) {
|
||||
const key = `${blbId}_${listenerType}_${item.listenerPort}`;
|
||||
listeners.push({
|
||||
value: key,
|
||||
label: key,
|
||||
});
|
||||
|
||||
if (item.additionalCertDomains && item.additionalCertDomains.length > 0) {
|
||||
for (const addi of item.additionalCertDomains) {
|
||||
const addiKey = `${key}_${addi.host}`;
|
||||
listeners.push({
|
||||
value: addiKey,
|
||||
label: `${addiKey}【扩展】`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!listeners || listeners.length === 0) {
|
||||
throw new Error("未找到https/SSL监听器");
|
||||
}
|
||||
return listeners;
|
||||
}
|
||||
|
||||
private async getListeners(client: BaiduYunClient, blbId: string, listenerType: string, listenerPort?: number | string) {
|
||||
const query: any = {
|
||||
maxItems: 1000,
|
||||
};
|
||||
if (listenerPort) {
|
||||
query.listenerPort = listenerPort;
|
||||
}
|
||||
const res = await client.doRequest({
|
||||
host: `blb.${this.region}.baidubce.com`,
|
||||
uri: `/v1/${this.blbType}/${blbId}/${listenerType}`,
|
||||
method: "GET",
|
||||
query,
|
||||
});
|
||||
return res.listenerList;
|
||||
}
|
||||
|
||||
async onGetBLBList(data: PageSearch = {}) {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const client = new BaiduYunClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /v{version}/appblb?address={address}&name={name}&blbId={blbId}&marker={marker}&maxKeys={maxKeys} HTTP/1.1
|
||||
* Host: blb.bj.baidubce.com
|
||||
*/
|
||||
const res = await client.doRequest({
|
||||
host: `blb.${this.region}.baidubce.com`,
|
||||
uri: `/v1/${this.blbType}`,
|
||||
method: "GET",
|
||||
query: {
|
||||
maxItems: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
const list = res.blbList;
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有数据,你可以手动输入");
|
||||
}
|
||||
const options: any[] = [];
|
||||
for (const item of list) {
|
||||
options.push({
|
||||
value: item.blbId,
|
||||
label: item.name,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private async updateListenerCert(param: { client: BaiduYunClient; blbId: string; listenerType: string; listenerPort: string; certId?: any; additionalCertDomains?: any[] }) {
|
||||
/**
|
||||
* PUT /v{version}/appblb/{blbId}/SSLlistener?clientToken={clientToken}&listenerPort={listenerPort} HTTP/1.1
|
||||
* Host: blb.bj.baidubce.com
|
||||
* Authorization: authorization string
|
||||
*
|
||||
* {
|
||||
* "scheduler":scheduler,
|
||||
* "certIds":[certId],
|
||||
* "encryptionType":encryptionType,
|
||||
* "encryptionProtocols":[protocol1, protacol2],
|
||||
* "dualAuth":false,
|
||||
* "clientCertIds":[clientCertId],
|
||||
* "description":description
|
||||
* }
|
||||
*/
|
||||
const { client, blbId, listenerType, listenerPort, certId, additionalCertDomains } = param;
|
||||
const body: any = {};
|
||||
if (additionalCertDomains) {
|
||||
body.additionalCertDomains = additionalCertDomains;
|
||||
}
|
||||
if (certId) {
|
||||
body.certIds = [certId];
|
||||
}
|
||||
const res = await client.doRequest({
|
||||
host: `blb.${this.region}.baidubce.com`,
|
||||
uri: `/v1/${this.blbType}/${blbId}/${listenerType}`,
|
||||
method: "PUT",
|
||||
query: {
|
||||
listenerPort,
|
||||
},
|
||||
body,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
new BaiduDeployToBLBPlugin();
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { BaiduYunCertClient, BaiduYunClient } from "../client.js";
|
||||
import { createCertDomainGetterInputDefine } from "@certd/plugin-lib";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "BaiduDeployToCDN",
|
||||
title: "百度云-部署证书到CDN",
|
||||
icon: "ant-design:baidu-outlined",
|
||||
group: pluginGroups.baidu.key,
|
||||
desc: "部署到百度云CDN",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class BaiduDeployToCDNPlugin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames, "BaiduUploadCert"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo | string;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine())
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "百度云授权",
|
||||
helper: "百度云授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baidu",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "CDN域名",
|
||||
component: {
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
action: "GetDomainList",
|
||||
watches: ["certDomains", "accessId"],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
domains!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const client = new BaiduYunClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
|
||||
const certClient = new BaiduYunCertClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
|
||||
let certId = this.cert as string;
|
||||
if (typeof this.cert !== "string") {
|
||||
this.logger.info("上传证书到百度云");
|
||||
const res = await certClient.createCert({
|
||||
cert: this.cert,
|
||||
certName: CertReader.buildCertName(this.cert),
|
||||
});
|
||||
certId = res.certId;
|
||||
this.logger.info(`上传证书到百度云成功:${certId}`);
|
||||
}
|
||||
|
||||
const body = {
|
||||
https: {
|
||||
enabled: true,
|
||||
certId: certId,
|
||||
},
|
||||
};
|
||||
for (const domain of this.domains) {
|
||||
await client.doRequest({
|
||||
host: "cdn.baidubce.com",
|
||||
uri: `/v2/domain/${domain}/config`,
|
||||
body,
|
||||
query: {
|
||||
https: "",
|
||||
},
|
||||
method: "put",
|
||||
});
|
||||
this.logger.info(`部署证书到${domain}成功`);
|
||||
}
|
||||
}
|
||||
|
||||
async onGetDomainList() {
|
||||
// if (!isPlus()) {
|
||||
// throw new Error("自动获取站点列表为专业版功能,您可以手动输入站点域名/站点名称进行部署");
|
||||
// }
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const client = new BaiduYunClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.ctx.http,
|
||||
});
|
||||
|
||||
const res = await client.doRequest({
|
||||
host: "cdn.baidubce.com",
|
||||
uri: `/v2/domain`,
|
||||
method: "GET",
|
||||
query: {
|
||||
maxItems: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
const list = res.domains;
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("未找到加速域名,你可以手动输入");
|
||||
}
|
||||
const options: any[] = [];
|
||||
for (const item of list) {
|
||||
options.push({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
domain: item.name,
|
||||
});
|
||||
}
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
|
||||
new BaiduDeployToCDNPlugin();
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
|
||||
import { BaiduAccess } from "../access.js";
|
||||
import { BaiduYunCertClient } from "../client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "BaiduUploadCert",
|
||||
title: "百度云-上传到证书托管",
|
||||
icon: "ant-design:baidu-outlined",
|
||||
desc: "上传证书到百度云证书托管中心",
|
||||
group: pluginGroups.baidu.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class BaiduUploadCert extends AbstractTaskPlugin {
|
||||
// @TaskInput({ title: '证书名称' })
|
||||
// name!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: any;
|
||||
|
||||
@TaskInput({
|
||||
title: "Access授权",
|
||||
helper: "access授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baidu",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: "百度云CertId",
|
||||
})
|
||||
baiduCertId?: string;
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<BaiduAccess>(this.accessId);
|
||||
|
||||
const certClient = new BaiduYunCertClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.http,
|
||||
});
|
||||
|
||||
const certItem = await certClient.createCert({
|
||||
cert: this.cert,
|
||||
certName: CertReader.buildCertName(this.cert),
|
||||
});
|
||||
|
||||
this.baiduCertId = certItem.certId;
|
||||
this.logger.info(`上传成功,证书ID:${certItem.certId}`);
|
||||
}
|
||||
}
|
||||
|
||||
new BaiduUploadCert();
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "baishan",
|
||||
title: "白山云授权",
|
||||
desc: "",
|
||||
icon: "material-symbols:shield-outline",
|
||||
})
|
||||
export class BaishanAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "token",
|
||||
component: {
|
||||
placeholder: "token",
|
||||
},
|
||||
helper: "自行联系提供商申请",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
token = "";
|
||||
}
|
||||
|
||||
new BaishanAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-update-cert.js";
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { BaishanAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "BaishanUpdateCert",
|
||||
title: "白山云-更新证书",
|
||||
icon: "material-symbols:shield-outline",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class BaishanUpdateCert extends AbstractTaskPlugin {
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: "证书ID",
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "证书ID,在证书管理页面查看,每条记录都有证书id",
|
||||
})
|
||||
certId!: number;
|
||||
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: "证书名称",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "给证书设置一个名字,便于区分",
|
||||
})
|
||||
certName!: string;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "白山云授权",
|
||||
helper: "白山云授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baishan",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<BaishanAccess>(this.accessId);
|
||||
// https://cdn.api.baishan.com/v2/domain/certificate
|
||||
try {
|
||||
const res = await this.ctx.http.request({
|
||||
url: "/v2/domain/certificate?token=" + access.token,
|
||||
baseURL: "https://cdn.api.baishan.com",
|
||||
method: "post",
|
||||
data: {
|
||||
cert_id: this.certId,
|
||||
name: this.certName,
|
||||
certificate: this.cert.crt,
|
||||
key: this.cert.key,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.code !== 0) {
|
||||
throw new Error("修改证书失败:" + res.message || res.msg || JSON.stringify(res));
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message?.indexOf("this certificate is exists") > -1) {
|
||||
// this certificate is exists, cert_id is (224995)
|
||||
//提取id
|
||||
const id = e.message.match(/\d+/);
|
||||
if (id && id.length > 0 && id[0] !== this.certId + "") {
|
||||
throw new Error("证书已存在,但证书id不一致,当前证书id为" + this.certId + ",已存在证书id为" + id);
|
||||
}
|
||||
this.logger.info("证书已存在,无需更新");
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
this.logger.info("证书更新成功");
|
||||
}
|
||||
}
|
||||
|
||||
new BaishanUpdateCert();
|
||||
@@ -0,0 +1,86 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
import { HttpClient } from "@certd/basic";
|
||||
|
||||
import { BaotaClient } from "./lib/client.js";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "baota",
|
||||
title: "baota授权",
|
||||
desc: "",
|
||||
icon: "svg:icon-bt",
|
||||
order: 2,
|
||||
})
|
||||
export class BaotaAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "宝塔URL地址",
|
||||
component: {
|
||||
placeholder: "http://192.168.42.237:41896",
|
||||
},
|
||||
helper: "宝塔面板的url地址,不要带安全入口,例如:http://192.168.42.237:41896",
|
||||
required: true,
|
||||
})
|
||||
panelUrl = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "接口密钥",
|
||||
component: {
|
||||
placeholder: "接口密钥",
|
||||
},
|
||||
helper: "宝塔面板设置->面板设置->API接口->接口配置->接口密钥。\n必须要加IP白名单,您可以点击下方测试按钮,报错之后会打印IP,将IP加入白名单之后再次测试即可",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiSecret = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "忽略证书校验",
|
||||
value: true,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "如果面板的url是https,且使用的是自签名证书,则需要开启此选项,其他情况可以关闭",
|
||||
})
|
||||
skipSslVerify = true;
|
||||
|
||||
@AccessInput({
|
||||
title: "windows版",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "是否是windows版",
|
||||
})
|
||||
isWindows = false;
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "TestRequest",
|
||||
},
|
||||
helper: "点击测试接口看是否正常",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const http: HttpClient = this.ctx.http;
|
||||
const client = new BaotaClient(this, http);
|
||||
|
||||
if (this.isWindows) {
|
||||
await client.doWindowsRequest("/site/get_site_types", {}, { skipCheckRes: false });
|
||||
return "ok";
|
||||
}
|
||||
const url = "/site?action=get_site_types";
|
||||
const data = {};
|
||||
await client.doRequest(url, null, data, { skipCheckRes: false });
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
new BaotaAccess();
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
export * from "./waf-access.js";
|
||||
export * from "./lib/client.js";
|
||||
@@ -0,0 +1,88 @@
|
||||
import crypto from "node:crypto";
|
||||
import { BaotaAccess } from "../access.js";
|
||||
import { HttpClient, HttpRequestConfig } from "@certd/basic";
|
||||
import * as querystring from "node:querystring";
|
||||
|
||||
export class BaotaClient {
|
||||
access: BaotaAccess;
|
||||
http: HttpClient;
|
||||
|
||||
constructor(access: BaotaAccess, http: HttpClient) {
|
||||
this.access = access;
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
//将以上 java代码 翻译成nodejs 代码
|
||||
getRequestToken() {
|
||||
const timestamps = Math.floor(new Date().getTime() / 1000);
|
||||
const md5Sign = this.getMd5(this.access.apiSecret);
|
||||
const temp = timestamps + md5Sign;
|
||||
return {
|
||||
request_token: this.getMd5(temp),
|
||||
request_time: "" + timestamps,
|
||||
};
|
||||
}
|
||||
|
||||
getMd5(content: string) {
|
||||
return crypto.createHash("md5").update(content).digest("hex");
|
||||
}
|
||||
|
||||
async doRequest(path: string, action: string, data: any = {}, options?: HttpRequestConfig<any>) {
|
||||
const token = this.getRequestToken();
|
||||
const body = {
|
||||
...token,
|
||||
...data,
|
||||
};
|
||||
const bodyStr = querystring.stringify(body);
|
||||
// const agent = new https.Agent({
|
||||
// rejectUnauthorized: false,
|
||||
// });
|
||||
let panelUrl = this.access.panelUrl;
|
||||
if (panelUrl.endsWith("/")) {
|
||||
panelUrl = panelUrl.substring(0, panelUrl.length - 1);
|
||||
}
|
||||
let url = `${panelUrl}${path}`;
|
||||
if (action) {
|
||||
url = `${url}?action=${action}`;
|
||||
}
|
||||
const res: any = await this.http.request({
|
||||
url: url,
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: bodyStr,
|
||||
// httpsAgent: agent,
|
||||
...options,
|
||||
skipSslVerify: this.access.skipSslVerify ?? true,
|
||||
});
|
||||
if (!options?.skipCheckRes && res.status === false) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async doWindowsRequest(path: string, data: any, options?: HttpRequestConfig<any>) {
|
||||
const token = this.getRequestToken();
|
||||
const body = {
|
||||
...token,
|
||||
...data,
|
||||
};
|
||||
// const agent = new https.Agent({
|
||||
// rejectUnauthorized: false,
|
||||
// });
|
||||
const url = `${this.access.panelUrl}${path}`;
|
||||
const res: any = await this.http.request({
|
||||
url: url,
|
||||
method: "post",
|
||||
data: body,
|
||||
// httpsAgent: agent,
|
||||
...options,
|
||||
skipSslVerify: this.access.skipSslVerify ?? true,
|
||||
});
|
||||
if (!options?.skipCheckRes && res.status === false) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./plugin-deploy-to-panel.js";
|
||||
export * from "./plugin-deploy-to-website.js";
|
||||
export * from "./plugin-deploy-to-aawaf.js";
|
||||
export * from "./plugin-deploy-to-website-win.js";
|
||||
export * from "./plugin-delete-expiring-cert.js";
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { BaotaClient } from "../lib/client.js";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
import dayjs from "dayjs";
|
||||
@IsTaskPlugin({
|
||||
name: "BaotaDeleteExpiringCert",
|
||||
title: "宝塔-删除过期证书",
|
||||
icon: "svg:icon-bt",
|
||||
group: pluginGroups.panel.key,
|
||||
desc: "删除证书夹中过期证书",
|
||||
showRunStrategy: true,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.AlwaysRun,
|
||||
},
|
||||
},
|
||||
needPlus: true,
|
||||
})
|
||||
export class BaotaDeleteExpiringCert extends AbstractPlusTaskPlugin {
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "宝塔授权",
|
||||
helper: "baota的接口密钥",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baota",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const { accessId } = this;
|
||||
const access = await this.getAccess(accessId);
|
||||
const http = this.ctx.http;
|
||||
const client = new BaotaClient(access, http);
|
||||
|
||||
//https://baota.docmirror.cn:20001/ssl?action=get_cert_list
|
||||
const res = await client.doRequest("/ssl", "get_cert_list", null);
|
||||
|
||||
const now = new Date().getTime();
|
||||
for (const item of res) {
|
||||
if (dayjs(item.not_after).valueOf() < now) {
|
||||
//https://baota.docmirror.cn:20001/ssl?action=remove_cloud_cert
|
||||
/**
|
||||
* local: 1
|
||||
* ssl_hash: fbe087d5253b78ba37264486415181ab
|
||||
*/
|
||||
this.logger.info(`证书: ${item.name} 过期时间: ${item.not_after},已过期,删除`);
|
||||
try {
|
||||
await client.doRequest("/ssl", "remove_cloud_cert", {
|
||||
local: 1,
|
||||
ssl_hash: item.hash,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(`删除证书: ${item.name} 失败`, e);
|
||||
}
|
||||
|
||||
await this.ctx.utils.sleep(1000);
|
||||
} else {
|
||||
this.logger.info(`证书: ${item.name} 过期时间: ${item.not_after},未过期`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(res?.msg);
|
||||
}
|
||||
}
|
||||
new BaotaDeleteExpiringCert();
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine } from "@certd/plugin-lib";
|
||||
import { BaotaWafAccess } from "../waf-access.js";
|
||||
|
||||
type SiteItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
domain: string;
|
||||
};
|
||||
@IsTaskPlugin({
|
||||
name: "BaotaDeployWAF",
|
||||
title: "宝塔-WAF证书部署",
|
||||
icon: "svg:icon-bt",
|
||||
group: pluginGroups.panel.key,
|
||||
desc: "部署宝塔云WAF/aaWAF",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class BaotaDeployWAF extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine())
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "宝塔WAF授权",
|
||||
helper: "aaWAF的接口密钥",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baotawaf",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "站点ID",
|
||||
component: {
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
action: "onGetSiteList",
|
||||
search: true,
|
||||
watches: ["certDomains", "accessId"],
|
||||
},
|
||||
required: true,
|
||||
helper: "将会自动获取证书匹配的站点,请选择要部署证书的站点",
|
||||
})
|
||||
siteIds!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const { cert, accessId } = this;
|
||||
const access = await this.getAccess<BaotaWafAccess>(accessId);
|
||||
|
||||
const lockKey = `baota-lock-${accessId}`;
|
||||
|
||||
for (const siteId of this.siteIds) {
|
||||
this.logger.info(`为站点:${siteId}设置证书`);
|
||||
const info = await this.getSiteInfo(access, siteId);
|
||||
const listen_ssl_port = info.server.listen_ssl_port;
|
||||
await this.ctx.utils.locker.execute(lockKey, async () => {
|
||||
await access.doRequest({
|
||||
url: "/api/wafmastersite/modify_site",
|
||||
data: {
|
||||
types: "openCert",
|
||||
site_id: siteId,
|
||||
server: {
|
||||
listen_ssl_port,
|
||||
ssl: { is_ssl: 1, full_chain: cert.crt, private_key: cert.key },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
this.logger.info(`站点:${siteId} 证书部署成功`);
|
||||
}
|
||||
this.logger.info(`部署成功`);
|
||||
}
|
||||
|
||||
async getSiteInfo(access: BaotaWafAccess, siteId: string) {
|
||||
// /api/wafmastersite/get_site_list
|
||||
const res = await access.doRequest({
|
||||
url: "/api/wafmastersite/get_site_list",
|
||||
data: {
|
||||
site_id: siteId,
|
||||
p_size: 1,
|
||||
p: 1,
|
||||
site_name: "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.list || res.list.length === 0) {
|
||||
throw new Error(`未找到站点:${siteId}`);
|
||||
}
|
||||
return res.list[0];
|
||||
}
|
||||
|
||||
async onGetSiteList(data: { searchKey?: string }) {
|
||||
// if (!isPlus()) {
|
||||
// throw new Error("自动获取站点列表为专业版功能,您可以手动输入站点域名/站点名称进行部署");
|
||||
// }
|
||||
const access = await this.getAccess<BaotaWafAccess>(this.accessId);
|
||||
|
||||
const res = await access.getSiteList({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
query: data.searchKey || "",
|
||||
});
|
||||
|
||||
const list = res.list;
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("未找到站点,你可以手动输入");
|
||||
}
|
||||
const options: SiteItem[] = [];
|
||||
for (const item of list) {
|
||||
options.push({
|
||||
value: item.site_id,
|
||||
label: `${item.site_name}<${item.site_id}>`,
|
||||
domain: item.server.server_name,
|
||||
});
|
||||
}
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
new BaotaDeployWAF();
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
import { BaotaClient } from "../lib/client.js";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
name: "BaotaDeployPanelCert",
|
||||
title: "宝塔-面板证书部署",
|
||||
icon: "svg:icon-bt",
|
||||
group: pluginGroups.panel.key,
|
||||
desc: "部署宝塔面板本身的ssl证书",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: true,
|
||||
})
|
||||
export class BaotaDeployPanelCertPlugin extends AbstractPlusTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "宝塔授权",
|
||||
helper: "baota的接口密钥",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baota",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const { cert, accessId } = this;
|
||||
const access = await this.getAccess(accessId);
|
||||
const http = this.ctx.http;
|
||||
const client = new BaotaClient(access, http);
|
||||
|
||||
const lockKey = `baota-lock-${accessId}`;
|
||||
await this.ctx.utils.locker.execute(lockKey, async () => {
|
||||
const res = await client.doRequest(
|
||||
"/config",
|
||||
"SavePanelSSL",
|
||||
{
|
||||
privateKey: cert.key,
|
||||
certPem: cert.crt,
|
||||
},
|
||||
{
|
||||
skipSslVerify: true,
|
||||
}
|
||||
);
|
||||
this.logger.info(res?.msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
new BaotaDeployPanelCertPlugin();
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import { HttpClient } from "@certd/basic";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { BaotaClient } from "../lib/client.js";
|
||||
import { createCertDomainGetterInputDefine } from "@certd/plugin-lib";
|
||||
|
||||
type SiteItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
domain: string;
|
||||
};
|
||||
@IsTaskPlugin({
|
||||
name: "BaotaDeployWebSiteWin",
|
||||
title: "宝塔win-网站证书部署",
|
||||
icon: "svg:icon-bt",
|
||||
group: pluginGroups.panel.key,
|
||||
desc: "部署到Windows版宝塔管理的站点的ssl证书",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class BaotaDeployWebSiteWin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine())
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "宝塔授权",
|
||||
helper: "baota的接口密钥",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baota",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "站点Id",
|
||||
component: {
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
action: "GetSiteList",
|
||||
watches: ["certDomains", "accessId"],
|
||||
},
|
||||
required: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
`,
|
||||
helper: "将会自动获取证书匹配的站点名称",
|
||||
})
|
||||
siteIds!: number[];
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const { cert, accessId } = this;
|
||||
const access = await this.getAccess(accessId);
|
||||
const http: HttpClient = this.ctx.http;
|
||||
const client = new BaotaClient(access, http);
|
||||
this.logger.info(`siteIds:${this.siteIds}`);
|
||||
|
||||
const siteIds = this.siteIds ?? [];
|
||||
|
||||
const lockKey = `baota-lock-${accessId}`;
|
||||
for (const site of siteIds) {
|
||||
await this.ctx.utils.locker.execute(lockKey, async () => {
|
||||
this.logger.info(`为站点:${site}设置证书`);
|
||||
const res = await client.doWindowsRequest("/site/set_site_ssl", {
|
||||
siteid: site,
|
||||
status: true,
|
||||
sslType: "",
|
||||
cert: cert.crt,
|
||||
key: cert.key,
|
||||
});
|
||||
this.logger.info(res?.msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onGetSiteList() {
|
||||
// if (!isPlus()) {
|
||||
// throw new Error("自动获取站点列表为专业版功能,您可以手动输入站点域名/站点名称进行部署");
|
||||
// }
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const http: HttpClient = this.ctx.http;
|
||||
const client = new BaotaClient(access, http);
|
||||
|
||||
const domains = this.certDomains;
|
||||
let all = [];
|
||||
const getPhpSite = async () => {
|
||||
const url = "/datalist/get_data_list";
|
||||
const data = {
|
||||
table: "sites",
|
||||
limit: 500,
|
||||
};
|
||||
const res = await client.doWindowsRequest(url, data, { skipCheckRes: false });
|
||||
this.logger.info(res.data);
|
||||
all = res.data || [];
|
||||
};
|
||||
|
||||
//查找docker 站点
|
||||
|
||||
await getPhpSite();
|
||||
|
||||
if (!all || all.length === 0) {
|
||||
throw new Error("未找到站点,你可以手动输入");
|
||||
}
|
||||
const options: SiteItem[] = [];
|
||||
for (const item of all) {
|
||||
options.push({
|
||||
value: item.id,
|
||||
label: `${item.name}<${item.id}>`,
|
||||
domain: item.name,
|
||||
});
|
||||
}
|
||||
return this.ctx.utils.options.buildGroupOptions(options, domains);
|
||||
}
|
||||
}
|
||||
new BaotaDeployWebSiteWin();
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import { HttpClient } from "@certd/basic";
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { BaotaClient } from "../lib/client.js";
|
||||
import { createCertDomainGetterInputDefine } from "@certd/plugin-lib";
|
||||
import { uniq } from "lodash-es";
|
||||
|
||||
export type SiteItem = {
|
||||
value: string;
|
||||
label: string;
|
||||
domain: string;
|
||||
};
|
||||
@IsTaskPlugin({
|
||||
name: "BaotaDeployWebSiteCert",
|
||||
title: "宝塔-网站证书部署",
|
||||
icon: "svg:icon-bt",
|
||||
group: pluginGroups.panel.key,
|
||||
desc: "部署宝塔管理的站点的ssl证书,目前支持宝塔网站站点、docker站点等。本插件也支持aaPanel。",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine())
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "宝塔授权",
|
||||
helper: "baota的接口密钥",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "baota",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "是否Docker站点",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "是否为docker站点",
|
||||
required: true,
|
||||
})
|
||||
isDockerSite = false;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "站点名称",
|
||||
component: {
|
||||
name: "remote-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
action: "GetSiteList",
|
||||
watches: ["certDomains", "accessId", "isDockerSite"],
|
||||
},
|
||||
required: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
}
|
||||
`,
|
||||
helper: "将会自动获取证书匹配的站点名称\n宝塔版本低于9.0.0时,此处会获取失败,忽略错误,手动输入站点域名即可",
|
||||
})
|
||||
siteName!: string | string[];
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const { cert, accessId } = this;
|
||||
const access = await this.getAccess(accessId);
|
||||
const http: HttpClient = this.ctx.http;
|
||||
const client = new BaotaClient(access, http);
|
||||
this.logger.info(`siteName:${this.siteName}`);
|
||||
|
||||
const siteNames = [];
|
||||
if (typeof this.siteName === "string") {
|
||||
siteNames.push(this.siteName);
|
||||
} else {
|
||||
siteNames.push(...this.siteName);
|
||||
}
|
||||
|
||||
const lockKey = `baota-lock-${accessId}`;
|
||||
|
||||
for (const site of siteNames) {
|
||||
// 加锁,防止并发部署证书, 宝塔并发部署会导致nginx的conf错乱
|
||||
await this.ctx.utils.locker.execute(lockKey, async () => {
|
||||
this.logger.info(`为站点:${site}设置证书,目前支持宝塔网站站点、docker站点`);
|
||||
if (this.isDockerSite) {
|
||||
const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
|
||||
site_name: site,
|
||||
key: cert.key,
|
||||
csr: cert.crt,
|
||||
});
|
||||
this.logger.info(res?.msg);
|
||||
} else {
|
||||
const res = await client.doRequest("/site", "SetSSL", {
|
||||
type: 0,
|
||||
siteName: site,
|
||||
key: cert.key,
|
||||
csr: cert.crt,
|
||||
});
|
||||
this.logger.info(res?.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//上传证书
|
||||
// const uploadCertUrl = "/ssl/cert/save_cert";
|
||||
// const uploadCertData = {
|
||||
// csr: cert.crt,
|
||||
// key: cert.key,
|
||||
// };
|
||||
// const uploadCertRes = await client.doRequest(uploadCertUrl, null, uploadCertData, {
|
||||
// skipCheckRes: true,
|
||||
// });
|
||||
// if (uploadCertRes.msg === "证书已存在") {
|
||||
// this.logger.info(`证书已存在:${JSON.stringify(uploadCertRes)}`);
|
||||
// } else if (uploadCertRes.status === false) {
|
||||
// this.logger.info(`上传证书失败:${JSON.stringify(uploadCertRes)}`);
|
||||
// } else {
|
||||
// this.logger.info(`上传证书成功:${JSON.stringify(uploadCertRes)}`);
|
||||
// }
|
||||
|
||||
// const certHash = this.ctx.utils.hash.md5(cert.crt + "\n");
|
||||
// for (const site of siteNames) {
|
||||
// const url = "/ssl/cert/SetCertToSite";
|
||||
// const data = {
|
||||
// siteName: site,
|
||||
// ssl_hash: certHash,
|
||||
// };
|
||||
// this.logger.info(`开始部署站点【${site}】的证书:${JSON.stringify(data)}`);
|
||||
// const res = await client.doRequest(url, null, data);
|
||||
// this.logger.info(`站点【${site}】部署证书成功:${res.msg}`);
|
||||
// }
|
||||
|
||||
// const batchInfo = [];
|
||||
// for (const site of siteNames) {
|
||||
// batchInfo.push({
|
||||
// ssl_hash: certHash,
|
||||
// siteName: site,
|
||||
// certName: this.certDomains[0],
|
||||
// });
|
||||
// }
|
||||
// const batchInfoStr = JSON.stringify(batchInfo);
|
||||
// const data = { BatchInfo: batchInfoStr };
|
||||
// this.logger.info("body=", data);
|
||||
// const res = await client.doRequest("/ssl", "SetBatchCertToSite", data);
|
||||
// if (res.failed > 0) {
|
||||
// throw new Error(`部署失败:${JSON.stringify(res)}`);
|
||||
// }
|
||||
// this.logger.info(`部署成功:${JSON.stringify(res)}`);
|
||||
}
|
||||
|
||||
async onGetSiteList() {
|
||||
// if (!isPlus()) {
|
||||
// throw new Error("自动获取站点列表为专业版功能,您可以手动输入站点域名/站点名称进行部署");
|
||||
// }
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const http: HttpClient = this.ctx.http;
|
||||
const client = new BaotaClient(access, http);
|
||||
|
||||
const domains = this.certDomains;
|
||||
let all = [];
|
||||
const getPhpSite = async () => {
|
||||
const url = "/ssl?action=GetSiteDomain";
|
||||
const data = {
|
||||
cert_list: JSON.stringify(domains),
|
||||
};
|
||||
const res = await client.doRequest(url, null, data, { skipCheckRes: false });
|
||||
this.logger.info(res);
|
||||
all = res.all || [];
|
||||
};
|
||||
|
||||
//查找docker 站点
|
||||
const getDockerSite = async () => {
|
||||
const url2 = "/mod/docker/com/get_site_list";
|
||||
const res2 = await client.doRequest(url2, null, {});
|
||||
this.logger.info(res2);
|
||||
if (res2.data) {
|
||||
const dockerDomains = res2.data.map(item => {
|
||||
return item.name;
|
||||
});
|
||||
all = [...all, ...dockerDomains];
|
||||
all = uniq(all);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.isDockerSite) {
|
||||
await getDockerSite();
|
||||
} else {
|
||||
await getPhpSite();
|
||||
}
|
||||
|
||||
if (!all || all.length === 0) {
|
||||
throw new Error("未找到站点,你可以手动输入");
|
||||
}
|
||||
const options: SiteItem[] = [];
|
||||
for (const item of all) {
|
||||
options.push({
|
||||
value: item,
|
||||
label: item,
|
||||
domain: item,
|
||||
});
|
||||
}
|
||||
return this.ctx.utils.options.buildGroupOptions(options, domains);
|
||||
}
|
||||
}
|
||||
new BaotaDeployWebSiteCert();
|
||||
@@ -0,0 +1,128 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { HttpRequestConfig, utils } from "@certd/basic";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "baotawaf",
|
||||
title: "宝塔云WAF授权",
|
||||
desc: "用于连接和管理宝塔云WAF服务的授权配置",
|
||||
icon: "svg:icon-bt",
|
||||
})
|
||||
export class BaotaWafAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "在宝塔WAF URL",
|
||||
component: {
|
||||
placeholder: "http://192.168.42.237:41896",
|
||||
},
|
||||
helper: "在宝塔WAF的URL地址,不要带安全入口,例如:http://192.168.42.237:41896",
|
||||
required: true,
|
||||
})
|
||||
panelUrl = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "WAF API 密钥",
|
||||
component: {
|
||||
placeholder: "请输入WAF API接口密钥",
|
||||
},
|
||||
helper: "在宝塔WAF设置页面 - API接口中获取的API密钥。\n必须添加IP白名单,请确保已将CertD服务器IP加入白名单",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiSecret = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "忽略SSL证书校验",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "如果面板使用的是自签名SSL证书,则需要开启此选项",
|
||||
})
|
||||
skipSslVerify = false;
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "onTestRequest",
|
||||
},
|
||||
helper: "点击测试WAF请求",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const body = {
|
||||
p: 1,
|
||||
p_size: 1,
|
||||
site_name: "",
|
||||
};
|
||||
// 发送测试请求
|
||||
await this.doRequest({
|
||||
url: "/api/wafmastersite/get_site_list",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
async getSiteList(req: { query?: string; pageNo?: number; pageSize?: number } = {}) {
|
||||
const body = {
|
||||
p: req.pageNo ?? 1,
|
||||
p_size: req.pageSize ?? 100,
|
||||
site_name: req.query ?? "",
|
||||
};
|
||||
return await this.doRequest({
|
||||
url: "/api/wafmastersite/get_site_list",
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
async doRequest(req: HttpRequestConfig) {
|
||||
const http = this.ctx.http;
|
||||
|
||||
let panelUrl = this.panelUrl;
|
||||
if (panelUrl.endsWith("/")) {
|
||||
panelUrl = panelUrl.substring(0, panelUrl.length - 1);
|
||||
}
|
||||
// 构建请求头
|
||||
/**
|
||||
* __WAF_KEY = 接口密钥 (在WAF设置页面 - API 接口中获取)
|
||||
* waf_request_time = 当前请求时间的 uinx 时间戳 ( php: time() / python: time.time() )
|
||||
* waf_request_token = md5(string(request_time) + md5(api_sk))
|
||||
*/
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const token = utils.hash.md5(timestamp + utils.hash.md5(this.apiSecret));
|
||||
|
||||
const headers = {
|
||||
waf_request_time: timestamp,
|
||||
waf_request_token: token,
|
||||
...req.headers,
|
||||
};
|
||||
|
||||
// 发送测试请求
|
||||
const response = await http.request({
|
||||
// https://192.168.182.201:8379/api/wafmastersite/get_site_list
|
||||
baseURL: panelUrl,
|
||||
method: "POST",
|
||||
data: req.data,
|
||||
skipSslVerify: this.skipSslVerify,
|
||||
...req,
|
||||
headers: {
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
|
||||
// 检查响应是否成功
|
||||
if (response && response.code === 0) {
|
||||
return response.res;
|
||||
} else {
|
||||
throw new Error(`请求失败: ${response.msg || "未知错误"}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 实例化插件
|
||||
new BaotaWafAccess();
|
||||
@@ -0,0 +1,198 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { HttpRequestConfig } from "@certd/basic";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "cdnfly",
|
||||
title: "cdnfly授权",
|
||||
desc: "",
|
||||
icon: "majesticons:cloud-line",
|
||||
})
|
||||
export class CdnflyAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "cdnfly系统网址",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "例如:http://demo.cdnfly.cn",
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "授权方式",
|
||||
value: "apikey",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ label: "接口密钥", value: "apikey" },
|
||||
{ label: "模拟登录", value: "password" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
type = "apikey";
|
||||
|
||||
@AccessInput({
|
||||
title: "用户名",
|
||||
component: {
|
||||
placeholder: "username",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.type === 'password';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
})
|
||||
username = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "密码",
|
||||
component: {
|
||||
placeholder: "password",
|
||||
},
|
||||
helper: "",
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.type === 'password';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "api_key",
|
||||
component: {
|
||||
placeholder: "api_key",
|
||||
},
|
||||
helper: "登录cdnfly控制台->账户中心->Api密钥,点击开启后获取",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.type === 'apikey';
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
apiKey = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "api_secret",
|
||||
component: {
|
||||
placeholder: "api_secret",
|
||||
},
|
||||
helper: "登录cdnfly控制台->账户中心->Api密钥,点击开启后获取",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.type === 'apikey';
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
apiSecret = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "onTestRequest",
|
||||
},
|
||||
helper: "点击测试接口看是否正常\nIP需要加白名单,如果是同一台机器部署的,可以试试面板的url使用网卡docker0的ip,白名单使用172.16.0.0/12",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
accessToken!: string;
|
||||
async onTestRequest() {
|
||||
const certUrl = `/v1/certs`;
|
||||
const query: any = {
|
||||
limit: 100,
|
||||
};
|
||||
await this.doRequest({
|
||||
url: certUrl,
|
||||
method: "GET",
|
||||
data: query,
|
||||
});
|
||||
return "ok";
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
if (this.type !== "password") {
|
||||
throw new Error("only support password type");
|
||||
}
|
||||
if (this.accessToken) {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
const res = await this.ctx.http.request({
|
||||
url: "/v1/login",
|
||||
baseURL: this.url,
|
||||
method: "POST",
|
||||
data: {
|
||||
account: this.username,
|
||||
password: this.password,
|
||||
},
|
||||
});
|
||||
if (res.code != 0) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
this.accessToken = res.data.access_token;
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
async doRequest(config?: HttpRequestConfig) {
|
||||
const http = this.ctx.http;
|
||||
|
||||
let headers: any = {};
|
||||
if (this.type === "password") {
|
||||
//模拟登陆
|
||||
await this.getToken();
|
||||
headers = {
|
||||
"Access-Token": `${this.accessToken}`,
|
||||
};
|
||||
} else {
|
||||
const { apiKey, apiSecret } = this;
|
||||
headers = {
|
||||
"api-key": apiKey,
|
||||
"api-secret": apiSecret,
|
||||
};
|
||||
}
|
||||
const data = config.data;
|
||||
const method = config.method || "POST";
|
||||
const baseURL = config.baseURL || this.url;
|
||||
if (!baseURL) {
|
||||
throw new Error("请配置授权内的url参数");
|
||||
}
|
||||
const res: any = await http.request({
|
||||
url: config.url,
|
||||
baseURL: baseURL,
|
||||
method: method,
|
||||
headers,
|
||||
logRes: false,
|
||||
params: method === "GET" ? data : {},
|
||||
data: method !== "GET" ? data : undefined,
|
||||
});
|
||||
if (res.code != 0) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
new CdnflyAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-deploy-to-cdn.js";
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CdnflyAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "CdnflyDeployToCDN",
|
||||
title: "cdnfly-部署证书到cdnfly",
|
||||
icon: "majesticons:cloud-line",
|
||||
group: pluginGroups.cdn.key,
|
||||
desc: "cdnfly",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class CdnflyDeployToCDNPlugin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(
|
||||
createCertDomainGetterInputDefine({
|
||||
props: { required: false },
|
||||
})
|
||||
)
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "cdnfly授权",
|
||||
helper: "cdnfly授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "cdnfly",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "自动匹配站点",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "是否自动匹配站点进行部署\n如果选择自动匹配,则下方参数无需填写",
|
||||
})
|
||||
autoMatch!: boolean;
|
||||
|
||||
//测试参数
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "证书ID",
|
||||
helper: "请选择证书Id,需要先手动上传一次证书,后续可以自动更新证书【推荐】",
|
||||
search: true,
|
||||
typeName: "CdnflyDeployToCDNPlugin",
|
||||
action: CdnflyDeployToCDNPlugin.prototype.onGetCertList.name,
|
||||
watches: ["cert", "accessId"],
|
||||
required: false,
|
||||
})
|
||||
)
|
||||
certId!: number | number[];
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "网站Id",
|
||||
helper: "请选择要部署证书的网站Id",
|
||||
search: true,
|
||||
action: CdnflyDeployToCDNPlugin.prototype.onGetSiteList.name,
|
||||
watches: ["url", "cert", "accessId"],
|
||||
required: false,
|
||||
})
|
||||
)
|
||||
siteId!: number[];
|
||||
|
||||
access: CdnflyAccess;
|
||||
|
||||
uploadCertId!: number;
|
||||
|
||||
async onInstance() {
|
||||
this.access = await this.getAccess<CdnflyAccess>(this.accessId);
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const { cert, siteId, certId } = this;
|
||||
if (this.autoMatch) {
|
||||
this.logger.info(`自动匹配站点更新证书`);
|
||||
await this.updateByDomain();
|
||||
return;
|
||||
}
|
||||
|
||||
if (certId != null) {
|
||||
let certIds = certId as number[];
|
||||
if (!Array.isArray(certId)) {
|
||||
certIds = [certId];
|
||||
}
|
||||
for (const item of certIds) {
|
||||
await this.updateByCertId(cert, item);
|
||||
}
|
||||
}
|
||||
if (siteId != null) {
|
||||
let siteIds = siteId as number[];
|
||||
if (!Array.isArray(siteId)) {
|
||||
siteIds = [siteId];
|
||||
}
|
||||
for (const item of siteIds) {
|
||||
await this.updateBySiteId(cert, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateByCertId(cert: CertInfo, certId: number | number[]) {
|
||||
this.logger.info(`更新证书,证书ID:${certId}`);
|
||||
const url = `/v1/certs/${certId}`;
|
||||
await this.doRequest(url, "PUT", {
|
||||
cert: cert.crt,
|
||||
key: cert.key,
|
||||
});
|
||||
}
|
||||
|
||||
async doRequest(url: string, method: string, data: any) {
|
||||
return await this.access.doRequest({
|
||||
url: url,
|
||||
method: method,
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
private async updateByDomain() {
|
||||
//查询站点
|
||||
const sites = await this.querySite();
|
||||
for (const row of sites) {
|
||||
const domains = row.domain.split(" ");
|
||||
if (this.ctx.utils.domain.match(domains, this.certDomains)) {
|
||||
this.logger.info(`站点:${row.id},${row.domain},域名已匹配`);
|
||||
await this.updateBySiteId(this.cert, row.id);
|
||||
} else {
|
||||
this.logger.info(`站点:${row.id},${row.domain},域名未匹配`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateBySiteId(cert: CertInfo, siteId: any) {
|
||||
const siteInfoUrl = `/v1/sites/${siteId}`;
|
||||
const site = await this.doRequest(siteInfoUrl, "GET", {});
|
||||
if (!site) {
|
||||
throw new Error(`站点:${siteId}不存在`);
|
||||
}
|
||||
this.logger.info(`更新站点证书:${siteId}`);
|
||||
|
||||
const data = site.data;
|
||||
let https_listen = data.https_listen;
|
||||
if (https_listen && typeof https_listen === "string") {
|
||||
https_listen = JSON.parse(https_listen);
|
||||
}
|
||||
if (https_listen?.cert) {
|
||||
//该网站已有证书id
|
||||
const certId = https_listen.cert;
|
||||
this.logger.info(`该站点已有证书,更新证书,证书ID:${certId}`);
|
||||
await this.updateByCertId(cert, certId);
|
||||
return;
|
||||
}
|
||||
if (!this.uploadCertId) {
|
||||
//创建证书
|
||||
this.logger.info(`创建证书,域名:${this.certDomains}`);
|
||||
const certUrl = `/v1/certs`;
|
||||
const name = this.buildCertName(this.certDomains[0]);
|
||||
await this.doRequest(certUrl, "POST", {
|
||||
name,
|
||||
type: "custom",
|
||||
cert: cert.crt,
|
||||
key: cert.key,
|
||||
});
|
||||
|
||||
const certs: any = await this.doRequest(certUrl, "GET", {
|
||||
name,
|
||||
});
|
||||
this.uploadCertId = certs.data[0].id;
|
||||
}
|
||||
|
||||
const siteUrl = `/v1/sites`;
|
||||
await this.doRequest(siteUrl, "PUT", { id: site.id, https_listen: { cert: this.uploadCertId } });
|
||||
}
|
||||
|
||||
async querySite(domain?: string) {
|
||||
const siteUrl = `/v1/sites`;
|
||||
const query: any = {
|
||||
limit: 100,
|
||||
};
|
||||
if (domain) {
|
||||
query.domain = domain;
|
||||
}
|
||||
const res = await this.doRequest(siteUrl, "GET", query);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async queryCert(domain?: string) {
|
||||
const certUrl = `/v1/certs`;
|
||||
const query: any = {
|
||||
limit: 100,
|
||||
};
|
||||
if (domain) {
|
||||
query.domain = domain;
|
||||
}
|
||||
const res = await this.doRequest(certUrl, "GET", query);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async onGetSiteList(data: { searchKey: string }) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
|
||||
const list = await this.querySite(data?.searchKey);
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到任何站点,您可以手动输入网站Id");
|
||||
}
|
||||
|
||||
const options = list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.id}<${item.domain}>`,
|
||||
value: item.id,
|
||||
domain: item.domain.split(" "),
|
||||
};
|
||||
});
|
||||
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
|
||||
async onGetCertList(data: any) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
|
||||
const list = await this.queryCert(data?.searchKey);
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到证书列表,您可以手动输入证书Id");
|
||||
}
|
||||
const options = list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.id}<${item.domain}>`,
|
||||
value: item.id,
|
||||
domain: item.domain.split(" "),
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
|
||||
new CdnflyDeployToCDNPlugin();
|
||||
@@ -0,0 +1,32 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "ctyun",
|
||||
title: "天翼云授权",
|
||||
desc: "",
|
||||
icon: "ant-design:aliyun-outlined",
|
||||
order: 2,
|
||||
})
|
||||
export class CtyunAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "accessKeyId",
|
||||
component: {
|
||||
placeholder: "accessKeyId",
|
||||
},
|
||||
helper: "[前往创建天翼云AccessKey](https://iam.ctyun.cn/myAccessKey)",
|
||||
required: true,
|
||||
})
|
||||
accessKeyId = "";
|
||||
@AccessInput({
|
||||
title: "securityKey",
|
||||
component: {
|
||||
placeholder: "securityKey",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
helper: "",
|
||||
})
|
||||
securityKey = "";
|
||||
}
|
||||
|
||||
new CtyunAccess();
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access/ctyun-access.js";
|
||||
export * from "./lib.js";
|
||||
@@ -0,0 +1,249 @@
|
||||
import { HttpClient, ILogger } from "@certd/basic";
|
||||
import * as querystring from "node:querystring";
|
||||
import { CtyunAccess } from "./access/ctyun-access.js";
|
||||
export type CtyunCdnDomainInfo = {
|
||||
area_scope: number;
|
||||
insert_date: number;
|
||||
domain: string;
|
||||
cname: string;
|
||||
record_num: string;
|
||||
product_code: string;
|
||||
product_name: string;
|
||||
status: number;
|
||||
};
|
||||
export type CtyunClientOptions = {
|
||||
access: CtyunAccess;
|
||||
logger: ILogger;
|
||||
http: HttpClient;
|
||||
};
|
||||
|
||||
export class CtyunClient {
|
||||
opts: CtyunClientOptions;
|
||||
access: CtyunAccess;
|
||||
|
||||
constructor(opts: CtyunClientOptions) {
|
||||
this.opts = opts;
|
||||
this.access = opts.access;
|
||||
}
|
||||
|
||||
encode(str) {
|
||||
return Buffer.from(str, "utf-8").toString("base64");
|
||||
}
|
||||
|
||||
// 解码
|
||||
decode(str) {
|
||||
return Buffer.from(str, "base64").toString("utf-8");
|
||||
}
|
||||
|
||||
// 用 - 和 _ 来代替 + 和 / ,以适应URL中的传输
|
||||
urlEncode(str) {
|
||||
const encoded = this.encode(str);
|
||||
return encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
urlDecode(str) {
|
||||
str = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||
// 补全字符数为4的整数
|
||||
while (str.length % 4) {
|
||||
str += "=";
|
||||
}
|
||||
return this.decode(str);
|
||||
}
|
||||
|
||||
async hmacsha256(content, key) {
|
||||
const crypto = await import("crypto");
|
||||
const h = crypto.createHmac("sha256", Buffer.from(key, "base64")).update(content).digest();
|
||||
return this.urlEncode(h);
|
||||
}
|
||||
|
||||
async getHeader(uri) {
|
||||
// AK
|
||||
const AK = this.access.accessKeyId;
|
||||
// SK
|
||||
const SK = this.access.securityKey;
|
||||
|
||||
const dateNow = +new Date();
|
||||
const content = AK + "\n" + dateNow + "\n" + uri;
|
||||
const authorizedKey = await this.hmacsha256(AK + ":" + parseInt(String(dateNow / 86400000)), SK);
|
||||
const signature = await this.hmacsha256(content, authorizedKey);
|
||||
|
||||
return {
|
||||
"x-alogic-now": dateNow,
|
||||
"x-alogic-app": AK,
|
||||
"x-alogic-signature": signature, // 对本次调用信息的签名
|
||||
"x-alogic-ac": "app", // 访问控制器id,取固定值app
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口描述:调用本接口创建证书。
|
||||
* 请求方式:post
|
||||
* 请求路径:/v1/cert/create
|
||||
* 使用说明: 单个用户一分钟限制调用10000次,并发不超过100;
|
||||
*
|
||||
* 请求参数说明:
|
||||
*
|
||||
* 参数 类型 是否必传 名称 描述
|
||||
* name string 是 证书备注名
|
||||
* key string 是 证书私钥 仅支持PEM格式
|
||||
* certs string 是 证书公钥 仅支持PEM格式
|
||||
* email string 否 用户邮箱
|
||||
* 返回参数说明:
|
||||
*
|
||||
* 参数 类型 是否必传 名称及描述
|
||||
* code int 是 状态码
|
||||
* message string 是 描述信息
|
||||
* id int 是 证书id
|
||||
* 示例:
|
||||
* 请求路径:https://open.ctcdn.cn/api/v1/cert/create
|
||||
*
|
||||
* 示例1:
|
||||
* 请求参数:
|
||||
*
|
||||
* {
|
||||
* "name": "xxxx",
|
||||
* "certs": "xxxxx",
|
||||
* "key": "xxxxxx"
|
||||
* }
|
||||
*
|
||||
* 返回结果:
|
||||
*
|
||||
* {
|
||||
* "code": 100000,
|
||||
* "message": "success",
|
||||
* "id": 7028
|
||||
* }
|
||||
*/
|
||||
async doRequest({ uri, method, data }: any) {
|
||||
const http = this.opts.http;
|
||||
|
||||
const body: any = {};
|
||||
if (method === "get") {
|
||||
if (data) {
|
||||
uri = uri + "?" + querystring.stringify(data);
|
||||
}
|
||||
} else {
|
||||
body.data = data;
|
||||
}
|
||||
const header = await this.getHeader(uri);
|
||||
|
||||
const url = "https://open.ctcdn.cn" + uri;
|
||||
|
||||
const res = await http.request({
|
||||
url,
|
||||
method,
|
||||
...body,
|
||||
headers: {
|
||||
...header,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (res.code !== 100000) {
|
||||
throw new Error(`请求失败:${res.message}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async certCreate(name: string, crt: string, key: string) {
|
||||
const uri = "/v1/cert/create";
|
||||
const method = "post";
|
||||
const data = {
|
||||
name,
|
||||
key,
|
||||
certs: crt,
|
||||
};
|
||||
return this.doRequest({ uri, method, data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口描述:调用本接口查询域名列表及域名的基础信息
|
||||
* 请求方式:get
|
||||
* 请求路径:/v2/domain/query
|
||||
* 使用说明:
|
||||
*
|
||||
* 新建的域名需要配置部署完毕(预计10分钟),本接口才能查询到
|
||||
* 单个用户一分钟限制调用10000次,并发不超过10
|
||||
* 请求参数说明:
|
||||
*
|
||||
* 参数
|
||||
* 类型
|
||||
* 是否必填
|
||||
* 名称
|
||||
* 说明
|
||||
* access_mode int 否 接入方式 枚举值:1(域名接入方式),2(无域名接入方式),不传默认1
|
||||
* domain string 否 域名 域名,不填默认所有域名
|
||||
* instance string 否 实例名称 不超过10个字的中/英文/数字组合;当access_mode=2时,必填
|
||||
* product_code string 否 产品类型 支持产品类型:“001”(静态加速),“003”(下载加速),“004”(视频点播加速),“008”(CDN加速),“007”(安全加速),“005”(直播加速),“006”(全站加速),“009”(应用加速),“010”(web应用防火墙(边缘云版)),“011”(高防DDoS(边缘云版)),“014”(下载加速闲时),“020”(边缘安全加速),“024”(边缘接入),不填默认所有产品
|
||||
* status int 否 域名状态 枚举值:4(已启用),6(已停止);不填默认所有状态
|
||||
* area_scope int 否 加速范围 1(国内);2(海外);3(全球),不填默认所有加速范围
|
||||
* page int 否 页码 不填默认1
|
||||
* page_size int 否 每页条数 不填默认50,最大100
|
||||
* 返回参数说明:
|
||||
*
|
||||
* 参数 类型 是否必传 名称及描述
|
||||
* code int 是 状态码
|
||||
* message string 是 描述信息
|
||||
* total int 否 查询结果总条数
|
||||
* total_count int 否 查询结果总条数
|
||||
* page int 否 当前页数
|
||||
* page_size int 否 每页条数
|
||||
* page_count int 否 查询结果总页数
|
||||
* result list<object> 否 返回结果列表
|
||||
* result[*].domain string 否 域名
|
||||
* result[*].cname string 否 cname
|
||||
* result[*].product_code string 否 产品类型
|
||||
* result[*].product_name string 否 产品名称
|
||||
* result[*].status int 否 域名状态
|
||||
* result[*].insert_date int 否 域名创建时间,单位毫秒
|
||||
* result[*].area_scope int 否 加速范围
|
||||
* result[*].record_num string 否 备案号
|
||||
* 示例:
|
||||
* 请求路径:https://open.ctcdn.cn/api/v2/domain/query
|
||||
*
|
||||
* 示例1:https://open.ctcdn.cn/api/v2/domain/query?page=1&page_size=2
|
||||
* 返回结果:
|
||||
*
|
||||
* {
|
||||
* "code": 100000,
|
||||
* "message": "success",
|
||||
* "total": 52,
|
||||
* "total_count": 52,
|
||||
* "page": 1,
|
||||
* "page_count": 26,
|
||||
* "page_size": 2,
|
||||
* "result": [
|
||||
* {
|
||||
* "area_scope": 1,
|
||||
* "insert_date": 1667882163000,
|
||||
* "domain": "sd54sdhmytest.baidu.ctyun.cn",
|
||||
* "cname": "sd54sdhmytest.baidu.ctyun.cn.ctadns.cn.",
|
||||
* "record_num": "京ICP证030173号-1",
|
||||
* "product_code": "008",
|
||||
* "product_name": "CDN加速",
|
||||
* "status": 4
|
||||
* },
|
||||
* {
|
||||
* "area_scope": 1,
|
||||
* "insert_date": 1666765345000,
|
||||
* "domain": "sd54sd.baidu.ctyun.cn",
|
||||
* "cname": "sd54sd.baidu.ctyun.cn.ctadns.cn.",
|
||||
* "record_num": "京ICP证030173号-1",
|
||||
* "product_code": "008",
|
||||
* "product_name": "CDN加速",
|
||||
* "status": 6
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* @param cert_list
|
||||
*/
|
||||
async getDomainList({ productCode }: any): Promise<CtyunCdnDomainInfo[]> {
|
||||
const uri = "/v2/domain/query";
|
||||
const method = "get";
|
||||
const data = {
|
||||
product_code: productCode,
|
||||
page_size: 100,
|
||||
};
|
||||
const res = await this.doRequest({ uri, method, data });
|
||||
return res.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-deploy-to-cdn.js";
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { HttpClient } from "@certd/basic";
|
||||
import { CtyunClient } from "../lib.js";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
name: "CtyunDeployToCDN",
|
||||
title: "天翼云-部署证书到CDN",
|
||||
icon: "svg:icon-ctyun",
|
||||
group: pluginGroups.cdn.key,
|
||||
desc: "部署证书到天翼云CDN和全站加速",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class CtyunDeployToCDN extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine())
|
||||
certDomains!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "产品类型",
|
||||
helper: "加速产品类型",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [
|
||||
/**
|
||||
* “001”(静态加速),“003”:(下载加速), “004”(视频点播加速),“008”(CDN加速),“006”(全站加速),“007”(安全加速) ,“014”(下载加速闲时)
|
||||
*/
|
||||
{ label: "静态加速", value: "001" },
|
||||
{ label: "下载加速", value: "003" },
|
||||
{ label: "视频点播加速", value: "004" },
|
||||
{ label: "CDN加速", value: "008" },
|
||||
{ label: "全站加速", value: "006" },
|
||||
{ label: "安全加速", value: "007" },
|
||||
{ label: "下载加速闲时", value: "014" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
productCode: string;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "天翼云授权",
|
||||
helper: "天翼云授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "ctyun",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "加速域名",
|
||||
helper: "请选择加速域名",
|
||||
typeName: "CtyunDeployToCDN",
|
||||
action: CtyunDeployToCDN.prototype.onGetDomainList.name,
|
||||
})
|
||||
)
|
||||
domains!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
/*
|
||||
接口描述:调用本接口批量修改加速域名配置信息
|
||||
请求方式:post
|
||||
请求路径:/v1/domain/batch_update_configuration_information
|
||||
使用说明:
|
||||
|
||||
修改域名之前,您需要先开通对应产品类型的服务,且保证资源包/按需服务有效;
|
||||
该域名没有在途工单;
|
||||
单个用户一分钟限制调用10次
|
||||
*/
|
||||
|
||||
const access = await this.getAccess(this.accessId);
|
||||
|
||||
const client = new CtyunClient({
|
||||
access,
|
||||
http: this.ctx.http,
|
||||
logger: this.ctx.logger,
|
||||
});
|
||||
|
||||
const certName = this.appendTimeSuffix("certd");
|
||||
await client.certCreate(certName, this.cert.crt, this.cert.key);
|
||||
const uri = "/v1/domain/batch_update_configuration_information";
|
||||
|
||||
const lockKey = `ctyun-deploy-to-cdn-${this.accessId}`;
|
||||
await this.ctx.utils.locker.execute(lockKey, async () => {
|
||||
const res = await client.doRequest({
|
||||
uri,
|
||||
method: "post",
|
||||
data: {
|
||||
domain: this.domains,
|
||||
product_code: this.productCode,
|
||||
cert_name: certName,
|
||||
https_status: "on",
|
||||
},
|
||||
});
|
||||
|
||||
const domain_details = res.domain_details;
|
||||
const errorMessage = "";
|
||||
for (const domainDetail of domain_details) {
|
||||
// "code":200002,"domain":"ctyun.handsfree.work","message":"参数cert_name只在https_status为on时才有效"}
|
||||
if (domainDetail.code !== 100000) {
|
||||
const thisMessage = `部署失败[${domainDetail.code}]:${domainDetail.domain}:${domainDetail.message}`;
|
||||
if (thisMessage.includes("已有进行中的工单") || errorMessage.includes("域名正在操作中")) {
|
||||
this.logger.warn(thisMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMessage) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
await this.ctx.utils.sleep(5000);
|
||||
});
|
||||
|
||||
this.logger.info("部署成功");
|
||||
}
|
||||
|
||||
async onGetDomainList() {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const http: HttpClient = this.ctx.http;
|
||||
const client = new CtyunClient({
|
||||
access,
|
||||
http,
|
||||
logger: this.ctx.logger,
|
||||
});
|
||||
|
||||
const all = await client.getDomainList({ productCode: this.productCode });
|
||||
|
||||
if (!all || all.length === 0) {
|
||||
throw new Error("未找到加速域名,你可以手动输入");
|
||||
}
|
||||
const options = all.map(item => {
|
||||
return {
|
||||
label: item.domain,
|
||||
value: item.domain,
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
new CtyunDeployToCDN();
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugins/index.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-upload-to-ftp.js";
|
||||
@@ -0,0 +1,215 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { FtpAccess } from "../../../plugin-lib/ftp/access.js";
|
||||
import { FtpClient } from "../../../plugin-lib/ftp/client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "UploadCertToFTP",
|
||||
title: "FTP-上传证书到FTP",
|
||||
icon: "mdi:folder-upload-outline",
|
||||
group: pluginGroups.host.key,
|
||||
desc: "将证书上传到FTP服务器",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class UploadCertToFTPPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "证书格式",
|
||||
helper: "要部署的证书格式,支持pem、pfx、der、jks",
|
||||
component: {
|
||||
name: "a-select",
|
||||
options: [
|
||||
{ value: "pem", label: "pem,Nginx等大部分应用" },
|
||||
{ value: "pfx", label: "pfx,一般用于IIS" },
|
||||
{ value: "der", label: "der,一般用于Apache" },
|
||||
{ value: "jks", label: "jks,一般用于JAVA应用" },
|
||||
{ value: "one", label: "一体化证书,证书和私钥合并为一个pem文件" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
certType!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "PEM证书保存路径",
|
||||
helper: "需要有写入权限,路径要包含文件名",
|
||||
component: {
|
||||
placeholder: "/test/fullchain.pem",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.certType === 'pem';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
rules: [{ type: "filepath" }],
|
||||
})
|
||||
crtPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "私钥保存路径",
|
||||
helper: "需要有写入权限,路径要包含文件名",
|
||||
component: {
|
||||
placeholder: "/test/privatekey.pem",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.certType === 'pem';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
rules: [{ type: "filepath" }],
|
||||
})
|
||||
keyPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "中间证书保存路径",
|
||||
helper: "需要有写入权限,路径要包含文件名",
|
||||
component: {
|
||||
placeholder: "/test/immediate.pem",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.certType === 'pem';
|
||||
})
|
||||
}
|
||||
`,
|
||||
rules: [{ type: "filepath" }],
|
||||
})
|
||||
icPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "PFX证书保存路径",
|
||||
helper: "需要有写入权限,路径要包含文件名",
|
||||
component: {
|
||||
placeholder: "/test/cert.pfx",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.certType === 'pfx';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
rules: [{ type: "filepath" }],
|
||||
})
|
||||
pfxPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "DER证书保存路径",
|
||||
helper: "需要有写入权限,路径要包含文件名\n.der和.cer是相同的东西,改个后缀名即可",
|
||||
component: {
|
||||
placeholder: "/test/cert.der 或 /test/cert.cer",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.certType === 'der';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
rules: [{ type: "filepath" }],
|
||||
})
|
||||
derPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "jks证书保存路径",
|
||||
helper: "证书原本的保存路径,路径要包含文件名",
|
||||
component: {
|
||||
placeholder: "/test/javaapp/cert.jks",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.certType === 'jks';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
rules: [{ type: "filepath" }],
|
||||
})
|
||||
jksPath!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "一体化证书保存路径",
|
||||
helper: "证书原本的保存路径,路径要包含文件名",
|
||||
component: {
|
||||
placeholder: "/app/ssl/one.pem",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.certType === 'one';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
rules: [{ type: "filepath" }],
|
||||
})
|
||||
onePath!: string;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "FTP授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "ftp",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const { cert, accessId } = this;
|
||||
const access = await this.getAccess<FtpAccess>(accessId);
|
||||
const client = new FtpClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
});
|
||||
await client.connect(async () => {
|
||||
const certReader = new CertReader(cert);
|
||||
const handle = async ({ reader, tmpCrtPath, tmpKeyPath, tmpDerPath, tmpPfxPath, tmpIcPath, tmpJksPath, tmpOnePath }) => {
|
||||
try {
|
||||
await client.upload(tmpCrtPath, this.crtPath);
|
||||
await client.upload(tmpKeyPath, this.keyPath);
|
||||
await client.upload(tmpIcPath, this.icPath);
|
||||
await client.upload(tmpPfxPath, this.pfxPath);
|
||||
await client.upload(tmpDerPath, this.derPath);
|
||||
await client.upload(tmpJksPath, this.jksPath);
|
||||
await client.upload(tmpOnePath, this.onePath);
|
||||
} catch (e) {
|
||||
this.logger.error("请确认路径是否包含文件名,路径本身不能是目录,路径不能有*?之类的特殊符号,要有写入权限");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
await certReader.readCertFile({ logger: this.logger, handle });
|
||||
});
|
||||
|
||||
this.logger.info("执行完成");
|
||||
}
|
||||
}
|
||||
new UploadCertToFTPPlugin();
|
||||
@@ -0,0 +1,21 @@
|
||||
export * from "./baota/index.js";
|
||||
export * from "./yidun/index.js";
|
||||
export * from "./ftp/index.js";
|
||||
export * from "./cdnfly/index.js";
|
||||
export * from "./synology/index.js";
|
||||
export * from "./k8s/index.js";
|
||||
export * from "./1panel/index.js";
|
||||
export * from "./baidu/index.js";
|
||||
export * from "./lecdn/index.js";
|
||||
export * from "./baishan/index.js";
|
||||
export * from "./plesk/index.js";
|
||||
export * from "./yizhifu/index.js";
|
||||
export * from "./alipay/index.js";
|
||||
export * from "./wxpay/index.js";
|
||||
export * from "./safeline/index.js";
|
||||
export * from "./ctyun/index.js";
|
||||
export * from "./lucky/index.js";
|
||||
export * from "./kuocai/index.js";
|
||||
export * from "./unicloud/index.js";
|
||||
export * from "./maoyun/index.js";
|
||||
export * from "./xinnet/index.js";
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "k8s",
|
||||
title: "k8s授权",
|
||||
desc: "",
|
||||
icon: "mdi:kubernetes",
|
||||
})
|
||||
export class K8sAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "kubeconfig",
|
||||
component: {
|
||||
name: "a-textarea",
|
||||
vModel: "value",
|
||||
placeholder: "kubeconfig",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
kubeconfig = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "忽略证书校验",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
required: false,
|
||||
encrypt: false,
|
||||
})
|
||||
skipTLSVerify: boolean;
|
||||
}
|
||||
|
||||
new K8sAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./access.js";
|
||||
export * from "./plugins/index.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugin-secret.js";
|
||||
export * from "./plugin-ingress.js";
|
||||
export * from "./plugin-apply.js";
|
||||
@@ -0,0 +1,135 @@
|
||||
import { utils } from "@certd/basic";
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import dayjs from "dayjs";
|
||||
import { get } from "lodash-es";
|
||||
import { K8sAccess } from "../access.js";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
@IsTaskPlugin({
|
||||
name: "K8sApply",
|
||||
title: "K8S-Apply自定义yaml",
|
||||
icon: "mdi:kubernetes",
|
||||
desc: "apply自定义yaml到k8s",
|
||||
needPlus: true,
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class K8sApplyPlugin extends AbstractPlusTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput({
|
||||
title: "前置任务输出",
|
||||
helper: "请选择前置任务输出的内容",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: ["::"],
|
||||
},
|
||||
required: false,
|
||||
})
|
||||
preOutput!: any;
|
||||
|
||||
@TaskInput({
|
||||
title: "k8s授权",
|
||||
helper: "kubeconfig",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "k8s",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
// @TaskInput({
|
||||
// title: "命名空间",
|
||||
// value: "default",
|
||||
// component: {
|
||||
// placeholder: "命名空间",
|
||||
// },
|
||||
// required: true,
|
||||
// })
|
||||
// namespace!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "yaml",
|
||||
required: true,
|
||||
helper: "apply yaml,模板变量:主域名=${mainDomain}、全部域名=${domains}、过期时间=${expiresTime}、证书PEM=${crt}、证书私钥=${key}、中间证书/CA证书=${ic}、前置任务输出=${preOutput}",
|
||||
component: {
|
||||
name: "a-textarea",
|
||||
vModel: "value",
|
||||
rows: 6,
|
||||
},
|
||||
})
|
||||
yamlContent!: string;
|
||||
|
||||
K8sClient: any;
|
||||
async onInstance() {
|
||||
const sdk = await import("@certd/lib-k8s");
|
||||
this.K8sClient = sdk.K8sClient;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const access: K8sAccess = await this.getAccess(this.accessId);
|
||||
const client = new this.K8sClient({
|
||||
kubeConfigStr: access.kubeconfig,
|
||||
logger: this.logger,
|
||||
skipTLSVerify: access.skipTLSVerify,
|
||||
});
|
||||
if (!this.yamlContent) {
|
||||
throw new Error("yamlContent is empty");
|
||||
}
|
||||
|
||||
const certReader = new CertReader(this.cert);
|
||||
const mainDomain = certReader.getMainDomain();
|
||||
const domains = certReader.getAllDomains().join(",");
|
||||
|
||||
const data = {
|
||||
mainDomain,
|
||||
domains,
|
||||
expiresTime: dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss"),
|
||||
crt: this.cert.crt,
|
||||
key: this.cert.key,
|
||||
ic: this.cert.ic,
|
||||
preOutput: this.preOutput,
|
||||
};
|
||||
|
||||
const compile = this.compile(this.yamlContent);
|
||||
const compiledYaml = compile(data);
|
||||
// 解析 YAML 内容(可能包含多个文档)
|
||||
// const yamlDocs = yaml.loadAll(compiledYaml);
|
||||
|
||||
try {
|
||||
// this.logger.info("apply yaml:", compiledYaml);
|
||||
// this.logger.info("apply yamlDoc:", JSON.stringify(doc));
|
||||
const res = await client.apply(compiledYaml);
|
||||
this.logger.info("apply result:", res);
|
||||
} catch (e) {
|
||||
if (e.response?.body) {
|
||||
throw new Error(JSON.stringify(e.response.body));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
await utils.sleep(5000); // 停留2秒,等待secret部署完成
|
||||
}
|
||||
|
||||
compile(templateString: string) {
|
||||
return function (data) {
|
||||
return templateString.replace(/\${(.*?)}/g, (match, key) => {
|
||||
const value = get(data, key, "");
|
||||
return String(value);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
new K8sApplyPlugin();
|
||||
@@ -0,0 +1,130 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { utils } from "@certd/basic";
|
||||
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
import { K8sAccess } from "../access.js";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
name: "K8sDeployToIngress",
|
||||
title: "K8S-Ingress 证书部署",
|
||||
icon: "mdi:kubernetes",
|
||||
desc: "部署证书到k8s的Ingress",
|
||||
needPlus: false,
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class K8sDeployToIngressPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "命名空间",
|
||||
value: "default",
|
||||
component: {
|
||||
placeholder: "命名空间",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
namespace!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "IngressName",
|
||||
required: true,
|
||||
helper: "Ingress名称,根据名称查找证书Secret,然后更新",
|
||||
})
|
||||
ingressName!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "k8s授权",
|
||||
helper: "kubeconfig",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "k8s",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput({
|
||||
title: "Secret自动创建",
|
||||
helper: "如果Secret不存在,则创建",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
})
|
||||
createOnNotFound: boolean;
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access: K8sAccess = await this.getAccess(this.accessId);
|
||||
const sdk = await import("@certd/lib-k8s");
|
||||
const K8sClient = sdk.K8sClient;
|
||||
const k8sClient = new K8sClient({
|
||||
kubeConfigStr: access.kubeconfig,
|
||||
logger: this.logger,
|
||||
skipTLSVerify: access.skipTLSVerify,
|
||||
});
|
||||
|
||||
const ingressList = await k8sClient.getIngressList({
|
||||
namespace: this.namespace,
|
||||
});
|
||||
const ingress = ingressList.items.find((ingress: any) => ingress.metadata.name === this.ingressName);
|
||||
if (!ingress) {
|
||||
throw new Error(`Ingress不存在:${this.ingressName}`);
|
||||
}
|
||||
if (!ingress.spec.tls) {
|
||||
throw new Error(`Ingress:${this.ingressName} 还未配置证书,请先手动配置好证书,创建一个Secret`);
|
||||
}
|
||||
const secretNames = ingress.spec.tls.map((tls: any) => tls.secretName);
|
||||
if (!secretNames || secretNames.length === 0) {
|
||||
throw new Error(`Ingress:${this.ingressName} 未找到证书Secret`);
|
||||
}
|
||||
await this.patchNginxCertSecret({ cert: this.cert, k8sClient, secretNames });
|
||||
await utils.sleep(5000); // 停留5秒,等待secret部署完成
|
||||
}
|
||||
|
||||
async patchNginxCertSecret(options: { cert: CertInfo; k8sClient: any; secretNames: string[] }) {
|
||||
const { cert, k8sClient } = options;
|
||||
const crt = cert.crt;
|
||||
const key = cert.key;
|
||||
const crtBase64 = Buffer.from(crt).toString("base64");
|
||||
const keyBase64 = Buffer.from(key).toString("base64");
|
||||
|
||||
const { namespace } = this;
|
||||
|
||||
const body: any = {
|
||||
data: {
|
||||
"tls.crt": crtBase64,
|
||||
"tls.key": keyBase64,
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd"),
|
||||
},
|
||||
},
|
||||
};
|
||||
for (const secret of options.secretNames) {
|
||||
this.logger.info(`更新ingress cert Secret:${secret}`);
|
||||
await k8sClient.patchSecret({ namespace, secretName: secret, body, createOnNotFound: this.createOnNotFound });
|
||||
this.logger.info(`ingress cert Secret已更新:${secret}`);
|
||||
}
|
||||
await utils.sleep(5000); // 停留5秒,等待secret部署完成
|
||||
if (this.ingressName) {
|
||||
await k8sClient.restartIngress(namespace, [this.ingressName], { certd: this.appendTimeSuffix("certd") });
|
||||
}
|
||||
}
|
||||
}
|
||||
new K8sDeployToIngressPlugin();
|
||||
@@ -0,0 +1,148 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { utils } from "@certd/basic";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
import { K8sAccess } from "../access.js";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
name: "K8sDeployToSecret",
|
||||
title: "K8S-部署证书到Secret",
|
||||
icon: "mdi:kubernetes",
|
||||
desc: "部署证书到k8s的secret",
|
||||
needPlus: false,
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class K8sDeployToSecretPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: "命名空间",
|
||||
value: "default",
|
||||
component: {
|
||||
placeholder: "命名空间",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
namespace!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "保密字典Id",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
},
|
||||
helper: "原本存储证书的secret的name",
|
||||
required: true,
|
||||
})
|
||||
secretName!: string | string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "k8s授权",
|
||||
helper: "kubeconfig",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "k8s",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput({
|
||||
title: "ingress名称",
|
||||
required: false,
|
||||
helper: "填写之后会自动重启ingress",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
},
|
||||
})
|
||||
ingressName!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "Secret自动创建",
|
||||
helper: "如果Secret不存在,则创建",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
})
|
||||
createOnNotFound: boolean;
|
||||
|
||||
K8sClient: any;
|
||||
async onInstance() {
|
||||
const sdk = await import("@certd/lib-k8s");
|
||||
this.K8sClient = sdk.K8sClient;
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
const access: K8sAccess = await this.getAccess(this.accessId);
|
||||
const k8sClient = new this.K8sClient({
|
||||
kubeConfigStr: access.kubeconfig,
|
||||
logger: this.logger,
|
||||
skipTLSVerify: access.skipTLSVerify,
|
||||
});
|
||||
try {
|
||||
await this.patchCertSecret({ cert: this.cert, k8sClient });
|
||||
} catch (e) {
|
||||
if (e.response?.body) {
|
||||
throw new Error(JSON.stringify(e.response.body));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
await utils.sleep(5000); // 停留2秒,等待secret部署完成
|
||||
}
|
||||
|
||||
async patchCertSecret(options: { cert: CertInfo; k8sClient: any }) {
|
||||
const { cert, k8sClient } = options;
|
||||
const crt = cert.crt;
|
||||
const key = cert.key;
|
||||
const crtBase64 = Buffer.from(crt).toString("base64");
|
||||
const keyBase64 = Buffer.from(key).toString("base64");
|
||||
|
||||
const { namespace, secretName } = this;
|
||||
|
||||
const body: any = {
|
||||
data: {
|
||||
"tls.crt": crtBase64,
|
||||
"tls.key": keyBase64,
|
||||
},
|
||||
metadata: {
|
||||
labels: {
|
||||
certd: this.appendTimeSuffix("certd"),
|
||||
},
|
||||
},
|
||||
};
|
||||
let secretNames: any = secretName;
|
||||
if (typeof secretName === "string") {
|
||||
secretNames = [secretName];
|
||||
}
|
||||
for (const secret of secretNames) {
|
||||
body.metadata.name = secret;
|
||||
await k8sClient.patchSecret({ namespace, secretName: secret, body, createOnNotFound: this.createOnNotFound });
|
||||
this.logger.info(`ingress cert Secret已更新:${secret}`);
|
||||
}
|
||||
await utils.sleep(5000); // 停留5秒,等待secret部署完成
|
||||
if (this.ingressName && this.ingressName.length > 0) {
|
||||
await k8sClient.restartIngress(namespace, this.ingressName, { certd: this.appendTimeSuffix("certd") });
|
||||
}
|
||||
}
|
||||
}
|
||||
new K8sDeployToSecretPlugin();
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "kuocaicdn",
|
||||
title: "括彩云cdn授权",
|
||||
icon: "material-symbols:shield-outline",
|
||||
desc: "括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8)",
|
||||
})
|
||||
export class KuocaiCdnAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "账户",
|
||||
component: {
|
||||
placeholder: "手机号",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
username = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "密码",
|
||||
component: {
|
||||
placeholder: "password",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = "";
|
||||
}
|
||||
|
||||
new KuocaiCdnAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-deploy-to-cdn.js";
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { KuocaiCdnAccess } from "../access.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "KuocaiDeployToRCDN",
|
||||
title: "括彩云-部署到括彩云CDN",
|
||||
icon: "material-symbols:shield-outline",
|
||||
group: pluginGroups.cdn.key,
|
||||
desc: "括彩云CDN,每月免费30G,[注册即领](https://kuocaicdn.com/register?code=8mn536rrzfbf8)",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class KuocaiDeployToCDNPlugin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "括彩云CDN授权",
|
||||
helper: "括彩云CDN授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "kuocaicdn",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "域名列表",
|
||||
helper: "选择要部署证书的站点域名",
|
||||
typeName: "KuocaiDeployToCDNPlugin",
|
||||
action: KuocaiDeployToCDNPlugin.prototype.onGetDomainList.name,
|
||||
})
|
||||
)
|
||||
domains!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<KuocaiCdnAccess>(this.accessId);
|
||||
const loginRes = await this.getLoginToken(access);
|
||||
|
||||
const curl = "https://kuocaicdn.com/CdnDomainHttps/httpsConfiguration";
|
||||
for (const domain of this.domains) {
|
||||
// const data = {
|
||||
// doMainId: domain,
|
||||
// https: {
|
||||
// https_status: "off"
|
||||
// },
|
||||
// }
|
||||
// //先关闭https
|
||||
// const res = await this.doRequest(curl, loginRes, data);
|
||||
|
||||
const cert = this.cert;
|
||||
const update = {
|
||||
doMainId: domain,
|
||||
https: {
|
||||
https_status: "on",
|
||||
certificate_name: this.appendTimeSuffix("certd"),
|
||||
certificate_source: "0",
|
||||
certificate_value: cert.crt,
|
||||
private_key: cert.key,
|
||||
},
|
||||
};
|
||||
await this.doRequest(curl, loginRes, update);
|
||||
this.logger.info(`站点${domain}证书更新成功`);
|
||||
}
|
||||
}
|
||||
|
||||
async getLoginToken(access: KuocaiCdnAccess) {
|
||||
const url = "https://kuocaicdn.com/login/loginUser";
|
||||
const data = {
|
||||
userAccount: access.username,
|
||||
userPwd: access.password,
|
||||
remember: true,
|
||||
};
|
||||
const http = this.ctx.http;
|
||||
const res: any = await http.request({
|
||||
url,
|
||||
method: "POST",
|
||||
data,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
returnOriginRes: true,
|
||||
});
|
||||
if (!res.data?.success) {
|
||||
throw new Error(res.data?.message);
|
||||
}
|
||||
|
||||
const jsessionId = this.ctx.utils.request.getCookie(res, "JSESSIONID");
|
||||
const token = res.data?.data;
|
||||
return {
|
||||
jsessionId,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
async getDomainList(loginRes: any) {
|
||||
const url = "https://kuocaicdn.com/CdnDomain/queryForDatatables";
|
||||
const data = {
|
||||
draw: 1,
|
||||
start: 0,
|
||||
length: 1000,
|
||||
search: {
|
||||
value: "",
|
||||
regex: false,
|
||||
},
|
||||
};
|
||||
|
||||
const res = await this.doRequest(url, loginRes, data);
|
||||
return res.data?.data;
|
||||
}
|
||||
|
||||
private async doRequest(url: string, loginRes: any, data: any) {
|
||||
const http = this.ctx.http;
|
||||
const res: any = await http.request({
|
||||
url,
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `JSESSIONID=${loginRes.jsessionId};kuocai_cdn_token=${loginRes.token}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
if (!res.success) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async onGetDomainList(data: any) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
const access = await this.getAccess<KuocaiCdnAccess>(this.accessId);
|
||||
|
||||
const loginRes = await this.getLoginToken(access);
|
||||
|
||||
const list = await this.getDomainList(loginRes);
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("您账户下还没有站点域名,请先添加域名");
|
||||
}
|
||||
return list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.domainName}<${item.id}>`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
new KuocaiDeployToCDNPlugin();
|
||||
@@ -0,0 +1,90 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "lecdn",
|
||||
title: "LeCDN授权",
|
||||
desc: "",
|
||||
icon: "material-symbols:shield-outline",
|
||||
})
|
||||
export class LeCDNAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "LeCDN系统网址",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "例如:http://demo.xxxx.cn",
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "认证类型",
|
||||
component: {
|
||||
placeholder: "请选择",
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "token", label: "API访问令牌" },
|
||||
{ value: "password", label: "账号密码(旧版本)" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
type!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "用户名",
|
||||
component: {
|
||||
placeholder: "username",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
show:ctx.compute(({form})=>{
|
||||
return form.access.type === 'password';
|
||||
})
|
||||
}
|
||||
`,
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
username = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "登录密码",
|
||||
component: {
|
||||
placeholder: "password",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
show:ctx.compute(({form})=>{
|
||||
return form.access.type === 'password';
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
password = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "Api访问令牌",
|
||||
component: {
|
||||
placeholder: "apiToken",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
show:ctx.compute(({form})=>{
|
||||
return form.access.type === 'token';
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
apiToken = "";
|
||||
}
|
||||
|
||||
new LeCDNAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugin-update-cert.js";
|
||||
export * from "./plugin-update-cert-v2.js";
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { LeCDNAccess } from "../access.js";
|
||||
import { merge } from "lodash-es";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { utils } from "@certd/basic";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "LeCDNUpdateCertV2",
|
||||
title: "LeCDN-更新证书V2",
|
||||
desc: "支持新版本LeCDN",
|
||||
icon: "material-symbols:shield-outline",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class LeCDNUpdateCertV2 extends AbstractTaskPlugin {
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "LeCDN授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "lecdn",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//测试参数
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "证书ID",
|
||||
helper: "选择要更新的证书id,注意域名是否与证书匹配",
|
||||
typeName: "LeCDNUpdateCertV2",
|
||||
action: LeCDNUpdateCertV2.prototype.onGetCertList.name,
|
||||
})
|
||||
)
|
||||
certIds!: number[];
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
access: LeCDNAccess;
|
||||
token: string;
|
||||
|
||||
async onInstance() {
|
||||
this.access = await this.getAccess<LeCDNAccess>(this.accessId);
|
||||
this.token = await this.getToken();
|
||||
}
|
||||
|
||||
async doRequest(config: any) {
|
||||
const access = this.access;
|
||||
const Authorization = this.access.type === "token" ? this.access.apiToken : `Bearer ${this.token}`;
|
||||
const res = await this.ctx.http.request({
|
||||
baseURL: access.url,
|
||||
headers: {
|
||||
Authorization,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
this.checkRes(res);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
if (this.access.type === "token") {
|
||||
return this.access.apiToken;
|
||||
}
|
||||
// http://cdnadmin.kxfox.com/prod-api/login
|
||||
const access = this.access;
|
||||
const res = await this.ctx.http.request({
|
||||
url: `/prod-api/login`,
|
||||
baseURL: access.url,
|
||||
method: "post",
|
||||
data: {
|
||||
//新旧版本不一样,旧版本是username,新版本是email
|
||||
email: access.username,
|
||||
username: access.username,
|
||||
password: access.password,
|
||||
},
|
||||
});
|
||||
this.checkRes(res);
|
||||
//新旧版本不一样,旧版本是access_token,新版本是token
|
||||
return res.data.access_token || res.data.token;
|
||||
}
|
||||
|
||||
async getCertInfo(id: number) {
|
||||
// http://cdnadmin.kxfox.com/prod-api/certificate/9
|
||||
// Bearer edGkiOiIJ8
|
||||
return await this.doRequest({
|
||||
url: `/prod-api/certificate/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
async updateCert(id: number, cert: CertInfo) {
|
||||
const certInfo = await this.getCertInfo(id);
|
||||
const body = {
|
||||
ssl_key: utils.hash.base64(cert.key),
|
||||
ssl_pem: utils.hash.base64(cert.crt),
|
||||
};
|
||||
|
||||
merge(certInfo, body);
|
||||
|
||||
this.logger.info(`证书名称:${certInfo.name}`);
|
||||
|
||||
return await this.doRequest({
|
||||
url: `/prod-api/certificate/${id}`,
|
||||
method: "put",
|
||||
data: certInfo,
|
||||
});
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
for (const certId of this.certIds) {
|
||||
this.logger.info(`更新证书:${certId}`);
|
||||
await this.updateCert(certId, this.cert);
|
||||
this.logger.info(`更新证书成功:${certId}`);
|
||||
}
|
||||
|
||||
this.logger.info(`更新证书完成`);
|
||||
}
|
||||
|
||||
private checkRes(res: any) {
|
||||
if (res.code !== 0 && res.code !== 200) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
}
|
||||
|
||||
async onGetCertList(data: any) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
const res = await this.getCerts();
|
||||
//新旧版本不一样,一个data 一个是items
|
||||
const list = res.items || res.data;
|
||||
if (!res || list.length === 0) {
|
||||
throw new Error("没有找到证书,请先手动上传一次证书,并让站点使用该证书");
|
||||
}
|
||||
|
||||
return list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.name}-${item.domain_name}<${item.id}>`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async getCerts() {
|
||||
// http://cdnadmin.kxfox.com/prod-api/certificate?current_page=1&total=3&page_size=10
|
||||
return await this.doRequest({
|
||||
url: `/prod-api/certificate`,
|
||||
method: "get",
|
||||
params: {
|
||||
current_page: 1,
|
||||
page_size: 1000,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new LeCDNUpdateCertV2();
|
||||
@@ -0,0 +1,165 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
import { LeCDNAccess } from "../access.js";
|
||||
import { merge } from "lodash-es";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
name: "LeCDNUpdateCert",
|
||||
title: "LeCDN-更新证书",
|
||||
icon: "material-symbols:shield-outline",
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class LeCDNUpdateCert extends AbstractTaskPlugin {
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "LeCDN授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "lecdn",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//测试参数
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "证书ID",
|
||||
helper: "选择要更新的证书id,注意域名是否与证书匹配",
|
||||
typeName: "LeCDNUpdateCert",
|
||||
action: LeCDNUpdateCert.prototype.onGetCertList.name,
|
||||
})
|
||||
)
|
||||
certIds!: number[];
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
access: LeCDNAccess;
|
||||
token: string;
|
||||
|
||||
async onInstance() {
|
||||
this.access = await this.getAccess<LeCDNAccess>(this.accessId);
|
||||
this.token = await this.getToken();
|
||||
}
|
||||
|
||||
async doRequest(config: any) {
|
||||
const access = this.access;
|
||||
const res = await this.ctx.http.request({
|
||||
baseURL: access.url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
...config,
|
||||
});
|
||||
this.checkRes(res);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
// http://cdnadmin.kxfox.com/prod-api/login
|
||||
const access = this.access;
|
||||
const res = await this.ctx.http.request({
|
||||
url: `/prod-api/login`,
|
||||
baseURL: access.url,
|
||||
method: "post",
|
||||
data: {
|
||||
username: access.username,
|
||||
password: access.password,
|
||||
},
|
||||
});
|
||||
this.checkRes(res);
|
||||
return res.data.access_token;
|
||||
}
|
||||
|
||||
async getCertInfo(id: number) {
|
||||
// http://cdnadmin.kxfox.com/prod-api/certificate/9
|
||||
// Bearer edGkiOiIJ8
|
||||
return await this.doRequest({
|
||||
url: `/prod-api/certificate/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
async updateCert(id: number, cert: CertInfo) {
|
||||
const certInfo = await this.getCertInfo(id);
|
||||
const body = {
|
||||
ssl_key: cert.key,
|
||||
ssl_pem: cert.crt,
|
||||
};
|
||||
|
||||
merge(certInfo, body);
|
||||
|
||||
this.logger.info(`证书名称:${certInfo.name}`);
|
||||
|
||||
return await this.doRequest({
|
||||
url: `/prod-api/certificate/${id}`,
|
||||
method: "put",
|
||||
data: certInfo,
|
||||
});
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
for (const certId of this.certIds) {
|
||||
this.logger.info(`更新证书:${certId}`);
|
||||
await this.updateCert(certId, this.cert);
|
||||
this.logger.info(`更新证书成功:${certId}`);
|
||||
}
|
||||
|
||||
this.logger.info(`更新证书完成`);
|
||||
}
|
||||
|
||||
private checkRes(res: any) {
|
||||
if (res.code !== 0) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
}
|
||||
|
||||
async onGetCertList(data: any) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
const res = await this.getCerts();
|
||||
|
||||
if (!res || res.data.length === 0) {
|
||||
throw new Error("没有找到证书,请先手动上传一次证书,并让站点使用该证书");
|
||||
}
|
||||
|
||||
return res.data.map((item: any) => {
|
||||
return {
|
||||
label: `${item.name}-${item.description}<${item.id}>`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async getCerts() {
|
||||
// http://cdnadmin.kxfox.com/prod-api/certificate?current_page=1&total=3&page_size=10
|
||||
return await this.doRequest({
|
||||
url: `/prod-api/certificate`,
|
||||
method: "get",
|
||||
params: {
|
||||
current_page: 1,
|
||||
page_size: 1000,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new LeCDNUpdateCert();
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "lucky",
|
||||
title: "lucky",
|
||||
desc: "",
|
||||
icon: "svg:icon-lucky",
|
||||
})
|
||||
export class LuckyAccess extends BaseAccess {
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
title: "访问url",
|
||||
component: {
|
||||
placeholder: "http://xxx.xx.xx:16301",
|
||||
},
|
||||
helper: "不要带安全入口",
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
url = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "安全入口",
|
||||
component: {
|
||||
placeholder: "/your_safe_path",
|
||||
},
|
||||
helper: "请参考lucky设置中关于安全入口的配置,",
|
||||
required: false,
|
||||
encrypt: true,
|
||||
})
|
||||
safePath = "";
|
||||
|
||||
/**
|
||||
* 授权属性配置
|
||||
*/
|
||||
@AccessInput({
|
||||
title: "OpenToken",
|
||||
component: {
|
||||
placeholder: "OpenToken",
|
||||
},
|
||||
helper: "设置->最下面开发者设置->启用OpenToken",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
openToken = "";
|
||||
}
|
||||
|
||||
new LuckyAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-upload.js";
|
||||
@@ -0,0 +1,170 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { LuckyAccess } from "../access.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
import { isArray } from "lodash-es";
|
||||
|
||||
@IsTaskPlugin({
|
||||
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
|
||||
name: "LuckyUpdateCert",
|
||||
title: "lucky-更新Lucky证书",
|
||||
icon: "svg:icon-lucky",
|
||||
//插件分组
|
||||
group: pluginGroups.panel.key,
|
||||
needPlus: true,
|
||||
default: {
|
||||
//默认值配置照抄即可
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
//类名规范,跟上面插件名称(name)一致
|
||||
export class LuckyUpdateCert extends AbstractPlusTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
// required: true, // 必填
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "Lucky授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "lucky", //固定授权类型
|
||||
},
|
||||
required: true, //必填
|
||||
})
|
||||
accessId!: string;
|
||||
//
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "Lucky证书",
|
||||
helper: "要更新的Lucky证书",
|
||||
typeName: "LuckyUpdateCert",
|
||||
action: LuckyUpdateCert.prototype.onGetCertList.name,
|
||||
watches: ["accessId"],
|
||||
})
|
||||
)
|
||||
certList!: string[];
|
||||
|
||||
//插件实例化时执行的方法
|
||||
async onInstance() {}
|
||||
|
||||
//插件执行方法
|
||||
async execute(): Promise<void> {
|
||||
const { cert } = this;
|
||||
|
||||
const access: LuckyAccess = await this.getAccess<LuckyAccess>(this.accessId);
|
||||
const list = await this.onGetCertList();
|
||||
|
||||
const certMap: any = {};
|
||||
list.forEach(item => {
|
||||
certMap[item.value] = item.item;
|
||||
});
|
||||
|
||||
for (const item of this.certList) {
|
||||
this.logger.info(`开始更新证书:${item}`);
|
||||
const old = certMap[item];
|
||||
if (!old) {
|
||||
throw new Error(`没有找到证书:Key=${item},请确认该证书是否存在`);
|
||||
}
|
||||
const remark = old.Remark;
|
||||
const res = await this.doRequest({
|
||||
access,
|
||||
urlPath: "/api/ssl",
|
||||
method: "PUT",
|
||||
data: {
|
||||
AddFrom: "file",
|
||||
CertBase64: this.ctx.utils.hash.base64(cert.crt),
|
||||
Enable: true,
|
||||
Key: item,
|
||||
MappingChangeScript: "",
|
||||
MappingPath: "",
|
||||
MappingToPath: false,
|
||||
KeyBase64: this.ctx.utils.hash.base64(cert.key),
|
||||
IssuerCertificate: "",
|
||||
ExtParams: {},
|
||||
Remark: remark,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.info(`更新成功:${JSON.stringify(res)}`);
|
||||
}
|
||||
|
||||
this.logger.info("部署成功");
|
||||
}
|
||||
|
||||
async doRequest(req: { access: LuckyAccess; urlPath: string; data: any; method?: string }) {
|
||||
const { access, urlPath, data, method } = req;
|
||||
let url = `${access.url}/${access.safePath || ""}${urlPath}?_=${Math.floor(new Date().getTime())}`;
|
||||
// 从第7个字符起,将//替换成/
|
||||
const protocol = url.substring(0, 7);
|
||||
let suffix = url.substring(7);
|
||||
suffix = suffix.replaceAll("//", "/");
|
||||
suffix = suffix.replaceAll("//", "/");
|
||||
url = protocol + suffix;
|
||||
|
||||
const headers: any = {
|
||||
// Origin: access.url,
|
||||
"Content-Type": "application/json",
|
||||
// "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36",
|
||||
};
|
||||
headers["openToken"] = access.openToken;
|
||||
const res = await this.http.request({
|
||||
method: method || "POST",
|
||||
url,
|
||||
data,
|
||||
headers,
|
||||
skipSslVerify: true,
|
||||
});
|
||||
if (res.ret !== 0) {
|
||||
throw new Error(`请求失败:${res.msg}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async onGetCertList() {
|
||||
const access: LuckyAccess = await this.getAccess<LuckyAccess>(this.accessId);
|
||||
const res = await this.doRequest({
|
||||
access,
|
||||
urlPath: "/api/ssl",
|
||||
data: {},
|
||||
method: "GET",
|
||||
});
|
||||
const list = res.list;
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到证书,请先在SSL/TLS证书页面中手动上传一次证书");
|
||||
}
|
||||
|
||||
const options = list.map((item: any) => {
|
||||
const certsInfo = item.CertsInfo;
|
||||
let label = "";
|
||||
if (isArray(certsInfo)) {
|
||||
label = item.CertsInfo[0].Domains;
|
||||
} else {
|
||||
label = item.CertsInfo.Domains[0];
|
||||
}
|
||||
return {
|
||||
label: `${item.Remark}<${label}>`,
|
||||
value: item.Key,
|
||||
item: item,
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
//实例化一下,注册插件
|
||||
new LuckyUpdateCert();
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "maoyun",
|
||||
title: "猫云授权",
|
||||
desc: "",
|
||||
icon: "svg:icon-lucky",
|
||||
})
|
||||
export class MaoyunAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "用户名",
|
||||
component: {
|
||||
placeholder: "username/手机号/email",
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "用户名",
|
||||
required: true,
|
||||
})
|
||||
username!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "password",
|
||||
component: {
|
||||
placeholder: "密码",
|
||||
component: {
|
||||
name: "a-input-password",
|
||||
vModel: "value",
|
||||
},
|
||||
},
|
||||
encrypt: true,
|
||||
helper: "密码",
|
||||
required: true,
|
||||
})
|
||||
password!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "HttpProxy",
|
||||
component: {
|
||||
placeholder: "http://192.168.x.x:10811",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
},
|
||||
encrypt: false,
|
||||
required: false,
|
||||
})
|
||||
httpProxy!: string;
|
||||
}
|
||||
|
||||
new MaoyunAccess();
|
||||
@@ -0,0 +1,98 @@
|
||||
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
|
||||
import { MaoyunAccess } from "./access.js";
|
||||
|
||||
export class MaoyunClient {
|
||||
privateKeyPem = "";
|
||||
http: HttpClient;
|
||||
logger: ILogger;
|
||||
access: MaoyunAccess;
|
||||
token: string;
|
||||
|
||||
constructor(opts: { logger: ILogger; http: HttpClient; access: MaoyunAccess }) {
|
||||
this.logger = opts.logger;
|
||||
this.http = opts.http;
|
||||
this.access = opts.access;
|
||||
this.privateKeyPem =
|
||||
"\n-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAt83xKlUSU0i09/pwwQ0MQQ0v71IULdVGJ3AFo+anwLX1TRCp\nxmY5i+xmT9tshHqiPGN8qeg+lDaqA+iwmS6zqi+KlNmmKJc3kUx/h24MI3nff0xy\nz605ZfDgJhwBkJpTI6Sk4+OLX+lZxOiET0nOT7jrhKiFCKX8+0ZXjTJ1cmdifKaj\nqXmjD+XYZzBwA2fCr1kPq2xKvU097Ksu6QvM+La5X/tt+FJOuedmuqZmsb6YQ+6O\n6mJ0bcY0kFDGNkoeY6dEyeJAkIDJbda3n0I71KwRR2J0CSN3TF+w1hSQa7Hp1rXw\n+zQvR6p7O2VY8zQeZZKRKGl7OGdKW5F79iz2fQIDAQABAoIBAEN0BaRGciI0VY2H\n0CdY1X1uDIBke9lSIpvIhZlfxYJ4hFxS2CtiSo4qJGX8HbgElVNaI17rR0P3R6+F\njoG43OCA7/euZEcTL6ZYD5kw7q16RWYfNSc36A+cNXZm4sAhko9LFeQ4FmcNaQ9V\nUXEToe4p6+zUN3Y0DEJezzSXJvjjjodT5L03i2HCW+/xZIHi6oh1DuXdy7h1Ah8s\nSxN188HsX7/SoDHAxDqi/SSGyoYg/SvtOetPtrcZCfqoHfxkR+jQHNaOTq3vGmsu\np8KPtRBoFvSPMxSSHNLb4qbIFvlWRLNXfIhYnenTPtmCnnqogotZZ9CoCHL9dX5R\nt4q5L6ECgYEA5jYhqpRIhqSZOTJopGgy3LBy5T1PHDTfedTuSxnoywYWCuGNwgjI\nRgd94jcUuizO9euobxvDUTdOZ6LdK1NStfwOspb2NojvlE+9SfC8JDv7ZeRz8egB\nClrT6jtCUr80K1I0eF31ha0YMjgi7WZJvTMp53fqI0b1yQO2FaBNgWUCgYEAzGT6\nay+QlO2Fdt9mqeIJy9QiugItC7lk75fQMg5fa8A8wj9DO86o/2k4rKhl7SPg0H+R\nSJQoZGuS4M2f9muEHnLmVF8EzizuHZoR3HO4mie2adVf9NfAmkFsCluRAZKtQkNc\nt/VwlJEC6dChoZkU8Wzd0fSJKrdhjik2ayGXmzkCgYEAuie9s5UyzIXfTSwhCAkm\nT+TzE8Iu7Y0nxPnVM6+g2kNyoZvgqK23XUGDnuCRhzbiqGPGkQovN8Z0RUOiev1m\n3bgUHoAKWvECYrjURS1AxkAmuy8wPsYvyTLHOBpxOD5bLkjMGyVHe7AL59gTDktv\nh2oPEZibIamo6MJyhCxbYC0CgYAIZhnYL7MsO3phgRqR3oTyiDwJEq/RLIQWSFG4\nzNhk8BhPDxRvL7XIEQXQKndNwEyrpKJOri/euIDnlet9z7s1GRmX2/OxmS0LsFoN\nif/K7djUDn2L7RWwAQI0hsC1pNZTw7raoE5I/JB3FSifIFA4/3U5/GdqhvCOS+k9\ni7rUGQKBgQDPspapfGj2ozgWChJ2xMTGBhJhynM81w3j9w7MLvO/7/U43zYzKzyc\n7YJzApQOSwX/nLdquzi+UIbvuCB3npZVZl52S4f7BBcgLNQpdmcfWrAbDv5lySfn\n/KTN22Wxmhh20QgiNSxj+o+KIgdAgZCgWt7NrkZ5UX7Lo+ZfYU1xbg==\n-----END RSA PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
async sign(data: string) {
|
||||
const { KJUR, KEYUTIL, hextob64 } = await import("jsrsasign");
|
||||
const privateKey = KEYUTIL.getKey(this.privateKeyPem);
|
||||
// 创建签名实例
|
||||
const signature = new KJUR.crypto.Signature({
|
||||
alg: "SHA256withRSA",
|
||||
});
|
||||
|
||||
// 初始化私钥
|
||||
signature.init(privateKey);
|
||||
|
||||
// 更新待签名数据(假设原文是字符串)
|
||||
signature.updateString(data);
|
||||
|
||||
// 生成签名(默认返回十六进制字符串)
|
||||
const hexSignature = signature.sign();
|
||||
|
||||
// 转换为 Base64(假设 Ix 是 Base64 编码)
|
||||
return hextob64(hexSignature);
|
||||
}
|
||||
|
||||
async doRequest(req: HttpRequestConfig) {
|
||||
const timestamp = Date.now();
|
||||
|
||||
let data = "";
|
||||
if (req.method.toLowerCase() === "get") {
|
||||
// area_codes=&channel_type=0,1,2&domain_name=&https_status=&nonce=1747242446238&order=&page=1&page_size=10&status=×tamp=1747242446238
|
||||
let queryList = [];
|
||||
for (const key in req.params) {
|
||||
queryList.push(`${key}=${req.params[key]}`);
|
||||
}
|
||||
queryList.push(`nonce=${timestamp}`);
|
||||
queryList.push(`timestamp=${timestamp}`);
|
||||
//sort
|
||||
queryList = queryList.sort();
|
||||
data = queryList.join("&");
|
||||
} else {
|
||||
data = `body=${JSON.stringify(req.data || {})}&nonce=${timestamp}×tamp=${timestamp}`;
|
||||
}
|
||||
const sign = await this.sign(data);
|
||||
const headers: any = {
|
||||
sign: sign,
|
||||
timestamp: timestamp,
|
||||
nonce: timestamp,
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers.Token = this.token;
|
||||
}
|
||||
|
||||
const res = await this.http.request({
|
||||
...req,
|
||||
headers,
|
||||
baseURL: "https://testaa.5678.jp",
|
||||
});
|
||||
|
||||
if (!res.success && res.code !== 200) {
|
||||
throw new Error(`请求失败:${res.msg}`);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async login() {
|
||||
const req = {
|
||||
email: this.access.username,
|
||||
password: this.access.password,
|
||||
accountType: 1,
|
||||
};
|
||||
const res = await this.doRequest({
|
||||
url: "/api/vcloud/v1/userApi/noAuth/login",
|
||||
method: "post",
|
||||
data: req,
|
||||
logRes: false,
|
||||
logParams: false,
|
||||
});
|
||||
const { token } = res;
|
||||
this.logger.info(`登录成功`);
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// 隐藏 通过下载插件形式分发
|
||||
// export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
export * from "./client.js";
|
||||
@@ -0,0 +1 @@
|
||||
// export * from "./plugin-deploy-to-cdn.js";
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
import { MaoyunAccess } from "../access.js";
|
||||
import { MaoyunClient } from "../client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
//命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名
|
||||
name: "MaoyunDeployToCdn",
|
||||
title: "Maoyun-更新猫云CDN证书",
|
||||
icon: "svg:icon-lucky",
|
||||
//插件分组
|
||||
group: pluginGroups.cdn.key,
|
||||
needPlus: true,
|
||||
default: {
|
||||
//默认值配置照抄即可
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
//类名规范,跟上面插件名称(name)一致
|
||||
export class MaoyunDeployToCdn extends AbstractPlusTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
// required: true, // 必填
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "Maoyun授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "maoyun", //固定授权类型
|
||||
},
|
||||
required: true, //必填
|
||||
})
|
||||
accessId!: string;
|
||||
//
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "CDN加速域名",
|
||||
helper: "要部署证书的域名",
|
||||
action: MaoyunDeployToCdn.prototype.onGetDomainList.name,
|
||||
watches: ["accessId"],
|
||||
})
|
||||
)
|
||||
domainList!: string[];
|
||||
|
||||
//插件实例化时执行的方法
|
||||
async onInstance() {}
|
||||
|
||||
//插件执行方法
|
||||
async execute(): Promise<void> {
|
||||
const { cert } = this;
|
||||
|
||||
const access: MaoyunAccess = await this.getAccess<MaoyunAccess>(this.accessId);
|
||||
|
||||
const client = new MaoyunClient({
|
||||
http: this.ctx.http,
|
||||
logger: this.logger,
|
||||
access,
|
||||
});
|
||||
await client.login();
|
||||
for (const item of this.domainList) {
|
||||
this.logger.info(`开始更新证书:${item}`);
|
||||
|
||||
// https://testaa.5678.jp/cdn/domain/6219/https_conf
|
||||
/**
|
||||
* {status: 1, new_certificate: {name: "certd",…}}
|
||||
* new_certificate
|
||||
* :
|
||||
* {name: "certd",…}
|
||||
* content
|
||||
* :
|
||||
* name
|
||||
* :
|
||||
* "certd"
|
||||
* private_key
|
||||
* :
|
||||
* status
|
||||
* :
|
||||
* 1
|
||||
*/
|
||||
await client.doRequest({
|
||||
method: "PUT",
|
||||
url: `/cdn/domain/${item}/https_conf`,
|
||||
data: {
|
||||
status: 1,
|
||||
new_certificate: {
|
||||
name: this.appendTimeSuffix("certd"),
|
||||
content: cert.crt,
|
||||
private_key: cert.key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.info(`部署${item}证书成功`);
|
||||
}
|
||||
|
||||
this.logger.info("部署完成");
|
||||
}
|
||||
|
||||
async onGetDomainList() {
|
||||
const access: MaoyunAccess = await this.getAccess<MaoyunAccess>(this.accessId);
|
||||
const client = new MaoyunClient({
|
||||
http: this.ctx.http,
|
||||
logger: this.logger,
|
||||
access,
|
||||
});
|
||||
await client.login();
|
||||
const res = await client.doRequest({
|
||||
url: "/cdn/domain",
|
||||
data: {},
|
||||
params: {
|
||||
channel_type: "0,1,2",
|
||||
page: 1,
|
||||
page_size: 1000,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
const list = res.data;
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到加速域名,请先在控制台添加加速域名");
|
||||
}
|
||||
|
||||
const options = list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.domain}<${item.id}>`,
|
||||
value: item.id,
|
||||
domain: item.domain,
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
//实例化一下,注册插件
|
||||
new MaoyunDeployToCdn();
|
||||
@@ -0,0 +1,204 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import FormData from "form-data";
|
||||
import { HttpError, HttpRequestConfig } from "@certd/basic";
|
||||
|
||||
export type PleskReq = {
|
||||
sessionId?: string;
|
||||
token?: string;
|
||||
formData?: FormData;
|
||||
checkRes?: boolean;
|
||||
} & HttpRequestConfig;
|
||||
/**
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "plesk",
|
||||
title: "plesk授权",
|
||||
desc: "",
|
||||
icon: "svg:icon-plesk",
|
||||
})
|
||||
export class PleskAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "Plesk网址",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "例如:https://xxxx.xxxxx:8443/",
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "用户名",
|
||||
component: {
|
||||
placeholder: "username",
|
||||
},
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
username = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "登录密码",
|
||||
component: {
|
||||
placeholder: "password",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "测试",
|
||||
component: {
|
||||
name: "api-test",
|
||||
action: "onTestRequest",
|
||||
},
|
||||
helper: "点击测试接口看是否正常",
|
||||
})
|
||||
testRequest = true;
|
||||
|
||||
async onTestRequest() {
|
||||
const sessionId = await this.getSessionId();
|
||||
if (!sessionId) {
|
||||
throw new Error("获取sessionId失败");
|
||||
}
|
||||
return "ok";
|
||||
}
|
||||
|
||||
async getSessionId() {
|
||||
const formData = new FormData();
|
||||
formData.append("login_name", this.username);
|
||||
formData.append("passwd", this.password);
|
||||
formData.append("locale_id", "default");
|
||||
|
||||
try {
|
||||
await this.ctx.http.request({
|
||||
url: `/login_up.php`,
|
||||
baseURL: this.url,
|
||||
method: "post",
|
||||
headers: {
|
||||
origin: this.url,
|
||||
...formData.getHeaders(),
|
||||
},
|
||||
data: formData,
|
||||
withCredentials: true,
|
||||
logRes: false,
|
||||
returnOriginRes: true,
|
||||
maxRedirects: 0,
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e && e instanceof HttpError && e.status === 303 && e.response) {
|
||||
//获取cookie
|
||||
const cookies = e.response.headers.get("set-cookie");
|
||||
const sessId = cookies[0].match(/PLESKSESSID=(.*?);/)[1];
|
||||
if (sessId) {
|
||||
this.ctx.logger.info(`获取Plesk SessionId 成功`);
|
||||
return sessId;
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async getCertList(sessionId: string) {
|
||||
const detail = await this.doGetRequest({
|
||||
url: "modules/sslit/index.php/main-page/index",
|
||||
sessionId,
|
||||
});
|
||||
|
||||
// "domainList": [ ... "domainSummaryEnabled":
|
||||
|
||||
const listStr = detail.match(/"domainList":(.*?),"domainSummaryEnabled"/)[1];
|
||||
|
||||
const list = JSON.parse(listStr);
|
||||
|
||||
const token = this.getTokenFromDetail(detail);
|
||||
|
||||
return { list, token };
|
||||
}
|
||||
|
||||
getTokenFromDetail(detail: string) {
|
||||
return detail.match(/forgery_protection_token" content="(.*?)"/)[1];
|
||||
}
|
||||
|
||||
async doGetRequest(req: PleskReq) {
|
||||
const detail = await this.ctx.http.request({
|
||||
//https://vps-b6941c0f.vps.ovh.net:8443/modules/sslit/index.php/index/certificate/id/2
|
||||
url: req.url,
|
||||
baseURL: this.url,
|
||||
method: req.method ?? "get",
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
Cookie: `PLESKSESSID=${req.sessionId}`,
|
||||
},
|
||||
logRes: false,
|
||||
});
|
||||
return detail;
|
||||
}
|
||||
|
||||
async doEditRequest(req: PleskReq) {
|
||||
const res = await this.ctx.http.request({
|
||||
url: req.url,
|
||||
baseURL: this.url,
|
||||
method: req.method ?? "post",
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
// Origin:
|
||||
// https://vps-b6941c0f.vps.ovh.net:8443
|
||||
// Referer:
|
||||
// https://vps-b6941c0f.vps.ovh.net:8443/modules/sslit/index.php/index/certificate/id/1
|
||||
Cookie: `PLESKSESSID=${req.sessionId}`,
|
||||
Origin: this.url,
|
||||
"X-Forgery-Protection-Token": req.token,
|
||||
...req.formData.getHeaders(),
|
||||
},
|
||||
logRes: req.logRes ?? false,
|
||||
data: req.formData,
|
||||
});
|
||||
|
||||
if (req.checkRes === false) {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (res && res.status === "success") {
|
||||
return res;
|
||||
} else {
|
||||
throw new Error(`${JSON.stringify(res.actionMessages || res.statusMessages || res)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUnusedCert(req: { sessionId: any; token: string; siteDomainId: number }) {
|
||||
//查询哪些证书是未使用的
|
||||
let detail = await this.doGetRequest({
|
||||
url: `/smb/ssl-certificate/list/id/${req.siteDomainId}`,
|
||||
sessionId: req.sessionId,
|
||||
});
|
||||
detail = detail.substring(detail.indexOf("Plesk.require('app/ssl-certificate/list',"));
|
||||
// "data": [....] "locale":
|
||||
const listStr1 = detail.match(/"data":(.*?),"locale"/)[1];
|
||||
const listStr = listStr1.match(/"data":(.*)/)[1];
|
||||
const list = JSON.parse(listStr);
|
||||
const unused = list.filter((item: any) => item.usageCount === "0").map(item => item.id);
|
||||
if (unused.length === 0) {
|
||||
this.ctx.logger.info(`没有未使用的证书`);
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
for (let i = 0; i < unused.length; i++) {
|
||||
formData.append(`ids[${i}]`, unused[i]);
|
||||
}
|
||||
|
||||
await this.doEditRequest({
|
||||
url: `/smb/ssl-certificate/delete/id/${req.siteDomainId}`,
|
||||
sessionId: req.sessionId,
|
||||
method: "post",
|
||||
formData,
|
||||
token: req.token,
|
||||
logRes: false,
|
||||
});
|
||||
this.ctx.logger.info(`删除未使用的证书成功`);
|
||||
}
|
||||
}
|
||||
|
||||
new PleskAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugin-deploy-cert.js";
|
||||
// 暂时不可用,需要保持私钥不变才能更新证书
|
||||
// export * from "./plugin-refresh-cert.js";
|
||||
@@ -0,0 +1,141 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
import { PleskAccess } from "../access.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import FormData from "form-data";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "PleskDeploySiteCert",
|
||||
title: "Plesk-部署Plesk网站证书",
|
||||
icon: "svg:icon-plesk",
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: true,
|
||||
})
|
||||
export class PleskDeploySiteCert extends AbstractPlusTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "Plesk授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "plesk",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//测试参数
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "网站域名列表",
|
||||
helper: "选择要更新的站点域名,注意域名是否与证书匹配",
|
||||
action: PleskDeploySiteCert.prototype.onGetDomainList.name,
|
||||
})
|
||||
)
|
||||
siteDomainIds!: number[];
|
||||
|
||||
@TaskInput({
|
||||
title: "删除未使用证书",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
required: false,
|
||||
})
|
||||
clearUnused!: boolean;
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<PleskAccess>(this.accessId);
|
||||
//get cookie
|
||||
const sessionId = await access.getSessionId();
|
||||
|
||||
for (const siteDomainId of this.siteDomainIds) {
|
||||
const formData = new FormData();
|
||||
formData.append("id", siteDomainId);
|
||||
formData.append("fileContent", this.cert.one);
|
||||
formData.append("fileName", "cert.pem");
|
||||
|
||||
const detail = await access.doGetRequest({
|
||||
//https://vps-b6941c0f.vps.ovh.net:8443/modules/sslit/index.php/index/certificate/id/2
|
||||
url: `/modules/sslit/index.php/index/certificate/id/${siteDomainId}`,
|
||||
sessionId,
|
||||
});
|
||||
//获取防伪令牌
|
||||
// <meta name="forgery_protection_token" id="forgery_protection_token" content="0206c10e5c19c9cbc3ea89ccd485822a">
|
||||
const token = access.getTokenFromDetail(detail);
|
||||
|
||||
await access.doEditRequest({
|
||||
url: `/modules/sslit/index.php/index/upload/`,
|
||||
sessionId,
|
||||
token,
|
||||
formData,
|
||||
});
|
||||
this.logger.info(`部署站点<${siteDomainId}>证书成功`);
|
||||
|
||||
//删除未使用的证书
|
||||
if (this.clearUnused) {
|
||||
await this.ctx.utils.sleep(3000);
|
||||
this.logger.info(`开始删除未使用的证书`);
|
||||
try {
|
||||
await access.deleteUnusedCert({ sessionId, token, siteDomainId });
|
||||
} catch (e) {
|
||||
this.logger.warn(`删除未使用的证书失败:${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`部署证书完成`);
|
||||
}
|
||||
|
||||
async onGetDomainList(data: any) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
|
||||
const access = await this.getAccess<PleskAccess>(this.accessId);
|
||||
|
||||
const res = await this.http.request({
|
||||
url: `/api/v2/domains`,
|
||||
baseURL: access.url,
|
||||
method: "get",
|
||||
headers: {
|
||||
// authorization: Basic =='
|
||||
Authorization: `Basic ${Buffer.from(`${access.username}:${access.password}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res || res.length === 0) {
|
||||
throw new Error("没有找站点域名");
|
||||
}
|
||||
const options = res.map((item: any) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
domain: item.name,
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
|
||||
new PleskDeploySiteCert();
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
import { PleskAccess } from "../access.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import FormData from "form-data";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "PleskRefreshCert",
|
||||
title: "Plesk-更新证书",
|
||||
icon: "svg:icon-plesk",
|
||||
desc: "不会创建新证书记录,直接更新旧的证书",
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: true,
|
||||
})
|
||||
export class PleskRefreshCert extends AbstractPlusTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "Plesk授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "plesk",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//测试参数
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "证书列表",
|
||||
helper: "选择要更新的站点域名,注意域名是否与证书匹配",
|
||||
action: PleskRefreshCert.prototype.onGetCertList.name,
|
||||
})
|
||||
)
|
||||
domainCertIds!: string[];
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<PleskAccess>(this.accessId);
|
||||
//get cookie
|
||||
const sessionId = await access.getSessionId();
|
||||
|
||||
const { token } = await access.getCertList(sessionId);
|
||||
|
||||
const certReader = new CertReader(this.cert);
|
||||
for (const certIds of this.domainCertIds) {
|
||||
const [domainId, certId] = certIds.split("_")[0];
|
||||
const formData = new FormData();
|
||||
formData.append("name", this.buildCertName(certReader.getMainDomain()));
|
||||
formData.append("type", "sendText");
|
||||
formData.append("uploadText[certificateText]", this.cert.crt);
|
||||
formData.append("uploadText[privateKeyText]", this.cert.key); // 这里没用
|
||||
formData.append("forgery_protection_token", token);
|
||||
|
||||
const res = await access.doEditRequest({
|
||||
url: `/smb/ssl-certificate/edit/id/${domainId}/certificateId/${certId}`,
|
||||
sessionId,
|
||||
token,
|
||||
formData,
|
||||
checkRes: false,
|
||||
});
|
||||
|
||||
this.logger.info(`更新证书成功:${certIds}_${certReader.getMainDomain()}`, res);
|
||||
}
|
||||
|
||||
this.logger.info(`部署证书完成`);
|
||||
}
|
||||
|
||||
async onGetCertList(data: any) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
|
||||
const access = await this.getAccess<PleskAccess>(this.accessId);
|
||||
|
||||
const sessionId = await access.getSessionId();
|
||||
|
||||
const { list } = await access.getCertList(sessionId);
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("没有找到证书");
|
||||
}
|
||||
const options = list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.domainName}(${item.domainId}_${item.certificateId})`,
|
||||
value: `${item.domainId}_${item.certificateId}`,
|
||||
domain: item.domainName,
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
|
||||
new PleskRefreshCert();
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "safeline",
|
||||
title: "长亭雷池授权",
|
||||
icon: "svg:icon-safeline",
|
||||
})
|
||||
export class SafelineAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "雷池的访问url",
|
||||
component: {
|
||||
placeholder: "https://xxxx.com:9443",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
baseUrl = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "ApiToken",
|
||||
component: {
|
||||
placeholder: "apiToken",
|
||||
},
|
||||
helper: "",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiToken = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "忽略证书校验",
|
||||
value: true,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "如果面板的url是https,且使用的是自签名证书,则需要开启此选项,其他情况可以关闭",
|
||||
})
|
||||
skipSslVerify = true;
|
||||
}
|
||||
|
||||
new SafelineAccess();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { HttpRequestConfig } from "@certd/basic";
|
||||
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { SafelineAccess } from "../access.js";
|
||||
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "SafelineDeployToWebsitePlugin",
|
||||
title: "雷池-更新证书",
|
||||
icon: "svg:icon-safeline",
|
||||
desc: "更新长亭雷池WAF的证书",
|
||||
group: pluginGroups.panel.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class SafelineDeployToWebsitePlugin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "雷池授权",
|
||||
helper: "长亭雷池授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "safeline",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "雷池证书",
|
||||
typeName: "SafelineDeployToWebsitePlugin",
|
||||
action: SafelineDeployToWebsitePlugin.prototype.onGetCertIds.name,
|
||||
helper: "请选择要更新的雷池的证书Id,需要先手动到雷池控制台上传一次",
|
||||
required: true,
|
||||
})
|
||||
)
|
||||
certIds!: number[];
|
||||
|
||||
access: SafelineAccess;
|
||||
async onInstance() {
|
||||
this.access = await this.getAccess(this.accessId);
|
||||
}
|
||||
async execute(): Promise<void> {
|
||||
for (const certId of this.certIds) {
|
||||
await this.uploadCert(certId);
|
||||
}
|
||||
this.logger.info("雷池证书更新完成");
|
||||
}
|
||||
|
||||
async uploadCert(certId: number) {
|
||||
await this.doRequest({
|
||||
url: "/api/open/cert",
|
||||
method: "post",
|
||||
data: {
|
||||
id: certId,
|
||||
manual: {
|
||||
crt: this.cert.crt,
|
||||
key: this.cert.key,
|
||||
},
|
||||
type: 2,
|
||||
},
|
||||
});
|
||||
this.logger.info(`证书<${certId}>更新成功`);
|
||||
}
|
||||
|
||||
async doRequest(config: HttpRequestConfig<any>) {
|
||||
config.baseURL = this.access.baseUrl;
|
||||
config.skipSslVerify = this.access.skipSslVerify ?? false;
|
||||
config.logRes = false;
|
||||
config.logParams = false;
|
||||
config.headers = {
|
||||
"X-SLCE-API-TOKEN": this.access.apiToken,
|
||||
};
|
||||
const res = await this.ctx.http.request(config);
|
||||
if (!res.err) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
|
||||
// requestHandle
|
||||
|
||||
async onGetCertIds() {
|
||||
const res = await this.doRequest({
|
||||
url: "/api/open/cert",
|
||||
method: "get",
|
||||
data: {},
|
||||
});
|
||||
const nodes = res?.nodes;
|
||||
if (!nodes || nodes.length === 0) {
|
||||
throw new Error("没有找到证书,请先在雷池控制台中手动上传证书,并关联防护站点,后续才可以自动更新");
|
||||
}
|
||||
const options = nodes.map(item => {
|
||||
return {
|
||||
label: `<${item.id}>${item.domains.join(",")}`,
|
||||
value: item.id,
|
||||
domain: item.domains,
|
||||
};
|
||||
});
|
||||
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
|
||||
}
|
||||
}
|
||||
new SafelineDeployToWebsitePlugin();
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./deploy-to-website.js";
|
||||
@@ -0,0 +1,135 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
import { SynologyClient } from "./client.js";
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "synology",
|
||||
title: "群晖登录授权",
|
||||
desc: "",
|
||||
icon: "simple-icons:synology",
|
||||
})
|
||||
export class SynologyAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "群晖版本",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ label: "7.x", value: "7" },
|
||||
{ label: "6.x", value: "6" },
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
version = "7";
|
||||
|
||||
@AccessInput({
|
||||
title: "群晖面板的url",
|
||||
component: {
|
||||
placeholder: "https://yourdomain:5006",
|
||||
},
|
||||
helper: "群晖面板的访问地址,例如:https://yourdomain:5006",
|
||||
required: true,
|
||||
})
|
||||
baseUrl = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "账号",
|
||||
component: {
|
||||
placeholder: "账号",
|
||||
},
|
||||
helper: "群晖面板登录账号,必须是处于管理员用户组",
|
||||
required: true,
|
||||
})
|
||||
username = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "密码",
|
||||
component: {
|
||||
placeholder: "密码",
|
||||
},
|
||||
helper: "群晖面板登录密码",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "双重认证",
|
||||
value: false,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "是否启用了双重认证",
|
||||
required: true,
|
||||
})
|
||||
otp = false;
|
||||
|
||||
@AccessInput({
|
||||
title: "设备ID",
|
||||
component: {
|
||||
placeholder: "设备ID",
|
||||
name: "synology-device-id-getter",
|
||||
type: "access",
|
||||
typeName: "synology",
|
||||
},
|
||||
mergeScript: `
|
||||
return {
|
||||
component:{
|
||||
form: ctx.compute(({form})=>{
|
||||
return form
|
||||
})
|
||||
},
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.access.otp
|
||||
})
|
||||
}
|
||||
`,
|
||||
helper: `1.如果开启了双重认证,需要获取设备ID
|
||||
2.填好上面的必填项,然后点击获取设备ID,输入双重认证APP上的码,确认即可获得设备ID,此操作只需要做一次
|
||||
3.注意:必须勾选‘安全性->允许网页浏览器的用户通过信任设备来跳过双重验证
|
||||
4.注意:在群晖信任设备页面里面会生成一条记录,不要删除
|
||||
5.注意:需要将流水线证书申请过期前多少天设置为30天以下,避免设备ID过期`,
|
||||
required: false,
|
||||
encrypt: true,
|
||||
})
|
||||
deviceId = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "忽略证书校验",
|
||||
value: true,
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "如果面板的url是https,且使用的是自签名证书,则需要开启此选项,其他情况可以关闭",
|
||||
})
|
||||
skipSslVerify = true;
|
||||
|
||||
/**
|
||||
* 请求超时时间设置
|
||||
* @param data
|
||||
*/
|
||||
@AccessInput({
|
||||
title: "请求超时",
|
||||
value: 120,
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "请求超时时间,单位:秒",
|
||||
})
|
||||
timeout = 120;
|
||||
|
||||
onLoginWithOPTCode(data: { otpCode: string }) {
|
||||
console.log("onLoginWithOPTCode", this);
|
||||
const ctx = this.ctx;
|
||||
const client = new SynologyClient(this, ctx.http, ctx.logger, this.skipSslVerify);
|
||||
return client.doLoginWithOTPCode(data.otpCode);
|
||||
}
|
||||
}
|
||||
|
||||
new SynologyAccess();
|
||||
@@ -0,0 +1,190 @@
|
||||
import { SynologyAccess } from "./access.js";
|
||||
import { HttpClient, ILogger } from "@certd/basic";
|
||||
import qs from "querystring";
|
||||
|
||||
export type SynologyAccessToken = {
|
||||
sid: string;
|
||||
did?: string;
|
||||
synotoken: string;
|
||||
};
|
||||
|
||||
export type SynologyRequest = {
|
||||
method?: string;
|
||||
apiParams: {
|
||||
api: string;
|
||||
version: number;
|
||||
method: string;
|
||||
};
|
||||
params?: any;
|
||||
data?: any;
|
||||
form?: any;
|
||||
headers?: any;
|
||||
useSynoToken?: boolean;
|
||||
};
|
||||
|
||||
const device_name = "certd";
|
||||
|
||||
export class SynologyClient {
|
||||
access: SynologyAccess;
|
||||
http: HttpClient;
|
||||
logger: ILogger;
|
||||
skipSslVerify: boolean;
|
||||
|
||||
token: SynologyAccessToken;
|
||||
constructor(access: SynologyAccess, http: HttpClient, logger: ILogger, skipSslVerify: boolean) {
|
||||
this.access = access;
|
||||
this.http = http;
|
||||
this.logger = logger;
|
||||
this.skipSslVerify = skipSslVerify;
|
||||
}
|
||||
|
||||
// 登录 DSM 的函数
|
||||
async doLogin() {
|
||||
const access = this.access;
|
||||
if (access.otp && access.deviceId != null) {
|
||||
this.logger.info("OTP登录");
|
||||
return await this.doLoginWithDeviceId(access.deviceId);
|
||||
}
|
||||
this.logger.info("使用普通登录");
|
||||
|
||||
const loginUrl = this.getLoginUrl();
|
||||
const res = await this.http.request({
|
||||
url: loginUrl,
|
||||
method: "GET",
|
||||
params: {
|
||||
api: "SYNO.API.Auth",
|
||||
version: 6,
|
||||
method: "login",
|
||||
account: access.username,
|
||||
passwd: access.password,
|
||||
session: "Certd",
|
||||
format: "sid",
|
||||
enable_syno_token: "yes",
|
||||
},
|
||||
skipSslVerify: this.skipSslVerify ?? true,
|
||||
timeout: this.access.timeout * 1000 || 120000,
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`登录失败: `, res.error);
|
||||
}
|
||||
this.logger.info("登录成功");
|
||||
|
||||
this.token = res.data as SynologyAccessToken;
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async doLoginWithOTPCode(otpCode: string) {
|
||||
const loginUrl = this.getLoginUrl();
|
||||
const access = this.access;
|
||||
const res = await this.http.request({
|
||||
url: loginUrl,
|
||||
method: "GET",
|
||||
params: {
|
||||
api: "SYNO.API.Auth",
|
||||
version: 6,
|
||||
method: "login",
|
||||
account: access.username,
|
||||
passwd: access.password,
|
||||
otp_code: otpCode,
|
||||
enable_device_token: "yes",
|
||||
device_name,
|
||||
},
|
||||
timeout: this.access.timeout * 1000 || 30000,
|
||||
skipSslVerify: this.skipSslVerify ?? true,
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`登录失败: `, res.error);
|
||||
}
|
||||
this.logger.info("登录成功");
|
||||
|
||||
this.token = res.data as SynologyAccessToken;
|
||||
return this.token;
|
||||
}
|
||||
|
||||
private getLoginUrl() {
|
||||
const access = this.access;
|
||||
const loginPath = access.version === "6" ? "auth.cgi" : "entry.cgi";
|
||||
return `${access.baseUrl}/webapi/${loginPath}`;
|
||||
}
|
||||
|
||||
async doLoginWithDeviceId(device_id: string) {
|
||||
const access = this.access;
|
||||
const loginUrl = this.getLoginUrl();
|
||||
const res = await this.http.request({
|
||||
url: loginUrl,
|
||||
method: "GET",
|
||||
params: {
|
||||
api: "SYNO.API.Auth",
|
||||
version: 6,
|
||||
method: "login",
|
||||
account: access.username,
|
||||
passwd: access.password,
|
||||
device_name,
|
||||
device_id,
|
||||
session: "Certd",
|
||||
format: "sid",
|
||||
enable_syno_token: "yes",
|
||||
},
|
||||
timeout: this.access.timeout * 1000 || 30000,
|
||||
skipSslVerify: this.skipSslVerify ?? true,
|
||||
});
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(`登录失败: `, res.error);
|
||||
}
|
||||
this.logger.info("登录成功");
|
||||
|
||||
this.token = res.data as SynologyAccessToken;
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async doRequest(req: SynologyRequest) {
|
||||
const sid = this.token.sid;
|
||||
const method = req.method || "POST";
|
||||
const params = {
|
||||
...req.apiParams,
|
||||
_sid: sid, // 使用登录后获得的 session ID
|
||||
...req.params,
|
||||
SynoToken: this.token.synotoken,
|
||||
};
|
||||
|
||||
const res = await this.http.request({
|
||||
url: `${this.access.baseUrl}/webapi/entry.cgi?${qs.stringify(params)}`,
|
||||
method,
|
||||
data: req.data,
|
||||
headers: req.headers,
|
||||
skipSslVerify: this.skipSslVerify ?? true,
|
||||
timeout: this.access.timeout * 1000 || 30000,
|
||||
});
|
||||
if (!res.success) {
|
||||
throw new Error(`API 调用失败: ${JSON.stringify(res.error)}`);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
async getCertList() {
|
||||
this.logger.info("获取证书列表");
|
||||
return await this.doRequest({
|
||||
method: "GET",
|
||||
apiParams: {
|
||||
api: "SYNO.Core.Certificate.CRT",
|
||||
version: 1,
|
||||
method: "list",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getInfo() {
|
||||
this.logger.info("获取信息");
|
||||
return await this.doRequest({
|
||||
method: "GET",
|
||||
apiParams: {
|
||||
api: "SYNO.API.Info",
|
||||
version: 1,
|
||||
method: "query",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
export * from "./client.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-deploy-to-panel.js";
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertInfo, CertReader } from "@certd/plugin-cert";
|
||||
import { AbstractPlusTaskPlugin } from "@certd/plugin-lib";
|
||||
import { SynologyClient } from "../client.js";
|
||||
import fs from "fs";
|
||||
import FormData from "form-data";
|
||||
import { SynologyAccess } from "../access.js";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
name: "SynologyDeployToPanel",
|
||||
title: "群晖-部署证书到群晖面板",
|
||||
icon: "simple-icons:synology",
|
||||
group: pluginGroups.panel.key,
|
||||
desc: "Synology,支持6.x以上版本",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: true,
|
||||
})
|
||||
export class SynologyDeployToPanel extends AbstractPlusTaskPlugin {
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: "群晖证书描述",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
placeholder: "群晖证书描述",
|
||||
},
|
||||
required: false,
|
||||
helper: "在群晖证书管理页面里面,选择证书,点击操作,给证书设置描述,然后填写到这里\n如果不填,则覆盖更新全部证书",
|
||||
})
|
||||
certName!: string;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "群晖授权",
|
||||
helper: "群晖登录授权,请确保账户是管理员用户组",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "synology",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const access: SynologyAccess = await this.getAccess<SynologyAccess>(this.accessId);
|
||||
const client = new SynologyClient(access, this.ctx.http, this.ctx.logger, access.skipSslVerify);
|
||||
// await client.init();
|
||||
await client.doLogin();
|
||||
// const res = await client.getInfo();
|
||||
// this.logger.info(res);
|
||||
const certListRes = await client.getCertList();
|
||||
if (this.certName) {
|
||||
const certItem = certListRes.certificates.find((item: any) => {
|
||||
return item.desc === this.certName || item.subject.common_name === this.certName;
|
||||
});
|
||||
if (!certItem) {
|
||||
throw new Error(`未找到证书: ${this.certName}`);
|
||||
}
|
||||
this.logger.info(`找到证书: ${certItem.id}`);
|
||||
await this.updateCertToPanel(client, certItem);
|
||||
} else {
|
||||
this.logger.info("开始更新全部证书");
|
||||
for (const item of certListRes.certificates) {
|
||||
this.logger.info(`更新证书: ${item.id}`);
|
||||
await this.updateCertToPanel(client, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateCertToPanel(client: SynologyClient, certItem: any) {
|
||||
/**
|
||||
* query
|
||||
* api: SYNO.Core.Certificate
|
||||
* method: import
|
||||
* version: 1
|
||||
* SynoToken: Bvum9p7BNeSc6
|
||||
*
|
||||
* key: (二进制)
|
||||
* cert: (二进制)
|
||||
* inter_cert: (二进制)
|
||||
* id: yxTtcC
|
||||
* desc: certd
|
||||
* as_default:
|
||||
*/
|
||||
this.logger.info(`更新证书:${certItem.id}`);
|
||||
const certReader = new CertReader(this.cert);
|
||||
|
||||
return certReader.readCertFile({
|
||||
logger: this.logger,
|
||||
handle: async (ctx) => {
|
||||
const form = new FormData();
|
||||
const { tmpCrtPath, tmpKeyPath, tmpIcPath } = ctx;
|
||||
this.logger.info(`上传证书:${tmpCrtPath},${tmpKeyPath}`);
|
||||
form.append("key", fs.createReadStream(tmpKeyPath));
|
||||
form.append("cert", fs.createReadStream(tmpCrtPath));
|
||||
if (certReader.cert.ic) {
|
||||
this.logger.info(`包含中间证书:${tmpIcPath}`);
|
||||
form.append("inter_cert", fs.createReadStream(tmpIcPath));
|
||||
}
|
||||
form.append("id", certItem.id);
|
||||
form.append("desc", certItem.desc);
|
||||
// form传输必须是string,bool要改成string
|
||||
// form.append("as_default", certItem.is_default + "");
|
||||
|
||||
console.log(JSON.stringify(form.getHeaders()));
|
||||
return await client.doRequest({
|
||||
method: "POST",
|
||||
apiParams: {
|
||||
api: "SYNO.Core.Certificate",
|
||||
version: 1,
|
||||
method: "import",
|
||||
},
|
||||
data: form,
|
||||
headers: {
|
||||
...form.getHeaders(),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
new SynologyDeployToPanel();
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "unicloud",
|
||||
title: "uniCloud",
|
||||
icon: "material-symbols:shield-outline",
|
||||
desc: "unicloud授权",
|
||||
})
|
||||
export class UniCloudAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "账号",
|
||||
component: {
|
||||
placeholder: "email",
|
||||
},
|
||||
helper: "登录邮箱",
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
email = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "密码",
|
||||
component: {
|
||||
placeholder: "密码",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = "";
|
||||
}
|
||||
|
||||
new UniCloudAccess();
|
||||
@@ -0,0 +1,169 @@
|
||||
import { UniCloudAccess } from "./access.js";
|
||||
import { http, HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
|
||||
import { CertInfo } from "@certd/plugin-cert";
|
||||
|
||||
type UniCloudClientOpts = { access: UniCloudAccess; logger: ILogger; http: HttpClient };
|
||||
|
||||
export class UniCloudClient {
|
||||
opts: UniCloudClientOpts;
|
||||
|
||||
deviceId: string;
|
||||
xToken: string;
|
||||
token: string;
|
||||
cookie: string;
|
||||
|
||||
constructor(opts: UniCloudClientOpts) {
|
||||
this.opts = opts;
|
||||
this.deviceId = new Date().getTime() + Math.floor(Math.random() * 1000000) + "";
|
||||
}
|
||||
|
||||
async sign(data: any, secretKey: string) {
|
||||
const Crypto = await import("crypto-js");
|
||||
const CryptoJS = Crypto.default;
|
||||
let content = "";
|
||||
Object.keys(data)
|
||||
.sort()
|
||||
.forEach(function (key) {
|
||||
if (data[key]) {
|
||||
content = content + "&" + key + "=" + data[key];
|
||||
}
|
||||
});
|
||||
content = content.slice(1);
|
||||
return CryptoJS.HmacMD5(content, secretKey).toString();
|
||||
}
|
||||
async doRequest(req: HttpRequestConfig) {
|
||||
const res = await http.request({
|
||||
...req,
|
||||
logRes: false,
|
||||
returnOriginRes: true,
|
||||
});
|
||||
const data = res.data;
|
||||
if (data.ret != null) {
|
||||
if (data.ret !== 0) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
if (!data.success) {
|
||||
throw new Error(JSON.stringify(data.error));
|
||||
}
|
||||
if (data.data?.errCode) {
|
||||
throw new Error(JSON.stringify(data.data));
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
|
||||
async login() {
|
||||
if (this.xToken) {
|
||||
return this.xToken;
|
||||
}
|
||||
const deviceId = this.deviceId;
|
||||
const username = this.opts.access.email;
|
||||
const password = this.opts.access.password;
|
||||
function getClientInfo(appId) {
|
||||
return `{"PLATFORM":"web","OS":"windows","APPID":"${appId}","DEVICEID":"${deviceId}","scene":1001,"appId":"${appId}","appLanguage":"zh-Hans","appName":"账号中心","appVersion":"1.0.0","appVersionCode":"100","browserName":"chrome","browserVersion":"122.0.6261.95","deviceId":"174585375190823882061","deviceModel":"PC","deviceType":"pc","hostName":"chrome","hostVersion":"122.0.6261.95","osName":"windows","osVersion":"10 x64","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36","uniCompilerVersion":"4.45","uniPlatform":"web","uniRuntimeVersion":"4.45","locale":"zh-Hans","LOCALE":"zh-Hans"}`;
|
||||
}
|
||||
const clientInfo = getClientInfo("__UNI__uniid_server");
|
||||
const loginData = {
|
||||
method: "serverless.function.runtime.invoke",
|
||||
params: `{"functionTarget":"uni-id-co","functionArgs":{"method":"login","params":[{"password":"${password}","captcha":"","resetAppId":"__UNI__unicloud_console","resetUniPlatform":"web","isReturnToken":false,"email":"${username}"}],"clientInfo":${clientInfo}}}`,
|
||||
spaceId: "uni-id-server",
|
||||
timestamp: new Date().getTime(),
|
||||
};
|
||||
|
||||
const secretKey = "ba461799-fde8-429f-8cc4-4b6d306e2339";
|
||||
const xSign = await this.sign(loginData, secretKey);
|
||||
const res = await this.doRequest({
|
||||
url: "https://account.dcloud.net.cn/client",
|
||||
method: "POST",
|
||||
data: loginData,
|
||||
headers: {
|
||||
"X-Serverless-Sign": xSign,
|
||||
Origin: "https://account.dcloud.net.cn",
|
||||
Referer: "https://account.dcloud.net.cn",
|
||||
},
|
||||
});
|
||||
|
||||
const token = res.newToken.token;
|
||||
// const uid = res.data.uid;
|
||||
this.xToken = token;
|
||||
this.opts.logger.info("登录成功:", token);
|
||||
return token;
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
if (this.token) {
|
||||
return {
|
||||
token: this.token,
|
||||
cookie: this.cookie,
|
||||
};
|
||||
}
|
||||
const xToken = await this.login();
|
||||
|
||||
const deviceId = this.deviceId;
|
||||
const secretKey = "4c1f7fbf-c732-42b0-ab10-4634a8bbe834";
|
||||
const clientInfo = `{"PLATFORM":"web","OS":"windows","APPID":"__UNI__unicloud_console","DEVICEID":"${deviceId}","scene":1001,"appId":"__UNI__unicloud_console","appLanguage":"zh-Hans","appName":"uniCloud控制台","appVersion":"1.0.0","appVersionCode":"100","browserName":"chrome","browserVersion":"122.0.6261.95","deviceId":"${deviceId}","deviceModel":"PC","deviceType":"pc","hostName":"chrome","hostVersion":"122.0.6261.95","osName":"windows","osVersion":"10 x64","ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36","uniCompilerVersion":"4.57","uniPlatform":"web","uniRuntimeVersion":"4.57","locale":"zh-Hans","LOCALE":"zh-Hans"}`;
|
||||
|
||||
const body = {
|
||||
method: "serverless.function.runtime.invoke",
|
||||
params: `{"functionTarget":"uni-cloud-kernel","functionArgs":{"action":"user/getUserToken","data":{"isLogin":true},"clientInfo":${clientInfo},"uniIdToken":"${xToken}"}}`,
|
||||
spaceId: "dc-6nfabcn6ada8d3dd",
|
||||
timestamp: new Date().getTime(),
|
||||
};
|
||||
|
||||
const xSign = await this.sign(body, secretKey);
|
||||
const res = await this.doRequest({
|
||||
url: "https://unicloud.dcloud.net.cn/client",
|
||||
method: "POST",
|
||||
data: body,
|
||||
headers: {
|
||||
"X-Client-Info": encodeURIComponent(clientInfo),
|
||||
"X-Serverless-Sign": xSign,
|
||||
"X-Client-Token": xToken,
|
||||
Origin: "https://unicloud.dcloud.net.cn",
|
||||
Referer: "https://unicloud.dcloud.net.cn",
|
||||
},
|
||||
});
|
||||
|
||||
const token = res.data.data.token;
|
||||
const cookies = res.headers["set-cookie"];
|
||||
let cookie = "";
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const item = cookies[i].substring(0, cookies[i].indexOf(";"));
|
||||
cookie += item + ";";
|
||||
}
|
||||
this.token = token;
|
||||
this.opts.logger.info("获取token成功:", token);
|
||||
this.cookie = cookie;
|
||||
return {
|
||||
token,
|
||||
cookie,
|
||||
};
|
||||
}
|
||||
|
||||
async createCert(req: { spaceId: string; domain: string; provider: string; cert: CertInfo }) {
|
||||
await this.getToken();
|
||||
const { spaceId, domain, cert, provider } = req;
|
||||
this.opts.logger.info(`开始部署证书, provider:${provider},spaceId:${spaceId},domain:${domain}`);
|
||||
const crt = encodeURIComponent(cert.crt);
|
||||
const key = encodeURIComponent(cert.key);
|
||||
const body = {
|
||||
appid: "",
|
||||
provider,
|
||||
spaceId: spaceId,
|
||||
domain: domain,
|
||||
cert: crt,
|
||||
key,
|
||||
};
|
||||
const res = await this.doRequest({
|
||||
url: "https://unicloud-api.dcloud.net.cn/unicloud/api/host/create-domain-with-cert",
|
||||
method: "POST",
|
||||
data: body,
|
||||
headers: {
|
||||
Token: this.token,
|
||||
Cookie: this.cookie,
|
||||
},
|
||||
});
|
||||
this.opts.logger.info("证书部署成功:", JSON.stringify(res));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
export * from "./client.js";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin-deploy-to-space.js";
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { UniCloudAccess } from "../access.js";
|
||||
import { UniCloudClient } from "../client.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "UniCloudDeployToSpace",
|
||||
title: "uniCloud-部署到服务空间",
|
||||
icon: "material-symbols:shield-outline",
|
||||
group: pluginGroups.panel.key,
|
||||
desc: "部署到服务空间",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class UniCloudDeployToSpace extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "uniCloud授权",
|
||||
helper: "uniCloud授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "unicloud",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: "服务空间ID",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "spaceId",
|
||||
})
|
||||
spaceId!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "空间提供商",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{
|
||||
label: "阿里云",
|
||||
value: "aliyun",
|
||||
},
|
||||
{
|
||||
label: "腾讯云",
|
||||
value: "tencent",
|
||||
},
|
||||
{
|
||||
label: "支付宝云",
|
||||
value: "alipay",
|
||||
},
|
||||
],
|
||||
},
|
||||
helper: "空间提供商",
|
||||
})
|
||||
provider!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: "空间域名",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
},
|
||||
helper: "空间域名",
|
||||
})
|
||||
domains!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<UniCloudAccess>(this.accessId);
|
||||
const client = new UniCloudClient({
|
||||
access,
|
||||
logger: this.logger,
|
||||
http: this.http,
|
||||
});
|
||||
|
||||
for (const domain of this.domains) {
|
||||
await client.createCert({
|
||||
domain,
|
||||
provider: this.provider,
|
||||
spaceId: this.spaceId,
|
||||
cert: this.cert,
|
||||
});
|
||||
}
|
||||
this.logger.info("部署成功");
|
||||
}
|
||||
}
|
||||
new UniCloudDeployToSpace();
|
||||
@@ -0,0 +1,65 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
@IsAccess({
|
||||
name: "wxpay",
|
||||
title: "微信支付",
|
||||
icon: "tdesign:logo-wechatpay-filled",
|
||||
})
|
||||
export class WxpayAccess extends BaseAccess {
|
||||
/**
|
||||
* appId: "<-- 请填写您的AppId,例如:2019091767145019 -->",
|
||||
* privateKey: "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->",
|
||||
* alipayPublicKey: "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->",
|
||||
*/
|
||||
@AccessInput({
|
||||
title: "AppId",
|
||||
component: {
|
||||
placeholder: "201909176714xxxx",
|
||||
},
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
appId: string;
|
||||
@AccessInput({
|
||||
title: "商户ID",
|
||||
component: {
|
||||
placeholder: "201909176714xxxx",
|
||||
},
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
mchid: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "公钥",
|
||||
component: {
|
||||
name: "a-textarea",
|
||||
rows: 3,
|
||||
placeholder: "MIIBIjANBg...",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
publicKey: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "私钥",
|
||||
component: {
|
||||
placeholder: "MIIEvQIBADANB...",
|
||||
name: "a-textarea",
|
||||
rows: 3,
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
privateKey: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "APIv3密钥",
|
||||
helper: "微信商户平台—>账户设置—>API安全—>设置APIv3密钥",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
key: string;
|
||||
}
|
||||
|
||||
new WxpayAccess();
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./access.js";
|
||||
@@ -0,0 +1,314 @@
|
||||
import crypto from "crypto-js";
|
||||
import { HttpClient, HttpRequestConfig, ILogger, utils } from "@certd/basic";
|
||||
import { Pager, PageSearch } from "@certd/pipeline";
|
||||
|
||||
export class XinnetClient {
|
||||
access = null;
|
||||
http = null;
|
||||
logger = null;
|
||||
|
||||
xTickets = null;
|
||||
loginCookies = null;
|
||||
domainTokenCookie = null;
|
||||
|
||||
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0";
|
||||
|
||||
constructor(opts: { access: { username: string; password: string }; logger: ILogger; http: HttpClient }) {
|
||||
this.access = opts.access;
|
||||
this.http = opts.http;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
async doRedirectRequest(conf: HttpRequestConfig) {
|
||||
let resRedirect = null;
|
||||
try {
|
||||
resRedirect = await this.http.request(conf);
|
||||
} catch (e) {
|
||||
resRedirect = e.response;
|
||||
this.logger.info(resRedirect.headers);
|
||||
if (!resRedirect) {
|
||||
throw new Error("请求失败:", e);
|
||||
}
|
||||
}
|
||||
return resRedirect;
|
||||
}
|
||||
|
||||
getCookie(response: any) {
|
||||
const setCookie = response.headers["set-cookie"];
|
||||
if (!setCookie || setCookie.length === 0) {
|
||||
throw new Error("未获取到cookie", response);
|
||||
}
|
||||
return setCookie
|
||||
.map(item => {
|
||||
return item.split(";")[0];
|
||||
})
|
||||
.join(";");
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
const res = await this.http.request({
|
||||
url: "https://login.xinnet.com/queryUOne",
|
||||
method: "get",
|
||||
});
|
||||
this.logger.info("queryUOne", res.data);
|
||||
const { uOne, uTwo } = res.data;
|
||||
|
||||
const res1 = await this.doRedirectRequest({
|
||||
url: "https://login.xinnet.com/newlogin",
|
||||
method: "get",
|
||||
headers: {
|
||||
Host: "login.xinnet.com",
|
||||
Origin: "https://login.xinnet.com",
|
||||
Referer: "https://login.xinnet.com/separatePage/?service=https://www.xinnet.com/",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
returnOriginRes: true,
|
||||
});
|
||||
|
||||
const cookie = this.getCookie(res1);
|
||||
this.logger.info("firstCookie", cookie);
|
||||
|
||||
function encrypt(password, utwo) {
|
||||
// return "" + crypto.encrypt(password, utwo);
|
||||
return crypto.AES.encrypt(password, utwo).toString();
|
||||
}
|
||||
|
||||
const data = {
|
||||
username: this.access.username,
|
||||
password: encrypt(this.access.password, uTwo),
|
||||
uOne: uOne,
|
||||
randStr: "",
|
||||
ticket: "",
|
||||
service: "",
|
||||
isRemoteLogin: false,
|
||||
};
|
||||
const formData = new FormData();
|
||||
for (const key in data) {
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
const res2 = await this.http.request({
|
||||
url: "https://login.xinnet.com/newlogin",
|
||||
method: "post",
|
||||
headers: {
|
||||
Origin: "https://login.xinnet.com",
|
||||
Referer: "https://login.xinnet.com/separatePage/?service=https://www.xinnet.com/",
|
||||
"Content-Type": "multipart/form-data",
|
||||
Cookie: cookie,
|
||||
},
|
||||
data: formData,
|
||||
withCredentials: true,
|
||||
returnOriginRes: true,
|
||||
});
|
||||
// console.log(res2.data);
|
||||
const loginedCookie = this.getCookie(res2);
|
||||
this.logger.info("登录成功,loginCookie:", loginedCookie);
|
||||
const tickets = res2.data.data.xTickets;
|
||||
this.logger.info("tickets:", tickets);
|
||||
|
||||
this.xTickets = tickets;
|
||||
this.loginCookies = loginedCookie;
|
||||
|
||||
const xticketArr = this.xTickets.split("###");
|
||||
// const ssoTiccket = xticketArr[0];
|
||||
const domainTicket = xticketArr[3];
|
||||
|
||||
// "jsonp_" + (Math.floor(1e5 * Math.random()) * Date.now()).toString(16)
|
||||
const jsonp = "jsonp_" + (Math.floor(1e5 * Math.random()) * Date.now()).toString(16);
|
||||
|
||||
const xtokenUrl = `https://domain.xinnet.com/domainsso/getXtoken?xticket=${domainTicket}&callback=${jsonp}`;
|
||||
console.log("getxtoken-------", xtokenUrl);
|
||||
const res4 = await this.doRedirectRequest({
|
||||
// https://domain.xinnet.com/domainsso/getXtoken?xticket=gZNBBDObcyxKaQqRVDj&callback=jsonp_6227d9fe0004c4
|
||||
url: xtokenUrl,
|
||||
method: "get",
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||
cookie: loginedCookie,
|
||||
},
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
returnOriginRes: true,
|
||||
});
|
||||
|
||||
const cookie4 = this.getCookie(res4);
|
||||
this.logger.info("获取domainXtoken成功:", cookie4);
|
||||
this.domainTokenCookie = cookie4;
|
||||
}
|
||||
|
||||
async getDomainList(data: PageSearch): Promise<{ totalRows: number; list: { domainName: string; serviceCode: string }[] }> {
|
||||
if (!this.domainTokenCookie) {
|
||||
await this.getToken();
|
||||
}
|
||||
|
||||
const pager = new Pager(data);
|
||||
const domainListUrl = "https://domain.xinnet.com/domainManage/domainList";
|
||||
|
||||
const res = await this.doDomainRequest({
|
||||
url: domainListUrl,
|
||||
method: "post",
|
||||
data: {
|
||||
pageNo: pager.pageNo,
|
||||
pageSize: pager.pageSize,
|
||||
domainName: data.searchKey ?? "",
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
async doDomainRequest(conf: HttpRequestConfig) {
|
||||
if (!this.domainTokenCookie) {
|
||||
await this.getToken();
|
||||
}
|
||||
const res = await this.http.request({
|
||||
url: conf.url,
|
||||
method: conf.method ?? "post",
|
||||
headers: {
|
||||
Host: "domain.xinnet.com",
|
||||
Origin: "https://domain.xinnet.com",
|
||||
Referer: "https://domain.xinnet.com/",
|
||||
"User-Agent": this.userAgent,
|
||||
cookie: this.domainTokenCookie,
|
||||
},
|
||||
data: conf.data,
|
||||
withCredentials: true,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
async getDcpCookie(opts: { serviceCode: string }) {
|
||||
if (!this.domainTokenCookie) {
|
||||
await this.getToken();
|
||||
}
|
||||
const domainTokenCookie = this.domainTokenCookie;
|
||||
const serviceCode = opts.serviceCode;
|
||||
const redirectDcpUrl = "https://domain.xinnet.com/dcp?serviceCode=" + serviceCode + "&type=analytic";
|
||||
const res10 = await this.doRedirectRequest({
|
||||
url: redirectDcpUrl,
|
||||
method: "get",
|
||||
headers: {
|
||||
cookie: domainTokenCookie,
|
||||
},
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
returnOriginRes: true,
|
||||
});
|
||||
|
||||
const location = res10.headers["location"];
|
||||
console.log("跳转到dcp:", location);
|
||||
|
||||
const resRedirect = await this.doRedirectRequest({
|
||||
url: location,
|
||||
method: "get",
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
returnOriginRes: true,
|
||||
});
|
||||
|
||||
const newCookie = this.getCookie(resRedirect);
|
||||
this.logger.info("dcpCookie", newCookie);
|
||||
return newCookie;
|
||||
}
|
||||
|
||||
async getDomainDnsList(opts: { serviceCode: string; recordValue?: string; dcpCookie }) {
|
||||
const dnsListURL = "https://dcp.xinnet.com/dcp/domaincloudanalytic/list";
|
||||
const res = await this.http.request({
|
||||
url: dnsListURL,
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
Host: "dcp.xinnet.com",
|
||||
Origin: "https://dcp.xinnet.com",
|
||||
Referer: "https://dcp.xinnet.com/dcpProduct.html",
|
||||
cookie: opts.dcpCookie,
|
||||
},
|
||||
data: {
|
||||
type: "ALL",
|
||||
content: opts.recordValue || "",
|
||||
skip: 1,
|
||||
limit: 10,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
if (res.code != 0) {
|
||||
this.logger.error("获取DNS列表失败", JSON.stringify(res));
|
||||
throw new Error("获取DNS列表失败");
|
||||
}
|
||||
return res.data?.list;
|
||||
}
|
||||
|
||||
async addDomainDnsRecord(req: { recordName: string; type: string; recordValue: string }, opts: { serviceCode: string; dcpCookie: string }) {
|
||||
const addDnsUrl = "https://dcp.xinnet.com/dcp/domaincloudanalytic/add";
|
||||
const addRes = await this.doDcpRequest(
|
||||
{
|
||||
url: addDnsUrl,
|
||||
method: "post",
|
||||
data: {
|
||||
recordName: req.recordName,
|
||||
type: req.type,
|
||||
content: req.recordValue,
|
||||
ttl: 600,
|
||||
phoneCode: 1,
|
||||
},
|
||||
},
|
||||
opts
|
||||
);
|
||||
this.logger.info(addRes);
|
||||
|
||||
await utils.sleep(3000);
|
||||
|
||||
const res = await this.getDomainDnsList({
|
||||
serviceCode: opts.serviceCode,
|
||||
recordValue: req.recordValue,
|
||||
dcpCookie: opts.dcpCookie,
|
||||
});
|
||||
// console.log(res.data);
|
||||
if (!res || res.length === 0) {
|
||||
throw new Error("未找到添加的DNS记录");
|
||||
}
|
||||
const item = res[0];
|
||||
return {
|
||||
recordId: item.id,
|
||||
recordFullName: item.name,
|
||||
recordValue: item.content,
|
||||
type: item.type,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteDomainDnsRecord(req: { recordId: number; recordFullName: string; type: string; recordValue: string }, opts: { serviceCode: string; dcpCookie }) {
|
||||
const delDnsUrl = "https://dcp.xinnet.com/dcp/domaincloudanalytic/delete";
|
||||
const res13 = await this.doDcpRequest(
|
||||
{
|
||||
url: delDnsUrl,
|
||||
method: "post",
|
||||
data: {
|
||||
recordId: req.recordId,
|
||||
recordName: req.recordFullName,
|
||||
content: req.recordValue,
|
||||
type: req.type,
|
||||
isBatch: 0,
|
||||
phoneCode: 1,
|
||||
},
|
||||
},
|
||||
opts
|
||||
);
|
||||
console.log(res13.data);
|
||||
return res13;
|
||||
}
|
||||
|
||||
async doDcpRequest(req: HttpRequestConfig, opts: { serviceCode: string; dcpCookie: string }) {
|
||||
return await this.http.request({
|
||||
url: req.url,
|
||||
method: req.method ?? "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
Host: "dcp.xinnet.com",
|
||||
Origin: "https://dcp.xinnet.com",
|
||||
Referer: "https://dcp.xinnet.com/dcpProduct.html",
|
||||
cookie: opts.dcpCookie,
|
||||
},
|
||||
withCredentials: true,
|
||||
data: req.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./client.js";
|
||||
@@ -0,0 +1,161 @@
|
||||
import axios from "axios";
|
||||
import crypto from "crypto-js";
|
||||
import https from "https";
|
||||
import qs from "qs";
|
||||
|
||||
function getCookie(res1) {
|
||||
let setCookie = res1.headers["set-cookie"];
|
||||
console.log(setCookie);
|
||||
let cookie = setCookie
|
||||
.map(item => {
|
||||
return item.split(";")[0];
|
||||
})
|
||||
.join(";");
|
||||
return cookie;
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false, // 这里可以设置为 false 来忽略 SSL 证书验证
|
||||
});
|
||||
const instance = axios.create({
|
||||
timeout: 3000, // 请求超时时间
|
||||
withCredentials: true,
|
||||
httpsAgent,
|
||||
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0" }, // 设置请求头
|
||||
});
|
||||
|
||||
//
|
||||
// const res = await instance.get("https://login.xinnet.com/queryUOne");
|
||||
//
|
||||
// console.log(res.data?.data);
|
||||
// const { uOne, uTwo } = res.data.data;
|
||||
|
||||
let res1 = null;
|
||||
try {
|
||||
res1 = await instance.request({
|
||||
url: "https://dcp.xinnet.com/",
|
||||
method: "get",
|
||||
headers: {},
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e.response.headers);
|
||||
res1 = e.response;
|
||||
}
|
||||
|
||||
let cookie = getCookie(res1);
|
||||
|
||||
console.log(cookie);
|
||||
|
||||
function encrypt(password, secret) {
|
||||
// return "" + crypto.encrypt(password, utwo);
|
||||
return crypto.AES.encrypt(password, secret).toString();
|
||||
}
|
||||
|
||||
const codeGetUrl = "https://dcp.xinnet.com/domain/getValidatePic";
|
||||
|
||||
const res2 = await instance.request({
|
||||
url: codeGetUrl,
|
||||
method: "get",
|
||||
responseType: "arraybuffer",
|
||||
headers: {
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||
Host: "dcp.xinnet.com",
|
||||
cookie: cookie,
|
||||
},
|
||||
// maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
});
|
||||
console.log("status:", res2.status);
|
||||
const imageData = res2.data;
|
||||
let imageBuffer = Buffer.from(imageData, "binary");
|
||||
|
||||
const res3 = await axios.request({
|
||||
url: "https://ocr.com/ocr",
|
||||
method: "post",
|
||||
headers: {
|
||||
Authorization: "Basic " + Buffer.from("username:password").toString("base64"),
|
||||
},
|
||||
httpsAgent,
|
||||
data: {
|
||||
image: imageBuffer.toString("base64"),
|
||||
},
|
||||
});
|
||||
|
||||
console.log(res3.data.result.ocr_response);
|
||||
let text = res3.data.result.ocr_response.map(item => item.text.trim()).join("");
|
||||
text = text.replaceAll(" ", "");
|
||||
console.log(text);
|
||||
|
||||
const url = "https://dcp.xinnet.com/domain/validEnter";
|
||||
const password = encrypt("jidian1zu", "this is temp before https");
|
||||
// const body = {
|
||||
// domainName: "ulogin.top",
|
||||
// password: encodeURIComponent(password),
|
||||
// checkCode: encodeURIComponent( text),
|
||||
// }
|
||||
const body = {
|
||||
domainName: "ulogin.top",
|
||||
password: password,
|
||||
checkCode: text,
|
||||
};
|
||||
const query = qs.stringify(body);
|
||||
console.log(query);
|
||||
const res4 = await instance.request({
|
||||
url: url,
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
// cookie: cookie,
|
||||
Host: "dcp.xinnet.com",
|
||||
Origin: "https://dcp.xinnet.com",
|
||||
Referer: "https://dcp.xinnet.com/",
|
||||
cookie: cookie,
|
||||
},
|
||||
data: body,
|
||||
withCredentials: true,
|
||||
});
|
||||
console.log(res4.data);
|
||||
|
||||
const domainEnterUrl = "https://dcp.xinnet.com/domain/domainEnter";
|
||||
const res6 = await instance.request({
|
||||
url: domainEnterUrl,
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
Host: "dcp.xinnet.com",
|
||||
Origin: "https://dcp.xinnet.com",
|
||||
Referer: "https://dcp.xinnet.com/",
|
||||
cookie: cookie,
|
||||
},
|
||||
data: body,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
console.log(res6.data);
|
||||
|
||||
const listUrl = "https://dcp.xinnet.com/dcp/domaincloudanalytic/list";
|
||||
const res5 = await instance.request({
|
||||
url: listUrl,
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
Host: "dcp.xinnet.com",
|
||||
Origin: "https://dcp.xinnet.com",
|
||||
Referer: "https://dcp.xinnet.com/dcpProduct.html",
|
||||
cookie: cookie,
|
||||
},
|
||||
withCredentials: true,
|
||||
data: {
|
||||
type: "ALL",
|
||||
content: "",
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
console.log(res5.data);
|
||||
}
|
||||
|
||||
login();
|
||||
@@ -0,0 +1,341 @@
|
||||
import axios from "axios";
|
||||
import crypto from "crypto-js";
|
||||
|
||||
function getCookie(res1) {
|
||||
let setCookie = res1.headers["set-cookie"];
|
||||
console.log(setCookie);
|
||||
let cookie = setCookie
|
||||
.map(item => {
|
||||
return item.split(";")[0];
|
||||
})
|
||||
.join(";");
|
||||
return cookie;
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const instance = axios.create({
|
||||
timeout: 3000, // 请求超时时间
|
||||
withCredentials: true,
|
||||
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0" }, // 设置请求头
|
||||
});
|
||||
|
||||
const res = await instance.get("https://login.xinnet.com/queryUOne");
|
||||
|
||||
console.log(res.data?.data);
|
||||
const { uOne, uTwo } = res.data.data;
|
||||
|
||||
let res1 = null;
|
||||
try {
|
||||
res1 = await instance.request({
|
||||
url: "https://login.xinnet.com/newlogin",
|
||||
method: "get",
|
||||
headers: {
|
||||
Host: "login.xinnet.com",
|
||||
Origin: "https://login.xinnet.com",
|
||||
Referer: "https://login.xinnet.com/separatePage/?service=https://www.xinnet.com/",
|
||||
},
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e.response.headers);
|
||||
res1 = e.response;
|
||||
}
|
||||
|
||||
let cookie = getCookie(res1);
|
||||
|
||||
function encrypt(password, utwo) {
|
||||
// return "" + crypto.encrypt(password, utwo);
|
||||
return crypto.AES.encrypt(password, utwo).toString();
|
||||
}
|
||||
|
||||
const data = {
|
||||
username: 18603046467,
|
||||
password: encrypt("xxxxxxxxxxxxxpassword", uTwo),
|
||||
uOne: uOne,
|
||||
randStr: "",
|
||||
ticket: "",
|
||||
service: "",
|
||||
isRemoteLogin: false,
|
||||
};
|
||||
const formData = new FormData();
|
||||
for (const key in data) {
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
const res2 = await instance.request({
|
||||
url: "https://login.xinnet.com/newlogin",
|
||||
method: "post",
|
||||
headers: {
|
||||
Origin: "https://login.xinnet.com",
|
||||
Referer: "https://login.xinnet.com/separatePage/?service=https://www.xinnet.com/",
|
||||
"Content-Type": "multipart/form-data",
|
||||
Cookie: cookie,
|
||||
},
|
||||
data: formData,
|
||||
withCredentials: true,
|
||||
});
|
||||
console.log(res2.data);
|
||||
let loginedCookie = getCookie(res2);
|
||||
console.log("loginCookie", loginedCookie);
|
||||
|
||||
const tickets = res2.data.data.xTickets;
|
||||
|
||||
const xticketArr = tickets.split("###");
|
||||
const ssoTiccket = xticketArr[0];
|
||||
const domainTicket = xticketArr[3];
|
||||
|
||||
// "jsonp_" + (Math.floor(1e5 * Math.random()) * Date.now()).toString(16)
|
||||
const jsonp = "jsonp_" + (Math.floor(1e5 * Math.random()) * Date.now()).toString(16);
|
||||
|
||||
// const ssoUrl = `https://www.xinnet.com/sso/getXtoken?xticket=${ssoTiccket}&callback=${jsonp}`;
|
||||
// const res3 = await axios.request({
|
||||
// url: ssoUrl,
|
||||
// method: "get",
|
||||
// headers: {
|
||||
// Host: "www.xinnet.com",
|
||||
// Origin: "https://login.xinnet.com",
|
||||
// Referer: "https://login.xinnet.com/separatePage/?service=https://www.xinnet.com/",
|
||||
// "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/}"
|
||||
// },
|
||||
// cookie:loginedCookie,
|
||||
// maxRedirects: 0,
|
||||
// withCredentials: true
|
||||
// });
|
||||
//
|
||||
// console.log(res3.data);
|
||||
// let cookie2 = getCookie(res3);
|
||||
// console.log(cookie2);
|
||||
|
||||
let res4 = null;
|
||||
try {
|
||||
const xtokenUrl = `https://domain.xinnet.com/domainsso/getXtoken?xticket=${domainTicket}&callback=${jsonp}`;
|
||||
console.log("getxtoken-------", xtokenUrl);
|
||||
res4 = await instance.request({
|
||||
// https://domain.xinnet.com/domainsso/getXtoken?xticket=gZNBBDObcyxKaQqRVDj&callback=jsonp_6227d9fe0004c4
|
||||
url: xtokenUrl,
|
||||
method: "get",
|
||||
headers: {
|
||||
/**
|
||||
* Host:
|
||||
* ssl.xinnet.com
|
||||
* Referer:
|
||||
* https://login.xinnet.com/separatePage/?service=https://www.xinnet.com/
|
||||
*/
|
||||
// Host: "ssl.xinnet.com",
|
||||
// Referer: "https://login.xinnet.com/separatePage/?service=https://www.xinnet.com/",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||
cookie: loginedCookie,
|
||||
},
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
});
|
||||
} catch (e) {
|
||||
res4 = e.response;
|
||||
console.log(res4.headers);
|
||||
}
|
||||
console.log(res4.data);
|
||||
let domainTokenCookie = getCookie(res4);
|
||||
console.log("domainTokenCookie", domainTokenCookie);
|
||||
|
||||
//
|
||||
// let res8 = null;
|
||||
// const consoleXtokenUrl = `https://console.xinnet.com/sso/getXtoken?xticket=${domainTicket}&callback=${jsonp}`;
|
||||
// console.log("getConsolextoken-------", consoleXtokenUrl);
|
||||
// res8 = await instance.request({
|
||||
// // https://domain.xinnet.com/domainsso/getXtoken?xticket=gZNBBDObcyxKaQqRVDj&callback=jsonp_6227d9fe0004c4
|
||||
// url: consoleXtokenUrl,
|
||||
// method: "get",
|
||||
// headers: {
|
||||
// "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||
// cookie: loginedCookie
|
||||
// },
|
||||
// maxRedirects: 0,
|
||||
// withCredentials: true
|
||||
// });
|
||||
//
|
||||
// console.log(res8.data);
|
||||
// let consoleTokenCookie = getCookie(res8);
|
||||
// console.log("consoleTokenCookie", consoleTokenCookie);
|
||||
|
||||
// const consoleIdUrl = "https://console.xinnet.com/usercommon/getShopcartNum";
|
||||
//
|
||||
// const res7 = await instance.request({
|
||||
// url: consoleIdUrl,
|
||||
// method: "get",
|
||||
// headers: {
|
||||
// Host: "console.xinnet.com",
|
||||
// Referer: "https://domain.xinnet.com/",
|
||||
// cookie: consoleTokenCookie +";"+ loginedCookie
|
||||
// }
|
||||
// });
|
||||
// console.log(res7.data);
|
||||
// let consoleIdCookie = getCookie(res7);
|
||||
// console.log("consoleIdCookie",consoleIdCookie);
|
||||
|
||||
const domainListUrl = "https://domain.xinnet.com/domainManage/domainList";
|
||||
|
||||
const res5 = await instance.request({
|
||||
url: domainListUrl,
|
||||
method: "post",
|
||||
headers: {
|
||||
/**
|
||||
* Host:
|
||||
* domain.xinnet.com
|
||||
* Origin:
|
||||
* https://domain.xinnet.com
|
||||
* Referer:
|
||||
* https://domain.xinnet.com/
|
||||
*/
|
||||
Host: "domain.xinnet.com",
|
||||
Origin: "https://domain.xinnet.com",
|
||||
Referer: "https://domain.xinnet.com/",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||
cookie: domainTokenCookie,
|
||||
},
|
||||
data: {
|
||||
/**
|
||||
* pageNo: 1
|
||||
* pageSize: 10
|
||||
* orderByProperty: expire_date
|
||||
* orderByType: asc
|
||||
*/
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
console.log(res5.data);
|
||||
|
||||
// const bindUrls = [
|
||||
// "https://domain.xinnet.com/domainManage/inspectDomainRealname",
|
||||
// "https://domain.xinnet.com/domainManage/inspectDomainBindPhone",
|
||||
// "https://domain.xinnet.com/domainManage/inspectDomainXinnetDns",
|
||||
// "https://domain.xinnet.com/domainManage/inspectDomainEvents"
|
||||
// ];
|
||||
// const consoleIdCookie = consoleTokenCookie.split(";")[0];
|
||||
// for (const url of bindUrls) {
|
||||
// console.log("do bind:", url);
|
||||
// const cookie1 = consoleIdCookie + ";" + domainTokenCookie;
|
||||
// console.log("cookie1", cookie1);
|
||||
// const res9 = await instance.request({
|
||||
// url: url,
|
||||
// method: "post",
|
||||
// headers: {
|
||||
// "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
// Host: "domain.xinnet.com",
|
||||
// Origin: "https://domain.xinnet.com",
|
||||
// Referer: "https://domain.xinnet.com",
|
||||
// cookie: cookie1
|
||||
// },
|
||||
// data: {
|
||||
// domainName: "ulogin.top"
|
||||
// },
|
||||
// withCredentials: true
|
||||
// });
|
||||
// console.log(res9.data);
|
||||
// }
|
||||
|
||||
const serviceCode = "D76534287817377";
|
||||
const redirectDcpUrl = "https://domain.xinnet.com/dcp?serviceCode=" + serviceCode + "&type=analytic";
|
||||
let res10 = null;
|
||||
try {
|
||||
res10 = await instance.request({
|
||||
url: redirectDcpUrl,
|
||||
method: "get",
|
||||
headers: {
|
||||
cookie: domainTokenCookie,
|
||||
},
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
});
|
||||
} catch (e) {
|
||||
res10 = e.response;
|
||||
console.log(res10.headers);
|
||||
}
|
||||
const location = res10.headers["location"];
|
||||
console.log("跳转到dcp:", location);
|
||||
|
||||
let resRedirect = null;
|
||||
try {
|
||||
resRedirect = await instance.request({
|
||||
url: location,
|
||||
method: "get",
|
||||
maxRedirects: 0,
|
||||
withCredentials: true,
|
||||
});
|
||||
} catch (e) {
|
||||
resRedirect = e.response;
|
||||
console.log(resRedirect.headers);
|
||||
}
|
||||
|
||||
const newCookie = getCookie(resRedirect);
|
||||
console.log("newCookie", newCookie);
|
||||
|
||||
const dnsListURL = "https://dcp.xinnet.com/dcp/domaincloudanalytic/list";
|
||||
const res11 = await instance.request({
|
||||
url: dnsListURL,
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
Host: "dcp.xinnet.com",
|
||||
Origin: "https://dcp.xinnet.com",
|
||||
Referer: "https://dcp.xinnet.com/dcpProduct.html",
|
||||
cookie: newCookie,
|
||||
},
|
||||
data: {
|
||||
type: "ALL",
|
||||
content: "",
|
||||
skip: 1,
|
||||
limit: 10,
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
console.log(res11.data);
|
||||
|
||||
//add dns
|
||||
// const addDnsUrl = "https://dcp.xinnet.com/dcp/domaincloudanalytic/add"
|
||||
// const res12 = await instance.request({
|
||||
// url: addDnsUrl,
|
||||
// method: "post",
|
||||
// headers: {
|
||||
// "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
// Host: "dcp.xinnet.com",
|
||||
// Origin: "https://dcp.xinnet.com",
|
||||
// Referer: "https://dcp.xinnet.com/dcpProduct.html",
|
||||
// cookie: newCookie
|
||||
// },
|
||||
// data: {
|
||||
// recordName: "343533",
|
||||
// type: "TXT",
|
||||
// content: "456456",
|
||||
// ttl: 600,
|
||||
// phoneCode: 1,
|
||||
// }
|
||||
// })
|
||||
// console.log(res12.data);
|
||||
//
|
||||
// const delDnsUrl = "https://dcp.xinnet.com/dcp/domaincloudanalytic/delete"
|
||||
//
|
||||
// const res13 = await instance.request({
|
||||
// url: delDnsUrl,
|
||||
// method: "post",
|
||||
// headers: {
|
||||
// "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
// Host: "dcp.xinnet.com",
|
||||
// Origin: "https://dcp.xinnet.com",
|
||||
// Referer: "https://dcp.xinnet.com/dcpProduct.html",
|
||||
// cookie: newCookie
|
||||
// },
|
||||
// data:{
|
||||
// recordId: 167529045,
|
||||
// recordName: "aaaa.ulogin.top",
|
||||
// content: "aaaa",
|
||||
// type: "TXT",
|
||||
// isBatch: 0,
|
||||
// phoneCode: 1,
|
||||
// }
|
||||
// })
|
||||
// console.log(res13.data);
|
||||
}
|
||||
|
||||
login();
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "yidunrcdn",
|
||||
title: "易盾rcdn授权",
|
||||
icon: "material-symbols:shield-outline",
|
||||
desc: "易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8)",
|
||||
})
|
||||
export class YidunRcdnAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "账户",
|
||||
component: {
|
||||
placeholder: "手机号",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
username = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "密码",
|
||||
component: {
|
||||
placeholder: "password",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = "";
|
||||
}
|
||||
|
||||
new YidunRcdnAccess();
|
||||
@@ -0,0 +1,36 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "yfysms",
|
||||
title: "易发云短信",
|
||||
icon: "material-symbols:shield-outline",
|
||||
desc: "sms.yfyidc.cn/",
|
||||
})
|
||||
export class YfySmsAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "KeyID",
|
||||
component: {
|
||||
placeholder: "api_key",
|
||||
},
|
||||
helper: "[获取密钥](http://sms.yfyidc.cn/user/index#)",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
keyId = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "KeySecret",
|
||||
component: {
|
||||
placeholder: "",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
keySecret = "";
|
||||
}
|
||||
|
||||
new YfySmsAccess();
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
/**
|
||||
* 这个注解将注册一个授权配置
|
||||
* 在certd的后台管理系统中,用户可以选择添加此类型的授权
|
||||
*/
|
||||
@IsAccess({
|
||||
name: "yidun",
|
||||
title: "易盾DCDN授权",
|
||||
icon: "material-symbols:shield-outline",
|
||||
desc: "https://user.yiduncdn.com",
|
||||
})
|
||||
export class YidunAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "api_key",
|
||||
component: {
|
||||
placeholder: "api_key",
|
||||
},
|
||||
helper: "http://user.yiduncdn.com/console/index.html#/account/config/api,点击开启后获取",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiKey = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "api_secret",
|
||||
component: {
|
||||
placeholder: "api_secret",
|
||||
},
|
||||
helper: "http://user.yiduncdn.com/console/index.html#/account/config/api,点击开启后获取",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
apiSecret = "";
|
||||
}
|
||||
|
||||
new YidunAccess();
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./plugins/index.js";
|
||||
export * from "./access.js";
|
||||
export * from "./access-rcdn.js";
|
||||
export * from "./access-sms.js";
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./plugin-deploy-to-cdn.js";
|
||||
export * from "./plugin-deploy-to-rcdn.js";
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "YidunDeployToCDN",
|
||||
title: "易盾-部署到易盾DCDN",
|
||||
icon: "material-symbols:shield-outline",
|
||||
group: pluginGroups.cdn.key,
|
||||
desc: "主要是防御,http://user.yiduncdn.com/",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class YidunDeployToCDNPlugin extends AbstractTaskPlugin {
|
||||
//测试参数
|
||||
@TaskInput({
|
||||
title: "证书ID",
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "证书ID,在证书管理页面查看,每条记录都有证书id",
|
||||
})
|
||||
certId!: number;
|
||||
|
||||
@TaskInput({
|
||||
title: "网站域名",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
helper: "网站域名和证书ID选填其中一个,填了证书ID,则忽略网站域名",
|
||||
})
|
||||
domain!: number;
|
||||
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "易盾授权",
|
||||
helper: "易盾CDN授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "yidun",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const { domain, certId, cert } = this;
|
||||
if (!domain && !certId) {
|
||||
throw new Error("证书ID和网站域名必须填写一个");
|
||||
}
|
||||
|
||||
if (certId > 0) {
|
||||
await this.updateByCertId(cert, certId);
|
||||
} else {
|
||||
await this.updateByDomain(cert);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateByCertId(cert: CertInfo, certId: number) {
|
||||
this.logger.info(`更新证书,证书ID:${certId}`);
|
||||
const url = `http://user.yiduncdn.com/v1/certs/${certId}`;
|
||||
await this.doRequest(url, "PUT", {
|
||||
cert: cert.crt,
|
||||
key: cert.key,
|
||||
});
|
||||
}
|
||||
|
||||
async doRequest(url: string, method: string, data: any) {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
const { apiKey, apiSecret } = access;
|
||||
const http = this.ctx.http;
|
||||
const res: any = await http.request({
|
||||
url,
|
||||
method,
|
||||
headers: {
|
||||
"api-key": apiKey,
|
||||
"api-secret": apiSecret,
|
||||
},
|
||||
data,
|
||||
});
|
||||
if (res.code != 0) {
|
||||
throw new Error(res.msg);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private async updateByDomain(cert: CertInfo) {
|
||||
//查询站点
|
||||
const siteUrl = "http://user.yiduncdn.com/v1/sites";
|
||||
const res = await this.doRequest(siteUrl, "GET", { domain: this.domain });
|
||||
if (res.data.length === 0) {
|
||||
throw new Error(`未找到域名相关站点:${this.domain}`);
|
||||
}
|
||||
let site = null;
|
||||
for (const row of res.data) {
|
||||
if (row.domain === this.domain) {
|
||||
site = row;
|
||||
}
|
||||
}
|
||||
if (!site) {
|
||||
throw new Error(`未找到域名匹配的站点:${this.domain}`);
|
||||
}
|
||||
if (site.https_listen?.cert) {
|
||||
//有证书id
|
||||
const certId = site.https_listen.cert;
|
||||
await this.updateByCertId(cert, certId);
|
||||
} else {
|
||||
//创建证书
|
||||
this.logger.info(`创建证书,域名:${this.domain}`);
|
||||
const certUrl = `http://user.yiduncdn.com/v1/certs`;
|
||||
const name = this.domain + "_" + new Date().getTime();
|
||||
await this.doRequest(certUrl, "POST", {
|
||||
name,
|
||||
type: "custom",
|
||||
cert: cert.crt,
|
||||
key: cert.key,
|
||||
});
|
||||
|
||||
const certs: any = await this.doRequest(certUrl, "GET", {
|
||||
name,
|
||||
});
|
||||
const certId = certs.data[0].id;
|
||||
|
||||
const siteUrl = "http://user.yiduncdn.com/v1/sites";
|
||||
await this.doRequest(siteUrl, "PUT", { id: site.id, https_listen: { cert: certId } });
|
||||
}
|
||||
}
|
||||
}
|
||||
new YidunDeployToCDNPlugin();
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
|
||||
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { YidunRcdnAccess } from "../access-rcdn.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: "YidunDeployToRCDN",
|
||||
title: "易盾-部署到易盾RCDN",
|
||||
icon: "material-symbols:shield-outline",
|
||||
group: pluginGroups.cdn.key,
|
||||
desc: "易盾CDN,每月免费30G,[注册即领](https://rhcdn.yiduncdn.com/register?code=8mn536rrzfbf8)",
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
needPlus: false,
|
||||
})
|
||||
export class YidunDeployToRCDNPlugin extends AbstractTaskPlugin {
|
||||
//证书选择,此项必须要有
|
||||
@TaskInput({
|
||||
title: "域名证书",
|
||||
helper: "请选择前置任务输出的域名证书",
|
||||
component: {
|
||||
name: "output-selector",
|
||||
from: [...CertApplyPluginNames],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
|
||||
//授权选择框
|
||||
@TaskInput({
|
||||
title: "易盾RCDN授权",
|
||||
helper: "易盾RCDN授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "yidunrcdn",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
|
||||
@TaskInput(
|
||||
createRemoteSelectInputDefine({
|
||||
title: "域名列表",
|
||||
helper: "选择要部署证书的站点域名",
|
||||
typeName: "YidunDeployToRCDNPlugin",
|
||||
action: YidunDeployToRCDNPlugin.prototype.onGetDomainList.name,
|
||||
})
|
||||
)
|
||||
domains!: string[];
|
||||
|
||||
async onInstance() {}
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess<YidunRcdnAccess>(this.accessId);
|
||||
const loginRes = await this.getLoginToken(access);
|
||||
|
||||
const curl = "https://rhcdn.yiduncdn.com/CdnDomainHttps/httpsConfiguration";
|
||||
for (const domain of this.domains) {
|
||||
// const data = {
|
||||
// doMainId: domain,
|
||||
// https: {
|
||||
// https_status: "off"
|
||||
// },
|
||||
// }
|
||||
// //先关闭https
|
||||
// const res = await this.doRequest(curl, loginRes, data);
|
||||
|
||||
const cert = this.cert;
|
||||
const update = {
|
||||
doMainId: domain,
|
||||
https: {
|
||||
https_status: "on",
|
||||
certificate_name: this.appendTimeSuffix("certd"),
|
||||
certificate_source: "0",
|
||||
certificate_value: cert.crt,
|
||||
private_key: cert.key,
|
||||
},
|
||||
};
|
||||
await this.doRequest(curl, loginRes, update);
|
||||
this.logger.info(`站点${domain}证书更新成功`);
|
||||
}
|
||||
}
|
||||
|
||||
async getLoginToken(access: YidunRcdnAccess) {
|
||||
const url = "https://rhcdn.yiduncdn.com/login/loginUser";
|
||||
const data = {
|
||||
userAccount: access.username,
|
||||
userPwd: access.password,
|
||||
remember: true,
|
||||
};
|
||||
const http = this.ctx.http;
|
||||
const res: any = await http.request({
|
||||
url,
|
||||
method: "POST",
|
||||
data,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
returnOriginRes: true,
|
||||
});
|
||||
if (!res.data?.success) {
|
||||
throw new Error(res.data?.message);
|
||||
}
|
||||
|
||||
const jsessionId = this.ctx.utils.request.getCookie(res, "JSESSIONID");
|
||||
const token = res.data?.data;
|
||||
return {
|
||||
jsessionId,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
async getDomainList(loginRes: any) {
|
||||
const url = "https://rhcdn.yiduncdn.com/CdnDomain/queryForDatatables";
|
||||
const data = {
|
||||
draw: 1,
|
||||
start: 0,
|
||||
length: 1000,
|
||||
search: {
|
||||
value: "",
|
||||
regex: false,
|
||||
},
|
||||
};
|
||||
|
||||
const res = await this.doRequest(url, loginRes, data);
|
||||
return res.data?.data;
|
||||
}
|
||||
|
||||
private async doRequest(url: string, loginRes: any, data: any) {
|
||||
const http = this.ctx.http;
|
||||
const res: any = await http.request({
|
||||
url,
|
||||
method: "POST",
|
||||
headers: {
|
||||
Cookie: `JSESSIONID=${loginRes.jsessionId};kuocai_cdn_token=${loginRes.token}`,
|
||||
},
|
||||
data,
|
||||
});
|
||||
if (!res.success) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async onGetDomainList(data: any) {
|
||||
if (!this.accessId) {
|
||||
throw new Error("请选择Access授权");
|
||||
}
|
||||
const access = await this.getAccess<YidunRcdnAccess>(this.accessId);
|
||||
|
||||
const loginRes = await this.getLoginToken(access);
|
||||
|
||||
const list = await this.getDomainList(loginRes);
|
||||
|
||||
if (!list || list.length === 0) {
|
||||
throw new Error("您账户下还没有站点域名,请先添加域名");
|
||||
}
|
||||
return list.map((item: any) => {
|
||||
return {
|
||||
label: `${item.domainName}<${item.id}>`,
|
||||
value: item.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
new YidunDeployToRCDNPlugin();
|
||||
@@ -0,0 +1,70 @@
|
||||
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
|
||||
@IsAccess({
|
||||
name: "yizhifu",
|
||||
title: "易支付",
|
||||
icon: "svg:icon-yizhifu",
|
||||
})
|
||||
export class YizhifuAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "url",
|
||||
component: {
|
||||
placeholder: "https://pay.xxxx.com",
|
||||
},
|
||||
helper: "易支付系统地址",
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
url: string;
|
||||
@AccessInput({
|
||||
title: "商户id",
|
||||
component: {
|
||||
placeholder: "pid",
|
||||
},
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
pid: string;
|
||||
@AccessInput({
|
||||
title: "key",
|
||||
component: {
|
||||
placeholder: "key",
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
key: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "固定支付方式",
|
||||
component: {
|
||||
placeholder: "固定一种支付方式,也就是submit.php中的type参数",
|
||||
},
|
||||
helper: "不填则跳转到收银台由用户自己选择,如果您的易支付系统不支持收银台,则必须填写",
|
||||
required: false,
|
||||
encrypt: false,
|
||||
})
|
||||
payType: string;
|
||||
|
||||
@AccessInput({
|
||||
title: "签名方式",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{
|
||||
label: "MD5",
|
||||
value: "MD5",
|
||||
},
|
||||
{
|
||||
label: "SHA256",
|
||||
value: "SHA256",
|
||||
},
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
signType: string;
|
||||
}
|
||||
|
||||
new YizhifuAccess();
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./access.js";
|
||||
Reference in New Issue
Block a user