mirror of
https://github.com/certd/certd.git
synced 2026-05-18 14:27:36 +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,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();
|
||||
Reference in New Issue
Block a user