perf: dns-provider 支持bind9 ,support bind9

https://github.com/certd/certd/issues/683
https://github.com/certd/certd/discussions/668
This commit is contained in:
xiaojunnuo
2026-03-15 23:55:49 +08:00
parent cf10faf61c
commit 76d12d6062
11 changed files with 249 additions and 14 deletions

View File

@@ -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<PageRes<DomainRecord>> {
//输出日志必须使用ctx.logger

View File

@@ -2,7 +2,7 @@
## 什么是插件转换工具
插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,方便插件的注册和管理
插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,可以让插件分发和在线注册
## 工具位置

View File

@@ -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, // 必填

View File

@@ -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 = {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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];

View File

@@ -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<CnameRecordEntity> {
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<CnameRecordEntity> {
});
}
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,

View File

@@ -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();

View File

@@ -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<Bind9Record> {
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<Bind9Record> {
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// 构建 nsupdate 命令
// 格式: update add <name> <ttl> <type> <value>
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<Bind9Record>): Promise<void> {
const { fullRecord, value, type, domain } = options.recordRes;
this.logger.info('删除域名解析:', fullRecord, value, type, domain);
// 构建 nsupdate 命令
// 格式: update delete <name> <type> <value>
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();

View File

@@ -0,0 +1,2 @@
export * from './dns-provider.js';
export * from './access.js';