perf: 支持cloudflare域名

pull/68/head
xiaojunnuo 2024-06-15 02:17:34 +08:00
parent 368132daae
commit fbb9a47e8f
14 changed files with 206 additions and 106 deletions

View File

@ -119,6 +119,7 @@ module.exports = async function(client, userOpts) {
try { try {
recordItem = await opts.challengeCreateFn(authz, challenge, keyAuthorization); recordItem = await opts.challengeCreateFn(authz, challenge, keyAuthorization);
// throw new Error('测试异常');
/* Challenge verification */ /* Challenge verification */
if (opts.skipChallengeVerification === true) { if (opts.skipChallengeVerification === true) {
log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`); log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`);
@ -177,30 +178,43 @@ module.exports = async function(client, userOpts) {
}); });
// let promise = Promise.resolve(); function runAllPromise(tasks) {
// function runPromisesSerially(tasks) { let promise = Promise.resolve();
// tasks.forEach((task) => { tasks.forEach((task) => {
// promise = promise.then(task); promise = promise.then(task);
// }); });
// return promise; return promise;
// }
function runPromiseParallel(tasks) {
return Promise.all(tasks.map((task) => task()));
} }
// function runPromisePa(tasks) {
// return Promise.all(tasks.map((task) => task()));
// }
try { try {
log('开始challenge'); log('开始challenge');
await runPromiseParallel(challengePromises); await runAllPromise(challengePromises);
log('challenge结束');
// log('[auto] Waiting for challenge valid status');
// await Promise.all(challengePromises);
/**
* Finalize order and download certificate
*/
log('[auto] Finalizing order and downloading certificate');
const finalized = await client.finalizeOrder(order, opts.csr);
return await client.getCertificate(finalized, opts.preferredChain);
} }
catch (e) { catch (e) {
log('challenge失败'); log('证书申请失败');
throw e; throw e;
} }
finally { finally {
log('清理challenge痕迹'); log(`清理challenge痕迹length:${clearTasks.length}`);
await runPromiseParallel(clearTasks); await runAllPromise(clearTasks);
} }
// try { // try {
@ -210,19 +224,4 @@ module.exports = async function(client, userOpts) {
// log('清理challenge'); // log('清理challenge');
// await Promise.allSettled(clearTasks); // await Promise.allSettled(clearTasks);
// } // }
log('challenge结束');
// log('[auto] Waiting for challenge valid status');
// await Promise.all(challengePromises);
/**
* Finalize order and download certificate
*/
log('[auto] Finalizing order and downloading certificate');
const finalized = await client.finalizeOrder(order, opts.csr);
return client.getCertificate(finalized, opts.preferredChain);
}; };

View File

@ -6,7 +6,7 @@ import { ContextFactory, IContext } from "./context";
import { IStorage } from "./storage"; import { IStorage } from "./storage";
import { logger } from "../utils/util.log"; import { logger } from "../utils/util.log";
import { Logger } from "log4js"; import { Logger } from "log4js";
import { request } from "../utils/util.request"; import { createAxiosService } from "../utils/util.request";
import { IAccessService } from "../access"; import { IAccessService } from "../access";
import { RegistryItem } from "../registry"; import { RegistryItem } from "../registry";
import { Decorator } from "../decorator"; import { Decorator } from "../decorator";
@ -213,11 +213,12 @@ export class Executor {
} }
}); });
const http = createAxiosService({ logger: currentLogger });
const taskCtx: TaskInstanceContext = { const taskCtx: TaskInstanceContext = {
pipeline: this.pipeline, pipeline: this.pipeline,
step, step,
lastStatus, lastStatus,
http: request, http,
logger: currentLogger, logger: currentLogger,
accessService: this.options.accessService, accessService: this.options.accessService,
emailService: this.options.emailService, emailService: this.options.emailService,

View File

@ -2,10 +2,11 @@ import axios from "axios";
// @ts-ignore // @ts-ignore
import qs from "qs"; import qs from "qs";
import { logger } from "./util.log"; import { logger } from "./util.log";
import { Logger } from "log4js";
/** /**
* @description * @description
*/ */
function createService() { export function createAxiosService({ logger }: { logger: Logger }) {
// 创建一个 axios 实例 // 创建一个 axios 实例
const service = axios.create(); const service = axios.create();
// 请求拦截 // 请求拦截
@ -18,18 +19,19 @@ function createService() {
}); // 序列化请求参数 }); // 序列化请求参数
delete config.formData; delete config.formData;
} }
logger.info(`http request:${config.url}method:${config.method}`);
return config; return config;
}, },
(error: Error) => { (error: Error) => {
// 发送失败 // 发送失败
logger.error(error); logger.error("接口请求失败:", error);
return Promise.reject(error); return Promise.reject(error);
} }
); );
// 响应拦截 // 响应拦截
service.interceptors.response.use( service.interceptors.response.use(
(response: any) => { (response: any) => {
logger.info("http response:", JSON.stringify(response.data)); logger.info("http response:", JSON.stringify(response?.data));
return response.data; return response.data;
}, },
(error: any) => { (error: any) => {
@ -48,11 +50,12 @@ function createService() {
// case 505: error.message = 'HTTP版本不受支持'; break // case 505: error.message = 'HTTP版本不受支持'; break
// default: break // default: break
// } // }
logger.error("请求出错:", error.response.config.url, error); logger.error(`请求出错url:${error?.response?.config.url},method:${error.response.config.method},status:${error?.response?.status}`);
logger.info("返回数据:", JSON.stringify(error?.response?.data));
return Promise.reject(error); return Promise.reject(error);
} }
); );
return service; return service;
} }
export const request = createService(); export const request = createAxiosService({ logger });

View File

@ -13,8 +13,9 @@ export type CreateRecordOptions = {
value: any; value: any;
domain: string; domain: string;
}; };
export type RemoveRecordOptions = CreateRecordOptions & { export type RemoveRecordOptions<T> = CreateRecordOptions & {
record: any; // 本次创建的dns解析记录实际上就是createRecord接口的返回值
record: T;
}; };
export type DnsProviderContext = { export type DnsProviderContext = {
@ -23,9 +24,9 @@ export type DnsProviderContext = {
http: HttpClient; http: HttpClient;
}; };
export interface IDnsProvider { export interface IDnsProvider<T = any> {
onInstance(): Promise<void>; onInstance(): Promise<void>;
createRecord(options: CreateRecordOptions): Promise<any>; createRecord(options: CreateRecordOptions): Promise<T>;
removeRecord(options: RemoveRecordOptions): Promise<any>; removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
setCtx(ctx: DnsProviderContext): void; setCtx(ctx: DnsProviderContext): void;
} }

View File

@ -1,15 +1,15 @@
import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from "./api"; import { CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions } from "./api";
export abstract class AbstractDnsProvider implements IDnsProvider { export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
ctx!: DnsProviderContext; ctx!: DnsProviderContext;
setCtx(ctx: DnsProviderContext) { setCtx(ctx: DnsProviderContext) {
this.ctx = ctx; this.ctx = ctx;
} }
abstract createRecord(options: CreateRecordOptions): Promise<any>; abstract createRecord(options: CreateRecordOptions): Promise<T>;
abstract onInstance(): Promise<void>; abstract onInstance(): Promise<void>;
abstract removeRecord(options: RemoveRecordOptions): Promise<any>; abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
} }

View File

@ -107,7 +107,7 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
@TaskInput({ @TaskInput({
title: "CsrInfo", title: "CsrInfo",
}) })
csrInfo: any; csrInfo!: string;
acme!: AcmeService; acme!: AcmeService;
logger!: Logger; logger!: Logger;
@ -167,7 +167,6 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
/** /**
* *
* @param input
*/ */
async condition() { async condition() {
if (this.forceUpdate) { if (this.forceUpdate) {
@ -220,7 +219,7 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
organizationUnit: "IT Department", organizationUnit: "IT Department",
emailAddress: email, emailAddress: email,
}, },
this.csrInfo this.csrInfo ? JSON.parse(this.csrInfo) : {}
); );
this.logger.info("开始申请证书,", email, domains); this.logger.info("开始申请证书,", email, domains);

View File

@ -56,7 +56,10 @@ export default {
for (let node of nodes) { for (let node of nodes) {
if (currentHistory?.value?.logs != null) { if (currentHistory?.value?.logs != null) {
node.logs = computed(() => { node.logs = computed(() => {
return currentHistory.value.logs[node.node.id] || []; if(currentHistory?.value?.logs && currentHistory.value?.logs[node.node.id]!= null){
return currentHistory.value?.logs[node.node.id];
}
return [];
}); });
} }
} }

View File

@ -120,7 +120,7 @@ export class AliyunDnsProvider extends AbstractDnsProvider{
throw e; throw e;
} }
} }
async removeRecord(options: RemoveRecordOptions): Promise<any> { async removeRecord(options: RemoveRecordOptions<any>): Promise<any> {
const { fullRecord, value, record } = options; const { fullRecord, value, record } = options;
const params = { const params = {
RegionId: 'cn-hangzhou', RegionId: 'cn-hangzhou',

View File

@ -6,7 +6,7 @@ import { IsAccess, AccessInput } from '@certd/pipeline';
*/ */
@IsAccess({ @IsAccess({
name: 'cloudflare', name: 'cloudflare',
title: '授权插件示例', title: 'cloudflare授权',
desc: '', desc: '',
}) })
export class CloudflareAccess { export class CloudflareAccess {

View File

@ -2,62 +2,118 @@ import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOp
import { Autowire, HttpClient, ILogger } from "@certd/pipeline"; import { Autowire, HttpClient, ILogger } from "@certd/pipeline";
import { CloudflareAccess } from "./access"; import { CloudflareAccess } from "./access";
// TODO 这里注册一个dnsProvider export type CloudflareRecord = {
id: string;
type: string;
name: string;
content: string;
ttl: number;
proxied: boolean;
zone_id: string;
zone_name: string;
created_on: string;
modified_on: string;
};
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({ @IsDnsProvider({
name: 'cloudflare', name: 'cloudflare',
title: 'cloudflare', title: 'cloudflare',
desc: 'cloudflare dns provider示例', desc: 'cloudflare dns provider',
// 这里是对应的 cloudflare的access类型名称
accessType: 'cloudflare', accessType: 'cloudflare',
}) })
export class CloudflareDnsProvider extends AbstractDnsProvider{ export class CloudflareDnsProvider extends AbstractDnsProvider<CloudflareRecord>{
// 通过Autowire传递context
@Autowire() @Autowire()
logger! : ILogger; logger! : ILogger;
access!: CloudflareAccess; access!: CloudflareAccess;
http!: HttpClient; http!: HttpClient;
async onInstance() { async onInstance() {
//一些初始化的操作 //一些初始化的操作
// 也可以通过ctx成员变量传递context 与Autowire效果一样
this.access = this.ctx.access as CloudflareAccess; this.access = this.ctx.access as CloudflareAccess;
this.http = this.ctx.http this.http = this.ctx.http
} }
async getZoneId(domain:string){
const url = `https://api.cloudflare.com/client/v4/zones?name=${domain}`;
const res = await this.doRequestApi(url,null,"get");
return res.result[0].id
}
private async doRequestApi(url: string,data:any = null,method:string="post") {
const res = await this.http.request<any,any>({
url,
method,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.access.apiToken}`,
},
data
});
if (!res.success) {
throw new Error(`${JSON.stringify(res.errors)}`);
}
return res
}
/** /**
* curl --request POST \ * dns
* --url https://api.cloudflare.com/client/v4/zones/zone_id/dns_records \ */
* --header 'Content-Type: application/json' \ async createRecord(options: CreateRecordOptions): Promise<CloudflareRecord> {
* --header 'X-Auth-Email: ' \
* --data '{ /**
* "content": "198.51.100.4", * fullRecord: '_acme-challenge.test.example.com',
* "name": "example.com", * value: uuid
* "proxied": false, * type: 'TXT',
* "type": "A", * domain: 'example.com'
* "comment": "Domain verification record",
* "tags": [
* "owner:dns-team"
* ],
* "ttl": 60
* }'
*/ */
async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type,domain } = options; const { fullRecord, value, type,domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type,domain); this.logger.info('添加域名解析:', fullRecord, value, type,domain);
this.http.post('https://api.cloudflare.com/client/v4/zones/zone_id/dns_records') const zoneId = await this.getZoneId(domain);
this.logger.info('获取zoneId成功:',zoneId)
//TODO 然后调用接口创建txt类型的dns解析记录 // 给domain下创建txt类型的dns解析记录fullRecord
// .. 这里调用对应平台的后台接口 let url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`;
const access = this.access; const res = await this.doRequestApi(url, {
this.logger.debug('access', access); content: value,
name: fullRecord,
type: type,
ttl: 60,
})
const record = res.result as CloudflareRecord;
this.logger.info(`添加域名解析成功:fullRecord=${fullRecord},value=${value}`);
this.logger.info(`dns解析记录:${JSON.stringify(record)}`,)
//本接口需要返回本次创建的dns解析记录这个记录会在删除的时候用到
return record
} }
async removeRecord(options: RemoveRecordOptions): Promise<any> {
/**
* dns,
* @param options
*/
async removeRecord(options: RemoveRecordOptions<CloudflareRecord>): Promise<void> {
const { fullRecord, value, record } = options; const { fullRecord, value, record } = options;
this.logger.info('删除域名解析:', fullRecord, value, record); this.logger.info('删除域名解析:', fullRecord, value);
//TODO 这里调用删除txt dns解析记录接口 if(!record){
const access = this.access; this.logger.info('record不存在');
this.logger.debug('access', access); return
this.logger.info('删除域名解析成功:', fullRecord, value); }
//这里调用删除txt dns解析记录接口
const zoneId = record.zone_id;
const recordId = record.id;
let url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId}`;
await this.doRequestApi(url,null,"delete")
this.logger.info(`删除域名解析成功:fullRecord=${fullRecord},value=${value}`);
} }
} }
//TODO 实例化这个provider将其自动注册到系统中 //实例化这个provider将其自动注册到系统中
new CloudflareDnsProvider(); new CloudflareDnsProvider();

View File

@ -1,4 +1,4 @@
import { IsAccess, AccessInput } from '@certd/pipeline'; import { IsAccess, AccessInput, IAccess } from "@certd/pipeline";
/** /**
* *
@ -9,7 +9,7 @@ import { IsAccess, AccessInput } from '@certd/pipeline';
title: '授权插件示例', title: '授权插件示例',
desc: '', desc: '',
}) })
export class DemoAccess { export class DemoAccess implements IAccess{
/** /**
* *
*/ */

View File

@ -1,43 +1,81 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { Autowire, ILogger } from "@certd/pipeline"; import { Autowire, HttpClient, ILogger } from "@certd/pipeline";
import { DemoAccess } from "./access"; import { DemoAccess } from "./access";
// TODO 这里注册一个dnsProvider
type DemoRecord = {
// 这里定义Record记录的数据结构跟对应云平台接口返回值一样即可一般是拿到id就行用于删除txt解析记录清理申请痕迹
// id:string
}
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({ @IsDnsProvider({
name: 'demo', name: "demo",
title: 'Dns提供商Demo', title: "Dns提供商Demo",
desc: 'dns provider示例', desc: "dns provider示例",
accessType: 'demo', //这里是对应的access name // 这里是对应的云平台的access类型名称
accessType: "demo"
}) })
export class DemoDnsProvider extends AbstractDnsProvider { export class DemoDnsProvider extends AbstractDnsProvider<DemoRecord> {
// 通过Autowire注入工具对象
@Autowire() @Autowire()
access!: DemoAccess; access!: DemoAccess;
@Autowire() @Autowire()
logger!: ILogger; logger!: ILogger;
http!: HttpClient;
async onInstance() { async onInstance() {
const access: any = this.access; // 也可以通过ctx成员变量传递context 与Autowire效果一样
this.logger.debug('access', access); this.http = this.ctx.http;
this.logger.debug("access", this.access);
//初始化的操作 //初始化的操作
//... //...
} }
/**
* dns
*/
async createRecord(options: CreateRecordOptions): Promise<any> { async createRecord(options: CreateRecordOptions): Promise<any> {
/**
* options
* fullRecord: '_acme-challenge.example.com',
* value: uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options; const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type,domain); this.logger.info("添加域名解析:", fullRecord, value, type, domain);
//TODO 然后调用接口创建txt类型的dns解析记录
// .. 这里调用对应平台的后台接口 // 调用创建dns解析记录的对应的云端接口创建txt类型的dns解析记录
const access = this.access; // 请根据实际接口情况调用,例如:
this.logger.debug('access', access); // const createDnsRecordUrl = "xxx"
// const record = this.http.post(createDnsRecordUrl,{
// // 授权参数
// // 创建dns解析记录的参数
// })
// //返回本次创建的dns解析记录这个记录会在删除的时候用到
// return record
} }
async removeRecord(options: RemoveRecordOptions): Promise<any> {
/**
* dns,
* @param options
*/
async removeRecord(options: RemoveRecordOptions<DemoRecord>): Promise<void> {
const { fullRecord, value, record } = options; const { fullRecord, value, record } = options;
this.logger.info('删除域名解析:', fullRecord, value, record); this.logger.info("删除域名解析:", fullRecord, value, record);
//TODO 这里调用删除txt dns解析记录接口 //这里调用删除txt dns解析记录接口
const access = this.access; //请根据实际接口情况调用,例如:
this.logger.debug('access', access);
this.logger.info('删除域名解析成功:', fullRecord, value); // const deleteDnsRecordUrl = "xxx"
// const res = this.http.delete(deleteDnsRecordUrl,{
// // 授权参数
// // 删除dns解析记录的参数
// })
//
this.logger.info("删除域名解析成功:", fullRecord, value);
} }
} }

View File

@ -100,7 +100,7 @@ export class HuaweiDnsProvider extends AbstractDnsProvider{
throw e; throw e;
} }
} }
async removeRecord(options: RemoveRecordOptions): Promise<any> { async removeRecord(options: RemoveRecordOptions<any>): Promise<any> {
const { fullRecord, value, record } = options; const { fullRecord, value, record } = options;
const req: ApiRequestOptions = { const req: ApiRequestOptions = {
url: `${this.dnsEndpoint}/v2/zones/${record.zone_id}/recordsets/${record.id}`, url: `${this.dnsEndpoint}/v2/zones/${record.zone_id}/recordsets/${record.id}`,

View File

@ -91,7 +91,7 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
return ret.record; return ret.record;
} }
async removeRecord(options: RemoveRecordOptions) { async removeRecord(options: RemoveRecordOptions<any>) {
const { fullRecord, value, record } = options; const { fullRecord, value, record } = options;
const domain = await this.matchDomain(fullRecord); const domain = await this.matchDomain(fullRecord);