From 76d12d60624c0672fd3717a80a2cfef6845b14b8 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sun, 15 Mar 2026 23:55:49 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20dns-provider=20=E6=94=AF=E6=8C=81bind9?= =?UTF-8?q?=20=EF=BC=8Csupport=20bind9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/certd/certd/issues/683 https://github.com/certd/certd/discussions/668 --- .trae/skills/access-plugin-dev/SKILL.md | 16 ++- .trae/skills/plugin-converter/SKILL.md | 2 +- .trae/skills/task-plugin-dev/SKILL.md | 3 +- .../plugin-lib/src/cert/dns-provider/api.ts | 3 +- .../plugin-lib/src/cert/dns-provider/base.ts | 7 +- .../pipeline/component/task-form/index.vue | 3 +- .../views/certd/pipeline/pipeline/index.vue | 6 +- .../cname/service/cname-record-service.ts | 11 +- .../src/plugins/plugin-bind9/access.ts | 83 ++++++++++++ .../src/plugins/plugin-bind9/dns-provider.ts | 127 ++++++++++++++++++ .../src/plugins/plugin-bind9/index.ts | 2 + 11 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-bind9/access.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-bind9/dns-provider.ts create mode 100644 packages/ui/certd-server/src/plugins/plugin-bind9/index.ts diff --git a/.trae/skills/access-plugin-dev/SKILL.md b/.trae/skills/access-plugin-dev/SKILL.md index 35742f1d7..d2c76685e 100644 --- a/.trae/skills/access-plugin-dev/SKILL.md +++ b/.trae/skills/access-plugin-dev/SKILL.md @@ -65,6 +65,20 @@ demoKeyId = ''; encrypt: true, //该属性是否需要加密 }) demoKeySecret = ''; + + + +@AccessInput({ + title: '另外一个授权Id',//标题 + component: { + name:"access-selector", //access选择组件 + vModel:"modelValue", + type: "ssh", // access类型,让用户固定选择这种类型的access + }, + required: true, //text组件可以省略 +}) +otherAccessId; + ``` ### 4. 实现测试方法 @@ -93,7 +107,7 @@ async onTestRequest() { ```typescript /** - * 获api接口示例 取域名列表, + * api接口示例 获取域名列表, */ async GetDomainList(req: PageSearch): Promise> { //输出日志必须使用ctx.logger diff --git a/.trae/skills/plugin-converter/SKILL.md b/.trae/skills/plugin-converter/SKILL.md index 650e50d75..4a325d4ac 100644 --- a/.trae/skills/plugin-converter/SKILL.md +++ b/.trae/skills/plugin-converter/SKILL.md @@ -2,7 +2,7 @@ ## 什么是插件转换工具 -插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,方便插件的注册和管理。 +插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,可以让插件分发和在线注册。 ## 工具位置 diff --git a/.trae/skills/task-plugin-dev/SKILL.md b/.trae/skills/task-plugin-dev/SKILL.md index 84d16e690..b4f1dd021 100644 --- a/.trae/skills/task-plugin-dev/SKILL.md +++ b/.trae/skills/task-plugin-dev/SKILL.md @@ -80,7 +80,8 @@ certDomains!: string[]; helper: 'demoAccess授权', component: { name: 'access-selector', - type: 'demo', // 固定授权类型 + vModel:"modelValue", + type: "demo", // access类型,让用户固定选择这种类型的access }, // rules: [{ required: true, message: '此项必填' }], // required: true, // 必填 diff --git a/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts b/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts index cde0c26f8..11bcb8579 100644 --- a/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts +++ b/packages/plugins/plugin-lib/src/cert/dns-provider/api.ts @@ -1,5 +1,5 @@ import { HttpClient, ILogger, utils } from "@certd/basic"; -import { IAccess, IServiceGetter, PageRes, PageSearch, Registrable } from "@certd/pipeline"; +import { IAccess, IAccessService, IServiceGetter, PageRes, PageSearch, Registrable } from "@certd/pipeline"; export type DnsProviderDefine = Registrable & { accessType: string; @@ -26,6 +26,7 @@ export type DnsProviderContext = { utils: typeof utils; domainParser: IDomainParser; serviceGetter: IServiceGetter; + accessGetter?: IAccessService; }; export type DomainRecord = { diff --git a/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts b/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts index b1a8085f1..158b51e57 100644 --- a/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts +++ b/packages/plugins/plugin-lib/src/cert/dns-provider/base.ts @@ -1,5 +1,5 @@ import { HttpClient, ILogger } from "@certd/basic"; -import { PageRes, PageSearch } from "@certd/pipeline"; +import { IAccessService, PageRes, PageSearch } from "@certd/pipeline"; import punycode from "punycode.js"; import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js"; import { dnsProviderRegistry } from "./registry.js"; @@ -59,6 +59,11 @@ export async function createDnsProvider(opts: { dnsProviderType: string; context if (dnsProviderDefine.deprecated) { context.logger.warn(dnsProviderDefine.deprecated); } + + if (!context.accessGetter) { + const accessGetter: IAccessService = await context.serviceGetter.get("accessService"); + context.accessGetter = accessGetter; + } // @ts-ignore const dnsProvider: IDnsProvider = new DnsProviderClass(); dnsProvider.setCtx(context); diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue index 0d81031d7..e144c0097 100644 --- a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/component/task-form/index.vue @@ -72,14 +72,13 @@ import * as _ from "lodash-es"; import { nanoid } from "nanoid"; import PiStepForm from "../step-form/index.vue"; import { Modal } from "ant-design-vue"; -import { CopyOutlined } from "@ant-design/icons-vue"; import VDraggable from "vuedraggable"; import { useUserStore } from "/@/store/user"; import { useSettingStore } from "/@/store/settings"; import { filter } from "lodash-es"; export default { name: "PiTaskForm", - components: { CopyOutlined, PiStepForm, VDraggable }, + components: { PiStepForm, VDraggable }, props: { editMode: { type: Boolean, diff --git a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue index e0b71633a..50eaff95a 100644 --- a/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue +++ b/packages/ui/certd-client/src/views/certd/pipeline/pipeline/index.vue @@ -805,9 +805,11 @@ export default defineComponent({ let errorMessages: any = []; let errorIndex = 1; eachSteps(pp, (step: any, task: any, stage: any) => { - if (step.disabled !== true) { - stepIds.push(step.id); + if (step.disabled === true) { + return; } + stepIds.push(step.id); + if (step.input) { for (const key in step.input) { const value = step.input[key]; diff --git a/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts b/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts index 1fb8f13d6..551d6a46e 100644 --- a/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts +++ b/packages/ui/certd-server/src/modules/cname/service/cname-record-service.ts @@ -8,7 +8,7 @@ import { SysSettingsService, ValidateException } from "@certd/lib-server"; -import { CnameProvider, CnameRecord } from "@certd/pipeline"; +import { CnameProvider, CnameRecord, IAccessService } from "@certd/pipeline"; import { createDnsProvider, DomainParser, IDnsProvider } from "@certd/plugin-cert"; import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core"; import { InjectEntityModel } from "@midwayjs/typeorm"; @@ -251,9 +251,9 @@ export class CnameRecordService extends BaseService { return true; } - await this.getByDomain(bean.domain, bean.userId); + await this.getByDomain(bean.domain, bean.userId,bean.projectId); - const taskService = this.taskServiceBuilder.create({ userId: bean.userId }); + const taskService = this.taskServiceBuilder.create({ userId: bean.userId, projectId: bean.projectId }); const subDomainGetter = await taskService.getSubDomainsGetter(); const domainParser = new DomainParser(subDomainGetter); @@ -290,8 +290,9 @@ export class CnameRecordService extends BaseService { }); } - const serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId }); - const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId); + const serviceGetter = this.taskServiceBuilder.create({ userId: bean.userId, projectId: bean.projectId }); + const accessGetter:IAccessService = await serviceGetter.get("accessService"); + const access = await accessGetter.getById(cnameProvider.accessId); const context = { access, logger, http, utils, domainParser, serviceGetter }; const dnsProvider: IDnsProvider = await createDnsProvider({ dnsProviderType: cnameProvider.dnsProviderType, diff --git a/packages/ui/certd-server/src/plugins/plugin-bind9/access.ts b/packages/ui/certd-server/src/plugins/plugin-bind9/access.ts new file mode 100644 index 000000000..e437e58e4 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-bind9/access.ts @@ -0,0 +1,83 @@ +import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline"; + +@IsAccess({ + name: "bind9", + title: "BIND9 DNS 授权", + desc: "通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录", + icon: "clarity:host-line", +}) +export class Bind9Access extends BaseAccess { + @AccessInput({ + title: "SSH 授权", + helper: "选择已配置的 SSH 授权", + component: { + name: "access-selector", + type: "ssh", + vModel:"modelValue" + }, + required: true, + }) + sshAccessId!: string; + + @AccessInput({ + title: "DNS 服务器地址", + helper: "BIND9 DNS 服务器地址,用于 nsupdate 命令", + component: { + placeholder: "192.168.182.100", + }, + }) + dnsServer: string; + + @AccessInput({ + title: "DNS 服务器端口", + helper: "BIND9 DNS 服务器端口,用于 nsupdate 命令,默认为 53", + value: 53, + component: { + name: "a-input-number", + placeholder: "53", + }, + }) + dnsPort: number = 53; + + @AccessInput({ + title: "测试", + component: { + name: "api-test", + type: "access", + typeName: "bind9", + action: "TestRequest", + }, + mergeScript: ` + return { + component:{ + form: ctx.compute(({form})=>{ + return form + }) + }, + } + `, + helper: "点击测试 SSH 连接和 nsupdate 命令", + }) + testRequest = true; + + async onTestRequest() { + const { SshClient } = await import("../plugin-lib/ssh/ssh.js"); + const client = new SshClient(this.ctx.logger); + + // 获取 SSH 授权配置 + const sshAccess = await this.ctx.accessService.getById(this.sshAccessId); + if (!sshAccess) { + throw new Error("SSH 授权不存在"); + } + + // 测试 SSH 连接 + const script = ["echo 'SSH connection successful'", "which nsupdate", "exit"]; + await client.exec({ + connectConf: sshAccess, + script: script, + }); + return "SSH 连接成功,nsupdate 命令可用"; + } +} + +new Bind9Access(); diff --git a/packages/ui/certd-server/src/plugins/plugin-bind9/dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-bind9/dns-provider.ts new file mode 100644 index 000000000..1d2cd3044 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-bind9/dns-provider.ts @@ -0,0 +1,127 @@ +import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert'; +import { Bind9Access } from './access.js'; +import { SshClient } from '../plugin-lib/ssh/index.js'; + +export type Bind9Record = { + fullRecord: string; + value: string; + type: string; + domain: string; +}; + +@IsDnsProvider({ + name: 'bind9', + title: 'BIND9 DNS', + desc: '通过 SSH 连接到 BIND9 服务器,使用 nsupdate 命令管理 DNS 记录', + icon: 'clarity:host-line', + accessType: 'bind9', +}) +export class Bind9DnsProvider extends AbstractDnsProvider { + access!: Bind9Access; + sshClient!: SshClient; + + async onInstance() { + this.access = this.ctx.access as Bind9Access; + this.sshClient = new SshClient(this.logger); + } + + /** + * 获取 SSH 连接配置 + */ + private async getSshAccess() { + // 从 accessService 获取 SSH 授权配置 + const sshAccess = await this.ctx.accessGetter.getById(this.access.sshAccessId); + if (!sshAccess) { + throw new Error('SSH 授权不存在'); + } + return sshAccess; + } + + /** + * 构建 nsupdate 命令 + */ + private buildNsupdateCommand(commands: string[]): string { + const { dnsServer, dnsPort } = this.access; + const nsupdateScript = [ + `server ${dnsServer} ${dnsPort}`, + ...commands, + "send", + ].join("\n"); + + // 使用 heredoc 方式执行 nsupdate + return `nsupdate << 'EOF' +${nsupdateScript} +EOF`; + } + + /** + * 创建 DNS 解析记录,用于验证域名所有权 + */ + async createRecord(options: CreateRecordOptions): Promise { + const { fullRecord, value, type, domain } = options; + this.logger.info('添加域名解析:', fullRecord, value, type, domain); + + // 构建 nsupdate 命令 + // 格式: update add + const updateCommand = `update add ${fullRecord} 60 ${type} "${value}"`; + const nsupdateCmd = this.buildNsupdateCommand([updateCommand]); + + this.logger.info('执行 nsupdate 命令添加记录'); + + try { + const sshAccess = await this.getSshAccess(); + await this.sshClient.exec({ + connectConf: sshAccess, + script: nsupdateCmd, + throwOnStdErr: true, + }); + + this.logger.info(`添加域名解析成功: fullRecord=${fullRecord}, value=${value}`); + } catch (error: any) { + this.logger.error('添加域名解析失败:', error.message); + throw new Error(`添加 DNS 记录失败: ${error.message}`); + } + + // 返回记录信息,用于后续删除 + const record: Bind9Record = { + fullRecord, + value, + type, + domain, + }; + + return record; + } + + /** + * 删除 DNS 解析记录,清理申请痕迹 + */ + async removeRecord(options: RemoveRecordOptions): Promise { + const { fullRecord, value, type, domain } = options.recordRes; + this.logger.info('删除域名解析:', fullRecord, value, type, domain); + + // 构建 nsupdate 命令 + // 格式: update delete + const updateCommand = `update delete ${fullRecord} ${type} "${value}"`; + const nsupdateCmd = this.buildNsupdateCommand([updateCommand]); + + this.logger.info('执行 nsupdate 命令删除记录'); + + try { + const sshAccess = await this.getSshAccess(); + await this.sshClient.exec({ + connectConf: sshAccess, + script: nsupdateCmd, + throwOnStdErr: false, // 删除时忽略错误(记录可能已不存在) + }); + + this.logger.info(`删除域名解析成功: fullRecord=${fullRecord}, value=${value}`); + } catch (error: any) { + // 删除失败只记录警告,不抛出异常(清理操作不应影响主流程) + this.logger.warn('删除域名解析时出现警告:', error.message); + } + } +} + +// 实例化这个 provider,将其自动注册到系统中 +new Bind9DnsProvider(); diff --git a/packages/ui/certd-server/src/plugins/plugin-bind9/index.ts b/packages/ui/certd-server/src/plugins/plugin-bind9/index.ts new file mode 100644 index 000000000..db899c717 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-bind9/index.ts @@ -0,0 +1,2 @@ +export * from './dns-provider.js'; +export * from './access.js';