mirror of
https://github.com/certd/certd.git
synced 2026-04-24 12:27:25 +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;
|
utils: typeof utils;
|
||||||
accessService: IAccessService;
|
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({
|
const res = await this.ctx.http.request({
|
||||||
url: "https://api.demo.cn/api/",
|
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. 实例化插件
|
### 6. 实例化插件
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -204,11 +226,28 @@ export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
|
|||||||
|
|
||||||
this.logger.info('删除域名解析成功:', fullRecord, value);
|
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,将其自动注册到系统中
|
// 实例化这个 provider,将其自动注册到系统中
|
||||||
if (isDev()) {
|
new DemoDnsProvider();
|
||||||
// 你的实现 要去掉这个 if,不然生产环境将不会显示
|
|
||||||
new DemoDnsProvider();
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
@@ -21,7 +21,8 @@ const defaultOpts = {
|
|||||||
},
|
},
|
||||||
challengeRemoveFn: async () => {
|
challengeRemoveFn: async () => {
|
||||||
throw new Error("Missing challengeRemoveFn()");
|
throw new Error("Missing challengeRemoveFn()");
|
||||||
}
|
},
|
||||||
|
waitDnsDiffuseTime: 30,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ class AcmeClient {
|
|||||||
|
|
||||||
const verifyFn = async (abort) => {
|
const verifyFn = async (abort) => {
|
||||||
if (this.opts.signal && this.opts.signal.aborted) {
|
if (this.opts.signal && this.opts.signal.aborted) {
|
||||||
abort();
|
abort(true);
|
||||||
throw new CancelError('用户取消');
|
throw new CancelError('用户取消');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,15 +50,18 @@ class Backoff {
|
|||||||
|
|
||||||
async function retryPromise(fn, attempts, backoff, logger = log) {
|
async function retryPromise(fn, attempts, backoff, logger = log) {
|
||||||
let aborted = false;
|
let aborted = false;
|
||||||
|
let abortedFromUser = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const setAbort = () => { aborted = true; }
|
const setAbort = (fromUser = false) => { aborted = true; abortedFromUser = fromUser; }
|
||||||
const data = await fn(setAbort);
|
const data = await fn(setAbort);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (aborted){
|
if (aborted){
|
||||||
logger(`用户取消重试`);
|
if (abortedFromUser){
|
||||||
|
logger(`用户取消重试`);
|
||||||
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
if ( ((backoff.attempts + 1) >= attempts)) {
|
if ( ((backoff.attempts + 1) >= attempts)) {
|
||||||
|
|||||||
+1
@@ -68,6 +68,7 @@ export interface ClientAutoOptions {
|
|||||||
preferredChain?: string;
|
preferredChain?: string;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
profile?:string;
|
profile?:string;
|
||||||
|
waitDnsDiffuseTime?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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">
|
<a-select-option v-for="item of options" :key="item.value" :value="item.value" :label="item.label">
|
||||||
<span class="flex-o">
|
<span class="flex-o">
|
||||||
<fs-icon :icon="item.icon" class="fs-16 color-blue mr-5" />
|
<fs-icon :icon="item.icon" class="fs-16 color-blue mr-5" />
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ export class AcmeService {
|
|||||||
signal: this.options.signal,
|
signal: this.options.signal,
|
||||||
profile,
|
profile,
|
||||||
preferredChain,
|
preferredChain,
|
||||||
|
waitDnsDiffuseTime: this.options.waitDnsDiffuseTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
const crtString = crt.toString();
|
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 { isDev } from '../../utils/env.js';
|
||||||
|
import { DemoAccess } from './access.js';
|
||||||
|
|
||||||
type DemoRecord = {
|
type DemoRecord = {
|
||||||
// 这里定义Record记录的数据结构,跟对应云平台接口返回值一样即可,一般是拿到id就行,用于删除txt解析记录,清理申请痕迹
|
// 这里定义Record记录的数据结构,跟对应云平台接口返回值一样即可,一般是拿到id就行,用于删除txt解析记录,清理申请痕迹
|
||||||
@@ -16,7 +17,7 @@ type DemoRecord = {
|
|||||||
icon: 'clarity:plugin-line',
|
icon: 'clarity:plugin-line',
|
||||||
// 这里是对应的云平台的access类型名称
|
// 这里是对应的云平台的access类型名称
|
||||||
accessType: 'demo',
|
accessType: 'demo',
|
||||||
order:99,
|
order: 99,
|
||||||
})
|
})
|
||||||
export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
|
export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
|
||||||
access!: DemoAccess;
|
access!: DemoAccess;
|
||||||
@@ -74,6 +75,28 @@ export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
|
|||||||
|
|
||||||
this.logger.info('删除域名解析成功:', fullRecord, value);
|
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,将其自动注册到系统中
|
//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