mirror of
https://github.com/certd/certd.git
synced 2026-04-28 16:17:25 +08:00
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:
@@ -65,6 +65,20 @@ demoKeyId = '';
|
|||||||
encrypt: true, //该属性是否需要加密
|
encrypt: true, //该属性是否需要加密
|
||||||
})
|
})
|
||||||
demoKeySecret = '';
|
demoKeySecret = '';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@AccessInput({
|
||||||
|
title: '另外一个授权Id',//标题
|
||||||
|
component: {
|
||||||
|
name:"access-selector", //access选择组件
|
||||||
|
vModel:"modelValue",
|
||||||
|
type: "ssh", // access类型,让用户固定选择这种类型的access
|
||||||
|
},
|
||||||
|
required: true, //text组件可以省略
|
||||||
|
})
|
||||||
|
otherAccessId;
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 实现测试方法
|
### 4. 实现测试方法
|
||||||
@@ -93,7 +107,7 @@ async onTestRequest() {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
/**
|
/**
|
||||||
* 获api接口示例 取域名列表,
|
* api接口示例 获取域名列表,
|
||||||
*/
|
*/
|
||||||
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
|
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
|
||||||
//输出日志必须使用ctx.logger
|
//输出日志必须使用ctx.logger
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 什么是插件转换工具
|
## 什么是插件转换工具
|
||||||
|
|
||||||
插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,方便插件的注册和管理。
|
插件转换工具是一个用于将 Certd 插件转换为 YAML 配置文件的命令行工具。它可以分析单个插件文件,识别插件类型,并生成对应的 YAML 配置,可以让插件分发和在线注册。
|
||||||
|
|
||||||
## 工具位置
|
## 工具位置
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ certDomains!: string[];
|
|||||||
helper: 'demoAccess授权',
|
helper: 'demoAccess授权',
|
||||||
component: {
|
component: {
|
||||||
name: 'access-selector',
|
name: 'access-selector',
|
||||||
type: 'demo', // 固定授权类型
|
vModel:"modelValue",
|
||||||
|
type: "demo", // access类型,让用户固定选择这种类型的access
|
||||||
},
|
},
|
||||||
// rules: [{ required: true, message: '此项必填' }],
|
// rules: [{ required: true, message: '此项必填' }],
|
||||||
// required: true, // 必填
|
// required: true, // 必填
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HttpClient, ILogger, utils } from "@certd/basic";
|
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 & {
|
export type DnsProviderDefine = Registrable & {
|
||||||
accessType: string;
|
accessType: string;
|
||||||
@@ -26,6 +26,7 @@ export type DnsProviderContext = {
|
|||||||
utils: typeof utils;
|
utils: typeof utils;
|
||||||
domainParser: IDomainParser;
|
domainParser: IDomainParser;
|
||||||
serviceGetter: IServiceGetter;
|
serviceGetter: IServiceGetter;
|
||||||
|
accessGetter?: IAccessService;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DomainRecord = {
|
export type DomainRecord = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { HttpClient, ILogger } from "@certd/basic";
|
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 punycode from "punycode.js";
|
||||||
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
|
||||||
import { dnsProviderRegistry } from "./registry.js";
|
import { dnsProviderRegistry } from "./registry.js";
|
||||||
@@ -59,6 +59,11 @@ export async function createDnsProvider(opts: { dnsProviderType: string; context
|
|||||||
if (dnsProviderDefine.deprecated) {
|
if (dnsProviderDefine.deprecated) {
|
||||||
context.logger.warn(dnsProviderDefine.deprecated);
|
context.logger.warn(dnsProviderDefine.deprecated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!context.accessGetter) {
|
||||||
|
const accessGetter: IAccessService = await context.serviceGetter.get("accessService");
|
||||||
|
context.accessGetter = accessGetter;
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const dnsProvider: IDnsProvider = new DnsProviderClass();
|
const dnsProvider: IDnsProvider = new DnsProviderClass();
|
||||||
dnsProvider.setCtx(context);
|
dnsProvider.setCtx(context);
|
||||||
|
|||||||
+1
-2
@@ -72,14 +72,13 @@ import * as _ from "lodash-es";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import PiStepForm from "../step-form/index.vue";
|
import PiStepForm from "../step-form/index.vue";
|
||||||
import { Modal } from "ant-design-vue";
|
import { Modal } from "ant-design-vue";
|
||||||
import { CopyOutlined } from "@ant-design/icons-vue";
|
|
||||||
import VDraggable from "vuedraggable";
|
import VDraggable from "vuedraggable";
|
||||||
import { useUserStore } from "/@/store/user";
|
import { useUserStore } from "/@/store/user";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
import { filter } from "lodash-es";
|
import { filter } from "lodash-es";
|
||||||
export default {
|
export default {
|
||||||
name: "PiTaskForm",
|
name: "PiTaskForm",
|
||||||
components: { CopyOutlined, PiStepForm, VDraggable },
|
components: { PiStepForm, VDraggable },
|
||||||
props: {
|
props: {
|
||||||
editMode: {
|
editMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -805,9 +805,11 @@ export default defineComponent({
|
|||||||
let errorMessages: any = [];
|
let errorMessages: any = [];
|
||||||
let errorIndex = 1;
|
let errorIndex = 1;
|
||||||
eachSteps(pp, (step: any, task: any, stage: any) => {
|
eachSteps(pp, (step: any, task: any, stage: any) => {
|
||||||
if (step.disabled !== true) {
|
if (step.disabled === true) {
|
||||||
stepIds.push(step.id);
|
return;
|
||||||
}
|
}
|
||||||
|
stepIds.push(step.id);
|
||||||
|
|
||||||
if (step.input) {
|
if (step.input) {
|
||||||
for (const key in step.input) {
|
for (const key in step.input) {
|
||||||
const value = step.input[key];
|
const value = step.input[key];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
SysSettingsService,
|
SysSettingsService,
|
||||||
ValidateException
|
ValidateException
|
||||||
} from "@certd/lib-server";
|
} 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 { createDnsProvider, DomainParser, IDnsProvider } from "@certd/plugin-cert";
|
||||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||||
import { InjectEntityModel } from "@midwayjs/typeorm";
|
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||||
@@ -251,9 +251,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
|||||||
return true;
|
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 subDomainGetter = await taskService.getSubDomainsGetter();
|
||||||
const domainParser = new DomainParser(subDomainGetter);
|
const domainParser = new DomainParser(subDomainGetter);
|
||||||
|
|
||||||
@@ -290,8 +290,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId });
|
const serviceGetter = this.taskServiceBuilder.create({ userId: bean.userId, projectId: bean.projectId });
|
||||||
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
|
const accessGetter:IAccessService = await serviceGetter.get("accessService");
|
||||||
|
const access = await accessGetter.getById(cnameProvider.accessId);
|
||||||
const context = { access, logger, http, utils, domainParser, serviceGetter };
|
const context = { access, logger, http, utils, domainParser, serviceGetter };
|
||||||
const dnsProvider: IDnsProvider = await createDnsProvider({
|
const dnsProvider: IDnsProvider = await createDnsProvider({
|
||||||
dnsProviderType: cnameProvider.dnsProviderType,
|
dnsProviderType: cnameProvider.dnsProviderType,
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './dns-provider.js';
|
||||||
|
export * from './access.js';
|
||||||
Reference in New Issue
Block a user