mirror of
https://github.com/certd/certd.git
synced 2026-04-20 09:50:50 +08:00
perf(technitium): 添加Technitium DNS Server插件支持
- 新增Technitium DNS Server插件,包含DNS提供商和授权配置 - 实现DNS记录创建、删除和域名列表获取功能 - 添加默认DNS传播等待时间配置 - 优化用户取消操作时的错误处理 - 为图标选择组件添加过滤功能 - 更新DNS提供商开发文档
This commit is contained in:
@@ -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/",
|
||||
|
||||
@@ -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();
|
||||
|
||||
```
|
||||
@@ -21,7 +21,8 @@ const defaultOpts = {
|
||||
},
|
||||
challengeRemoveFn: async () => {
|
||||
throw new Error("Missing challengeRemoveFn()");
|
||||
}
|
||||
},
|
||||
waitDnsDiffuseTime: 30,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -577,7 +577,7 @@ class AcmeClient {
|
||||
|
||||
const verifyFn = async (abort) => {
|
||||
if (this.opts.signal && this.opts.signal.aborted) {
|
||||
abort();
|
||||
abort(true);
|
||||
throw new CancelError('用户取消');
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
-1
@@ -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';
|
||||
Reference in New Issue
Block a user