feat: 【破坏性更新】插件改为metadata加载模式,plugin-cert、plugin-lib包部分代码转移到certd-server中,影响自定义插件,需要修改相关import引用

ssh、aliyun、tencent、qiniu、oss等 access和client需要转移import
This commit is contained in:
xiaojunnuo
2025-12-31 17:01:37 +08:00
parent 9c26598831
commit a3fb24993d
312 changed files with 14321 additions and 597 deletions
@@ -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";
@@ -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传输必须是stringbool要改成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();