perf(technitium): 添加Technitium DNS Server插件支持

- 新增Technitium DNS Server插件,包含DNS提供商和授权配置
- 实现DNS记录创建、删除和域名列表获取功能
- 添加默认DNS传播等待时间配置
- 优化用户取消操作时的错误处理
- 为图标选择组件添加过滤功能
- 更新DNS提供商开发文档
This commit is contained in:
xiaojunnuo
2026-04-17 19:22:10 +08:00
parent 23b4658672
commit edeb817c39
14 changed files with 7863 additions and 13 deletions
+14
View File
@@ -145,6 +145,20 @@ async doRequest(req: { action: string, data?: any }) {
utils: typeof utils;
accessService: IAccessService;
}
// this.ctx.http 只有request方法
// 方法参数
export type HttpRequestConfig<D = any> = {
skipSslVerify?: boolean;
skipCheckRes?: boolean;
logParams?: boolean;
logRes?: boolean;
logData?: boolean;
httpProxy?: string;
returnOriginRes?: boolean;
} & AxiosRequestConfig<D>;
*/
const res = await this.ctx.http.request({
url: "https://api.demo.cn/api/",
+43 -4
View File
@@ -105,6 +105,28 @@ async removeRecord(options: RemoveRecordOptions<DemoRecord>): Promise<void> {
}
```
### 6. 实现 getDomainListPage 方法
```typescript
/**
* 实现获取域名列表
*/
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
const pager = new Pager(req);
const res = await this.http.request({
// 请求接口获取域名列表
})
const list = res.Domains?.map(item => ({
id: item.Id,
domain: item.DomainName,
})) || []
return {
list,
total: res.Total,
}
}
```
### 6. 实例化插件
```typescript
@@ -204,11 +226,28 @@ export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
this.logger.info('删除域名解析成功:', fullRecord, value);
}
/**
* 实现获取域名列表
*/
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
const pager = new Pager(req);
const res = await this.http.request({
// 请求接口获取域名列表
})
const list = res.Domains?.map(item => ({
id: item.Id,
domain: item.DomainName,
})) || []
return {
list,
total: res.Total,
}
}
}
// 实例化这个 provider,将其自动注册到系统中
if (isDev()) {
// 你的实现 要去掉这个 if,不然生产环境将不会显示
new DemoDnsProvider();
}
new DemoDnsProvider();
```
+2 -1
View File
@@ -21,7 +21,8 @@ const defaultOpts = {
},
challengeRemoveFn: async () => {
throw new Error("Missing challengeRemoveFn()");
}
},
waitDnsDiffuseTime: 30,
};
/**
+1 -1
View File
@@ -577,7 +577,7 @@ class AcmeClient {
const verifyFn = async (abort) => {
if (this.opts.signal && this.opts.signal.aborted) {
abort();
abort(true);
throw new CancelError('用户取消');
}
+5 -2
View File
@@ -50,15 +50,18 @@ class Backoff {
async function retryPromise(fn, attempts, backoff, logger = log) {
let aborted = false;
let abortedFromUser = false;
try {
const setAbort = () => { aborted = true; }
const setAbort = (fromUser = false) => { aborted = true; abortedFromUser = fromUser; }
const data = await fn(setAbort);
return data;
}
catch (e) {
if (aborted){
logger(`用户取消重试`);
if (abortedFromUser){
logger(`用户取消重试`);
}
throw e;
}
if ( ((backoff.attempts + 1) >= attempts)) {
+1
View File
@@ -68,6 +68,7 @@ export interface ClientAutoOptions {
preferredChain?: string;
signal?: AbortSignal;
profile?:string;
waitDnsDiffuseTime?: number;
}
export class Client {
@@ -1,5 +1,5 @@
<template>
<a-select :value="value" @update:value="onChange">
<a-select :value="value" :filter-option="true" @update:value="onChange">
<a-select-option v-for="item of options" :key="item.value" :value="item.value" :label="item.label">
<span class="flex-o">
<fs-icon :icon="item.icon" class="fs-16 color-blue mr-5" />
@@ -1,5 +1,5 @@
<template>
<icon-select class="dns-provider-selector" :value="modelValue" :options="options" @update:value="atChange"> </icon-select>
<icon-select class="dns-provider-selector" :value="modelValue" :options="options" :filter-option="true" @update:value="atChange"> </icon-select>
</template>
<script lang="ts">
@@ -423,6 +423,7 @@ export class AcmeService {
signal: this.options.signal,
profile,
preferredChain,
waitDnsDiffuseTime: this.options.waitDnsDiffuseTime,
});
const crtString = crt.toString();
@@ -1,7 +1,8 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { DemoAccess } from './access.js';
import { PageRes, PageSearch } from '@certd/pipeline';
import { isDev } from '../../utils/env.js';
import { DemoAccess } from './access.js';
type DemoRecord = {
// 这里定义Record记录的数据结构,跟对应云平台接口返回值一样即可,一般是拿到id就行,用于删除txt解析记录,清理申请痕迹
@@ -16,7 +17,7 @@ type DemoRecord = {
icon: 'clarity:plugin-line',
// 这里是对应的云平台的access类型名称
accessType: 'demo',
order:99,
order: 99,
})
export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
access!: DemoAccess;
@@ -74,6 +75,28 @@ export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
this.logger.info('删除域名解析成功:', fullRecord, value);
}
/**
*
* @param req 实现获取域名列表
* @returns
*/
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
const res = await this.http.request({
// 请求接口获取域名列表
})
const list = []
// const list = res.Domains?.map(item => ({
// id: item.Id,
// domain: item.DomainName,
// })) || []
return {
list,
total: res.Total,
}
}
}
//TODO 实例化这个provider,将其自动注册到系统中
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,179 @@
import { AccessInput, BaseAccess, IsAccess, Pager, PageRes, PageSearch } from '@certd/pipeline';
import { DomainRecord } from '@certd/plugin-lib';
/**
* Technitium DNS Server 授权配置
*/
@IsAccess({
name: 'technitium',
title: 'Technitium DNS Server',
icon: 'clarity:server-line',
desc: 'Technitium DNS Server 自建DNS服务器授权',
})
export class TechnitiumAccess extends BaseAccess {
/**
* API地址
*/
@AccessInput({
title: 'API地址',
value: 'http://localhost:5380',
component: {
name: "a-input",
allowClear: true,
placeholder: 'http://localhost:5380',
},
required: true,
})
apiUrl = 'http://localhost:5380';
/**
* 用户名
*/
@AccessInput({
title: '用户名',
component: {
name: "a-input",
allowClear: true,
placeholder: 'admin',
},
required: false,
})
username = 'admin';
/**
* 密码
*/
@AccessInput({
title: '密码',
component: {
name: "a-input",
type: "password",
allowClear: true,
placeholder: '密码',
},
required: false,
encrypt: true,
})
password = '';
/**
* 测试按钮
*/
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
token = '';
/**
* 通用API调用方法
*/
async doRequest(options: { url: string; method: 'get' | 'post'; params?: URLSearchParams }) {
// 每次请求前都获取最新的token
if (!options.url.includes('/api/user/login')) {
await this.getToken();
}
// 复制参数并添加token
const params = new URLSearchParams(options.params || '');
if (this.token && !options.url.includes('/api/user/login')) {
params.append('token', this.token);
}
let fullUrl = options.url;
if (params.toString()) {
fullUrl = `${options.url}?${params.toString()}`;
}
const response = await this.ctx.http.request({
url: fullUrl,
method: options.method,
});
if (response.status !== 'ok') {
throw new Error(`${response.errorMessage || 'API调用失败'}`);
}
return response;
}
/**
* 测试API连接
*/
async onTestRequest() {
// 测试获取区域列表
await this.GetDomainList({});
return "连接成功";
}
/**
* 获取域名列表
*/
async GetDomainList(req: PageSearch): Promise<PageRes<DomainRecord>> {
this.ctx.logger.info(`获取域名列表,req:${JSON.stringify(req)}`);
const pager = new Pager(req);
// 构建API URL
const apiUrl = `${this.apiUrl}/api/zones/list`;
// 构建查询参数
const params = new URLSearchParams();
// 调用API获取区域列表
const response = await this.doRequest({ url: apiUrl, method: 'get', params: params });
const zones = response.response.zones || [];
const total = zones.length;
// 转换为DomainRecord格式
let list = zones.map((zone: any) => ({
id: zone.name,
domain: zone.name,
}));
// 应用分页
list = list.slice(pager.getOffset(), pager.getOffset() + pager.pageSize);
return {
total,
list
};
}
/**
* 获取API Token
*/
async getToken() {
const apiUrl = `${this.apiUrl}/api/user/login`;
const params = new URLSearchParams({
user: this.username,
pass: this.password,
});
// 直接使用ctx.http.request,避免递归调用doRequest
const response = await this.ctx.http.request({
url: `${apiUrl}?${params.toString()}`,
method: 'post',
});
if (response.status !== 'ok') {
throw new Error(`登录失败: ${response.errorMessage || '未知错误'}`);
}
this.token = response.token;
return this.token;
}
}
@@ -0,0 +1,94 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { TechnitiumAccess } from "./access.js"
type TechnitiumRecord = {
// 记录创建时返回的数据结构
zone: {
name: string;
type: string;
internal: boolean;
dnssecStatus: string;
disabled: boolean;
};
addedRecord: {
disabled: boolean;
name: string;
type: string;
ttl: number;
rData: {
text: string;
};
dnssecStatus: string;
lastUsedOn: string;
};
};
// 注册Technitium DNS Server的DNS提供商
@IsDnsProvider({
name: 'technitium',
title: 'Technitium DNS Server',
desc: 'Technitium DNS Server 自建DNS服务器',
icon: 'clarity:server-line',
accessType: 'technitium',
order: 10,
})
export class TechnitiumDnsProvider extends AbstractDnsProvider<TechnitiumRecord> {
access!: TechnitiumAccess;
async onInstance() {
this.access = this.ctx.access as TechnitiumAccess;
this.logger.debug('access', this.access);
}
/**
* 创建DNS解析记录,用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<TechnitiumRecord> {
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
// 构建API URL
const apiUrl = `${this.access.apiUrl}/api/zones/records/add`;
// 构建查询参数
const params = new URLSearchParams({
domain: fullRecord,
type: type,
text: value,
});
// 调用Technitium API创建TXT记录
const response = await this.access.doRequest({ url: apiUrl, method: 'post', params: params });
this.logger.info('创建域名解析成功:', fullRecord, value);
return response as TechnitiumRecord;
}
/**
* 删除DNS解析记录,清理申请痕迹
*/
async removeRecord(options: RemoveRecordOptions<TechnitiumRecord>): Promise<void> {
const { fullRecord, value, domain } = options.recordReq;
const record = options.recordRes;
this.logger.info('删除域名解析:', domain, fullRecord, value, record);
// 构建API URL
const apiUrl = `${this.access.apiUrl}/api/zones/records/delete`;
// 构建查询参数
const params = new URLSearchParams({
domain: fullRecord,
type: 'TXT',
text: value,
});
// 调用Technitium API删除TXT记录
await this.access.doRequest({ url: apiUrl, method: 'post', params: params });
this.logger.info('删除域名解析成功:', fullRecord, value);
}
}
// 实例化这个provider,将其自动注册到系统中
new TechnitiumDnsProvider();
@@ -0,0 +1,2 @@
export * from './dns-provider.js';
export * from './access.js';