diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 45d2fa67..1ccda44f 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -59,7 +59,7 @@ jobs: password: ${{ secrets.aliyun_cs_password }} - name: Build and push - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6 with: platforms: linux/amd64,linux/arm64 push: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 30411db1..b39c7f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) + +### Bug Fixes + +* 部署到腾讯云cdn选择证书任务步骤限制只能选证书 ([3345c14](https://github.com/certd/certd/commit/3345c145b802170f75a098a35d0c4b8312efcd17)) +* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) +* 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/certd/certd/commit/17ead547aab25333603980304aa3aad3db1f73d5)) +* 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/certd/certd/commit/95122e28609333f4df55c266e5434897954c0fb3)) +* 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5)) +* 修复重置密码参数配置后无效的bug ([e358a88](https://github.com/certd/certd/commit/e358a8869696578687306e4cd0dcda53f898fe13)) +* 修复ssh无法连接成功,无法执行命令的bug ([41b9837](https://github.com/certd/certd/commit/41b9837582323fb400ef8525ce65e8b37ad4b36f)) + +### Features + +* 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473)) +* 支持google证书申请(需要使用代理) ([a593056](https://github.com/certd/certd/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe)) + +### Performance Improvements + +* 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd)) +* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b)) +* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) +* 优化dnspod的token id 说明 ([790bf11](https://github.com/certd/certd/commit/790bf11af06d6264ef74bc1bb919661f0354239a)) +* email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f)) + ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) ### Bug Fixes diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..bfab44e5 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,30 @@ +# Certd Open Source License + +- This project is licensed under the **GNU Affero General Public License (AGPL)** with the following additional terms. +- 本项目遵循 GNU Affero General Public License(AGPL),并附加以下条款。 + +## 1. License Terms ( 许可证条款 ) + +1. **Freedom to Use** (自由使用) + - You are free to use, copy, modify, and distribute the source code of this project for personal or organizational use, provided that you comply with the terms of this license. + - 您可以自由使用、复制、修改和分发本项目的源代码,前提是您遵循本许可证的条款。 + +2. **Modification for Personal Use** (个人使用的修改) + - Individuals and companies are allowed to modify the project according to their needs for non-commercial purposes. However, modifications to the logo, copyright information, or any code related to licensing are strictly prohibited. + - 个人和公司允许根据自身需求对本项目进行修改以供非商业用途。但任何对logo、版权信息或与许可相关代码的修改都是严格禁止的。 + +3. **Commercial Authorization** (商业授权) + - If you wish to make any form of monetary gain from this project, you must first obtain commercial authorization from the original author. Users should contact the author directly to negotiate the relevant licensing terms. + - 如果您希望从本项目获得任何形式的经济收益,您必须首先从原作者处获得商业授权,用户应直接与作者联系,以协商相关许可条款。 + +4. **Retention of Rights** (保留权利) + - All rights, title, and interest in the project remain with the original author. + - 本项目的所有权利、标题和利益仍归原作者所有。 + +## 2. As a contributor ( 作为贡献者 ) + - you should agree that your contributed code: + - 您应同意您贡献的代码: + 1. - The original author can adjust the open-source agreement to be more strict or relaxed. + - 原作者可以调整开源协议以使其更严格或更宽松。 + 2. - Can be used for commercial purposes. + - 可用于商业用途。 diff --git a/README.md b/README.md index ac24fae6..095fb867 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# CertD +# Certd -CertD 是一个免费全自动申请和自动部署更新SSL证书的工具。 -后缀D取自linux守护进程的命名风格,意为证书守护进程。 +Certd 是一个免费全自动申请和自动部署更新SSL证书的工具。 +后缀d取自linux守护进程的命名风格,意为证书守护进程。 关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签 @@ -147,6 +147,7 @@ http://your_server_ip:7001 * [Cloudflare](./doc/cf/cf.md) * [腾讯云](./doc/tencent/tencent.md) * [windows主机](./doc/host/host.md) +* [google证书](./doc/google/google.md) ## 八、问题处理 @@ -185,26 +186,37 @@ docker compose up -d

## 十、捐赠 -媳妇儿说:“一天到晚搞开源,也不管管老婆孩子!😡😡😡” -拜托各位捐赠支持一下,让媳妇儿开心开心,我也能有更多时间进行开源项目,感谢🙏🙏🙏 -

- -

+支持开源,为爱发电,我已入驻爱发电 +https://afdian.com/a/greper + +发电权益: +1. 可加入发电专属群(先加我好友,发送发电截图,我拉你进群) +2. 你的需求优先实现 +3. 可以获得作者一对一技术支持 +4. 更多权益陆续增加中... ## 十一、贡献代码 -[贡献插件教程](./plugin.md) +1. [贡献插件教程](./plugin.md) +2. 作为贡献者,代表您同意您贡献的代码如下许可: + 1. 可以调整开源协议以使其更严格或更宽松。 + 2. 可以用于商业用途。 +## 十二、 开源许可 +* 本项目遵循 GNU Affero General Public License(AGPL)开源协议。 +* 允许个人和公司使用、复制、修改和分发本项目,禁止任何形式的商业用途 +* 未获得商业授权情况下,禁止任何对logo、版权信息及授权许可相关代码的修改。 +* 如需商业授权,请联系作者。 -## 十二、我的其他项目(求Star) +## 十三、我的其他项目(求Star) * [袖手GPT](https://ai.handsfree.work/) ChatGPT,国内可用,无需FQ,每日免费额度 * [fast-crud](https://gitee.com/fast-crud/fast-crud/) 基于vue3的crud快速开发框架 * [dev-sidecar](https://github.com/docmirror/dev-sidecar/) 直连访问github工具,无需FQ,解决github无法访问的问题 -## 十三、更新日志 +## 十四、更新日志 更新日志:[CHANGELOG](./CHANGELOG.md) diff --git a/build.trigger b/build.trigger index 8bec97a6..d00491fd 100644 --- a/build.trigger +++ b/build.trigger @@ -1 +1 @@ -11:39 +1 diff --git a/doc/google/google.md b/doc/google/google.md new file mode 100644 index 00000000..3cf7e3c8 --- /dev/null +++ b/doc/google/google.md @@ -0,0 +1,37 @@ +# google证书申请教程 + +## 1、启用API +打开如下链接,启用 API + +https://console.cloud.google.com/apis/library/publicca.googleapis.com + +打开该链接后点击“启用”,随后等待右侧出现“API已启用”则可以关闭该页。 + +## 2、 申请Key +随后打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。 + +等待分配完成后在 Shell 窗口内输入如下命令: + +```shell +gcloud beta publicca external-account-keys create +``` +此时会弹出“为 Cloud Shell 提供授权”,点击授权即可。 + +执行完成后会返回类似如下输出;注意不要在没有收到 Google 的邮件时执行该命令,会返回命令不存在。 + +```shell +Created an external account key +[b64MacKey: xxxxxxxxxxxxx +keyId: xxxxxxxxx] +``` +记录以上信息备用 + + +## 3、 创建证书流水线 +选择证书提供商为google, 开启使用代理 + +## 4、 将key信息作为EAB授权信息 +google证书需要EAB授权, 使用第二步中的 keyId 和 b64MacKey 信息创建一条EAB授权记录 + +## 5、 其他就跟正常申请证书一样了 + diff --git a/docker/run/docker-compose.yaml b/docker/run/docker-compose.yaml index 0d7aa056..3ffe9d7f 100644 --- a/docker/run/docker-compose.yaml +++ b/docker/run/docker-compose.yaml @@ -15,11 +15,12 @@ services: # 如果出现getaddrinfo ENOTFOUND等错误,可以尝试修改或注释dns配置 - 223.5.5.5 - 223.6.6.6 - - 8.8.8.8 - - 8.8.4.4 + # ↓↓↓↓ ----------------------------------------------------------如果你服务器部署在国外,可以用8.8.8.8替换上面的dns【可选】 +# - 8.8.8.8 +# - 8.8.4.4 environment: # 环境变量 - TZ=Asia/Shanghai - - certd_system_resetAdminPassword=false + - certd_system_resetAdminPasswd=false # ↑↑↑↑↑---------------------------4、如果忘记管理员密码,可以设置为true,重启之后,管理员密码将改成123456,然后请及时修改回false【可选】 - certd_cron_immediateTriggerOnce=false # ↑↑↑↑↑---------------------------5、如果设置为true,启动后所有配置了cron的流水线任务都将被立即触发一次【可选】 diff --git a/lerna.json b/lerna.json index 2e6b40ee..f30e05e3 100644 --- a/lerna.json +++ b/lerna.json @@ -9,5 +9,5 @@ } }, "npmClient": "pnpm", - "version": "1.23.1" + "version": "1.24.0" } diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md index c4d74c0e..2f718cf5 100644 --- a/packages/core/acme-client/CHANGELOG.md +++ b/packages/core/acme-client/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.24.0](https://github.com/publishlab/node-acme-client/compare/v1.23.1...v1.24.0) (2024-08-25) + +### Bug Fixes + +* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/publishlab/node-acme-client/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) +* 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/publishlab/node-acme-client/commit/17ead547aab25333603980304aa3aad3db1f73d5)) +* 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/publishlab/node-acme-client/commit/95122e28609333f4df55c266e5434897954c0fb3)) + +### Features + +* 支持google证书申请(需要使用代理) ([a593056](https://github.com/publishlab/node-acme-client/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe)) + +### Performance Improvements + +* 优化证书申请成功率 ([968c469](https://github.com/publishlab/node-acme-client/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) + ## [1.22.6](https://github.com/publishlab/node-acme-client/compare/v1.22.5...v1.22.6) (2024-08-03) **Note:** Version bump only for package @certd/acme-client @@ -110,10 +126,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline # Changelog -## v5.4.0 +## v5.4.0 (2024-07-16) * `added` Directory URLs for [Google](https://cloud.google.com/certificate-manager/docs/overview) ACME provider -* `fixed` Invalidate ACME directory cache after 24 hours +* `fixed` Invalidate ACME provider directory cache after 24 hours +* `fixed` Retry HTTP requests on server errors or when rate limited - [#89](https://github.com/publishlab/node-acme-client/issues/89) ## v5.3.1 (2024-05-22) @@ -123,7 +140,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ## v5.3.0 (2024-02-05) * `added` Support and tests for satisfying `tls-alpn-01` challenges -* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing +* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR handling * `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge * Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously * This change is not considered breaking since the previous behavior was incorrect diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json index de5f76f5..15b7f60d 100644 --- a/packages/core/acme-client/package.json +++ b/packages/core/acme-client/package.json @@ -3,7 +3,7 @@ "description": "Simple and unopinionated ACME client", "private": false, "author": "nmorsman", - "version": "1.22.6", + "version": "1.24.0", "main": "src/index.js", "types": "types/index.d.ts", "license": "MIT", @@ -16,24 +16,24 @@ "types" ], "dependencies": { - "@peculiar/x509": "^1.10.0", + "@peculiar/x509": "^1.11.0", "asn1js": "^3.0.5", "axios": "^1.7.2", - "debug": "^4.1.1", + "debug": "^4.3.5", "https-proxy-agent": "^7.0.4", "node-forge": "^1.3.1" }, "devDependencies": { - "@types/node": "^20.12.12", + "@types/node": "^20.14.10", "chai": "^4.4.1", "chai-as-promised": "^7.1.2", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-plugin-import": "^2.29.1", "jsdoc-to-markdown": "^8.0.1", - "mocha": "^10.4.0", + "mocha": "^10.6.0", "nock": "^13.5.4", - "tsd": "^0.31.0", + "tsd": "^0.31.1", "typescript": "^4.8.4", "uuid": "^8.3.2" }, diff --git a/packages/core/acme-client/src/api.js b/packages/core/acme-client/src/api.js index 2dddf1e2..9c251ca5 100644 --- a/packages/core/acme-client/src/api.js +++ b/packages/core/acme-client/src/api.js @@ -3,6 +3,7 @@ */ const util = require('./util'); +const { log } = require('./logger'); /** * AcmeApi @@ -17,6 +18,21 @@ class AcmeApi { this.accountUrl = accountUrl; } + getLocationFromHeader(resp) { + let locationUrl = resp.headers.location; + const mapping = this.http.urlMapping; + if (mapping.mappings) { + // eslint-disable-next-line guard-for-in,no-restricted-syntax + for (const key in mapping.mappings) { + const url = mapping.mappings[key]; + if (locationUrl.indexOf(url) > -1) { + locationUrl = locationUrl.replace(url, key); + } + } + } + return locationUrl; + } + /** * Get account URL * @@ -103,7 +119,7 @@ class AcmeApi { /* Set account URL */ if (resp.headers.location) { - this.accountUrl = resp.headers.location; + this.accountUrl = this.getLocationFromHeader(resp); } return resp; diff --git a/packages/core/acme-client/src/auto.js b/packages/core/acme-client/src/auto.js index 10e81fe5..ed59fba1 100644 --- a/packages/core/acme-client/src/auto.js +++ b/packages/core/acme-client/src/auto.js @@ -137,9 +137,13 @@ module.exports = async (client, userOpts) => { } else { log(`[auto] [${d}] Running challenge verification`); - await client.verifyChallenge(authz, challenge); + try { + await client.verifyChallenge(authz, challenge); + } + catch (e) { + log(`[auto] [${d}] challenge verification threw error: ${e.message}`); + } } - /* Complete challenge and wait for valid status */ log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`); await client.completeChallenge(challenge); @@ -170,11 +174,41 @@ module.exports = async (client, userOpts) => { throw e; } }; + const domainSets = []; - const challengePromises = authorizations.map((authz) => async () => { - await challengeFunc(authz); + authorizations.forEach((authz) => { + const d = authz.identifier.value; + let setd = false; + // eslint-disable-next-line no-restricted-syntax + for (const group of domainSets) { + if (!group[d]) { + group[d] = authz; + setd = true; + } + } + if (!setd) { + const group = {}; + group[d] = authz; + domainSets.push(group); + } }); + const allChallengePromises = []; + // eslint-disable-next-line no-restricted-syntax + 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]; + challengePromises.push(async () => { + log(`[auto] [${domain}] Starting challenge`); + await challengeFunc(authz); + }); + } + allChallengePromises.push(challengePromises); + } + + log(`[auto] challengeGroups:${allChallengePromises.length}`); function runAllPromise(tasks) { let promise = Promise.resolve(); tasks.forEach((task) => { @@ -195,9 +229,18 @@ module.exports = async (client, userOpts) => { } try { - log('开始challenge'); - await runPromisePa(challengePromises); - + 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 Error('用户取消'); + } + // eslint-disable-next-line no-await-in-loop + await runPromisePa(challengePromises); + } log('challenge结束'); // log('[auto] Waiting for challenge valid status'); diff --git a/packages/core/acme-client/src/axios.js b/packages/core/acme-client/src/axios.js index a4bba60a..84b0cc55 100644 --- a/packages/core/acme-client/src/axios.js +++ b/packages/core/acme-client/src/axios.js @@ -3,10 +3,14 @@ */ const axios = require('axios'); +const { parseRetryAfterHeader } = require('./util'); +const { log } = require('./logger'); const pkg = require('./../package.json'); +const { AxiosError } = axios; + /** - * Instance + * Defaults */ const instance = axios.create(); @@ -19,6 +23,9 @@ instance.defaults.acmeSettings = { httpChallengePort: 80, httpsChallengePort: 443, tlsAlpnChallengePort: 443, + + retryMaxAttempts: 5, + retryDefaultDelay: 5, }; // instance.defaults.proxy = { // host: '192.168.34.139', @@ -33,6 +40,85 @@ instance.defaults.acmeSettings = { instance.defaults.adapter = 'http'; +/** + * Retry requests on server errors or when rate limited + * + * https://datatracker.ietf.org/doc/html/rfc8555#section-6.6 + */ + +function isRetryableError(error) { + return (error.code !== 'ECONNABORTED') + && (error.code !== 'ERR_NOCK_NO_MATCH') + && (!error.response + || (error.response.status === 429) + || ((error.response.status >= 500) && (error.response.status <= 599))); +} + +/* https://github.com/axios/axios/blob/main/lib/core/settle.js */ +function validateStatus(response) { + const validator = response.config.retryValidateStatus; + + if (!response.status || !validator || validator(response.status)) { + return response; + } + + throw new AxiosError( + `Request failed with status code ${response.status}`, + (Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE, + response.config, + response.request, + response, + ); +} + +/* Pass all responses through the error interceptor */ +instance.interceptors.request.use((config) => { + if (!('retryValidateStatus' in config)) { + config.retryValidateStatus = config.validateStatus; + } + + config.validateStatus = () => false; + return config; +}); + +/* Handle request retries if applicable */ +instance.interceptors.response.use(null, async (error) => { + const { config, response } = error; + + if (!config) { + return Promise.reject(error); + } + + /* Pick up errors we want to retry */ + if (isRetryableError(error)) { + const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings; + config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1; + + if (config.retryAttempt <= retryMaxAttempts) { + const code = response ? `HTTP ${response.status}` : error.code; + log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`); + + /* Attempt to parse Retry-After header, fallback to default delay */ + let retryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0; + + if (retryAfter > 0) { + log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${retryAfter} seconds`); + } + else { + retryAfter = (retryDefaultDelay * config.retryAttempt); + log(`Unable to locate or parse retry-after response header, waiting ${retryAfter} seconds`); + } + + /* Wait and retry the request */ + await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); }); + return instance(config); + } + } + + /* Validate and return response */ + return validateStatus(response); +}); + /** * Export instance */ diff --git a/packages/core/acme-client/src/client.js b/packages/core/acme-client/src/client.js index 055696b3..f32cd17c 100644 --- a/packages/core/acme-client/src/client.js +++ b/packages/core/acme-client/src/client.js @@ -300,7 +300,8 @@ class AcmeClient { } /* Add URL to response */ - resp.data.url = resp.headers.location; + resp.data.url = this.api.getLocationFromHeader(resp); + return resp.data; } @@ -490,6 +491,9 @@ class AcmeClient { const keyAuthorization = await this.getChallengeKeyAuthorization(challenge); const verifyFn = async () => { + if (this.opts.signal && this.opts.signal.aborted) { + throw new Error('用户取消'); + } await verify[challenge.type](authz, challenge, keyAuthorization); }; @@ -513,6 +517,9 @@ class AcmeClient { */ async completeChallenge(challenge) { + if (this.opts.signal && this.opts.signal.aborted) { + throw new Error('用户取消'); + } const resp = await this.api.completeChallenge(challenge.url, {}); return resp.data; } @@ -550,6 +557,10 @@ class AcmeClient { } const verifyFn = async (abort) => { + if (this.opts.signal && this.opts.signal.aborted) { + throw new Error('用户取消'); + } + const resp = await this.api.apiRequest(item.url, null, [200]); /* Verify status */ diff --git a/packages/core/acme-client/src/crypto/forge.js b/packages/core/acme-client/src/crypto/forge.js index 4bf942c8..b13f4b1a 100644 --- a/packages/core/acme-client/src/crypto/forge.js +++ b/packages/core/acme-client/src/crypto/forge.js @@ -10,6 +10,7 @@ const net = require('net'); const { promisify } = require('util'); const forge = require('node-forge'); +const { createPrivateEcdsaKey, getPublicKey } = require('./index'); const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair); @@ -378,13 +379,17 @@ function formatCsrAltNames(altNames) { * }, certificateKey); */ -exports.createCsr = async (data, key = null) => { - if (!key) { +exports.createCsr = async (data, keyType = null) => { + let key = null; + if (keyType === 'ec') { + key = await createPrivateEcdsaKey(); + } + else { key = await createPrivateKey(data.keySize); } - else if (!Buffer.isBuffer(key)) { - key = Buffer.from(key); - } + // else if (!Buffer.isBuffer(key)) { + // key = Buffer.from(key); + // } if (typeof data.altNames === 'undefined') { data.altNames = []; @@ -396,6 +401,8 @@ exports.createCsr = async (data, key = null) => { const privateKey = forge.pki.privateKeyFromPem(key); const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); csr.publicKey = publicKey; + // const privateKey = key; + // csr.publicKey = getPublicKey(key); /* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */ if (data.commonName && !data.altNames.includes(data.commonName)) { diff --git a/packages/core/acme-client/src/http.js b/packages/core/acme-client/src/http.js index caf73dd7..150ed75d 100644 --- a/packages/core/acme-client/src/http.js +++ b/packages/core/acme-client/src/http.js @@ -55,7 +55,7 @@ class HttpClient { */ async request(url, method, opts = {}) { - if (this.urlMapping && this.urlMapping.enabled === true && this.urlMapping.mappings) { + if (this.urlMapping && this.urlMapping.mappings) { // eslint-disable-next-line no-restricted-syntax for (const key in this.urlMapping.mappings) { if (url.includes(key)) { @@ -93,9 +93,11 @@ class HttpClient { */ async getDirectory() { - const age = (Math.floor(Date.now() / 1000) - this.directoryTimestamp); + const now = Math.floor(Date.now() / 1000); + const age = (now - this.directoryTimestamp); if (!this.directoryCache || (age > this.directoryMaxAge)) { + log(`Refreshing ACME directory, age: ${age}`); const resp = await this.request(this.directoryUrl, 'get'); if (resp.status >= 400) { @@ -107,6 +109,7 @@ class HttpClient { } this.directoryCache = resp.data; + this.directoryTimestamp = now; } return this.directoryCache; @@ -131,7 +134,7 @@ class HttpClient { * * https://datatracker.ietf.org/doc/html/rfc8555#section-7.2 * - * @returns {Promise} nonce + * @returns {Promise} Nonce */ async getNonce() { diff --git a/packages/core/acme-client/src/util.js b/packages/core/acme-client/src/util.js index be2cdd49..242b47eb 100644 --- a/packages/core/acme-client/src/util.js +++ b/packages/core/acme-client/src/util.js @@ -84,9 +84,12 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) { } /** - * Parse URLs from link header + * Parse URLs from Link header * - * @param {string} header Link header contents + * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2 + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + * + * @param {string} header Header contents * @param {string} rel Link relation, default: `alternate` * @returns {string[]} Array of URLs */ @@ -102,6 +105,37 @@ function parseLinkHeader(header, rel = 'alternate') { return results.filter((r) => r); } +/** + * Parse date or duration from Retry-After header + * + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + * + * @param {string} header Header contents + * @returns {number} Retry duration in seconds + */ + +function parseRetryAfterHeader(header) { + const sec = parseInt(header, 10); + const date = new Date(header); + + /* Seconds into the future */ + if (Number.isSafeInteger(sec) && (sec > 0)) { + return sec; + } + + /* Future date string */ + if (date instanceof Date && !Number.isNaN(date)) { + const now = new Date(); + const diff = Math.ceil((date.getTime() - now.getTime()) / 1000); + + if (diff > 0) { + return diff; + } + } + + return 0; +} + /** * Find certificate chain with preferred issuer common name * - If issuer is found in multiple chains, the closest to root wins @@ -161,14 +195,16 @@ function findCertificateChainForIssuer(chains, issuer) { function formatResponseError(resp) { let result; - if (resp.data.error) { - result = resp.data.error.detail || resp.data.error; - } - else { - result = resp.data.detail || JSON.stringify(resp.data); + if (resp.data) { + if (resp.data.error) { + result = resp.data.error.detail || resp.data.error; + } + else { + result = resp.data.detail || JSON.stringify(resp.data); + } } - return result.replace(/\n/g, ''); + return (result || '').replace(/\n/g, ''); } /** @@ -296,6 +332,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) { module.exports = { retry, parseLinkHeader, + parseRetryAfterHeader, findCertificateChainForIssuer, formatResponseError, getAuthoritativeDnsResolver, diff --git a/packages/core/acme-client/test/10-http.spec.js b/packages/core/acme-client/test/10-http.spec.js index 6967d9ec..743b7b6e 100644 --- a/packages/core/acme-client/test/10-http.spec.js +++ b/packages/core/acme-client/test/10-http.spec.js @@ -12,33 +12,12 @@ const pkg = require('./../package.json'); describe('http', () => { let testClient; + const endpoint = `http://${uuid()}.example.com`; const defaultUserAgent = `node-${pkg.name}/${pkg.version}`; const customUserAgent = 'custom-ua-123'; - const primaryEndpoint = `http://${uuid()}.example.com`; - const defaultUaEndpoint = `http://${uuid()}.example.com`; - const customUaEndpoint = `http://${uuid()}.example.com`; - - /** - * HTTP mocking - */ - - before(() => { - const defaultUaOpts = { reqheaders: { 'User-Agent': defaultUserAgent } }; - const customUaOpts = { reqheaders: { 'User-Agent': customUserAgent } }; - - nock(primaryEndpoint) - .persist().get('/').reply(200, 'ok'); - - nock(defaultUaEndpoint, defaultUaOpts) - .persist().get('/').reply(200, 'ok'); - - nock(customUaEndpoint, customUaOpts) - .persist().get('/').reply(200, 'ok'); - }); - - after(() => { - axios.defaults.headers.common['User-Agent'] = defaultUserAgent; + afterEach(() => { + nock.cleanAll(); }); /** @@ -54,7 +33,8 @@ describe('http', () => { */ it('should http get', async () => { - const resp = await testClient.request(primaryEndpoint, 'get'); + nock(endpoint).get('/').reply(200, 'ok'); + const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 200); @@ -66,28 +46,76 @@ describe('http', () => { */ it('should request using default user-agent', async () => { - const resp = await testClient.request(defaultUaEndpoint, 'get'); + nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok'); + axios.defaults.headers.common['User-Agent'] = defaultUserAgent; + const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 200); assert.strictEqual(resp.data, 'ok'); }); - it('should not request using custom user-agent', async () => { - await assert.isRejected(testClient.request(customUaEndpoint, 'get')); + it('should reject using custom user-agent', async () => { + nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok'); + axios.defaults.headers.common['User-Agent'] = customUserAgent; + await assert.isRejected(testClient.request(endpoint, 'get')); }); it('should request using custom user-agent', async () => { + nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok'); axios.defaults.headers.common['User-Agent'] = customUserAgent; - const resp = await testClient.request(customUaEndpoint, 'get'); + const resp = await testClient.request(endpoint, 'get'); assert.isObject(resp); assert.strictEqual(resp.status, 200); assert.strictEqual(resp.data, 'ok'); }); - it('should not request using default user-agent', async () => { - axios.defaults.headers.common['User-Agent'] = customUserAgent; - await assert.isRejected(testClient.request(defaultUaEndpoint, 'get')); + it('should reject using default user-agent', async () => { + nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok'); + axios.defaults.headers.common['User-Agent'] = defaultUserAgent; + await assert.isRejected(testClient.request(endpoint, 'get')); + }); + + /** + * Retry on HTTP errors + */ + + it('should retry on 429 rate limit', async () => { + let rateLimitCount = 0; + + nock(endpoint).persist().get('/').reply(() => { + rateLimitCount += 1; + + if (rateLimitCount < 3) { + return [429, 'Rate Limit Exceeded', { 'Retry-After': 1 }]; + } + + return [200, 'ok']; + }); + + assert.strictEqual(rateLimitCount, 0); + const resp = await testClient.request(endpoint, 'get'); + + assert.isObject(resp); + assert.strictEqual(resp.status, 200); + assert.strictEqual(resp.data, 'ok'); + assert.strictEqual(rateLimitCount, 3); + }); + + it('should retry on 5xx server error', async () => { + let serverErrorCount = 0; + + nock(endpoint).persist().get('/').reply(() => { + serverErrorCount += 1; + return [500, 'Internal Server Error', { 'Retry-After': 1 }]; + }); + + assert.strictEqual(serverErrorCount, 0); + const resp = await testClient.request(endpoint, 'get'); + + assert.isObject(resp); + assert.strictEqual(resp.status, 500); + assert.strictEqual(serverErrorCount, 4); }); }); diff --git a/packages/core/acme-client/test/10-util.spec.js b/packages/core/acme-client/test/10-util.spec.js new file mode 100644 index 00000000..c9768be8 --- /dev/null +++ b/packages/core/acme-client/test/10-util.spec.js @@ -0,0 +1,145 @@ +/** + * Utility method tests + */ + +const dns = require('dns').promises; +const fs = require('fs').promises; +const path = require('path'); +const { assert } = require('chai'); +const util = require('./../src/util'); +const { readCertificateInfo } = require('./../src/crypto'); + +describe('util', () => { + const testCertPath1 = path.join(__dirname, 'fixtures', 'certificate.crt'); + const testCertPath2 = path.join(__dirname, 'fixtures', 'letsencrypt.crt'); + + it('retry()', async () => { + let attempts = 0; + const backoffOpts = { + min: 100, + max: 500, + }; + + await assert.isRejected(util.retry(() => { + throw new Error('oops'); + }, backoffOpts)); + + const r = await util.retry(() => { + attempts += 1; + + if (attempts < 3) { + throw new Error('oops'); + } + + return 'abc'; + }, backoffOpts); + + assert.strictEqual(r, 'abc'); + assert.strictEqual(attempts, 3); + }); + + it('parseLinkHeader()', () => { + const r1 = util.parseLinkHeader(';rel="alternate"'); + assert.isArray(r1); + assert.strictEqual(r1.length, 1); + assert.strictEqual(r1[0], 'https://example.com/a'); + + const r2 = util.parseLinkHeader(';rel="test"'); + assert.isArray(r2); + assert.strictEqual(r2.length, 0); + + const r3 = util.parseLinkHeader('; rel="test"', 'test'); + assert.isArray(r3); + assert.strictEqual(r3.length, 1); + assert.strictEqual(r3[0], 'http://example.com/c'); + + const r4 = util.parseLinkHeader(`; rel="alternate", + ; rel="nope", + ;rel="alternate", + ; rel="alternate"`); + assert.isArray(r4); + assert.strictEqual(r4.length, 3); + assert.strictEqual(r4[0], 'https://example.com/a'); + assert.strictEqual(r4[1], 'https://example.com/b'); + assert.strictEqual(r4[2], 'https://example.com/c'); + }); + + it('parseRetryAfterHeader()', () => { + const r1 = util.parseRetryAfterHeader(''); + assert.strictEqual(r1, 0); + + const r2 = util.parseRetryAfterHeader('abcdef'); + assert.strictEqual(r2, 0); + + const r3 = util.parseRetryAfterHeader('123'); + assert.strictEqual(r3, 123); + + const r4 = util.parseRetryAfterHeader('123.456'); + assert.strictEqual(r4, 123); + + const r5 = util.parseRetryAfterHeader('-555'); + assert.strictEqual(r5, 0); + + const r6 = util.parseRetryAfterHeader('Wed, 21 Oct 2015 07:28:00 GMT'); + assert.strictEqual(r6, 0); + + const now = new Date(); + const future = new Date(now.getTime() + 123000); + const r7 = util.parseRetryAfterHeader(future.toUTCString()); + assert.isTrue(r7 > 100); + }); + + it('findCertificateChainForIssuer()', async () => { + const certs = [ + (await fs.readFile(testCertPath1)).toString(), + (await fs.readFile(testCertPath2)).toString(), + ]; + + const r1 = util.findCertificateChainForIssuer(certs, 'abc123'); + const r2 = util.findCertificateChainForIssuer(certs, 'example.com'); + const r3 = util.findCertificateChainForIssuer(certs, 'E6'); + + [r1, r2, r3].forEach((r) => { + assert.isString(r); + assert.isNotEmpty(r); + }); + + assert.strictEqual(readCertificateInfo(r1).issuer.commonName, 'example.com'); + assert.strictEqual(readCertificateInfo(r2).issuer.commonName, 'example.com'); + assert.strictEqual(readCertificateInfo(r3).issuer.commonName, 'E6'); + }); + + it('formatResponseError()', () => { + const e1 = util.formatResponseError({ data: { error: 'aaa' } }); + assert.strictEqual(e1, 'aaa'); + + const e2 = util.formatResponseError({ data: { error: { detail: 'bbb' } } }); + assert.strictEqual(e2, 'bbb'); + + const e3 = util.formatResponseError({ data: { detail: 'ccc' } }); + assert.strictEqual(e3, 'ccc'); + + const e4 = util.formatResponseError({ data: { a: 123 } }); + assert.strictEqual(e4, '{"a":123}'); + + const e5 = util.formatResponseError({}); + assert.isString(e5); + assert.isEmpty(e5); + }); + + it('getAuthoritativeDnsResolver()', async () => { + /* valid domain - should not use global default */ + const r1 = await util.getAuthoritativeDnsResolver('example.com'); + assert.instanceOf(r1, dns.Resolver); + assert.isNotEmpty(r1.getServers()); + assert.notDeepEqual(r1.getServers(), dns.getServers()); + + /* invalid domain - fallback to global default */ + const r2 = await util.getAuthoritativeDnsResolver('invalid.xtldx'); + assert.instanceOf(r2, dns.Resolver); + assert.deepStrictEqual(r2.getServers(), dns.getServers()); + }); + + /* TODO: Figure out how to test this */ + it('retrieveTlsAlpnCertificate()'); +}); diff --git a/packages/core/acme-client/test/fixtures/letsencrypt.crt b/packages/core/acme-client/test/fixtures/letsencrypt.crt new file mode 100644 index 00000000..7efd44ca --- /dev/null +++ b/packages/core/acme-client/test/fixtures/letsencrypt.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDzzCCA1WgAwIBAgISA0ghDoSv5DpT3Pd3lqwjbVDDMAoGCCqGSM49BAMDMDIx +CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF +NjAeFw0yNDA2MTAxNzEyMjZaFw0yNDA5MDgxNzEyMjVaMBQxEjAQBgNVBAMTCWxl +bmNyLm9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHJ3DjN7pYV3mftHzaP +V/WI0RhOJnSI5AIFEPFHDi8UowOINRGIfm9FHGIDqrb4Rmyvr9JrrqBdFGDen8BW +6OGjggJnMIICYzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIdCTnxqmpOELDyzPaEM +seB36lUOMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUGCCsGAQUF +BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3JnMCIGCCsG +AQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMG8GA1UdEQRoMGaCCWxlbmNy +Lm9yZ4IPbGV0c2VuY3J5cHQuY29tgg9sZXRzZW5jcnlwdC5vcmeCDXd3dy5sZW5j +ci5vcmeCE3d3dy5sZXRzZW5jcnlwdC5jb22CE3d3dy5sZXRzZW5jcnlwdC5vcmcw +EwYDVR0gBAwwCjAIBgZngQwBAgEwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEAdgA/ +F0tP1yJHWJQdZRyEvg0S7ZA3fx+FauvBvyiF7PhkbgAAAZADWfneAAAEAwBHMEUC +IGlp+dPU2hLT2suTMYkYMlt/xbzSnKLZDA/wYSsPACP7AiEAxbAzx6mkzn0cs0hh +ti6sLf0pcbmDhxHdlJRjuo6SQZEAdwDf4VbrqgWvtZwPhnGNqMAyTq5W2W6n9aVq +AdHBO75SXAAAAZADWfqrAAAEAwBIMEYCIQCrAmDUrlX3oGhri1qCIb65Cuf8h2GR +LC1VfXBenX7dCAIhALXwbhCQ1vO1WLv4CqyihMHOwFaICYqN/N6ylaBlVAM4MAoG +CCqGSM49BAMDA2gAMGUCMFdgjOXGl+hE2ABDsAeuNq8wi34yTMUHk0KMTOjRAfy9 +rOCGQqvP0myoYlyzXOH9uQIxAMdkG1ZWBZS1dHavbPf1I/MjYpzX6gy0jVHIXXu5 +aYWylBi/Uf2RPj0LWFZh8tNa1Q== +-----END CERTIFICATE----- diff --git a/packages/core/acme-client/test/setup.js b/packages/core/acme-client/test/setup.js index 9be01fc0..749a7ec7 100644 --- a/packages/core/acme-client/test/setup.js +++ b/packages/core/acme-client/test/setup.js @@ -29,6 +29,13 @@ if (process.env.ACME_TLSALPN_PORT) { axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT; } +/** + * Greatly reduce retry duration while testing + */ + +axios.defaults.acmeSettings.retryMaxAttempts = 3; +axios.defaults.acmeSettings.retryDefaultDelay = 1; + /** * External account binding */ diff --git a/packages/core/acme-client/types/index.d.ts b/packages/core/acme-client/types/index.d.ts index 0969d885..406b546a 100644 --- a/packages/core/acme-client/types/index.d.ts +++ b/packages/core/acme-client/types/index.d.ts @@ -45,6 +45,7 @@ export interface ClientOptions { backoffMin?: number; backoffMax?: number; urlMapping?: UrlMapping; + signal?: AbortSignal; } export interface ClientExternalAccountBindingOptions { @@ -61,6 +62,7 @@ export interface ClientAutoOptions { skipChallengeVerification?: boolean; challengePriority?: string[]; preferredChain?: string; + signal?: AbortSignal; } export class Client { diff --git a/packages/core/pipeline/CHANGELOG.md b/packages/core/pipeline/CHANGELOG.md index 33816d00..f754abae 100644 --- a/packages/core/pipeline/CHANGELOG.md +++ b/packages/core/pipeline/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) + +### Bug Fixes + +* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) +* 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5)) + +### Features + +* 支持google证书申请(需要使用代理) ([a593056](https://github.com/certd/certd/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe)) + +### Performance Improvements + +* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b)) +* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) +* email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f)) + ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) **Note:** Version bump only for package @certd/pipeline diff --git a/packages/core/pipeline/build.md b/packages/core/pipeline/build.md index 212210ed..01df59fb 100644 --- a/packages/core/pipeline/build.md +++ b/packages/core/pipeline/build.md @@ -1 +1 @@ -11:38 +14:26 diff --git a/packages/core/pipeline/package.json b/packages/core/pipeline/package.json index 83fb1588..a48117d9 100644 --- a/packages/core/pipeline/package.json +++ b/packages/core/pipeline/package.json @@ -1,7 +1,7 @@ { "name": "@certd/pipeline", "private": false, - "version": "1.23.1", + "version": "1.24.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/core/pipeline/src/core/executor.ts b/packages/core/pipeline/src/core/executor.ts index 79b4b7d0..daba79ec 100644 --- a/packages/core/pipeline/src/core/executor.ts +++ b/packages/core/pipeline/src/core/executor.ts @@ -23,16 +23,19 @@ export type ExecutorOptions = { emailService: IEmailService; fileRootDir?: string; }; + export class Executor { pipeline: Pipeline; runtime!: RunHistory; contextFactory: ContextFactory; logger: Logger; pipelineContext!: IContext; + currentStatusMap!: RunnableCollection; lastStatusMap!: RunnableCollection; lastRuntime!: RunHistory; options: ExecutorOptions; - canceled = false; + abort: AbortController = new AbortController(); + onChanged: (history: RunHistory) => Promise; constructor(options: ExecutorOptions) { this.options = options; @@ -50,10 +53,11 @@ export class Executor { const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`); this.lastRuntime = lastRuntime; this.lastStatusMap = new RunnableCollection(lastRuntime?.pipeline); + this.currentStatusMap = new RunnableCollection(this.pipeline); } async cancel() { - this.canceled = true; + this.abort.abort(); this.runtime?.cancel(this.pipeline); await this.onChanged(this.runtime); } @@ -102,6 +106,8 @@ export class Executor { } } if (lastResult != null && lastResult === ResultType.success && !inputChanged) { + runnable.status!.output = lastNode?.status?.output; + runnable.status!.files = lastNode?.status?.files; this.runtime.skip(runnable); await this.onChanged(this.runtime); return ResultType.skip; @@ -113,10 +119,15 @@ export class Executor { // const timeout = runnable.timeout ?? 20 * 60 * 1000; try { - if (this.canceled) { + if (this.abort.signal.aborted) { + this.runtime.cancel(runnable); return ResultType.canceled; } await run(); + if (this.abort.signal.aborted) { + this.runtime.cancel(runnable); + return ResultType.canceled; + } this.runtime.success(runnable); return ResultType.success; } catch (e: any) { @@ -197,7 +208,7 @@ export class Executor { private async runStep(step: Step) { const currentLogger = this.runtime._loggers[step.id]; - + this.currentStatusMap.add(step); const lastStatus = this.lastStatusMap.get(step.id); //执行任务 const plugin: RegistryItem = pluginRegistry.get(step.type); @@ -211,16 +222,11 @@ export class Executor { if (item.component?.name === "pi-output-selector") { const contextKey = step.input[key]; if (contextKey != null) { - const value = this.runtime.context[contextKey]; - if (value == null) { - currentLogger.warn(`[step init] input ${define.title} is null,前置任务步骤输出值为空,请按如下步骤排查:`); - currentLogger.log(`1、检查前置任务(证书申请任务)是否是配置了成功后跳过,如果是,请改为正常执行`); - currentLogger.log( - `2、是否曾经删除过前置任务(证书申请任务),然后又重新添加了,如果是,请重新编辑当前任务,重新选择一下前置任务输出的参数(域名证书那一栏)` - ); - currentLogger.log(`3、以上都不是,联系作者吧`); - } - step.input[key] = value; + // "cert": "step.-BNFVPMKPu2O-i9NiOQxP.cert", + const arr = contextKey.split("."); + const id = arr[1]; + const outputKey = arr[2]; + step.input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey]; } } }); @@ -241,6 +247,7 @@ export class Executor { parent: this.runtime.id, rootDir: this.options.fileRootDir, }), + signal: this.abort.signal, }; instance.setCtx(taskCtx); @@ -254,8 +261,8 @@ export class Executor { //输出上下文变量到output context _.forEach(define.output, (item: any, key: any) => { step.status!.output[key] = instance[key]; - const stepOutputKey = `step.${step.id}.${key}`; - this.runtime.context[stepOutputKey] = instance[key]; + // const stepOutputKey = `step.${step.id}.${key}`; + // this.runtime.context[stepOutputKey] = instance[key]; }); step.status!.files = instance.getFiles(); diff --git a/packages/core/pipeline/src/core/file-store.ts b/packages/core/pipeline/src/core/file-store.ts index 1db22a31..040752cf 100644 --- a/packages/core/pipeline/src/core/file-store.ts +++ b/packages/core/pipeline/src/core/file-store.ts @@ -54,7 +54,10 @@ export class FileStore { deleteByParent(scope: string, parent: string) { const dir = path.join(this.rootDir, scope, parent); if (fs.existsSync(dir)) { - fs.unlinkSync(dir); + fs.rmSync(dir, { + recursive: true, + force: true, + }); } } } diff --git a/packages/core/pipeline/src/core/license.spec.ts b/packages/core/pipeline/src/core/license.spec.ts index 4f3c2543..772ad5cb 100644 --- a/packages/core/pipeline/src/core/license.spec.ts +++ b/packages/core/pipeline/src/core/license.spec.ts @@ -3,7 +3,6 @@ import { equal } from "assert"; describe("license", function () { it("#license", async function () { const req = { - appKey: "z4nXOeTeSnnpUpnmsV", subjectId: "999", license: "", }; diff --git a/packages/core/pipeline/src/core/license.ts b/packages/core/pipeline/src/core/license.ts index a5cef7e4..556dacb2 100644 --- a/packages/core/pipeline/src/core/license.ts +++ b/packages/core/pipeline/src/core/license.ts @@ -1,9 +1,18 @@ import { createVerify } from "node:crypto"; import { logger } from "../utils/index.js"; +import dayjs from "dayjs"; + +let SecreteKey = + "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQW9VWE1EWUhjdi82WFROWEZFSUI2RlpuR2FER0cwZnR5bTV1dVhPck9NaVl0UkxSb1lvSGMKNVZxenE0N00rdEFqRFBhaTBlOFhWS1c3aytUQUw3MUs0N2JCQVEyWTBxNU5Ya3lYcE5PTVdueVFMYXBwb0tWNgpPMkFJMnpFVURWMVJVa0ZtMFZTVjU0VXNzMDcrdjI2aW5aQU1CWitDMU42eWFDc2tZL3grNnVlNkVRNVcyZXdFCjZOWEhJcUU1bHdEUmU2SXJtdEpnU2doSnlHTS91azIyejN6NGEraFVPVUlWMy9DbEhYV0VhRHBBRFFsakt3NSsKeHR0dURiTHZyUmdzdWp6czB0dEI2OE1SbXE0R0FJL0JtNWVPWkhlNGxFQjBFVVhFUXdVWE1jV1N1VFZSMUE2cApUM21LRGo5MGcwVDFZUlNOdE5TMm9aRzgvRWIwOVlxK3Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K"; +let appKey = "kQth6FHM71IPV3qdWc"; +if (process.env.NODE_ENV !== "production") { + SecreteKey = + "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXY3TGtMaUp1dGM0NzhTU3RaTExjajVGZXh1YjJwR2NLMGxwa0hwVnlZWjhMY29rRFhuUlAKUGQ5UlJSTVRTaGJsbFl2Mzd4QUhOV1ZIQ0ZsWHkrQklVU001bUlBU1NDQTV0azlJNmpZZ2F4bEFDQm1BY0lGMwozKzBjeGZIYVkrVW9YdVluMkZ6YUt2Ym5GdFZIZ0lkMDg4a3d4clZTZzlCT3BDRVZIR1pxR2I5TWN5MXVHVXhUClFTVENCbmpoTWZlZ0p6cXVPYWVOY0ZPSE5tbmtWRWpLTythbTBPeEhNS1lyS3ZnQnVEbzdoVnFENlBFMUd6V3AKZHdwZUV4QXZDSVJxL2pWTkdRK3FtMkRWOVNJZ3U5bmF4MktmSUtFeU50dUFFS1VpekdqL0VmRFhDM1cxMExhegpKaGNYNGw1SUFZU1o3L3JWVmpGbExWSVl0WDU1T054L1Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K"; + appKey = "z4nXOeTeSnnpUpnmsV1"; +} + +export const AppKey = appKey; -const SecreteKey = - "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXY3TGtMaUp1dGM0NzhTU3RaTExjajVGZXh1YjJwR2NLMGxwa0hwVnlZWjhMY29rRFhuUlAKUGQ5UlJSTVRTaGJsbFl2Mzd4QUhOV1ZIQ0ZsWHkrQklVU001bUlBU1NDQTV0azlJNmpZZ2F4bEFDQm1BY0lGMwozKzBjeGZIYVkrVW9YdVluMkZ6YUt2Ym5GdFZIZ0lkMDg4a3d4clZTZzlCT3BDRVZIR1pxR2I5TWN5MXVHVXhUClFTVENCbmpoTWZlZ0p6cXVPYWVOY0ZPSE5tbmtWRWpLTythbTBPeEhNS1lyS3ZnQnVEbzdoVnFENlBFMUd6V3AKZHdwZUV4QXZDSVJxL2pWTkdRK3FtMkRWOVNJZ3U5bmF4MktmSUtFeU50dUFFS1VpekdqL0VmRFhDM1cxMExhegpKaGNYNGw1SUFZU1o3L3JWVmpGbExWSVl0WDU1T054L1Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K"; -const appKey = "z4nXOeTeSnnpUpnmsV"; export type LicenseVerifyReq = { subjectId: string; license: string; @@ -18,11 +27,16 @@ type License = { duration: number; version: number; secret: string; + vipType: string; signature: string; }; class LicenseHolder { isPlus = false; + expireTime = 0; + vipType = ""; + message?: string = undefined; + secret?: string = undefined; } const holder = new LicenseHolder(); holder.isPlus = false; @@ -35,9 +49,22 @@ class LicenseVerifier { return await this.verify(req); } - setPlus(value: boolean) { - holder.isPlus = value; - return value; + setPlus(value: boolean, info: any = {}) { + if (value && info) { + holder.isPlus = true; + holder.expireTime = info.expireTime; + holder.secret = info.secret; + holder.vipType = info.vipType; + } else { + holder.isPlus = false; + holder.expireTime = 0; + holder.vipType = ""; + holder.message = info.message; + holder.secret = undefined; + } + return { + ...holder, + }; } async verify(req: LicenseVerifyReq) { this.licenseReq = req; @@ -50,21 +77,29 @@ class LicenseVerifier { return this.setPlus(false); } - const licenseJson = Buffer.from(Buffer.from(license, "hex").toString(), "base64").toString(); + const licenseJson = Buffer.from(license, "base64").toString(); const json: License = JSON.parse(licenseJson); if (json.expireTime < Date.now()) { logger.warn("授权已过期"); - return this.setPlus(false); + return this.setPlus(false, { message: "授权已过期" }); } - const content = `${appKey},${this.licenseReq.subjectId},${json.code},${json.secret},${json.activeTime},${json.duration},${json.expireTime},${json.version}`; + const content = `${appKey},${this.licenseReq.subjectId},${json.code},${json.secret},${json.vipType},${json.activeTime},${json.duration},${json.expireTime},${json.version}`; + // content := fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d,%d", entity.AppKey, entity.SubjectId, entity.Code, entity.Secret, entity.Level, entity.ActiveTime, entity.Duration, entity.ExpireTime, entity.Version) + //z4nXOeTeSnnpUpnmsV,_m9jFTdNHktdaEN4xBDw_,HZz7rAAR3h3zGlDMhScO1wGBYPjXpZ9S_1,uUpr9I8p6K3jWSzu2Wh5NECvgG2FNynU,0,1724199847470,365,1787271324416,1 + logger.debug("content:", content); const publicKey = Buffer.from(SecreteKey, "base64").toString(); const res = this.verifySignature(content, json.signature, publicKey); this.checked = true; if (!res) { logger.warn("授权校验失败"); - return this.setPlus(false); + return this.setPlus(false, { message: "授权校验失败" }); } - return this.setPlus(true); + logger.info(`授权校验成功,到期时间:${dayjs(json.expireTime).format("YYYY-MM-DD HH:mm:ss")}`); + return this.setPlus(true, { + expireTime: json.expireTime, + vipType: json.vipType || "plus", + secret: json.secret, + }); } verifySignature(content: string, signature: any, publicKey: string) { @@ -77,9 +112,27 @@ class LicenseVerifier { const verifier = new LicenseVerifier(); export function isPlus() { - return holder.isPlus; + return holder.isPlus && holder.expireTime > Date.now(); +} + +export function isCommercial() { + return holder.isPlus && holder.vipType === "comm" && holder.expireTime > Date.now(); +} + +export function getPlusInfo() { + return { + isPlus: holder.isPlus, + vipType: holder.vipType, + expireTime: holder.expireTime, + secret: holder.secret, + }; } export async function verify(req: LicenseVerifyReq) { - return await verifier.reVerify(req); + try { + return await verifier.reVerify(req); + } catch (e) { + logger.error(e); + return verifier.setPlus(false, { message: "授权校验失败" }); + } } diff --git a/packages/core/pipeline/src/core/run-history.ts b/packages/core/pipeline/src/core/run-history.ts index bfdfc607..8288a362 100644 --- a/packages/core/pipeline/src/core/run-history.ts +++ b/packages/core/pipeline/src/core/run-history.ts @@ -1,4 +1,4 @@ -import { Context, HistoryResult, Pipeline, ResultType, Runnable, RunnableMap, Stage, Step, Task } from "../dt/index.js"; +import { HistoryResult, Pipeline, ResultType, Runnable, RunnableMap, Stage, Step, Task } from "../dt/index.js"; import _ from "lodash-es"; import { buildLogger } from "../utils/util.log.js"; import { Logger } from "log4js"; @@ -14,15 +14,12 @@ export type RunTrigger = { export function NewRunHistory(obj: any) { const history = new RunHistory(obj.id, obj.trigger, obj.pipeline); - history.context = obj.context; history.logs = obj.logs; history._loggers = obj.loggers; return history; } export class RunHistory { id!: string; - //运行时上下文变量 - context: Context = {}; pipeline!: Pipeline; logs: { [runnableId: string]: string[]; @@ -168,4 +165,8 @@ export class RunnableCollection { item.status = undefined; }); } + + add(runnable: Runnable) { + this.collection[runnable.id] = runnable; + } } diff --git a/packages/core/pipeline/src/plugin/api.ts b/packages/core/pipeline/src/plugin/api.ts index 4f18a968..edc5b3b2 100644 --- a/packages/core/pipeline/src/plugin/api.ts +++ b/packages/core/pipeline/src/plugin/api.ts @@ -6,7 +6,7 @@ import { IAccessService } from "../access/index.js"; import { IEmailService } from "../service/index.js"; import { IContext } from "../core/index.js"; import { AxiosInstance } from "axios"; -import { logger } from "../utils/index.js"; +import { ILogger, logger } from "../utils/index.js"; export enum ContextScope { global, @@ -64,11 +64,15 @@ export type TaskInstanceContext = { http: AxiosInstance; fileStore: FileStore; lastStatus?: Runnable; + signal: AbortSignal; }; export abstract class AbstractTaskPlugin implements ITaskPlugin { _result: TaskResult = { clearLastStatus: false, files: [], pipelineVars: {} }; ctx!: TaskInstanceContext; + logger!: ILogger; + accessService!: IAccessService; + clearLastStatus() { this._result.clearLastStatus = true; } @@ -79,6 +83,8 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin { setCtx(ctx: TaskInstanceContext) { this.ctx = ctx; + this.logger = ctx.logger; + this.accessService = ctx.accessService; } randomFileId() { diff --git a/packages/core/pipeline/src/utils/util.request.ts b/packages/core/pipeline/src/utils/util.request.ts index 874a91b1..58e7ce35 100644 --- a/packages/core/pipeline/src/utils/util.request.ts +++ b/packages/core/pipeline/src/utils/util.request.ts @@ -53,7 +53,22 @@ export function createAxiosService({ logger }: { logger: Logger }) { logger.error(`请求出错:url:${error?.response?.config.url},method:${error?.response?.config?.method},status:${error?.response?.status}`); logger.info("返回数据:", JSON.stringify(error?.response?.data)); delete error.config; - delete error.response; + const data = error?.response?.data; + if (!data) { + error.message = data.message || data.msg || data.error || data; + } + if (error?.response) { + return Promise.reject({ + status: error?.response?.status, + statusText: error?.response?.statusText, + request: { + url: error?.response?.config?.url, + method: error?.response?.config?.method, + data: error?.response?.data, + }, + data: error?.response?.data, + }); + } return Promise.reject(error); } ); diff --git a/packages/core/pipeline/src/utils/util.sp.ts b/packages/core/pipeline/src/utils/util.sp.ts index 3482247e..9d32eaa9 100644 --- a/packages/core/pipeline/src/utils/util.sp.ts +++ b/packages/core/pipeline/src/utils/util.sp.ts @@ -72,7 +72,7 @@ async function spawn(opts: SpawnOption): Promise { let stderr = ""; return safePromise((resolve, reject) => { const ls = childProcess.spawn(cmd, { - shell: process.platform == "win32", + shell: true, env: { ...process.env, ...opts.env, diff --git a/packages/libs/lib-k8s/CHANGELOG.md b/packages/libs/lib-k8s/CHANGELOG.md index 1874f00b..1efaf5fe 100644 --- a/packages/libs/lib-k8s/CHANGELOG.md +++ b/packages/libs/lib-k8s/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) + +### Performance Improvements + +* 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd)) + ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) **Note:** Version bump only for package @certd/lib-k8s diff --git a/packages/libs/lib-k8s/package.json b/packages/libs/lib-k8s/package.json index 57514d94..bd9d9267 100644 --- a/packages/libs/lib-k8s/package.json +++ b/packages/libs/lib-k8s/package.json @@ -1,7 +1,7 @@ { "name": "@certd/lib-k8s", "private": false, - "version": "1.23.1", + "version": "1.24.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,10 @@ "preview": "vite preview" }, "dependencies": { - "kubernetes-client": "^9.0.0", - "shelljs": "^0.8.5" + "@kubernetes/client-node": "0.21.0" }, "devDependencies": { - "@certd/pipeline": "^1.23.1", + "@certd/pipeline": "^1.24.0", "@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.1", diff --git a/packages/libs/lib-k8s/src/lib/k8s.client.ts b/packages/libs/lib-k8s/src/lib/k8s.client.ts index cc909e9d..6af30be2 100644 --- a/packages/libs/lib-k8s/src/lib/k8s.client.ts +++ b/packages/libs/lib-k8s/src/lib/k8s.client.ts @@ -1,32 +1,40 @@ -import kubernetesClient from 'kubernetes-client'; -//@ts-ignore +import { KubeConfig, CoreV1Api, V1Secret, NetworkingV1Api, V1Ingress } from '@kubernetes/client-node'; import dns from 'dns'; import { ILogger } from '@certd/pipeline'; -//@ts-ignore -const { KubeConfig, Client, Request } = kubernetesClient; - -export class K8sClient { +export type K8sClientOpts = { kubeConfigStr: string; - lookup!: any; - client!: any; logger: ILogger; - constructor(kubeConfigStr: string, logger: ILogger) { - this.kubeConfigStr = kubeConfigStr; - this.logger = logger; + //{ [domain]:{ip:'xxx.xx.xxx'} } + //暂时没用 + lookup?: any; +}; +export class K8sClient { + kubeconfig!: KubeConfig; + kubeConfigStr: string; + lookup!: (hostnameReq: any, options: any, callback: any) => void; + client!: CoreV1Api; + logger: ILogger; + constructor(opts: K8sClientOpts) { + this.kubeConfigStr = opts.kubeConfigStr; + this.logger = opts.logger; + this.setLookup(opts.lookup); this.init(); } init() { const kubeconfig = new KubeConfig(); kubeconfig.loadFromString(this.kubeConfigStr); - const reqOpts = { kubeconfig, request: {} } as any; - if (this.lookup) { - reqOpts.request.lookup = this.lookup; - } + this.kubeconfig = kubeconfig; + this.client = kubeconfig.makeApiClient(CoreV1Api); - const backend = new Request(reqOpts); - this.client = new Client({ backend, version: '1.13' }); + // const reqOpts = { kubeconfig, request: {} } as any; + // if (this.lookup) { + // reqOpts.request.lookup = this.lookup; + // } + // + // const backend = new Request(reqOpts); + // this.client = new Client({ backend, version: '1.13' }); } /** @@ -34,6 +42,9 @@ export class K8sClient { * @param localRecords { [domain]:{ip:'xxx.xx.xxx'} } */ setLookup(localRecords: { [key: string]: { ip: string } }) { + if (localRecords == null) { + return; + } this.lookup = (hostnameReq: any, options: any, callback: any) => { this.logger.info('custom lookup', hostnameReq, localRecords); if (localRecords[hostnameReq]) { @@ -43,7 +54,6 @@ export class K8sClient { dns.lookup(hostnameReq, options, callback); } }; - this.init(); } /** @@ -51,9 +61,9 @@ export class K8sClient { * @param opts = {namespace:default} * @returns secretsList */ - async getSecret(opts: { namespace: string }) { + async getSecrets(opts: { namespace: string }) { const namespace = opts.namespace || 'default'; - return await this.client.api.v1.namespaces(namespace).secrets.get(); + return await this.client.listNamespacedSecret(namespace); } /** @@ -61,59 +71,61 @@ export class K8sClient { * @param opts {namespace:default, body:yamlStr} * @returns {Promise<*>} */ - async createSecret(opts: any) { + async createSecret(opts: { namespace: string; body: V1Secret }) { const namespace = opts.namespace || 'default'; - const created = await this.client.api.v1.namespaces(namespace).secrets.post({ - body: opts.body, - }); - this.logger.info('new secrets:', created); - return created; + const created = await this.client.createNamespacedSecret(namespace, opts.body); + this.logger.info('new secrets:', created.body); + return created.body; } - async updateSecret(opts: any) { + // async updateSecret(opts: any) { + // const namespace = opts.namespace || 'default'; + // const secretName = opts.secretName; + // if (secretName == null) { + // throw new Error('secretName 不能为空'); + // } + // return await this.client.replaceNamespacedSecret(secretName, namespace, opts.body); + // } + + async patchSecret(opts: { namespace: string; secretName: string; body: V1Secret }) { const namespace = opts.namespace || 'default'; const secretName = opts.secretName; if (secretName == null) { throw new Error('secretName 不能为空'); } - return await this.client.api.v1.namespaces(namespace).secrets(secretName).put({ - body: opts.body, - }); + const res = await this.client.patchNamespacedSecret(secretName, namespace, opts.body); + this.logger.info('secret patched:', res.body); + return res.body; } - async patchSecret(opts: any) { + async getIngressList(opts: { namespace: string }) { const namespace = opts.namespace || 'default'; - const secretName = opts.secretName; - if (secretName == null) { - throw new Error('secretName 不能为空'); - } - return await this.client.api.v1.namespaces(namespace).secrets(secretName).patch({ - body: opts.body, - }); + const client = this.kubeconfig.makeApiClient(NetworkingV1Api); + const res = await client.listNamespacedIngress(namespace); + this.logger.info('ingress list get:', res.body); + return res.body; } - async getIngressList(opts: any) { - const namespace = opts.namespace || 'default'; - return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses.get(); - } + // async getIngress(opts: { namespace: string; ingressName: string }) { + // const namespace = opts.namespace || 'default'; + // const ingressName = opts.ingressName; + // if (!ingressName) { + // throw new Error('ingressName 不能为空'); + // } + // const client = this.kubeconfig.makeApiClient(NetworkingV1Api); + // const res = await client.listNamespacedIngress(); + // return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get(); + // } - async getIngress(opts: any) { + async patchIngress(opts: { namespace: string; ingressName: string; body: V1Ingress }) { const namespace = opts.namespace || 'default'; const ingressName = opts.ingressName; if (!ingressName) { throw new Error('ingressName 不能为空'); } - return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get(); - } - - async patchIngress(opts: any) { - const namespace = opts.namespace || 'default'; - const ingressName = opts.ingressName; - if (!ingressName) { - throw new Error('ingressName 不能为空'); - } - return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).patch({ - body: opts.body, - }); + const client = this.kubeconfig.makeApiClient(NetworkingV1Api); + const res = await client.patchNamespacedIngress(ingressName, namespace, opts.body); + this.logger.info('ingress patched:', res.body); + return res; } } diff --git a/packages/plugins/plugin-cert/CHANGELOG.md b/packages/plugins/plugin-cert/CHANGELOG.md index c05ba8b4..5eea171b 100644 --- a/packages/plugins/plugin-cert/CHANGELOG.md +++ b/packages/plugins/plugin-cert/CHANGELOG.md @@ -3,6 +3,24 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) + +### Bug Fixes + +* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7)) +* 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/certd/certd/commit/17ead547aab25333603980304aa3aad3db1f73d5)) +* 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/certd/certd/commit/95122e28609333f4df55c266e5434897954c0fb3)) + +### Features + +* 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473)) + +### Performance Improvements + +* 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd)) +* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b)) +* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) + ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) ### Performance Improvements diff --git a/packages/plugins/plugin-cert/package.json b/packages/plugins/plugin-cert/package.json index 0c60fab8..a847ad4c 100644 --- a/packages/plugins/plugin-cert/package.json +++ b/packages/plugins/plugin-cert/package.json @@ -1,7 +1,7 @@ { "name": "@certd/plugin-cert", "private": false, - "version": "1.23.1", + "version": "1.24.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,8 +13,8 @@ "preview": "vite preview" }, "dependencies": { - "@certd/acme-client": "^1.22.6", - "@certd/pipeline": "^1.23.1", + "@certd/acme-client": "^1.24.0", + "@certd/pipeline": "^1.24.0", "jszip": "^3.10.1", "node-forge": "^0.10.0", "psl": "^1.9.0" diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts index e078b2e1..b941474f 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/acme.ts @@ -13,7 +13,8 @@ export type CertInfo = { key: string; csr: string; }; -export type SSLProvider = "letsencrypt" | "buypass" | "zerossl"; +export type SSLProvider = "letsencrypt" | "google" | "zerossl"; +export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521"; type AcmeServiceOptions = { userContext: IContext; logger: Logger; @@ -21,6 +22,8 @@ type AcmeServiceOptions = { eab?: ClientExternalAccountBindingOptions; skipLocalVerify?: boolean; useMappingProxy?: boolean; + privateKeyType?: PrivateKeyType; + signal?: AbortSignal; }; export class AcmeService { @@ -42,8 +45,20 @@ export class AcmeService { }); } - async getAccountConfig(email: string): Promise { - return (await this.userContext.getObj(this.buildAccountKey(email))) || {}; + async getAccountConfig(email: string, urlMapping: UrlMapping): Promise { + const conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {}; + if (urlMapping && urlMapping.mappings) { + for (const key in urlMapping.mappings) { + if (Object.prototype.hasOwnProperty.call(urlMapping.mappings, key)) { + const element = urlMapping.mappings[key]; + if (conf.accountUrl?.indexOf(element) > -1) { + //如果用了代理url,要替换回去 + conf.accountUrl = conf.accountUrl.replace(element, key); + } + } + } + } + return conf; } buildAccountKey(email: string) { @@ -55,7 +70,14 @@ export class AcmeService { } async getAcmeClient(email: string, isTest = false): Promise { - const conf = await this.getAccountConfig(email); + const urlMapping: UrlMapping = { + enabled: false, + mappings: { + "acme-v02.api.letsencrypt.org": "letsencrypt.proxy.handsfree.work", + "dv.acme-v02.api.pki.goog": "google.proxy.handsfree.work", + }, + }; + const conf = await this.getAccountConfig(email, urlMapping); if (conf.key == null) { conf.key = await this.createNewKey(); await this.saveAccountConfig(email, conf); @@ -66,22 +88,19 @@ export class AcmeService { } else { directoryUrl = acme.directory[this.sslProvider].production; } - const urlMapping: UrlMapping = { enabled: false, mappings: {} }; if (this.options.useMappingProxy) { urlMapping.enabled = true; - urlMapping.mappings = { - "acme-v02.api.letsencrypt.org": "letsencrypt.proxy.handsfree.work", - }; } const client = new acme.Client({ directoryUrl: directoryUrl, accountKey: conf.key, accountUrl: conf.accountUrl, externalAccountBinding: this.eab, - backoffAttempts: 30, + backoffAttempts: 15, backoffMin: 5000, backoffMax: 10000, urlMapping, + signal: this.options.signal, }); if (conf.accountUrl == null) { @@ -193,18 +212,38 @@ export class AcmeService { } } - async order(options: { email: string; domains: string | string[]; dnsProvider: any; csrInfo: any; isTest?: boolean }) { + async order(options: { + email: string; + domains: string | string[]; + dnsProvider: any; + csrInfo: any; + isTest?: boolean; + privateKeyType?: string; + }): Promise { const { email, isTest, domains, csrInfo, dnsProvider } = options; const client: acme.Client = await this.getAcmeClient(email, isTest); /* Create CSR */ const { commonName, altNames } = this.buildCommonNameByDomains(domains); - - const [key, csr] = await acme.forge.createCsr({ - commonName, - ...csrInfo, - altNames, - }); + let privateKey = null; + const privateKeyType = options.privateKeyType || "rsa_2048"; + const privateKeyArr = privateKeyType.split("_"); + const type = privateKeyArr[0]; + const size = parseInt(privateKeyArr[1]); + if (type == "ec") { + const name: any = "P-" + size; + privateKey = await acme.crypto.createPrivateEcdsaKey(name); + } else { + privateKey = await acme.crypto.createPrivateRsaKey(size); + } + const [key, csr] = await acme.crypto.createCsr( + { + commonName, + ...csrInfo, + altNames, + }, + privateKey + ); if (dnsProvider == null) { throw new Error("dnsProvider 不能为空"); } @@ -221,6 +260,7 @@ export class AcmeService { challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise => { return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider); }, + signal: this.options.signal, }); const cert: CertInfo = { diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts index 527b0188..a443c0d2 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/base.ts @@ -1,7 +1,6 @@ -import { AbstractTaskPlugin, HttpClient, IAccessService, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline"; +import { AbstractTaskPlugin, HttpClient, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline"; import dayjs from "dayjs"; import type { CertInfo } from "./acme.js"; -import { Logger } from "log4js"; import { CertReader } from "./cert-reader.js"; import JSZip from "jszip"; @@ -91,9 +90,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { // }) csrInfo!: string; - logger!: Logger; userContext!: IContext; - accessService!: IAccessService; http!: HttpClient; lastStatus!: Step; @@ -103,8 +100,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { cert?: CertInfo; async onInstance() { - this.accessService = this.ctx.accessService; - this.logger = this.ctx.logger; this.userContext = this.ctx.userContext; this.http = this.ctx.http; this.lastStatus = this.ctx.lastStatus as Step; @@ -138,10 +133,10 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin { const cert: CertInfo = certReader.toCertInfo(); this.cert = cert; - this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.validity.notAfter).valueOf(); + this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf(); if (isNew) { - const applyTime = dayjs(certReader.detail.validity.notBefore).format("YYYYMMDD_HHmmss"); + const applyTime = dayjs(certReader.detail.notBefore).format("YYYYMMDD_HHmmss"); await this.zipCert(cert, applyTime); } else { this.extendsFiles(); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts index ec76f913..babe3af4 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/cert-reader.ts @@ -1,8 +1,8 @@ import { CertInfo } from "./acme.js"; import fs from "fs"; import os from "os"; -import forge from "node-forge"; import path from "path"; +import { crypto } from "@certd/acme-client"; export class CertReader implements CertInfo { crt: string; key: string; @@ -29,9 +29,8 @@ export class CertReader implements CertInfo { } getCrtDetail(crt: string) { - const pki = forge.pki; - const detail = pki.certificateFromPem(crt.toString()); - const expires = detail.validity.notAfter; + const detail = crypto.readCertificateInfo(crt.toString()); + const expires = detail.notAfter; return { detail, expires }; } diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts index e9ed9dbb..d9b5d5e0 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/index.ts @@ -1,5 +1,5 @@ import { Decorator, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; -import type { CertInfo, SSLProvider } from "./acme.js"; +import type { CertInfo, PrivateKeyType, SSLProvider } from "./acme.js"; import { AcmeService } from "./acme.js"; import _ from "lodash-es"; import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js"; @@ -33,14 +33,34 @@ export class CertApplyPlugin extends CertApplyBasePlugin { vModel: "value", options: [ { value: "letsencrypt", label: "Let's Encrypt" }, - // { value: "letsencrypt-proxy", label: "Let's Encrypt代理,letsencrypt.org无法访问时使用" }, - // { value: "buypass", label: "Buypass" }, + { value: "google", label: "Google" }, { value: "zerossl", label: "ZeroSSL" }, ], }, + helper: "如果letsencrypt.org或dv.acme-v02.api.pki.goog无法访问,请尝试开启代理选项\n如果使用ZeroSSL、google证书,需要提供EAB授权", + required: true, + }) + sslProvider!: SSLProvider; + + @TaskInput({ + title: "加密算法", + value: "rsa_2048", + component: { + name: "a-select", + vModel: "value", + options: [ + { value: "rsa_1024", label: "RSA 1024" }, + { value: "rsa_2048", label: "RSA 2048" }, + { value: "rsa_3072", label: "RSA 3072" }, + { value: "rsa_4096", label: "RSA 4096" }, + { value: "ec_256", label: "EC 256" }, + { value: "ec_384", label: "EC 384" }, + // { value: "ec_521", label: "EC 521" }, + ], + }, required: true, }) - sslProvider!: SSLProvider; + privateKeyType!: PrivateKeyType; @TaskInput({ title: "EAB授权", @@ -49,7 +69,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin { type: "eab", }, maybeNeed: true, - helper: "如果使用ZeroSSL证书,需要提供EAB授权, 请前往 https://app.zerossl.com/developer 生成 'EAB Credentials for ACME Clients' ", + helper: + "如果使用ZeroSSL或者google证书,需要提供EAB授权\nZeroSSL:请前往 https://app.zerossl.com/developer 生成 'EAB Credentials' \n Google:请前往https://github.com/certd/certd/blob/v2/doc/google/google.md", }) eabAccessId!: number; @@ -87,7 +108,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin { name: "a-switch", vModel: "checked", }, - helper: "如果acme-v02.api.letsencrypt.org被墙无法连接访问,请尝试开启此选项", + maybeNeed: true, + helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问,请尝试开启此选项", }) useProxy = false; @@ -116,6 +138,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { eab, skipLocalVerify: this.skipLocalVerify, useMappingProxy: this.useProxy, + privateKeyType: this.privateKeyType, }); } @@ -156,6 +179,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin { dnsProvider, csrInfo, isTest: false, + privateKeyType: this.privateKeyType, }); const certInfo = this.formatCerts(cert); diff --git a/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts b/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts index ac709556..e1fb6df2 100644 --- a/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts +++ b/packages/plugins/plugin-cert/src/plugin/cert-plugin/lego/index.ts @@ -97,7 +97,7 @@ export class CertApplyLegoPlugin extends CertApplyBasePlugin { this.http = this.ctx.http; this.lastStatus = this.ctx.lastStatus as Step; if (this.legoEabAccessId) { - this.eab = await this.ctx.accessService.getById(this.legoEabAccessId); + this.eab = await this.accessService.getById(this.legoEabAccessId); } } async onInit(): Promise {} diff --git a/packages/ui/certd-client/CHANGELOG.md b/packages/ui/certd-client/CHANGELOG.md index bc882fd4..b0a37bdd 100644 --- a/packages/ui/certd-client/CHANGELOG.md +++ b/packages/ui/certd-client/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25) + +### Bug Fixes + +* 部署到腾讯云cdn选择证书任务步骤限制只能选证书 ([3345c14](https://github.com/certd/certd/commit/3345c145b802170f75a098a35d0c4b8312efcd17)) +* 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5)) + +### Features + +* 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473)) + +### Performance Improvements + +* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b)) +* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345)) +* email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f)) + ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06) ### Performance Improvements diff --git a/packages/ui/certd-client/package.json b/packages/ui/certd-client/package.json index d14f5a45..4c7fbaf9 100644 --- a/packages/ui/certd-client/package.json +++ b/packages/ui/certd-client/package.json @@ -1,6 +1,6 @@ { "name": "@certd/ui-client", - "version": "1.23.1", + "version": "1.24.0", "private": true, "scripts": { "dev": "vite --open", @@ -57,7 +57,7 @@ "vuedraggable": "^2.24.3" }, "devDependencies": { - "@certd/pipeline": "^1.23.1", + "@certd/pipeline": "^1.24.0", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@types/chai": "^4.3.12", diff --git a/packages/ui/certd-client/src/api/modules/api.basic.ts b/packages/ui/certd-client/src/api/modules/api.basic.ts index efc433d7..4507857a 100644 --- a/packages/ui/certd-client/src/api/modules/api.basic.ts +++ b/packages/ui/certd-client/src/api/modules/api.basic.ts @@ -5,9 +5,20 @@ export type SysPublicSetting = { managerOtherUserPipeline: boolean; }; +export type SysInstallInfo = { + siteId: string; +}; + export async function getSysPublicSettings(): Promise { return await request({ url: "/basic/settings/public", method: "get" }); } + +export async function getInstallInfo(): Promise { + return await request({ + url: "/basic/settings/install", + method: "get" + }); +} diff --git a/packages/ui/certd-client/src/api/modules/api.user.ts b/packages/ui/certd-client/src/api/modules/api.user.ts index 6eccf47d..d51e38eb 100644 --- a/packages/ui/certd-client/src/api/modules/api.user.ts +++ b/packages/ui/certd-client/src/api/modules/api.user.ts @@ -60,7 +60,14 @@ export async function mine(): Promise { }); } return await request({ - url: "/sys/authority/user/mine", + url: "/mine/info", + method: "post" + }); +} + +export async function getPlusInfo() { + return await request({ + url: "/mine/plusInfo", method: "post" }); } diff --git a/packages/ui/certd-client/src/components/index.ts b/packages/ui/certd-client/src/components/index.ts index aadec6bf..1a522c5f 100644 --- a/packages/ui/certd-client/src/components/index.ts +++ b/packages/ui/certd-client/src/components/index.ts @@ -1,15 +1,18 @@ import PiContainer from "./container.vue"; import PiAccessSelector from "../views/certd/access/access-selector/index.vue"; import PiDnsProviderSelector from "./dns-provider-selector/index.vue"; -import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";import PiEditable from "./editable.vue"; +import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue"; +import PiEditable from "./editable.vue"; +import VipButton from "./vip-button/index.vue"; import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue"; export default { - install(app:any) { + install(app: any) { app.component("PiContainer", PiContainer); app.component("PiAccessSelector", PiAccessSelector); app.component("PiEditable", PiEditable); app.component("PiOutputSelector", PiOutputSelector); app.component("PiDnsProviderSelector", PiDnsProviderSelector); + app.component("VipButton", VipButton); app.component("CheckCircleOutlined", CheckCircleOutlined); app.component("InfoCircleOutlined", InfoCircleOutlined); diff --git a/packages/ui/certd-client/src/components/vip-button/api.ts b/packages/ui/certd-client/src/components/vip-button/api.ts new file mode 100644 index 00000000..19379fa0 --- /dev/null +++ b/packages/ui/certd-client/src/components/vip-button/api.ts @@ -0,0 +1,9 @@ +import { request } from "/src/api/service"; + +export async function doActive(form: any) { + return await request({ + url: "/sys/plus/active", + method: "post", + data: form + }); +} diff --git a/packages/ui/certd-client/src/components/vip-button/index.vue b/packages/ui/certd-client/src/components/vip-button/index.vue new file mode 100644 index 00000000..46dd4da6 --- /dev/null +++ b/packages/ui/certd-client/src/components/vip-button/index.vue @@ -0,0 +1,148 @@ + + + + diff --git a/packages/ui/certd-client/src/layout/layout-framework.vue b/packages/ui/certd-client/src/layout/layout-framework.vue index c3d58f08..0a84e037 100644 --- a/packages/ui/certd-client/src/layout/layout-framework.vue +++ b/packages/ui/certd-client/src/layout/layout-framework.vue @@ -12,14 +12,14 @@ -
+
+ +
- -
@@ -83,10 +83,11 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue"; import FsThemeSet from "/@/layout/components/theme/index.vue"; import { env } from "../utils/util.env"; import FsThemeModeSet from "./components/theme/mode-set.vue"; +import VipButton from "/@/components/vip-button/index.vue"; export default { name: "LayoutFramework", // eslint-disable-next-line vue/no-unused-components - components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet }, + components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet, VipButton }, setup() { const resourceStore = useResourceStore(); const frameworkMenus = computed(() => { @@ -133,6 +134,7 @@ export default { .fs-framework { height: 100%; overflow-x: hidden; + min-width: 1200px; .menu-fold { display: flex; justify-content: center; @@ -174,34 +176,41 @@ export default { padding: 5px; } } - .header-buttons { - display: flex; - align-items: center; - & > * { - cursor: pointer; - padding: 0 10px; - } - height: 100%; - & > .header-btn { - display: inline-flex; - justify-content: center; + .ant-layout-header.header { + display: flex; + justify-content: space-between; + align-items: center; + .header-buttons { + display: flex; align-items: center; + & > * { + cursor: pointer; + padding: 0 10px; + } height: 100%; - //border-bottom: 1px solid rgba(255, 255, 255, 0); - &:hover { - background-color: #fff; + + & > .header-btn { + display: inline-flex; + justify-content: center; + align-items: center; + height: 100%; + //border-bottom: 1px solid rgba(255, 255, 255, 0); + &:hover { + background-color: #fff; + } } } + .header-right { + justify-content: flex-end; + align-items: center; + display: flex; + } + .header-menu { + flex: 1; + } } - .header-right { - justify-content: flex-end; - align-items: center; - display: flex; - } - .header-menu { - flex: 1; - } + .aside-menu { flex: 1; ui { diff --git a/packages/ui/certd-client/src/layout/layout-outside.vue b/packages/ui/certd-client/src/layout/layout-outside.vue index 63e5cd16..0dc736fa 100644 --- a/packages/ui/certd-client/src/layout/layout-outside.vue +++ b/packages/ui/certd-client/src/layout/layout-outside.vue @@ -1,7 +1,6 @@ - +