From c39b1bf823ddc6216bed2049e4c87e6107def08a Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Fri, 4 Apr 2025 20:46:48 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BB=8E=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0dns=E8=8E=B7=E5=8F=96=E8=AE=B0=E5=BD=95=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/acme-client/src/verify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js index de0bbe5e..e6790bff 100644 --- a/packages/core/acme-client/src/verify.js +++ b/packages/core/acme-client/src/verify.js @@ -98,7 +98,7 @@ export async function walkTxtRecord(recordName,deep = 0) { try { /* Default DNS resolver first */ log('从本地DNS服务器获取TXT解析记录'); - const res = await walkDnsChallengeRecord(recordName,null,deep); + const res = await walkDnsChallengeRecord(recordName,dns,deep); if (res && res.length > 0) { for (const item of res) { txtRecords.push(item) From 857589b365c6f709e0ae67914d2f50ce182e6dd6 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Fri, 4 Apr 2025 23:16:25 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=E7=94=B3=E8=AF=B7=E9=80=9F=E5=BA=A6=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=9F=90=E4=BA=9B=E6=83=85=E5=86=B5=E4=B8=8Bletsencry?= =?UTF-8?q?pt=20=E6=A0=A1=E9=AA=8C=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/acme-client/src/auto.js | 227 ++++++++++++-------------- 1 file changed, 105 insertions(+), 122 deletions(-) diff --git a/packages/core/acme-client/src/auto.js b/packages/core/acme-client/src/auto.js index 323e02b5..92791f13 100644 --- a/packages/core/acme-client/src/auto.js +++ b/packages/core/acme-client/src/auto.js @@ -1,10 +1,10 @@ /** * ACME auto helper */ -import { readCsrDomains } from './crypto/index.js'; -import { log } from './logger.js'; -import { wait } from './wait.js'; -import { CancelError } from './error.js'; +import { readCsrDomains } from "./crypto/index.js"; +import { log } from "./logger.js"; +import { wait } from "./wait.js"; +import { CancelError } from "./error.js"; const defaultOpts = { @@ -13,13 +13,13 @@ const defaultOpts = { preferredChain: null, termsOfServiceAgreed: false, skipChallengeVerification: false, - challengePriority: ['http-01', 'dns-01'], + challengePriority: ["http-01", "dns-01"], challengeCreateFn: async () => { - throw new Error('Missing challengeCreateFn()'); + throw new Error("Missing challengeCreateFn()"); }, challengeRemoveFn: async () => { - throw new Error('Missing challengeRemoveFn()'); - }, + throw new Error("Missing challengeRemoveFn()"); + } }; /** @@ -30,7 +30,7 @@ const defaultOpts = { * @returns {Promise} Certificate */ -export default async (client, userOpts) => { +export default async (client, userOpts) => { const opts = { ...defaultOpts, ...userOpts }; const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed }; @@ -49,14 +49,13 @@ export default async (client, userOpts) => { * Register account */ - log('[auto] Checking account'); + log("[auto] Checking account"); try { client.getAccountUrl(); - log('[auto] Account URL already exists, skipping account registration( 证书申请账户已存在,跳过注册 )'); - } - catch (e) { - log('[auto] Registering account (注册证书申请账户)'); + log("[auto] Account URL already exists, skipping account registration( 证书申请账户已存在,跳过注册 )"); + } catch (e) { + log("[auto] Registering account (注册证书申请账户)"); await client.createAccount(accountPayload); } @@ -64,7 +63,7 @@ export default async (client, userOpts) => { * Parse domains from CSR */ - log('[auto] Parsing domains from Certificate Signing Request '); + log("[auto] Parsing domains from Certificate Signing Request "); const { commonName, altNames } = readCsrDomains(opts.csr); const uniqueDomains = Array.from(new Set([commonName].concat(altNames).filter((d) => d))); @@ -74,8 +73,8 @@ export default async (client, userOpts) => { * Place order */ - log('[auto] Placing new certificate order with ACME provider'); - const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: 'dns', value: d })) }; + log("[auto] Placing new certificate order with ACME provider"); + const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: "dns", value: d })) }; const order = await client.createOrder(orderPayload); const authorizations = await client.getAuthorizations(order); @@ -85,82 +84,81 @@ export default async (client, userOpts) => { * Resolve and satisfy challenges */ - log('[auto] Resolving and satisfying authorization challenges'); + log("[auto] Resolving and satisfying authorization challenges"); const clearTasks = []; + const localVerifyTasks = []; + const completeChallengeTasks = []; const challengeFunc = async (authz) => { const d = authz.identifier.value; let challengeCompleted = false; /* Skip authz that already has valid status */ - if (authz.status === 'valid') { + if (authz.status === "valid") { log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`); return; } const keyAuthorizationGetter = async (challenge) => { return await client.getChallengeKeyAuthorization(challenge); - } + }; - try { - log(`[auto] [${d}] Trigger challengeCreateFn()`); + async function deactivateAuth(e) { + log(`[auto] [${d}] Unable to complete challenge: ${e.message}`); try { - const { recordReq, recordRes, dnsProvider,challenge ,keyAuthorization} = await opts.challengeCreateFn(authz, keyAuthorizationGetter); - clearTasks.push(async () => { - /* Trigger challengeRemoveFn(), suppress errors */ - log(`[auto] [${d}] Trigger challengeRemoveFn()`); - try { - await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider); - } - catch (e) { - log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`); - } - }); - // throw new Error('测试异常'); - /* Challenge verification */ - if (opts.skipChallengeVerification === true) { - log(`[auto] [${d}] 跳过本地验证(skipChallengeVerification=true),等待 60s`); - await wait(60 * 1000); - } - else { - log(`[auto] [${d}] 开始本地验证, type = ${challenge.type}`); - try { - await client.verifyChallenge(authz, challenge); - } - catch (e) { - log(`[auto] [${d}] 本地验证失败,尝试请求ACME提供商获取状态: ${e.message}`); - } - } - /* Complete challenge and wait for valid status */ - log(`[auto] [${d}] 请求ACME提供商完成验证,等待返回valid状态`); - await client.completeChallenge(challenge); - challengeCompleted = true; - - await client.waitForValidStatus(challenge); - } - catch (e) { - log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`); - throw e; + log(`[auto] [${d}] Deactivating failed authorization`); + await client.deactivateAuthorization(authz); + } catch (f) { + /* Suppress deactivateAuthorization() errors */ + log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`); } } - catch (e) { - /* Deactivate pending authz when unable to complete challenge */ - if (!challengeCompleted) { - log(`[auto] [${d}] Unable to complete challenge: ${e.message}`); + log(`[auto] [${d}] Trigger challengeCreateFn()`); + try { + const { recordReq, recordRes, dnsProvider, challenge, keyAuthorization } = await opts.challengeCreateFn(authz, keyAuthorizationGetter); + clearTasks.push(async () => { + /* Trigger challengeRemoveFn(), suppress errors */ + log(`[auto] [${d}] Trigger challengeRemoveFn()`); try { - log(`[auto] [${d}] Deactivating failed authorization`); - await client.deactivateAuthorization(authz); + await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider); + } catch (e) { + log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`); } - catch (f) { - /* Suppress deactivateAuthorization() errors */ - log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`); - } - } + }); + localVerifyTasks.push(async () => { + /* Challenge verification */ + log(`[auto] [${d}] 开始本地验证, type = ${challenge.type}`); + try { + await client.verifyChallenge(authz, challenge); + } catch (e) { + log(`[auto] [${d}] 本地验证失败,尝试请求ACME提供商获取状态: ${e.message}`); + } + }); + + completeChallengeTasks.push(async () => { + /* Complete challenge and wait for valid status */ + log(`[auto] [${d}] 请求ACME提供商完成验证`); + try{ + await client.completeChallenge(challenge); + }catch (e) { + await deactivateAuth(e); + throw e; + } + challengeCompleted = true; + log(`[auto] [${d}] 等待返回valid状态`); + await client.waitForValidStatus(challenge); + }); + + + } catch (e) { + log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`); + await deactivateAuth(e); throw e; } + }; const domainSets = []; @@ -168,7 +166,7 @@ export default async (client, userOpts) => { const d = authz.identifier.value; log(`authorization:domain = ${d}, value = ${JSON.stringify(authz)}`); - if (authz.status === 'valid') { + if (authz.status === "valid") { log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`); return; } @@ -192,8 +190,9 @@ export default async (client, userOpts) => { const allChallengePromises = []; // eslint-disable-next-line no-restricted-syntax + const challengePromises = []; + allChallengePromises.push(challengePromises); for (const domainSet of domainSets) { - const challengePromises = []; // eslint-disable-next-line guard-for-in,no-restricted-syntax for (const domain in domainSet) { const authz = domainSet[domain]; @@ -202,12 +201,11 @@ export default async (client, userOpts) => { await challengeFunc(authz); }); } - allChallengePromises.push(challengePromises); } log(`[auto] challengeGroups:${allChallengePromises.length}`); - function runAllPromise(tasks) { + async function runAllPromise(tasks) { let promise = Promise.resolve(); tasks.forEach((task) => { promise = promise.then(task); @@ -215,73 +213,58 @@ export default async (client, userOpts) => { return promise; } - async function runPromisePa(tasks) { + async function runPromisePa(tasks, waitTime = 5000) { const results = []; // eslint-disable-next-line no-await-in-loop,no-restricted-syntax for (const task of tasks) { results.push(task()); // eslint-disable-next-line no-await-in-loop - await wait(10000); + await wait(waitTime); } return Promise.all(results); } - try { - log(`开始challenge,共${allChallengePromises.length}组`); - let i = 0; - // eslint-disable-next-line no-restricted-syntax - for (const challengePromises of allChallengePromises) { - i += 1; - log(`开始第${i}组`); - if (opts.signal && opts.signal.aborted) { - throw new CancelError('用户取消'); + log(`开始challenge,共${allChallengePromises.length}组`); + let i = 0; + // eslint-disable-next-line no-restricted-syntax + for (const challengePromises of allChallengePromises) { + i += 1; + log(`开始第${i}组`); + if (opts.signal && opts.signal.aborted) { + throw new CancelError("用户取消"); + } + + try { + // eslint-disable-next-line no-await-in-loop + await runPromisePa(challengePromises); + if (opts.skipChallengeVerification === true) { + log(`跳过本地验证(skipChallengeVerification=true),等待 60s`); + await wait(60 * 1000); + } else { + await runPromisePa(localVerifyTasks, 1000); } - try { - // eslint-disable-next-line no-await-in-loop - await runPromisePa(challengePromises); - } - catch (e) { - log(`证书申请失败${e.message}`); - throw e; - } - finally { - if (client.opts.sslProvider !== 'google') { - // letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除 - // zerossl 此方式测试无问题 - log(`清理challenge痕迹,length:${clearTasks.length}`); - try { - // eslint-disable-next-line no-await-in-loop - await runAllPromise(clearTasks); - } - catch (e) { - log('清理challenge失败'); - log(e); - } - } - } - } - } - finally { - if (client.opts.sslProvider === 'google') { - // google 相同的域名txt记录是一样的,不能提前删除,否则校验失败,报错如下 - // Error: The TXT record retrieved from _acme-challenge.bbc.handsfree.work. - // at the time the challenge was validated did not contain JshHVu7dt_DT6uYILWhokHefFVad2Q6Mw1L-fNZFcq8 - // (the base64url-encoded SHA-256 digest of RlJZNBR0LWnxNK_xd2zqtYVvCiNJOKJ3J1NmCjU_9BjaUJgL3k-qSpIhQ-uF4FBS.NRyqT8fRiq6THzzrvkgzgR5Xai2LsA2SyGLAq_wT3qc). - // See https://tools.ietf.org/html/rfc8555#section-8.4 for more information. + log("开始向提供商请求挑战验证"); + await runPromisePa(completeChallengeTasks, 1000); + } catch (e) { + log(`证书申请失败${e.message}`); + throw e; + } finally { + // letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除 + // zerossl 此方式测试无问题 log(`清理challenge痕迹,length:${clearTasks.length}`); try { - // eslint-disable-next-line no-await-in-loop + // eslint-disable-next-line no-await-in-loop await runAllPromise(clearTasks); - } - catch (e) { - log('清理challenge失败'); + } catch (e) { + log("清理challenge失败"); log(e); } } } - log('challenge结束'); + + log("challenge结束"); // log('[auto] Waiting for challenge valid status'); // await Promise.all(challengePromises); @@ -289,7 +272,7 @@ export default async (client, userOpts) => { * Finalize order and download certificate */ - log('[auto] Finalizing order and downloading certificate'); + log("[auto] Finalizing order and downloading certificate"); const finalized = await client.finalizeOrder(order, opts.csr); const res = await client.getCertificate(finalized, opts.preferredChain); return res; From 0948c5bc691d2ee6eb47c72a85da1b7453361878 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sat, 5 Apr 2025 00:24:57 +0800 Subject: [PATCH 3/5] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=8D=8E?= =?UTF-8?q?=E4=B8=BAdns=E8=A7=A3=E6=9E=90=E8=AE=B0=E5=BD=95=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E5=92=8C=E5=88=A0=E9=99=A4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/acme-client/src/auto.js | 4 +- packages/core/acme-client/src/client.js | 12 +- packages/core/acme-client/src/verify.js | 4 +- .../ui/certd-client/src/style/antdv4.less | 6 + .../dns-provider/huawei-dns-provider.ts | 152 ++++++++++++------ 5 files changed, 124 insertions(+), 54 deletions(-) diff --git a/packages/core/acme-client/src/auto.js b/packages/core/acme-client/src/auto.js index 92791f13..86c82192 100644 --- a/packages/core/acme-client/src/auto.js +++ b/packages/core/acme-client/src/auto.js @@ -149,7 +149,7 @@ export default async (client, userOpts) => { } challengeCompleted = true; log(`[auto] [${d}] 等待返回valid状态`); - await client.waitForValidStatus(challenge); + await client.waitForValidStatus(challenge,d); }); @@ -242,6 +242,8 @@ export default async (client, userOpts) => { await wait(60 * 1000); } else { await runPromisePa(localVerifyTasks, 1000); + log("本地校验完成,等待30s") + await wait(30 * 1000) } log("开始向提供商请求挑战验证"); diff --git a/packages/core/acme-client/src/client.js b/packages/core/acme-client/src/client.js index 3b42aa79..9f8b81e0 100644 --- a/packages/core/acme-client/src/client.js +++ b/packages/core/acme-client/src/client.js @@ -554,9 +554,9 @@ class AcmeClient { * ``` */ - async waitForValidStatus(item) { + async waitForValidStatus(item,d) { if (!item.url) { - throw new Error('Unable to verify status of item, URL not found'); + throw new Error(`[${d}] Unable to verify status of item, URL not found`); } const verifyFn = async (abort) => { @@ -568,23 +568,23 @@ class AcmeClient { const resp = await this.api.apiRequest(item.url, null, [200]); /* Verify status */ - log(`Item has status(挑战状态): ${resp.data.status}`); + log(`[${d}] Item has status(挑战状态): ${resp.data.status}`); if (invalidStates.includes(resp.data.status)) { abort(); throw new Error(util.formatResponseError(resp)); } else if (pendingStates.includes(resp.data.status)) { - throw new Error('Operation is pending or processing(当前仍然在等待状态)'); + throw new Error(`[${d}] Operation is pending or processing(当前仍然在等待状态)`); } else if (validStates.includes(resp.data.status)) { return resp.data; } - throw new Error(`Unexpected item status: ${resp.data.status}`); + throw new Error(`[${d}] Unexpected item status: ${resp.data.status}`); }; - log(`Waiting for valid status (等待valid状态): ${item.url}`, this.backoffOpts); + log(`[${d}] Waiting for valid status (等待valid状态): ${item.url}`, this.backoffOpts); return util.retry(verifyFn, this.backoffOpts); } diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js index e6790bff..2334331a 100644 --- a/packages/core/acme-client/src/verify.js +++ b/packages/core/acme-client/src/verify.js @@ -147,12 +147,12 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = ' let recordValues = await walkTxtRecord(recordName); //去重 recordValues = [...new Set(recordValues)]; - log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录`); + log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`); if (!recordValues.length || !recordValues.includes(keyAuthorization)) { throw new Error(`没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`); } - log(`关键授权匹配成功(${challenge.type}/${recordName}),校验成功, ACME challenge verified`); + log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`); return true; } diff --git a/packages/ui/certd-client/src/style/antdv4.less b/packages/ui/certd-client/src/style/antdv4.less index 6eb93abc..40c79d3b 100644 --- a/packages/ui/certd-client/src/style/antdv4.less +++ b/packages/ui/certd-client/src/style/antdv4.less @@ -60,3 +60,9 @@ footer{ background-color: hsl(var(--card)) !important; } + + +.ant-select-multiple .ant-select-selection-item-remove{ + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/packages/ui/certd-server/src/plugins/plugin-huawei/dns-provider/huawei-dns-provider.ts b/packages/ui/certd-server/src/plugins/plugin-huawei/dns-provider/huawei-dns-provider.ts index ddee06b5..e5d6404a 100644 --- a/packages/ui/certd-server/src/plugins/plugin-huawei/dns-provider/huawei-dns-provider.ts +++ b/packages/ui/certd-server/src/plugins/plugin-huawei/dns-provider/huawei-dns-provider.ts @@ -1,37 +1,38 @@ -import * as _ from 'lodash-es'; -import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert'; -import { Autowire } from '@certd/pipeline'; +import * as _ from "lodash-es"; +import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert"; +import { Autowire } from "@certd/pipeline"; -import { HuaweiAccess } from '../access/index.js'; -import { ApiRequestOptions, HuaweiYunClient } from '@certd/lib-huawei'; +import { HuaweiAccess } from "../access/index.js"; +import { ApiRequestOptions, HuaweiYunClient } from "@certd/lib-huawei"; export type SearchRecordOptions = { zoneId: string; } & CreateRecordOptions; @IsDnsProvider({ - name: 'huawei', - title: '华为云', - desc: '华为云DNS解析提供商', - accessType: 'huawei', - icon: 'svg:icon-huawei', + name: "huawei", + title: "华为云", + desc: "华为云DNS解析提供商", + accessType: "huawei", + icon: "svg:icon-huawei" }) export class HuaweiDnsProvider extends AbstractDnsProvider { client!: HuaweiYunClient; @Autowire() access!: HuaweiAccess; - domainEndpoint = 'https://domains-external.myhuaweicloud.com'; - dnsEndpoint = 'https://dns.cn-south-1.myhuaweicloud.com'; + domainEndpoint = "https://domains-external.myhuaweicloud.com"; + dnsEndpoint = "https://dns.cn-south-1.myhuaweicloud.com"; + async onInstance() { const access: any = this.access; - this.client = new HuaweiYunClient(access,this.logger); + this.client = new HuaweiYunClient(access, this.logger); } async getDomainList() { const url = `${this.dnsEndpoint}/v2/zones`; const ret = await this.client.request({ url, - method: 'GET', + method: "GET" }); return ret.zones; } @@ -40,21 +41,21 @@ export class HuaweiDnsProvider extends AbstractDnsProvider { const zoneList = await this.getDomainList(); let zoneRecord = null; for (const item of zoneList) { - if (_.endsWith(dnsRecord + '.', item.name)) { + if (_.endsWith(dnsRecord + ".", item.name)) { zoneRecord = item; break; } } if (!zoneRecord) { - throw new Error('can not find Domain ,' + dnsRecord); + throw new Error("can not find Domain ," + dnsRecord); } return zoneRecord; } async searchRecord(options: SearchRecordOptions): Promise { const req: ApiRequestOptions = { - url: `${this.dnsEndpoint}/v2/zones/${options.zoneId}/recordsets?name=${options.fullRecord}.`, - method: 'GET', + url: `${this.dnsEndpoint}/v2/zones/${options.zoneId}/recordsets?search_mode=equal&name=${options.fullRecord}.&type=${options.type}`, + method: "GET" }; const ret = await this.client.request(req); return ret.recordsets; @@ -62,59 +63,120 @@ export class HuaweiDnsProvider extends AbstractDnsProvider { async createRecord(options: CreateRecordOptions): Promise { const { fullRecord, value, type } = options; - this.logger.info('添加域名解析:', fullRecord, value); + this.logger.info("添加域名解析:", fullRecord, value); + this.logger.info("查询是否有重复记录"); const zoneRecord = await this.matchDomain(fullRecord); const zoneId = zoneRecord.id; const records: any = await this.searchRecord({ zoneId, - ...options, + ...options }); + this.logger.info(`查询${options.type}数量:${records.length}`); + let found = null; + const hwRecordValue = `"${value}"`; if (records && records.length > 0) { - for (const record of records) { - await this.removeRecord({ - recordRes: record, - recordReq: options, - }); + found = records[0]; + this.logger.info(`记录:${found.id},${found.records}`); + if (found.records.includes(hwRecordValue)) { + // this.logger.info(`删除重复记录:${record.id}`) + // await this.removeRecord({ + // recordRes: record, + // recordReq: options, + // }); + this.logger.info(`无需重复添加:${found.records}`); + return found; } } - try { + if (found) { + //修改 const req: ApiRequestOptions = { - url: `${this.dnsEndpoint}/v2/zones/${zoneId}/recordsets`, - method: 'POST', + url: `${this.dnsEndpoint}/v2/zones/${zoneId}/recordsets/${found.id}`, + method: "PUT", data: { - name: fullRecord + '.', + name: fullRecord + ".", type, - records: [`"${value}"`], - }, + records: [hwRecordValue, ...found.records] + } }; const ret = await this.client.request(req); - this.logger.info('添加域名解析成功:', value, ret); + this.logger.info("添加域名解析成功:", value, ret); return ret; - } catch (e: any) { - if (e.code === 'DNS.0312') { - return; + } else { + //创建 + try { + const req: ApiRequestOptions = { + url: `${this.dnsEndpoint}/v2/zones/${zoneId}/recordsets`, + method: "POST", + data: { + name: fullRecord + ".", + type, + records: [hwRecordValue] + } + }; + const ret = await this.client.request(req); + this.logger.info("添加域名解析成功:", value, ret); + return ret; + } catch (e: any) { + if (e.code === "DNS.0312") { + return; + } + this.logger.info("添加域名解析出错", e); + throw e; } - this.logger.info('添加域名解析出错', e); - throw e; } } + async removeRecord(options: RemoveRecordOptions): Promise { const { fullRecord, value } = options.recordReq; const record = options.recordRes; if (!record) { - this.logger.info('解析记录recordId为空,不执行删除', fullRecord, value); + this.logger.info("解析记录recordId为空,不执行删除", fullRecord, value); return; } - const req: ApiRequestOptions = { - url: `${this.dnsEndpoint}/v2/zones/${record.zone_id}/recordsets/${record.id}`, - method: 'DELETE', - }; + const zoneId = record.zone_id; - const ret = await this.client.request(req); - this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId); - return ret.RecordId; + //查询原来的记录 + const records: any = await this.searchRecord({ + zoneId, + ...options.recordReq + }); + const hwRecordValue = `"${value}"`; + + if (records && records.length > 0) { + //找到记录 + const found = records[0]; + if (found.records.includes(hwRecordValue)) { + if (found.records.length > 1) { + //修改 + + const req: ApiRequestOptions = { + url: `${this.dnsEndpoint}/v2/zones/${zoneId}/recordsets/${found.id}`, + method: "PUT", + data: { + name: fullRecord + ".", + type: found.type, + records: found.records.filter((item: string) => item !== hwRecordValue) + } + }; + const ret = await this.client.request(req); + this.logger.info("修改域名解析成功[put]:", value, ret); + } else { + //删除 + const req: ApiRequestOptions = { + url: `${this.dnsEndpoint}/v2/zones/${zoneId}/recordsets/${found.id}`, + method: "DELETE" + }; + const ret = await this.client.request(req); + this.logger.info("删除域名解析成功[delete]:", fullRecord, value, ret.RecordId); + } + }else{ + this.logger.info("没有找到records无需删除", fullRecord, value,found); + } + }else{ + this.logger.info("删除域名解析失败,没有找到解析记录", fullRecord, value); + } } } From 8449f8580da90c1f6b5d02d07c3236ebaf6cf161 Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sat, 5 Apr 2025 00:46:56 +0800 Subject: [PATCH 4/5] =?UTF-8?q?perf:=20=E5=8F=88=E6=8B=8D=E4=BA=91?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BA=91=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/plugins/plugin-upyun/client.ts | 77 ++++++++ .../src/plugins/plugin-upyun/index.ts | 1 + .../plugins/plugin-depoy-to-cdn.ts | 183 +++++++----------- 3 files changed, 150 insertions(+), 111 deletions(-) create mode 100644 packages/ui/certd-server/src/plugins/plugin-upyun/client.ts diff --git a/packages/ui/certd-server/src/plugins/plugin-upyun/client.ts b/packages/ui/certd-server/src/plugins/plugin-upyun/client.ts new file mode 100644 index 00000000..500af726 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-upyun/client.ts @@ -0,0 +1,77 @@ +import { UpyunAccess } from "./access.js"; +import { HttpClient, ILogger } from "@certd/basic"; +import { CertInfo } from "@certd/plugin-cert"; + +export type UpyunClientOptions = { + access: UpyunAccess + logger: ILogger; + http: HttpClient +} + +export class UpyunClient { + opts: UpyunClientOptions; + + constructor(opts: UpyunClientOptions) { + this.opts = opts; + } + + async uploadCert(cookie: string,cert:CertInfo) { + // https://console.upyun.com/api/https/certificate/ + const res = await this.doRequest({ + cookie: cookie, + url: "https://console.upyun.com/api/https/certificate/", + method: "POST", + data: { + certificate: cert.crt, + private_key: cert.key + } + }); + + return res.data.result.certificate_id; + } + + async getLoginToken() { + const access = this.opts.access + const http = this.opts.http; + const res = await http.request({ + url: "https://console.upyun.com/accounts/signin/", + method: "POST", + data: { + username: access.username, + password: access.password + }, + logRes: false, + returnResponse: true + }); + if (res.data?.errors?.length > 0) { + throw new Error(JSON.stringify(res.data.msg)); + } + const cookie = res.headers["set-cookie"]; + return cookie; + } + + async doRequest(req: { + cookie: string, + url: string, + method: string, + data: any + }) { + + const res = await this.opts.http.request({ + url: req.url, + method: req.method, + data: req.data, + headers: { + Cookie: req.cookie + } + }); + if (res.msg.errors.length > 0) { + throw new Error(JSON.stringify(res.msg)); + } + if(res.data?.error_code){ + throw new Error(res.data?.message); + } + return res; + } + +} diff --git a/packages/ui/certd-server/src/plugins/plugin-upyun/index.ts b/packages/ui/certd-server/src/plugins/plugin-upyun/index.ts index fdad254f..a5edb84e 100644 --- a/packages/ui/certd-server/src/plugins/plugin-upyun/index.ts +++ b/packages/ui/certd-server/src/plugins/plugin-upyun/index.ts @@ -1,2 +1,3 @@ export * from './plugins/index.js'; export * from './access.js'; +export * from './client.js'; diff --git a/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts b/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts index 17314c31..1442acea 100644 --- a/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts +++ b/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts @@ -1,35 +1,38 @@ -import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; -import { CertInfo } from '@certd/plugin-cert'; -import { AbstractPlusTaskPlugin } from '@certd/plugin-plus'; -import { UpyunAccess } from '../access.js'; -import {createCertDomainGetterInputDefine, createRemoteSelectInputDefine} from '@certd/plugin-lib'; -import { CertApplyPluginNames} from '@certd/plugin-cert'; -import {optionsUtils} from "@certd/basic/dist/utils/util.options.js"; +import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { CertInfo } from "@certd/plugin-cert"; +import { AbstractPlusTaskPlugin } from "@certd/plugin-plus"; +import { UpyunAccess } from "../access.js"; +import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; +import { CertApplyPluginNames } from "@certd/plugin-cert"; +import { optionsUtils } from "@certd/basic/dist/utils/util.options.js"; +import { UpyunClient } from "../client.js"; + @IsTaskPlugin({ //命名规范,插件名称+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 - name: 'UpyunDeployToCdn', - title: '又拍云-部署证书到CDN', - icon: 'svg:icon-upyun', + name: "UpyunDeployToCdn", + title: "又拍云-部署证书到CDN", + icon: "svg:icon-upyun", + desc:"支持又拍云CDN,云存储", //插件分组 group: pluginGroups.cdn.key, needPlus: true, default: { //默认值配置照抄即可 strategy: { - runStrategy: RunStrategy.SkipWhenSucceed, - }, - }, + runStrategy: RunStrategy.SkipWhenSucceed + } + } }) //类名规范,跟上面插件名称(name)一致 export class UpyunDeployToCdn extends AbstractPlusTaskPlugin { //证书选择,此项必须要有 @TaskInput({ - title: '域名证书', - helper: '请选择前置任务输出的域名证书', + title: "域名证书", + helper: "请选择前置任务输出的域名证书", component: { - name: 'output-selector', - from: [...CertApplyPluginNames], - }, + name: "output-selector", + from: [...CertApplyPluginNames] + } // required: true, // 必填 }) cert!: CertInfo; @@ -38,142 +41,99 @@ export class UpyunDeployToCdn extends AbstractPlusTaskPlugin { certDomains!: string[]; //授权选择框 @TaskInput({ - title: 'Upyun授权', + title: "Upyun授权", component: { - name: 'access-selector', - type: 'upyun', //固定授权类型 + name: "access-selector", + type: "upyun" //固定授权类型 }, - required: true, //必填 + required: true //必填 }) accessId!: string; // @TaskInput( createRemoteSelectInputDefine({ - title: 'CDN加速域名', - helper: '选择CDN加速域名,可以手动输入', - typeName: 'UpyunDeployToCdn', + title: "加速域名", + helper: "选择加速域名,可以手动输入", + typeName: "UpyunDeployToCdn", action: UpyunDeployToCdn.prototype.onGetCdnList.name, - watches: ['accessId'], + watches: ["accessId"] }) ) cdnList!: string[]; //插件实例化时执行的方法 - async onInstance() {} + async onInstance() { + } //插件执行方法 async execute(): Promise { + const access = await this.accessService.getById(this.accessId); - const cookie = await this.getLoginToken(); + const upyunClient = new UpyunClient({ + access, + logger: this.logger, + http: this.ctx.http + }); + const cookie = await upyunClient.getLoginToken(); this.logger.info(`登录成功`); - const certId = await this.uploadCert(cookie); + const certId = await upyunClient.uploadCert(cookie, this.cert); this.logger.info(`上传证书成功:${certId}`); for (const item of this.cdnList) { this.logger.info(`开始部署证书:${item}`); - const res = await this.doRequest({ - cookie:cookie, - url: 'https://console.upyun.com/api/https/migrate/domain', - method: 'POST', - data:{ + const res = await upyunClient.doRequest({ + cookie: cookie, + url: "https://console.upyun.com/api/https/migrate/domain", + method: "POST", + data: { crt_id: certId, - domain_name : item + domain_name: item } - }) + }); this.logger.info(`部署成功:${JSON.stringify(res)}`); } - this.logger.info('部署成功'); + this.logger.info("部署成功"); } - async uploadCert(cookie:string){ - // https://console.upyun.com/api/https/certificate/ - const res = await this.doRequest({ - cookie:cookie, - url: 'https://console.upyun.com/api/https/certificate/', - method: 'POST', - data:{ - certificate: this.cert.crt, - private_key: this.cert.key - } - }) - - return res.data.result.certificate_id - } - - async getLoginToken(){ - const access = await this.accessService.getById(this.accessId) - const res = await this.http.request({ - url: 'https://console.upyun.com/accounts/signin/', - method: 'POST', - data:{ - username: access.username, - password: access.password - }, - logRes:false, - returnResponse:true - }); - if (res.data?.errors?.length>0) { - throw new Error(JSON.stringify(res.data.msg)); - } - const cookie = res.headers['set-cookie']; - return cookie; - } - - async doRequest(req:{ - cookie:string, - url:string, - method:string, - data:any - }){ - - const res = await this.http.request({ - url: req.url, - method: req.method, - data:req.data, - headers:{ - Cookie: req.cookie - } - }) - if (res.msg.errors.length>0) { - throw new Error(JSON.stringify(res.msg)); - } - return res - } async onGetCdnList() { - if(!this.accessId){ - throw new Error('accessId不能为空'); + if (!this.accessId) { + throw new Error("accessId不能为空"); } + const access = await this.accessService.getById(this.accessId); - const cookie = await this.getLoginToken(); + const upyunClient = new UpyunClient({ + access, + logger: this.logger, + http: this.ctx.http + }); + const cookie = await upyunClient.getLoginToken(); const req = { cookie, - url: 'https://console.upyun.com/api/v2/buckets/?bucket_name=&with_domains=true&business_type=file&perPage=100&page=1&tag=all&state=all&type=ucdn&security_cdn=false', - method: 'GET', - data:{} - } - const res = await this.doRequest(req); + url: "https://console.upyun.com/api/account/domains/?limit=15&business_type=file&security_cdn=false&websocket=false&key=&domain=", + method: "GET", + data: {} + }; + const res = await upyunClient.doRequest(req); - const buckets = res.data?.buckets; - if(!buckets || buckets.length === 0){ - throw new Error('没有找到CDN加速域名'); + const domains = res.data?.domains; + if (!domains || domains.length === 0) { + throw new Error("没有找到加速域名"); } - const list= [] - for (const item of buckets) { - for (const domain of item.domains) { - list.push({ - domain:domain.domain, - bucket:item.bucket_name - }); - } + const list = []; + for (const domain of domains) { + list.push({ + domain: domain.domain, + bucket: domain.bucket_name + }); } const options = list.map((item: any) => { return { value: item.domain, label: `${item.domain}<${item.bucket}>`, - domain: item.domain, + domain: item.domain }; }); return optionsUtils.buildGroupOptions(options, this.certDomains); @@ -181,5 +141,6 @@ export class UpyunDeployToCdn extends AbstractPlusTaskPlugin { } } + //实例化一下,注册插件 new UpyunDeployToCdn(); From 9339b78f801d193472c0af25749e8e7a27ffb7af Mon Sep 17 00:00:00 2001 From: xiaojunnuo Date: Sat, 5 Apr 2025 00:47:34 +0800 Subject: [PATCH 5/5] =?UTF-8?q?perf:=20=E5=8F=88=E6=8B=8D=E4=BA=91?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BA=91=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts b/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts index 1442acea..8818e266 100644 --- a/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts +++ b/packages/ui/certd-server/src/plugins/plugin-upyun/plugins/plugin-depoy-to-cdn.ts @@ -10,9 +10,9 @@ import { UpyunClient } from "../client.js"; @IsTaskPlugin({ //命名规范,插件名称+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 name: "UpyunDeployToCdn", - title: "又拍云-部署证书到CDN", + title: "又拍云-部署证书到CDN/USS", icon: "svg:icon-upyun", - desc:"支持又拍云CDN,云存储", + desc:"支持又拍云CDN,又拍云云存储USS", //插件分组 group: pluginGroups.cdn.key, needPlus: true,