Merge remote-tracking branch 'origin/v2' into v2

pull/148/head
xiaojunnuo 2024-08-26 09:59:10 +08:00
commit f3ddcd3054
112 changed files with 2155 additions and 654 deletions

View File

@ -59,7 +59,7 @@ jobs:
password: ${{ secrets.aliyun_cs_password }} password: ${{ secrets.aliyun_cs_password }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6.5.0 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true

View File

@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
### Bug Fixes ### Bug Fixes

30
LICENSE.md Normal file
View File

@ -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 LicenseAGPL并附加以下条款。
## 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.
- 可用于商业用途。

View File

@ -1,7 +1,7 @@
# CertD # Certd
CertD 是一个免费全自动申请和自动部署更新SSL证书的工具。 Certd 是一个免费全自动申请和自动部署更新SSL证书的工具。
后缀D取自linux守护进程的命名风格意为证书守护进程。 后缀d取自linux守护进程的命名风格意为证书守护进程。
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签 关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签
@ -147,6 +147,7 @@ http://your_server_ip:7001
* [Cloudflare](./doc/cf/cf.md) * [Cloudflare](./doc/cf/cf.md)
* [腾讯云](./doc/tencent/tencent.md) * [腾讯云](./doc/tencent/tencent.md)
* [windows主机](./doc/host/host.md) * [windows主机](./doc/host/host.md)
* [google证书](./doc/google/google.md)
## 八、问题处理 ## 八、问题处理
@ -185,26 +186,37 @@ docker compose up -d
</p> </p>
## 十、捐赠 ## 十、捐赠
媳妇儿说:“一天到晚搞开源,也不管管老婆孩子!😡😡😡” 支持开源,为爱发电,我已入驻爱发电
拜托各位捐赠支持一下,让媳妇儿开心开心,我也能有更多时间进行开源项目,感谢🙏🙏🙏 https://afdian.com/a/greper
<p align="center">
<img height="380" src="./doc/images/donate.png"> 发电权益:
</p> 1. 可加入发电专属群(先加我好友,发送发电截图,我拉你进群)
2. 你的需求优先实现
3. 可以获得作者一对一技术支持
4. 更多权益陆续增加中...
## 十一、贡献代码 ## 十一、贡献代码
[贡献插件教程](./plugin.md) 1. [贡献插件教程](./plugin.md)
2. 作为贡献者,代表您同意您贡献的代码如下许可:
1. 可以调整开源协议以使其更严格或更宽松。
2. 可以用于商业用途。
## 十二、 开源许可
* 本项目遵循 GNU Affero General Public LicenseAGPL开源协议。
* 允许个人和公司使用、复制、修改和分发本项目,禁止任何形式的商业用途
* 未获得商业授权情况下禁止任何对logo、版权信息及授权许可相关代码的修改。
* 如需商业授权,请联系作者。
## 十二、我的其他项目求Star ## 十、我的其他项目求Star
* [袖手GPT](https://ai.handsfree.work/) ChatGPT国内可用无需FQ每日免费额度 * [袖手GPT](https://ai.handsfree.work/) ChatGPT国内可用无需FQ每日免费额度
* [fast-crud](https://gitee.com/fast-crud/fast-crud/) 基于vue3的crud快速开发框架 * [fast-crud](https://gitee.com/fast-crud/fast-crud/) 基于vue3的crud快速开发框架
* [dev-sidecar](https://github.com/docmirror/dev-sidecar/) 直连访问github工具无需FQ解决github无法访问的问题 * [dev-sidecar](https://github.com/docmirror/dev-sidecar/) 直连访问github工具无需FQ解决github无法访问的问题
## 十、更新日志 ## 十、更新日志
更新日志:[CHANGELOG](./CHANGELOG.md) 更新日志:[CHANGELOG](./CHANGELOG.md)

View File

@ -1 +1 @@
11:39 1

37
doc/google/google.md Normal file
View File

@ -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、 其他就跟正常申请证书一样了

View File

@ -15,11 +15,12 @@ services:
# 如果出现getaddrinfo ENOTFOUND等错误可以尝试修改或注释dns配置 # 如果出现getaddrinfo ENOTFOUND等错误可以尝试修改或注释dns配置
- 223.5.5.5 - 223.5.5.5
- 223.6.6.6 - 223.6.6.6
- 8.8.8.8 # ↓↓↓↓ ----------------------------------------------------------如果你服务器部署在国外可以用8.8.8.8替换上面的dns【可选】
- 8.8.4.4 # - 8.8.8.8
# - 8.8.4.4
environment: # 环境变量 environment: # 环境变量
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- certd_system_resetAdminPassword=false - certd_system_resetAdminPasswd=false
# ↑↑↑↑↑---------------------------4、如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false【可选】 # ↑↑↑↑↑---------------------------4、如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false【可选】
- certd_cron_immediateTriggerOnce=false - certd_cron_immediateTriggerOnce=false
# ↑↑↑↑↑---------------------------5、如果设置为true启动后所有配置了cron的流水线任务都将被立即触发一次【可选】 # ↑↑↑↑↑---------------------------5、如果设置为true启动后所有配置了cron的流水线任务都将被立即触发一次【可选】

View File

@ -9,5 +9,5 @@
} }
}, },
"npmClient": "pnpm", "npmClient": "pnpm",
"version": "1.23.1" "version": "1.24.0"
} }

View File

@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/acme-client
@ -110,10 +126,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# Changelog # 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 * `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) ## 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) ## v5.3.0 (2024-02-05)
* `added` Support and tests for satisfying `tls-alpn-01` challenges * `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 * `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 * Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
* This change is not considered breaking since the previous behavior was incorrect * This change is not considered breaking since the previous behavior was incorrect

View File

@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"private": false, "private": false,
"author": "nmorsman", "author": "nmorsman",
"version": "1.22.6", "version": "1.24.0",
"main": "src/index.js", "main": "src/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"license": "MIT", "license": "MIT",
@ -16,24 +16,24 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@peculiar/x509": "^1.10.0", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.7.2", "axios": "^1.7.2",
"debug": "^4.1.1", "debug": "^4.3.5",
"https-proxy-agent": "^7.0.4", "https-proxy-agent": "^7.0.4",
"node-forge": "^1.3.1" "node-forge": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.12", "@types/node": "^20.14.10",
"chai": "^4.4.1", "chai": "^4.4.1",
"chai-as-promised": "^7.1.2", "chai-as-promised": "^7.1.2",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"jsdoc-to-markdown": "^8.0.1", "jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.4.0", "mocha": "^10.6.0",
"nock": "^13.5.4", "nock": "^13.5.4",
"tsd": "^0.31.0", "tsd": "^0.31.1",
"typescript": "^4.8.4", "typescript": "^4.8.4",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },

View File

@ -3,6 +3,7 @@
*/ */
const util = require('./util'); const util = require('./util');
const { log } = require('./logger');
/** /**
* AcmeApi * AcmeApi
@ -17,6 +18,21 @@ class AcmeApi {
this.accountUrl = accountUrl; 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 * Get account URL
* *
@ -103,7 +119,7 @@ class AcmeApi {
/* Set account URL */ /* Set account URL */
if (resp.headers.location) { if (resp.headers.location) {
this.accountUrl = resp.headers.location; this.accountUrl = this.getLocationFromHeader(resp);
} }
return resp; return resp;

View File

@ -137,9 +137,13 @@ module.exports = async (client, userOpts) => {
} }
else { else {
log(`[auto] [${d}] Running challenge verification`); 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 */ /* Complete challenge and wait for valid status */
log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`); log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`);
await client.completeChallenge(challenge); await client.completeChallenge(challenge);
@ -170,11 +174,41 @@ module.exports = async (client, userOpts) => {
throw e; throw e;
} }
}; };
const domainSets = [];
const challengePromises = authorizations.map((authz) => async () => { authorizations.forEach((authz) => {
await challengeFunc(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) { function runAllPromise(tasks) {
let promise = Promise.resolve(); let promise = Promise.resolve();
tasks.forEach((task) => { tasks.forEach((task) => {
@ -195,9 +229,18 @@ module.exports = async (client, userOpts) => {
} }
try { try {
log('开始challenge'); log(`开始challenge${allChallengePromises.length}`);
await runPromisePa(challengePromises); 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('challenge结束');
// log('[auto] Waiting for challenge valid status'); // log('[auto] Waiting for challenge valid status');

View File

@ -3,10 +3,14 @@
*/ */
const axios = require('axios'); const axios = require('axios');
const { parseRetryAfterHeader } = require('./util');
const { log } = require('./logger');
const pkg = require('./../package.json'); const pkg = require('./../package.json');
const { AxiosError } = axios;
/** /**
* Instance * Defaults
*/ */
const instance = axios.create(); const instance = axios.create();
@ -19,6 +23,9 @@ instance.defaults.acmeSettings = {
httpChallengePort: 80, httpChallengePort: 80,
httpsChallengePort: 443, httpsChallengePort: 443,
tlsAlpnChallengePort: 443, tlsAlpnChallengePort: 443,
retryMaxAttempts: 5,
retryDefaultDelay: 5,
}; };
// instance.defaults.proxy = { // instance.defaults.proxy = {
// host: '192.168.34.139', // host: '192.168.34.139',
@ -33,6 +40,85 @@ instance.defaults.acmeSettings = {
instance.defaults.adapter = 'http'; 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 * Export instance
*/ */

View File

@ -300,7 +300,8 @@ class AcmeClient {
} }
/* Add URL to response */ /* Add URL to response */
resp.data.url = resp.headers.location; resp.data.url = this.api.getLocationFromHeader(resp);
return resp.data; return resp.data;
} }
@ -490,6 +491,9 @@ class AcmeClient {
const keyAuthorization = await this.getChallengeKeyAuthorization(challenge); const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);
const verifyFn = async () => { const verifyFn = async () => {
if (this.opts.signal && this.opts.signal.aborted) {
throw new Error('用户取消');
}
await verify[challenge.type](authz, challenge, keyAuthorization); await verify[challenge.type](authz, challenge, keyAuthorization);
}; };
@ -513,6 +517,9 @@ class AcmeClient {
*/ */
async completeChallenge(challenge) { async completeChallenge(challenge) {
if (this.opts.signal && this.opts.signal.aborted) {
throw new Error('用户取消');
}
const resp = await this.api.completeChallenge(challenge.url, {}); const resp = await this.api.completeChallenge(challenge.url, {});
return resp.data; return resp.data;
} }
@ -550,6 +557,10 @@ class AcmeClient {
} }
const verifyFn = async (abort) => { 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]); const resp = await this.api.apiRequest(item.url, null, [200]);
/* Verify status */ /* Verify status */

View File

@ -10,6 +10,7 @@
const net = require('net'); const net = require('net');
const { promisify } = require('util'); const { promisify } = require('util');
const forge = require('node-forge'); const forge = require('node-forge');
const { createPrivateEcdsaKey, getPublicKey } = require('./index');
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair); const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
@ -378,13 +379,17 @@ function formatCsrAltNames(altNames) {
* }, certificateKey); * }, certificateKey);
*/ */
exports.createCsr = async (data, key = null) => { exports.createCsr = async (data, keyType = null) => {
if (!key) { let key = null;
if (keyType === 'ec') {
key = await createPrivateEcdsaKey();
}
else {
key = await createPrivateKey(data.keySize); key = await createPrivateKey(data.keySize);
} }
else if (!Buffer.isBuffer(key)) { // else if (!Buffer.isBuffer(key)) {
key = Buffer.from(key); // key = Buffer.from(key);
} // }
if (typeof data.altNames === 'undefined') { if (typeof data.altNames === 'undefined') {
data.altNames = []; data.altNames = [];
@ -396,6 +401,8 @@ exports.createCsr = async (data, key = null) => {
const privateKey = forge.pki.privateKeyFromPem(key); const privateKey = forge.pki.privateKeyFromPem(key);
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
csr.publicKey = publicKey; 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 */ /* 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)) { if (data.commonName && !data.altNames.includes(data.commonName)) {

View File

@ -55,7 +55,7 @@ class HttpClient {
*/ */
async request(url, method, opts = {}) { 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 // eslint-disable-next-line no-restricted-syntax
for (const key in this.urlMapping.mappings) { for (const key in this.urlMapping.mappings) {
if (url.includes(key)) { if (url.includes(key)) {
@ -93,9 +93,11 @@ class HttpClient {
*/ */
async getDirectory() { 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)) { if (!this.directoryCache || (age > this.directoryMaxAge)) {
log(`Refreshing ACME directory, age: ${age}`);
const resp = await this.request(this.directoryUrl, 'get'); const resp = await this.request(this.directoryUrl, 'get');
if (resp.status >= 400) { if (resp.status >= 400) {
@ -107,6 +109,7 @@ class HttpClient {
} }
this.directoryCache = resp.data; this.directoryCache = resp.data;
this.directoryTimestamp = now;
} }
return this.directoryCache; return this.directoryCache;
@ -131,7 +134,7 @@ class HttpClient {
* *
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
* *
* @returns {Promise<string>} nonce * @returns {Promise<string>} Nonce
*/ */
async getNonce() { async getNonce() {

View File

@ -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` * @param {string} rel Link relation, default: `alternate`
* @returns {string[]} Array of URLs * @returns {string[]} Array of URLs
*/ */
@ -102,6 +105,37 @@ function parseLinkHeader(header, rel = 'alternate') {
return results.filter((r) => r); 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 * Find certificate chain with preferred issuer common name
* - If issuer is found in multiple chains, the closest to root wins * - If issuer is found in multiple chains, the closest to root wins
@ -161,14 +195,16 @@ function findCertificateChainForIssuer(chains, issuer) {
function formatResponseError(resp) { function formatResponseError(resp) {
let result; let result;
if (resp.data.error) { if (resp.data) {
result = resp.data.error.detail || resp.data.error; if (resp.data.error) {
} result = resp.data.error.detail || resp.data.error;
else { }
result = resp.data.detail || JSON.stringify(resp.data); 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 = { module.exports = {
retry, retry,
parseLinkHeader, parseLinkHeader,
parseRetryAfterHeader,
findCertificateChainForIssuer, findCertificateChainForIssuer,
formatResponseError, formatResponseError,
getAuthoritativeDnsResolver, getAuthoritativeDnsResolver,

View File

@ -12,33 +12,12 @@ const pkg = require('./../package.json');
describe('http', () => { describe('http', () => {
let testClient; let testClient;
const endpoint = `http://${uuid()}.example.com`;
const defaultUserAgent = `node-${pkg.name}/${pkg.version}`; const defaultUserAgent = `node-${pkg.name}/${pkg.version}`;
const customUserAgent = 'custom-ua-123'; const customUserAgent = 'custom-ua-123';
const primaryEndpoint = `http://${uuid()}.example.com`; afterEach(() => {
const defaultUaEndpoint = `http://${uuid()}.example.com`; nock.cleanAll();
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;
}); });
/** /**
@ -54,7 +33,8 @@ describe('http', () => {
*/ */
it('should http get', async () => { 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.isObject(resp);
assert.strictEqual(resp.status, 200); assert.strictEqual(resp.status, 200);
@ -66,28 +46,76 @@ describe('http', () => {
*/ */
it('should request using default user-agent', async () => { 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.isObject(resp);
assert.strictEqual(resp.status, 200); assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok'); assert.strictEqual(resp.data, 'ok');
}); });
it('should not request using custom user-agent', async () => { it('should reject using custom user-agent', async () => {
await assert.isRejected(testClient.request(customUaEndpoint, 'get')); 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 () => { 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; 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.isObject(resp);
assert.strictEqual(resp.status, 200); assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok'); assert.strictEqual(resp.data, 'ok');
}); });
it('should not request using default user-agent', async () => { it('should reject using default user-agent', async () => {
axios.defaults.headers.common['User-Agent'] = customUserAgent; nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok');
await assert.isRejected(testClient.request(defaultUaEndpoint, 'get')); 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);
}); });
}); });

View File

@ -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('<https://example.com/a>;rel="alternate"');
assert.isArray(r1);
assert.strictEqual(r1.length, 1);
assert.strictEqual(r1[0], 'https://example.com/a');
const r2 = util.parseLinkHeader('<https://example.com/b>;rel="test"');
assert.isArray(r2);
assert.strictEqual(r2.length, 0);
const r3 = util.parseLinkHeader('<http://example.com/c>; rel="test"', 'test');
assert.isArray(r3);
assert.strictEqual(r3.length, 1);
assert.strictEqual(r3[0], 'http://example.com/c');
const r4 = util.parseLinkHeader(`<https://example.com/a>; rel="alternate",
<https://example.com/x>; rel="nope",
<https://example.com/b>;rel="alternate",
<https://example.com/c>; 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()');
});

View File

@ -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-----

View File

@ -29,6 +29,13 @@ if (process.env.ACME_TLSALPN_PORT) {
axios.defaults.acmeSettings.tlsAlpnChallengePort = 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 * External account binding
*/ */

View File

@ -45,6 +45,7 @@ export interface ClientOptions {
backoffMin?: number; backoffMin?: number;
backoffMax?: number; backoffMax?: number;
urlMapping?: UrlMapping; urlMapping?: UrlMapping;
signal?: AbortSignal;
} }
export interface ClientExternalAccountBindingOptions { export interface ClientExternalAccountBindingOptions {
@ -61,6 +62,7 @@ export interface ClientAutoOptions {
skipChallengeVerification?: boolean; skipChallengeVerification?: boolean;
challengePriority?: string[]; challengePriority?: string[];
preferredChain?: string; preferredChain?: string;
signal?: AbortSignal;
} }
export class Client { export class Client {

View File

@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/pipeline

View File

@ -1 +1 @@
11:38 14:26

View File

@ -1,7 +1,7 @@
{ {
"name": "@certd/pipeline", "name": "@certd/pipeline",
"private": false, "private": false,
"version": "1.23.1", "version": "1.24.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",

View File

@ -23,16 +23,19 @@ export type ExecutorOptions = {
emailService: IEmailService; emailService: IEmailService;
fileRootDir?: string; fileRootDir?: string;
}; };
export class Executor { export class Executor {
pipeline: Pipeline; pipeline: Pipeline;
runtime!: RunHistory; runtime!: RunHistory;
contextFactory: ContextFactory; contextFactory: ContextFactory;
logger: Logger; logger: Logger;
pipelineContext!: IContext; pipelineContext!: IContext;
currentStatusMap!: RunnableCollection;
lastStatusMap!: RunnableCollection; lastStatusMap!: RunnableCollection;
lastRuntime!: RunHistory; lastRuntime!: RunHistory;
options: ExecutorOptions; options: ExecutorOptions;
canceled = false; abort: AbortController = new AbortController();
onChanged: (history: RunHistory) => Promise<void>; onChanged: (history: RunHistory) => Promise<void>;
constructor(options: ExecutorOptions) { constructor(options: ExecutorOptions) {
this.options = options; this.options = options;
@ -50,10 +53,11 @@ export class Executor {
const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`); const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`);
this.lastRuntime = lastRuntime; this.lastRuntime = lastRuntime;
this.lastStatusMap = new RunnableCollection(lastRuntime?.pipeline); this.lastStatusMap = new RunnableCollection(lastRuntime?.pipeline);
this.currentStatusMap = new RunnableCollection(this.pipeline);
} }
async cancel() { async cancel() {
this.canceled = true; this.abort.abort();
this.runtime?.cancel(this.pipeline); this.runtime?.cancel(this.pipeline);
await this.onChanged(this.runtime); await this.onChanged(this.runtime);
} }
@ -102,6 +106,8 @@ export class Executor {
} }
} }
if (lastResult != null && lastResult === ResultType.success && !inputChanged) { if (lastResult != null && lastResult === ResultType.success && !inputChanged) {
runnable.status!.output = lastNode?.status?.output;
runnable.status!.files = lastNode?.status?.files;
this.runtime.skip(runnable); this.runtime.skip(runnable);
await this.onChanged(this.runtime); await this.onChanged(this.runtime);
return ResultType.skip; return ResultType.skip;
@ -113,10 +119,15 @@ export class Executor {
// const timeout = runnable.timeout ?? 20 * 60 * 1000; // const timeout = runnable.timeout ?? 20 * 60 * 1000;
try { try {
if (this.canceled) { if (this.abort.signal.aborted) {
this.runtime.cancel(runnable);
return ResultType.canceled; return ResultType.canceled;
} }
await run(); await run();
if (this.abort.signal.aborted) {
this.runtime.cancel(runnable);
return ResultType.canceled;
}
this.runtime.success(runnable); this.runtime.success(runnable);
return ResultType.success; return ResultType.success;
} catch (e: any) { } catch (e: any) {
@ -197,7 +208,7 @@ export class Executor {
private async runStep(step: Step) { private async runStep(step: Step) {
const currentLogger = this.runtime._loggers[step.id]; const currentLogger = this.runtime._loggers[step.id];
this.currentStatusMap.add(step);
const lastStatus = this.lastStatusMap.get(step.id); const lastStatus = this.lastStatusMap.get(step.id);
//执行任务 //执行任务
const plugin: RegistryItem<AbstractTaskPlugin> = pluginRegistry.get(step.type); const plugin: RegistryItem<AbstractTaskPlugin> = pluginRegistry.get(step.type);
@ -211,16 +222,11 @@ export class Executor {
if (item.component?.name === "pi-output-selector") { if (item.component?.name === "pi-output-selector") {
const contextKey = step.input[key]; const contextKey = step.input[key];
if (contextKey != null) { if (contextKey != null) {
const value = this.runtime.context[contextKey]; // "cert": "step.-BNFVPMKPu2O-i9NiOQxP.cert",
if (value == null) { const arr = contextKey.split(".");
currentLogger.warn(`[step init] input ${define.title} is null前置任务步骤输出值为空请按如下步骤排查`); const id = arr[1];
currentLogger.log(`1、检查前置任务证书申请任务是否是配置了成功后跳过如果是请改为正常执行`); const outputKey = arr[2];
currentLogger.log( step.input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
`2、是否曾经删除过前置任务证书申请任务然后又重新添加了如果是请重新编辑当前任务重新选择一下前置任务输出的参数域名证书那一栏`
);
currentLogger.log(`3、以上都不是联系作者吧`);
}
step.input[key] = value;
} }
} }
}); });
@ -241,6 +247,7 @@ export class Executor {
parent: this.runtime.id, parent: this.runtime.id,
rootDir: this.options.fileRootDir, rootDir: this.options.fileRootDir,
}), }),
signal: this.abort.signal,
}; };
instance.setCtx(taskCtx); instance.setCtx(taskCtx);
@ -254,8 +261,8 @@ export class Executor {
//输出上下文变量到output context //输出上下文变量到output context
_.forEach(define.output, (item: any, key: any) => { _.forEach(define.output, (item: any, key: any) => {
step.status!.output[key] = instance[key]; step.status!.output[key] = instance[key];
const stepOutputKey = `step.${step.id}.${key}`; // const stepOutputKey = `step.${step.id}.${key}`;
this.runtime.context[stepOutputKey] = instance[key]; // this.runtime.context[stepOutputKey] = instance[key];
}); });
step.status!.files = instance.getFiles(); step.status!.files = instance.getFiles();

View File

@ -54,7 +54,10 @@ export class FileStore {
deleteByParent(scope: string, parent: string) { deleteByParent(scope: string, parent: string) {
const dir = path.join(this.rootDir, scope, parent); const dir = path.join(this.rootDir, scope, parent);
if (fs.existsSync(dir)) { if (fs.existsSync(dir)) {
fs.unlinkSync(dir); fs.rmSync(dir, {
recursive: true,
force: true,
});
} }
} }
} }

View File

@ -3,7 +3,6 @@ import { equal } from "assert";
describe("license", function () { describe("license", function () {
it("#license", async function () { it("#license", async function () {
const req = { const req = {
appKey: "z4nXOeTeSnnpUpnmsV",
subjectId: "999", subjectId: "999",
license: "", license: "",
}; };

View File

@ -1,9 +1,18 @@
import { createVerify } from "node:crypto"; import { createVerify } from "node:crypto";
import { logger } from "../utils/index.js"; 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 = { export type LicenseVerifyReq = {
subjectId: string; subjectId: string;
license: string; license: string;
@ -18,11 +27,16 @@ type License = {
duration: number; duration: number;
version: number; version: number;
secret: string; secret: string;
vipType: string;
signature: string; signature: string;
}; };
class LicenseHolder { class LicenseHolder {
isPlus = false; isPlus = false;
expireTime = 0;
vipType = "";
message?: string = undefined;
secret?: string = undefined;
} }
const holder = new LicenseHolder(); const holder = new LicenseHolder();
holder.isPlus = false; holder.isPlus = false;
@ -35,9 +49,22 @@ class LicenseVerifier {
return await this.verify(req); return await this.verify(req);
} }
setPlus(value: boolean) { setPlus(value: boolean, info: any = {}) {
holder.isPlus = value; if (value && info) {
return value; 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) { async verify(req: LicenseVerifyReq) {
this.licenseReq = req; this.licenseReq = req;
@ -50,21 +77,29 @@ class LicenseVerifier {
return this.setPlus(false); 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); const json: License = JSON.parse(licenseJson);
if (json.expireTime < Date.now()) { if (json.expireTime < Date.now()) {
logger.warn("授权已过期"); 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 publicKey = Buffer.from(SecreteKey, "base64").toString();
const res = this.verifySignature(content, json.signature, publicKey); const res = this.verifySignature(content, json.signature, publicKey);
this.checked = true; this.checked = true;
if (!res) { if (!res) {
logger.warn("授权校验失败"); 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) { verifySignature(content: string, signature: any, publicKey: string) {
@ -77,9 +112,27 @@ class LicenseVerifier {
const verifier = new LicenseVerifier(); const verifier = new LicenseVerifier();
export function isPlus() { 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) { 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: "授权校验失败" });
}
} }

View File

@ -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 _ from "lodash-es";
import { buildLogger } from "../utils/util.log.js"; import { buildLogger } from "../utils/util.log.js";
import { Logger } from "log4js"; import { Logger } from "log4js";
@ -14,15 +14,12 @@ export type RunTrigger = {
export function NewRunHistory(obj: any) { export function NewRunHistory(obj: any) {
const history = new RunHistory(obj.id, obj.trigger, obj.pipeline); const history = new RunHistory(obj.id, obj.trigger, obj.pipeline);
history.context = obj.context;
history.logs = obj.logs; history.logs = obj.logs;
history._loggers = obj.loggers; history._loggers = obj.loggers;
return history; return history;
} }
export class RunHistory { export class RunHistory {
id!: string; id!: string;
//运行时上下文变量
context: Context = {};
pipeline!: Pipeline; pipeline!: Pipeline;
logs: { logs: {
[runnableId: string]: string[]; [runnableId: string]: string[];
@ -168,4 +165,8 @@ export class RunnableCollection {
item.status = undefined; item.status = undefined;
}); });
} }
add(runnable: Runnable) {
this.collection[runnable.id] = runnable;
}
} }

View File

@ -6,7 +6,7 @@ import { IAccessService } from "../access/index.js";
import { IEmailService } from "../service/index.js"; import { IEmailService } from "../service/index.js";
import { IContext } from "../core/index.js"; import { IContext } from "../core/index.js";
import { AxiosInstance } from "axios"; import { AxiosInstance } from "axios";
import { logger } from "../utils/index.js"; import { ILogger, logger } from "../utils/index.js";
export enum ContextScope { export enum ContextScope {
global, global,
@ -64,11 +64,15 @@ export type TaskInstanceContext = {
http: AxiosInstance; http: AxiosInstance;
fileStore: FileStore; fileStore: FileStore;
lastStatus?: Runnable; lastStatus?: Runnable;
signal: AbortSignal;
}; };
export abstract class AbstractTaskPlugin implements ITaskPlugin { export abstract class AbstractTaskPlugin implements ITaskPlugin {
_result: TaskResult = { clearLastStatus: false, files: [], pipelineVars: {} }; _result: TaskResult = { clearLastStatus: false, files: [], pipelineVars: {} };
ctx!: TaskInstanceContext; ctx!: TaskInstanceContext;
logger!: ILogger;
accessService!: IAccessService;
clearLastStatus() { clearLastStatus() {
this._result.clearLastStatus = true; this._result.clearLastStatus = true;
} }
@ -79,6 +83,8 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
setCtx(ctx: TaskInstanceContext) { setCtx(ctx: TaskInstanceContext) {
this.ctx = ctx; this.ctx = ctx;
this.logger = ctx.logger;
this.accessService = ctx.accessService;
} }
randomFileId() { randomFileId() {

View File

@ -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.error(`请求出错url:${error?.response?.config.url},method:${error?.response?.config?.method},status:${error?.response?.status}`);
logger.info("返回数据:", JSON.stringify(error?.response?.data)); logger.info("返回数据:", JSON.stringify(error?.response?.data));
delete error.config; 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); return Promise.reject(error);
} }
); );

View File

@ -72,7 +72,7 @@ async function spawn(opts: SpawnOption): Promise<string> {
let stderr = ""; let stderr = "";
return safePromise((resolve, reject) => { return safePromise((resolve, reject) => {
const ls = childProcess.spawn(cmd, { const ls = childProcess.spawn(cmd, {
shell: process.platform == "win32", shell: true,
env: { env: {
...process.env, ...process.env,
...opts.env, ...opts.env,

View File

@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @certd/lib-k8s

View File

@ -1,7 +1,7 @@
{ {
"name": "@certd/lib-k8s", "name": "@certd/lib-k8s",
"private": false, "private": false,
"version": "1.23.1", "version": "1.24.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@ -13,11 +13,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"kubernetes-client": "^9.0.0", "@kubernetes/client-node": "0.21.0"
"shelljs": "^0.8.5"
}, },
"devDependencies": { "devDependencies": {
"@certd/pipeline": "^1.23.1", "@certd/pipeline": "^1.24.0",
"@rollup/plugin-commonjs": "^23.0.4", "@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-json": "^6.0.0", "@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-node-resolve": "^15.0.1",

View File

@ -1,32 +1,40 @@
import kubernetesClient from 'kubernetes-client'; import { KubeConfig, CoreV1Api, V1Secret, NetworkingV1Api, V1Ingress } from '@kubernetes/client-node';
//@ts-ignore
import dns from 'dns'; import dns from 'dns';
import { ILogger } from '@certd/pipeline'; import { ILogger } from '@certd/pipeline';
//@ts-ignore export type K8sClientOpts = {
const { KubeConfig, Client, Request } = kubernetesClient;
export class K8sClient {
kubeConfigStr: string; kubeConfigStr: string;
lookup!: any;
client!: any;
logger: ILogger; logger: ILogger;
constructor(kubeConfigStr: string, logger: ILogger) { //{ [domain]:{ip:'xxx.xx.xxx'} }
this.kubeConfigStr = kubeConfigStr; //暂时没用
this.logger = logger; 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(); this.init();
} }
init() { init() {
const kubeconfig = new KubeConfig(); const kubeconfig = new KubeConfig();
kubeconfig.loadFromString(this.kubeConfigStr); kubeconfig.loadFromString(this.kubeConfigStr);
const reqOpts = { kubeconfig, request: {} } as any; this.kubeconfig = kubeconfig;
if (this.lookup) { this.client = kubeconfig.makeApiClient(CoreV1Api);
reqOpts.request.lookup = this.lookup;
}
const backend = new Request(reqOpts); // const reqOpts = { kubeconfig, request: {} } as any;
this.client = new Client({ backend, version: '1.13' }); // 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'} } * @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
*/ */
setLookup(localRecords: { [key: string]: { ip: string } }) { setLookup(localRecords: { [key: string]: { ip: string } }) {
if (localRecords == null) {
return;
}
this.lookup = (hostnameReq: any, options: any, callback: any) => { this.lookup = (hostnameReq: any, options: any, callback: any) => {
this.logger.info('custom lookup', hostnameReq, localRecords); this.logger.info('custom lookup', hostnameReq, localRecords);
if (localRecords[hostnameReq]) { if (localRecords[hostnameReq]) {
@ -43,7 +54,6 @@ export class K8sClient {
dns.lookup(hostnameReq, options, callback); dns.lookup(hostnameReq, options, callback);
} }
}; };
this.init();
} }
/** /**
@ -51,9 +61,9 @@ export class K8sClient {
* @param opts = {namespace:default} * @param opts = {namespace:default}
* @returns secretsList * @returns secretsList
*/ */
async getSecret(opts: { namespace: string }) { async getSecrets(opts: { namespace: string }) {
const namespace = opts.namespace || 'default'; 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} * @param opts {namespace:default, body:yamlStr}
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
async createSecret(opts: any) { async createSecret(opts: { namespace: string; body: V1Secret }) {
const namespace = opts.namespace || 'default'; const namespace = opts.namespace || 'default';
const created = await this.client.api.v1.namespaces(namespace).secrets.post({ const created = await this.client.createNamespacedSecret(namespace, opts.body);
body: opts.body, this.logger.info('new secrets:', created.body);
}); return created.body;
this.logger.info('new secrets:', created);
return created;
} }
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 namespace = opts.namespace || 'default';
const secretName = opts.secretName; const secretName = opts.secretName;
if (secretName == null) { if (secretName == null) {
throw new Error('secretName 不能为空'); throw new Error('secretName 不能为空');
} }
return await this.client.api.v1.namespaces(namespace).secrets(secretName).put({ const res = await this.client.patchNamespacedSecret(secretName, namespace, opts.body);
body: 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 namespace = opts.namespace || 'default';
const secretName = opts.secretName; const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
if (secretName == null) { const res = await client.listNamespacedIngress(namespace);
throw new Error('secretName 不能为空'); this.logger.info('ingress list get:', res.body);
} return res.body;
return await this.client.api.v1.namespaces(namespace).secrets(secretName).patch({
body: opts.body,
});
} }
async getIngressList(opts: any) { // async getIngress(opts: { namespace: string; ingressName: string }) {
const namespace = opts.namespace || 'default'; // const namespace = opts.namespace || 'default';
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses.get(); // 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 namespace = opts.namespace || 'default';
const ingressName = opts.ingressName; const ingressName = opts.ingressName;
if (!ingressName) { if (!ingressName) {
throw new Error('ingressName 不能为空'); throw new Error('ingressName 不能为空');
} }
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get(); const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
} const res = await client.patchNamespacedIngress(ingressName, namespace, opts.body);
this.logger.info('ingress patched:', res.body);
async patchIngress(opts: any) { return res;
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,
});
} }
} }

View File

@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
### Performance Improvements ### Performance Improvements

View File

@ -1,7 +1,7 @@
{ {
"name": "@certd/plugin-cert", "name": "@certd/plugin-cert",
"private": false, "private": false,
"version": "1.23.1", "version": "1.24.0",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@ -13,8 +13,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@certd/acme-client": "^1.22.6", "@certd/acme-client": "^1.24.0",
"@certd/pipeline": "^1.23.1", "@certd/pipeline": "^1.24.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"node-forge": "^0.10.0", "node-forge": "^0.10.0",
"psl": "^1.9.0" "psl": "^1.9.0"

View File

@ -13,7 +13,8 @@ export type CertInfo = {
key: string; key: string;
csr: 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 = { type AcmeServiceOptions = {
userContext: IContext; userContext: IContext;
logger: Logger; logger: Logger;
@ -21,6 +22,8 @@ type AcmeServiceOptions = {
eab?: ClientExternalAccountBindingOptions; eab?: ClientExternalAccountBindingOptions;
skipLocalVerify?: boolean; skipLocalVerify?: boolean;
useMappingProxy?: boolean; useMappingProxy?: boolean;
privateKeyType?: PrivateKeyType;
signal?: AbortSignal;
}; };
export class AcmeService { export class AcmeService {
@ -42,8 +45,20 @@ export class AcmeService {
}); });
} }
async getAccountConfig(email: string): Promise<any> { async getAccountConfig(email: string, urlMapping: UrlMapping): Promise<any> {
return (await this.userContext.getObj(this.buildAccountKey(email))) || {}; 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) { buildAccountKey(email: string) {
@ -55,7 +70,14 @@ export class AcmeService {
} }
async getAcmeClient(email: string, isTest = false): Promise<acme.Client> { async getAcmeClient(email: string, isTest = false): Promise<acme.Client> {
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) { if (conf.key == null) {
conf.key = await this.createNewKey(); conf.key = await this.createNewKey();
await this.saveAccountConfig(email, conf); await this.saveAccountConfig(email, conf);
@ -66,22 +88,19 @@ export class AcmeService {
} else { } else {
directoryUrl = acme.directory[this.sslProvider].production; directoryUrl = acme.directory[this.sslProvider].production;
} }
const urlMapping: UrlMapping = { enabled: false, mappings: {} };
if (this.options.useMappingProxy) { if (this.options.useMappingProxy) {
urlMapping.enabled = true; urlMapping.enabled = true;
urlMapping.mappings = {
"acme-v02.api.letsencrypt.org": "letsencrypt.proxy.handsfree.work",
};
} }
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: directoryUrl, directoryUrl: directoryUrl,
accountKey: conf.key, accountKey: conf.key,
accountUrl: conf.accountUrl, accountUrl: conf.accountUrl,
externalAccountBinding: this.eab, externalAccountBinding: this.eab,
backoffAttempts: 30, backoffAttempts: 15,
backoffMin: 5000, backoffMin: 5000,
backoffMax: 10000, backoffMax: 10000,
urlMapping, urlMapping,
signal: this.options.signal,
}); });
if (conf.accountUrl == null) { 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<CertInfo> {
const { email, isTest, domains, csrInfo, dnsProvider } = options; const { email, isTest, domains, csrInfo, dnsProvider } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest); const client: acme.Client = await this.getAcmeClient(email, isTest);
/* Create CSR */ /* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains); const { commonName, altNames } = this.buildCommonNameByDomains(domains);
let privateKey = null;
const [key, csr] = await acme.forge.createCsr({ const privateKeyType = options.privateKeyType || "rsa_2048";
commonName, const privateKeyArr = privateKeyType.split("_");
...csrInfo, const type = privateKeyArr[0];
altNames, 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) { if (dnsProvider == null) {
throw new Error("dnsProvider 不能为空"); throw new Error("dnsProvider 不能为空");
} }
@ -221,6 +260,7 @@ export class AcmeService {
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => { challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider); return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider);
}, },
signal: this.options.signal,
}); });
const cert: CertInfo = { const cert: CertInfo = {

View File

@ -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 dayjs from "dayjs";
import type { CertInfo } from "./acme.js"; import type { CertInfo } from "./acme.js";
import { Logger } from "log4js";
import { CertReader } from "./cert-reader.js"; import { CertReader } from "./cert-reader.js";
import JSZip from "jszip"; import JSZip from "jszip";
@ -91,9 +90,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
// }) // })
csrInfo!: string; csrInfo!: string;
logger!: Logger;
userContext!: IContext; userContext!: IContext;
accessService!: IAccessService;
http!: HttpClient; http!: HttpClient;
lastStatus!: Step; lastStatus!: Step;
@ -103,8 +100,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
cert?: CertInfo; cert?: CertInfo;
async onInstance() { async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
this.userContext = this.ctx.userContext; this.userContext = this.ctx.userContext;
this.http = this.ctx.http; this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step; this.lastStatus = this.ctx.lastStatus as Step;
@ -138,10 +133,10 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
const cert: CertInfo = certReader.toCertInfo(); const cert: CertInfo = certReader.toCertInfo();
this.cert = cert; this.cert = cert;
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.validity.notAfter).valueOf(); this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
if (isNew) { 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); await this.zipCert(cert, applyTime);
} else { } else {
this.extendsFiles(); this.extendsFiles();

View File

@ -1,8 +1,8 @@
import { CertInfo } from "./acme.js"; import { CertInfo } from "./acme.js";
import fs from "fs"; import fs from "fs";
import os from "os"; import os from "os";
import forge from "node-forge";
import path from "path"; import path from "path";
import { crypto } from "@certd/acme-client";
export class CertReader implements CertInfo { export class CertReader implements CertInfo {
crt: string; crt: string;
key: string; key: string;
@ -29,9 +29,8 @@ export class CertReader implements CertInfo {
} }
getCrtDetail(crt: string) { getCrtDetail(crt: string) {
const pki = forge.pki; const detail = crypto.readCertificateInfo(crt.toString());
const detail = pki.certificateFromPem(crt.toString()); const expires = detail.notAfter;
const expires = detail.validity.notAfter;
return { detail, expires }; return { detail, expires };
} }

View File

@ -1,5 +1,5 @@
import { Decorator, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; 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 { AcmeService } from "./acme.js";
import _ from "lodash-es"; import _ from "lodash-es";
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js"; import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js";
@ -33,14 +33,34 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
vModel: "value", vModel: "value",
options: [ options: [
{ value: "letsencrypt", label: "Let's Encrypt" }, { value: "letsencrypt", label: "Let's Encrypt" },
// { value: "letsencrypt-proxy", label: "Let's Encrypt代理letsencrypt.org无法访问时使用" }, { value: "google", label: "Google" },
// { value: "buypass", label: "Buypass" },
{ value: "zerossl", label: "ZeroSSL" }, { 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, required: true,
}) })
sslProvider!: SSLProvider; privateKeyType!: PrivateKeyType;
@TaskInput({ @TaskInput({
title: "EAB授权", title: "EAB授权",
@ -49,7 +69,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
type: "eab", type: "eab",
}, },
maybeNeed: true, 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; eabAccessId!: number;
@ -87,7 +108,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "a-switch", name: "a-switch",
vModel: "checked", vModel: "checked",
}, },
helper: "如果acme-v02.api.letsencrypt.org被墙无法连接访问请尝试开启此选项", maybeNeed: true,
helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问请尝试开启此选项",
}) })
useProxy = false; useProxy = false;
@ -116,6 +138,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
eab, eab,
skipLocalVerify: this.skipLocalVerify, skipLocalVerify: this.skipLocalVerify,
useMappingProxy: this.useProxy, useMappingProxy: this.useProxy,
privateKeyType: this.privateKeyType,
}); });
} }
@ -156,6 +179,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
dnsProvider, dnsProvider,
csrInfo, csrInfo,
isTest: false, isTest: false,
privateKeyType: this.privateKeyType,
}); });
const certInfo = this.formatCerts(cert); const certInfo = this.formatCerts(cert);

View File

@ -97,7 +97,7 @@ export class CertApplyLegoPlugin extends CertApplyBasePlugin {
this.http = this.ctx.http; this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step; this.lastStatus = this.ctx.lastStatus as Step;
if (this.legoEabAccessId) { if (this.legoEabAccessId) {
this.eab = await this.ctx.accessService.getById(this.legoEabAccessId); this.eab = await this.accessService.getById(this.legoEabAccessId);
} }
} }
async onInit(): Promise<void> {} async onInit(): Promise<void> {}

View File

@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
### Performance Improvements ### Performance Improvements

View File

@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-client", "name": "@certd/ui-client",
"version": "1.23.1", "version": "1.24.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --open", "dev": "vite --open",
@ -57,7 +57,7 @@
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
}, },
"devDependencies": { "devDependencies": {
"@certd/pipeline": "^1.23.1", "@certd/pipeline": "^1.24.0",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12", "@types/chai": "^4.3.12",

View File

@ -5,9 +5,20 @@ export type SysPublicSetting = {
managerOtherUserPipeline: boolean; managerOtherUserPipeline: boolean;
}; };
export type SysInstallInfo = {
siteId: string;
};
export async function getSysPublicSettings(): Promise<SysPublicSetting> { export async function getSysPublicSettings(): Promise<SysPublicSetting> {
return await request({ return await request({
url: "/basic/settings/public", url: "/basic/settings/public",
method: "get" method: "get"
}); });
} }
export async function getInstallInfo(): Promise<SysInstallInfo> {
return await request({
url: "/basic/settings/install",
method: "get"
});
}

View File

@ -60,7 +60,14 @@ export async function mine(): Promise<UserInfoRes> {
}); });
} }
return await request({ 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" method: "post"
}); });
} }

View File

@ -1,15 +1,18 @@
import PiContainer from "./container.vue"; import PiContainer from "./container.vue";
import PiAccessSelector from "../views/certd/access/access-selector/index.vue"; import PiAccessSelector from "../views/certd/access/access-selector/index.vue";
import PiDnsProviderSelector from "./dns-provider-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"; import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
export default { export default {
install(app:any) { install(app: any) {
app.component("PiContainer", PiContainer); app.component("PiContainer", PiContainer);
app.component("PiAccessSelector", PiAccessSelector); app.component("PiAccessSelector", PiAccessSelector);
app.component("PiEditable", PiEditable); app.component("PiEditable", PiEditable);
app.component("PiOutputSelector", PiOutputSelector); app.component("PiOutputSelector", PiOutputSelector);
app.component("PiDnsProviderSelector", PiDnsProviderSelector); app.component("PiDnsProviderSelector", PiDnsProviderSelector);
app.component("VipButton", VipButton);
app.component("CheckCircleOutlined", CheckCircleOutlined); app.component("CheckCircleOutlined", CheckCircleOutlined);
app.component("InfoCircleOutlined", InfoCircleOutlined); app.component("InfoCircleOutlined", InfoCircleOutlined);

View File

@ -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
});
}

View File

@ -0,0 +1,148 @@
<template>
<div class="layout-vip isPlus">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line"></fs-icon>
<div class="text">
<template v-if="userStore.isPlus">
<a-tooltip>
<template #title> 到期时间{{ expireTime }} </template>
<span @click="openUpgrade">{{ texts.plus }}</span>
</a-tooltip>
</template>
<template v-else>
<a-tooltip>
<template #title> 升级专业版享受更多VIP特权 </template>
<span @click="openUpgrade"> {{ texts.free }} {{ expiredDays }} </span>
</a-tooltip>
</template>
</div>
</div>
</template>
<script lang="tsx" setup>
import { ref, reactive, computed } from "vue";
import { useUserStore } from "/src/store/modules/user";
import dayjs from "dayjs";
import { message, Modal } from "ant-design-vue";
import * as api from "./api";
import { useSettingStore } from "/@/store/modules/settings";
const props = defineProps<{
mode?: "button" | "nav";
}>();
type Texts = {
plus: string;
free: string;
};
const texts = computed<Texts>(() => {
if (props.mode === "button") {
return {
plus: "专业版已开通",
free: "此为专业版功能"
};
} else {
return {
plus: "专业版",
free: "免费版"
};
}
});
const userStore = useUserStore();
const expireTime = computed(() => {
if (userStore.isPlus) {
return dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD");
}
return "";
});
const expiredDays = computed(() => {
if (userStore.plusInfo?.isPlus && !userStore.isPlus) {
//
const days = dayjs().diff(dayjs(userStore.plusInfo.expireTime), "day");
return `专业版已过期${days}`;
}
return "";
});
const formState = reactive({
code: ""
});
async function doActive() {
if (!formState.code) {
message.error("请输入激活码");
throw new Error("请输入激活码");
}
const res = await api.doActive(formState);
if (res) {
await userStore.reInit();
Modal.success({
title: "激活成功",
content: `您已成功激活专业版,有效期至:${dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD")}`
});
}
}
const settingStore = useSettingStore();
const computedSiteId = computed(() => settingStore.installInfo?.siteId);
const [modal, contextHolder] = Modal.useModal();
function openUpgrade() {
const placeholder = "请输入激活码";
modal.confirm({
title: "升级/续期专业版",
async onOk() {
return await doActive();
},
okText: "激活",
width: 500,
content: () => {
return (
<div class="mt-10 mb-10">
<div>
<h3 class="block-header">专业版特权</h3>
<ul>
<li>证书流水线数量无限制</li>
<li>可加VIP群需求优先实现</li>
<li>更多特权敬请期待</li>
</ul>
</div>
<div>
<h3 class="block-header">立刻激活/续期</h3>
<div class="mt-10">
<div class="flex-o w-100">
<span>站点ID</span>
<fs-copyable class="flex-1" v-model={computedSiteId.value}></fs-copyable>
</div>
<a-input class="mt-10" v-model:value={formState.code} placeholder={placeholder} />
</div>
<div class="mt-10">
没有激活码
<a href="https://afdian.com/a/greper" target="_blank">
爱发电赞助VIP会员后获取
</a>
</div>
</div>
</div>
);
}
});
}
</script>
<style lang="less">
.layout-vip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
&.isPlus {
color: #c5913f;
}
.text {
margin-left: 5px;
}
}
</style>

View File

@ -12,14 +12,14 @@
<a-layout class="layout-body"> <a-layout class="layout-body">
<a-layout-header class="header"> <a-layout-header class="header">
<div class="header-buttons"> <div class="header-left header-buttons">
<div class="menu-fold" @click="asideCollapsedToggle"> <div class="menu-fold" @click="asideCollapsedToggle">
<MenuUnfoldOutlined v-if="asideCollapsed" /> <MenuUnfoldOutlined v-if="asideCollapsed" />
<MenuFoldOutlined v-else /> <MenuFoldOutlined v-else />
</div> </div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<vip-button class="flex-center header-btn"></vip-button>
</div> </div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<div class="header-right header-buttons"> <div class="header-right header-buttons">
<!-- <button--> <!-- <button-->
<!-- w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"--> <!-- w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"-->
@ -83,10 +83,11 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue";
import FsThemeSet from "/@/layout/components/theme/index.vue"; import FsThemeSet from "/@/layout/components/theme/index.vue";
import { env } from "../utils/util.env"; import { env } from "../utils/util.env";
import FsThemeModeSet from "./components/theme/mode-set.vue"; import FsThemeModeSet from "./components/theme/mode-set.vue";
import VipButton from "/@/components/vip-button/index.vue";
export default { export default {
name: "LayoutFramework", name: "LayoutFramework",
// eslint-disable-next-line vue/no-unused-components // 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() { setup() {
const resourceStore = useResourceStore(); const resourceStore = useResourceStore();
const frameworkMenus = computed(() => { const frameworkMenus = computed(() => {
@ -133,6 +134,7 @@ export default {
.fs-framework { .fs-framework {
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
min-width: 1200px;
.menu-fold { .menu-fold {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -174,34 +176,41 @@ export default {
padding: 5px; padding: 5px;
} }
} }
.header-buttons {
display: flex;
align-items: center;
& > * {
cursor: pointer;
padding: 0 10px;
}
height: 100%;
& > .header-btn { .ant-layout-header.header {
display: inline-flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center;
.header-buttons {
display: flex;
align-items: center; align-items: center;
& > * {
cursor: pointer;
padding: 0 10px;
}
height: 100%; height: 100%;
//border-bottom: 1px solid rgba(255, 255, 255, 0);
&:hover { & > .header-btn {
background-color: #fff; 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 { .aside-menu {
flex: 1; flex: 1;
ui { ui {

View File

@ -1,7 +1,6 @@
<template> <template>
<div id="userLayout" :class="['user-layout-wrapper']"> <div id="userLayout" :class="['user-layout-wrapper']">
<div class="login-container flex-center"> <div class="login-container flex-center">
<div class="user-layout-lang"></div>
<div class="user-layout-content"> <div class="user-layout-content">
<div class="top flex flex-col items-center justify-center"> <div class="top flex flex-col items-center justify-center">
<div class="header flex flex-row items-center"> <div class="header flex flex-row items-center">
@ -146,7 +145,6 @@ export default {
// position: absolute; // position: absolute;
width: 100%; width: 100%;
bottom: 0; bottom: 0;
padding: 0 16px;
margin: 48px 0 24px; margin: 48px 0 24px;
text-align: center; text-align: center;

View File

@ -33,6 +33,7 @@ router.beforeEach(async (to, from, next) => {
// 请根据自身业务需要修改 // 请根据自身业务需要修改
const token = userStore.getToken; const token = userStore.getToken;
if (token) { if (token) {
await userStore.init();
next(); next();
} else { } else {
// 没有登录的时候跳转到登录界面 // 没有登录的时候跳转到登录界面

View File

@ -5,7 +5,7 @@ import _ from "lodash-es";
import { LocalStorage } from "/src/utils/util.storage"; import { LocalStorage } from "/src/utils/util.storage";
import * as basicApi from "/@/api/modules/api.basic"; import * as basicApi from "/@/api/modules/api.basic";
import { SysPublicSetting } from "/@/api/modules/api.basic"; import { SysInstallInfo, SysPublicSetting } from "/@/api/modules/api.basic";
export type ThemeToken = { export type ThemeToken = {
token: { token: {
@ -21,6 +21,9 @@ export interface SettingState {
themeConfig?: ThemeConfig; themeConfig?: ThemeConfig;
themeToken: ThemeToken; themeToken: ThemeToken;
sysPublic?: SysPublicSetting; sysPublic?: SysPublicSetting;
installInfo?: {
siteId: string;
};
} }
const defaultThemeConfig = { const defaultThemeConfig = {
@ -39,6 +42,9 @@ export const useSettingStore = defineStore({
sysPublic: { sysPublic: {
registerEnabled: false, registerEnabled: false,
managerOtherUserPipeline: false managerOtherUserPipeline: false
},
installInfo: {
siteId: ""
} }
}), }),
getters: { getters: {
@ -47,12 +53,18 @@ export const useSettingStore = defineStore({
}, },
getSysPublic(): SysPublicSetting { getSysPublic(): SysPublicSetting {
return this.sysPublic; return this.sysPublic;
},
getInstallInfo(): SysInstallInfo {
return this.installInfo;
} }
}, },
actions: { actions: {
async loadSysSettings() { async loadSysSettings() {
const settings = await basicApi.getSysPublicSettings(); const settings = await basicApi.getSysPublicSettings();
_.merge(this.sysPublic, settings); _.merge(this.sysPublic, settings);
const installInfo = await basicApi.getInstallInfo();
_.merge(this.installInfo, installInfo);
}, },
persistThemeConfig() { persistThemeConfig() {
LocalStorage.set(SETTING_THEME_KEY, this.getThemeConfig); LocalStorage.set(SETTING_THEME_KEY, this.getThemeConfig);

View File

@ -1,21 +1,28 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "../index";
import router from "../../router"; import router from "../../router";
// @ts-ignore // @ts-ignore
import { LocalStorage } from "/src/utils/util.storage"; import { LocalStorage } from "/src/utils/util.storage";
// @ts-ignore // @ts-ignore
import * as UserApi from "/src/api/modules/api.user"; import * as UserApi from "/src/api/modules/api.user";
import { RegisterReq } from "/src/api/modules/api.user";
// @ts-ignore // @ts-ignore
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user"; import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
import { Modal, notification } from "ant-design-vue"; import { Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { mitter } from "/src/utils/util.mitt"; import { mitter } from "/src/utils/util.mitt";
import { RegisterReq } from "/src/api/modules/api.user";
interface UserState { interface UserState {
userInfo: Nullable<UserInfoRes>; userInfo: Nullable<UserInfoRes>;
token?: string; token?: string;
plusInfo?: PlusInfo;
inited: boolean;
}
interface PlusInfo {
vipType: string;
expireTime: number;
isPlus: boolean;
} }
const USER_INFO_KEY = "USER_INFO"; const USER_INFO_KEY = "USER_INFO";
@ -26,7 +33,10 @@ export const useUserStore = defineStore({
// user info // user info
userInfo: null, userInfo: null,
// token // token
token: undefined token: undefined,
// plus
plusInfo: null,
inited: false
}), }),
getters: { getters: {
getUserInfo(): UserInfoRes { getUserInfo(): UserInfoRes {
@ -37,6 +47,9 @@ export const useUserStore = defineStore({
}, },
isAdmin(): boolean { isAdmin(): boolean {
return this.getUserInfo?.id === 1; return this.getUserInfo?.id === 1;
},
isPlus(): boolean {
return this.plusInfo?.isPlus && this.plusInfo?.expireTime > new Date().getTime();
} }
}, },
actions: { actions: {
@ -73,10 +86,7 @@ export const useUserStore = defineStore({
// save token // save token
this.setToken(token, expire); this.setToken(token, expire);
// get user info // get user info
const userInfo = await this.getUserInfoAction(); return await this.onLoginSuccess(data);
await router.replace("/");
mitter.emit("app.login", { userInfo, token: data });
return userInfo;
} catch (error) { } catch (error) {
return null; return null;
} }
@ -86,6 +96,19 @@ export const useUserStore = defineStore({
this.setUserInfo(userInfo); this.setUserInfo(userInfo);
return userInfo; return userInfo;
}, },
async onLoginSuccess(loginData: any) {
await this.getUserInfoAction();
await this.loadPlusInfo();
const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { userInfo, token: loginData, plusInfo: this.plusInfo });
await router.replace("/");
return userInfo;
},
async loadPlusInfo() {
this.plusInfo = await UserApi.getPlusInfo();
},
/** /**
* @description: logout * @description: logout
*/ */
@ -108,6 +131,19 @@ export const useUserStore = defineStore({
await this.logout(true); await this.logout(true);
} }
}); });
},
async init() {
if (this.inited) {
return;
}
if (this.getToken) {
await this.loadPlusInfo();
}
this.inited = true;
},
async reInit() {
this.inited = false;
await this.init();
} }
} }
}); });

View File

@ -14,9 +14,6 @@ html, body {
box-sizing: border-box; box-sizing: border-box;
} }
body{
min-width: 1000px;
}
div#app { div#app {
height: 100% height: 100%
} }
@ -48,6 +45,11 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important; vertical-align: 0 !important;
} }
.flex-center{
display: flex;
justify-content: center;
align-items: center;
}
.flex-o{ .flex-o{
display: flex !important; display: flex !important;
align-items: center; align-items: center;
@ -108,3 +110,10 @@ h1, h2, h3, h4, h5, h6 {
.w-100{ .w-100{
width: 100%; width: 100%;
} }
.block-header{
margin:3px;
padding-top: 15px;
padding-bottom:3px;
border-bottom: 1px solid #dedede;
}

View File

@ -342,7 +342,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
title: "历史记录保持数", title: "历史记录保持数",
type: "number", type: "number",
form: { form: {
value: 30, value: 20,
helper: "历史记录保持条数,多余的会被删除" helper: "历史记录保持条数,多余的会被删除"
}, },
column: { column: {

View File

@ -12,6 +12,9 @@ export default {
modelValue: { modelValue: {
type: String, type: String,
default: undefined default: undefined
},
from: {
type: String
} }
}, },
emits: ["update:modelValue"], emits: ["update:modelValue"],
@ -23,7 +26,7 @@ export default {
const currentStepIndex = inject("currentStepIndex") as Ref<number>; const currentStepIndex = inject("currentStepIndex") as Ref<number>;
const currentTask = inject("currentTask") as Ref<any>; const currentTask = inject("currentTask") as Ref<any>;
const getPluginGroups = inject("getPluginGroups") as Ref<any>; const getPluginGroups = inject("getPluginGroups") as any;
const pluginGroups = getPluginGroups(); const pluginGroups = getPluginGroups();
function onCreate() { function onCreate() {
options.value = pluginGroups.getPreStepOutputOptions({ options.value = pluginGroups.getPreStepOutputOptions({
@ -32,6 +35,9 @@ export default {
currentStepIndex: currentStepIndex.value, currentStepIndex: currentStepIndex.value,
currentTask: currentTask.value currentTask: currentTask.value
}); });
if (props.from) {
options.value = options.value.filter((item: any) => item.type === props.from);
}
if (props.modelValue == null && options.value.length > 0) { if (props.modelValue == null && options.value.length > 0) {
ctx.emit("update:modelValue", options.value[0].value); ctx.emit("update:modelValue", options.value[0].value);
} }
@ -42,7 +48,7 @@ export default {
watch( watch(
() => { () => {
return pluginGroups.value.map; return pluginGroups.value?.map;
}, },
() => { () => {
onCreate(); onCreate();

View File

@ -61,24 +61,7 @@
<fs-form-item v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" /> <fs-form-item v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
</template> </template>
<fs-form-item <fs-form-item v-model="currentStep.strategy.runStrategy" :item="runStrategyProps" :get-context-fn="blankFn" />
v-model="currentStep.strategy.runStrategy"
:item="{
title: '运行策略',
key: 'strategy.runStrategy',
component: {
name: 'a-select',
vModel: 'value',
options: [
{ value: 0, label: '正常运行(证书申请任务请选择它)' },
{ value: 1, label: '成功后跳过(非证书任务请选择它)' }
]
},
helper: '该任务运行成功一次之后下次运行是否跳过,保持默认即可',
rules: [{ required: true, message: '此项必填' }]
}"
:get-context-fn="blankFn"
/>
</a-form> </a-form>
<template #footer> <template #footer>
@ -289,10 +272,36 @@ export default {
}; };
} }
const runStrategyProps = ref({
title: "运行策略",
key: "strategy.runStrategy",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: 0, label: "正常运行(证书申请任务请选择它)" },
{ value: 1, label: "成功后跳过(非证书任务请选择它)" }
]
},
helper: {
render: () => {
return (
<div>
<div>正常运行每次都运行证书任务需要每次都运行</div>
<div>成功后跳过在证书没变化时该任务成功一次之后跳过不重复部署</div>
<div>保持默认即可如果你想要再次测试部署可以临时设置为正常运行</div>
</div>
);
}
},
rules: [{ required: true, message: "此项必填" }]
});
return { return {
...useStepForm(), ...useStepForm(),
labelCol: { span: 6 }, labelCol: { span: 6 },
wrapperCol: { span: 16 } wrapperCol: { span: 16 },
runStrategyProps
}; };
} }
}; };

View File

@ -1,49 +1,77 @@
<template> <template>
<fs-page class="page-setting-email"> <fs-page class="page-setting-email">
<template #header> <template #header>
<div class="title">邮件设置</div> <div class="title">
邮件设置
<span class="sub">设置邮件发送服务器</span>
</div>
</template> </template>
<div class="email-form">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
<a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
<a-input v-model:value="formState.host" />
</a-form-item>
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]"> <div class="flex-o">
<a-input v-model:value="formState.port" /> <div v-if="!formState.usePlus" class="email-form">
</a-form-item> <a-form
:model="formState"
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]"> name="basic"
<a-input v-model:value="formState.auth.user" /> :label-col="{ span: 8 }"
</a-form-item> :wrapper-col="{ span: 16 }"
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]"> autocomplete="off"
<a-input-password v-model:value="formState.auth.pass" /> @finish="onFinish"
<div class="helper">如果是qq邮箱需要到qq邮箱的设置里面申请授权码作为密码</div> @finish-failed="onFinishFailed"
</a-form-item> >
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]"> <a-form-item label="使用自定义邮件服务器"> </a-form-item>
<a-input v-model:value="formState.sender" /> <a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
</a-form-item> <a-input v-model:value="formState.host" />
<a-form-item label="是否ssl" name="secure">
<a-switch v-model:checked="formState.secure" />
<div class="helper">ssl和非ssl的smtp端口是不一样的注意修改端口</div>
</a-form-item>
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">保存</a-button>
</a-form-item>
</a-form>
<div>
<a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend">
<a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]">
<a-input v-model:value="testFormState.receiver" />
</a-form-item> </a-form-item>
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
<a-input v-model:value="formState.port" />
</a-form-item>
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model:value="formState.auth.user" />
</a-form-item>
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model:value="formState.auth.pass" />
<div class="helper">如果是qq邮箱需要到qq邮箱的设置里面申请授权码作为密码</div>
</a-form-item>
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
<a-input v-model:value="formState.sender" />
</a-form-item>
<a-form-item label="是否ssl" name="secure">
<a-switch v-model:checked="formState.secure" />
<div class="helper">ssl和非ssl的smtp端口是不一样的注意修改端口</div>
</a-form-item>
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }"> <a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">测试</a-button> <a-button type="primary" html-type="submit">保存</a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </div>
<div class="email-form">
<a-form :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
<a-form-item label="使用官方邮件服务器">
<div class="flex-o">
<a-switch v-model:checked="formState.usePlus" :disabled="!userStore.isPlus" @change="onUsePlusChanged" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">使用官方邮箱服务器直接发邮件免除繁琐的配置</div>
</a-form-item>
</a-form>
</div>
</div>
<div class="email-form">
<a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend">
<a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]">
<a-input v-model:value="testFormState.receiver" />
<div class="helper">发送失败可以试试使用官方邮件服务器</div>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" :loading="testFormState.loading" html-type="submit">测试</a-button>
</a-form-item>
</a-form>
</div> </div>
</fs-page> </fs-page>
</template> </template>
@ -55,6 +83,7 @@ import * as emailApi from "./api.email";
import { SettingKeys } from "./api"; import { SettingKeys } from "./api";
import { notification } from "ant-design-vue"; import { notification } from "ant-design-vue";
import { useUserStore } from "/@/store/modules/user";
interface FormState { interface FormState {
host: string; host: string;
@ -69,6 +98,7 @@ interface FormState {
rejectUnauthorized?: boolean; rejectUnauthorized?: boolean;
}; };
sender: string; sender: string;
usePlus: boolean;
} }
const formState = reactive<Partial<FormState>>({ const formState = reactive<Partial<FormState>>({
@ -76,7 +106,8 @@ const formState = reactive<Partial<FormState>>({
user: "", user: "",
pass: "" pass: ""
}, },
tls: {} tls: {},
usePlus: false
}); });
async function load() { async function load() {
@ -99,6 +130,10 @@ const onFinishFailed = (errorInfo: any) => {
// console.log("Failed:", errorInfo); // console.log("Failed:", errorInfo);
}; };
async function onUsePlusChanged() {
await api.SettingsSave(SettingKeys.Email, formState);
}
interface TestFormState { interface TestFormState {
receiver: string; receiver: string;
loading: boolean; loading: boolean;
@ -118,6 +153,8 @@ async function onTestSend() {
testFormState.loading = false; testFormState.loading = false;
} }
} }
const userStore = useUserStore();
</script> </script>
<style lang="less"> <style lang="less">
@ -127,9 +164,9 @@ async function onTestSend() {
margin: 20px; margin: 20px;
} }
.helper{ .helper {
padding:1px; padding: 1px;
margin:0px; margin: 0px;
color: #999; color: #999;
font-size: 10px; font-size: 10px;
} }

View File

@ -1,3 +1,6 @@
koa: koa:
port: 7001 port: 7001
plus:
server:
baseUrl: 'http://127.0.0.1:11007'

View File

@ -1,6 +1,3 @@
koa:
port: 7001
flyway: flyway:
scriptDir: './db/migration-pg' scriptDir: './db/migration-pg'
@ -14,3 +11,10 @@ typeorm:
password: root password: root
database: postgres database: postgres
#plus:
# server:
# baseUrl: 'https://api.ai.handsfree.work'
plus:
server:
baseUrl: 'http://127.0.0.1:11007'

View File

@ -1,2 +1,2 @@
koa: preview:
port: 7001 enabled: true

View File

@ -1,2 +1,12 @@
koa: preview:
port: 7001 enabled: false
typeorm:
dataSource:
default:
logging: false
plus:
server:
baseUrl: 'https://api.ai.handsfree.work'

View File

@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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 ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5))
* 修复ssh无法连接成功无法执行命令的bug ([41b9837](https://github.com/certd/certd/commit/41b9837582323fb400ef8525ce65e8b37ad4b36f))
### 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))
* 优化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) ## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
### Bug Fixes ### Bug Fixes

View File

@ -0,0 +1,5 @@
alter table cd_access alter column setting type text using setting::text;
alter table sys_settings alter column setting type text using setting::text;
alter table user_settings alter column setting type text using setting::text;
alter table pi_history_log alter column logs type text using logs::text;
alter table pi_history alter column pipeline type text using pipeline::text;

View File

@ -1,6 +1,6 @@
{ {
"name": "@certd/ui-server", "name": "@certd/ui-server",
"version": "1.23.1", "version": "1.24.0",
"description": "fast-server base midway", "description": "fast-server base midway",
"private": true, "private": true,
"type": "module", "type": "module",
@ -21,12 +21,12 @@
"@alicloud/cs20151215": "^3.0.3", "@alicloud/cs20151215": "^3.0.3",
"@alicloud/openapi-client": "^0.4.0", "@alicloud/openapi-client": "^0.4.0",
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
"@certd/acme-client": "^1.22.6", "@certd/acme-client": "^1.24.0",
"@certd/lib-huawei": "^1.22.1", "@certd/lib-huawei": "^1.22.1",
"@certd/lib-k8s": "^1.23.1", "@certd/lib-k8s": "^1.24.0",
"@certd/midway-flyway-js": "^1.22.6", "@certd/midway-flyway-js": "^1.22.6",
"@certd/pipeline": "^1.23.1", "@certd/pipeline": "^1.24.0",
"@certd/plugin-cert": "^1.23.1", "@certd/plugin-cert": "^1.24.0",
"@koa/cors": "^3.4.3", "@koa/cors": "^3.4.3",
"@midwayjs/bootstrap": "^3.16.2", "@midwayjs/bootstrap": "^3.16.2",
"@midwayjs/cache": "^3.14.0", "@midwayjs/cache": "^3.14.0",
@ -60,10 +60,11 @@
"nodemailer": "^6.9.3", "nodemailer": "^6.9.3",
"pg": "^8.12.0", "pg": "^8.12.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"ssh2": "^0.8.9", "ssh2": "^1.15.0",
"svg-captcha": "^1.4.0", "svg-captcha": "^1.4.0",
"tencentcloud-sdk-nodejs": "^4.0.44", "tencentcloud-sdk-nodejs": "^4.0.44",
"tencentcloud-sdk-nodejs-dnspod": "^4.0.866", "tencentcloud-sdk-nodejs-dnspod": "^4.0.866",
"tencentcloud-sdk-nodejs-teo": "^4.0.919",
"typeorm": "^0.3.11" "typeorm": "^0.3.11"
}, },
"devDependencies": { "devDependencies": {
@ -75,7 +76,7 @@
"@types/mocha": "^10.0.1", "@types/mocha": "^10.0.1",
"@types/node": "16", "@types/node": "16",
"@types/nodemailer": "^6.4.8", "@types/nodemailer": "^6.4.8",
"@types/ssh2": "^1.11.6", "@types/ssh2": "^1.15.0",
"c8": "^8.0.1", "c8": "^8.0.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"mocha": "^10.2.0", "mocha": "^10.2.0",

View File

@ -1,4 +1,5 @@
export const Constants = { export const Constants = {
dataDir: './data',
role: { role: {
defaultUser: 3, defaultUser: 3,
}, },
@ -31,6 +32,10 @@ export const Constants = {
code: 10, code: 10,
message: '参数错误', message: '参数错误',
}, },
needvip: {
code: 88,
message: '需要VIP',
},
auth: { auth: {
code: 401, code: 401,
message: '您还未登录或token已过期', message: '您还未登录或token已过期',

View File

@ -0,0 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
/**
* vip
*/
export class NeedVIPException extends BaseException {
constructor(message) {
super('NeedVIPException', Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
}
}

View File

@ -38,7 +38,10 @@ const development = {
}, },
}, },
cron: { cron: {
//启动时立即触发一次
immediateTriggerOnce: false, immediateTriggerOnce: false,
//启动时仅注册adminid=1用户的
onlyAdminUser: false,
}, },
/** /**
* *
@ -85,6 +88,9 @@ const development = {
system: { system: {
resetAdminPasswd: false, resetAdminPasswd: false,
}, },
plus: {
serverBaseUrl: 'http://127.0.0.1:11007',
},
} as MidwayConfig; } as MidwayConfig;
mergeConfig(development, 'development'); mergeConfig(development, 'development');

View File

@ -1,21 +0,0 @@
import { MidwayConfig } from '@midwayjs/core';
import { mergeConfig } from './loader.js';
const preview = {
/**
*
*/
preview: {
enabled: true,
},
typeorm: {
dataSource: {
default: {
logging: false,
},
},
},
} as MidwayConfig;
mergeConfig(preview, 'preview');
export default preview;

View File

@ -1,21 +0,0 @@
import { MidwayConfig } from '@midwayjs/core';
import { mergeConfig } from './loader.js';
const production = {
/**
*
*/
preview: {
enabled: false,
},
typeorm: {
dataSource: {
default: {
logging: false,
},
},
},
} as MidwayConfig;
mergeConfig(production, 'production');
export default production;

View File

@ -1,11 +0,0 @@
import { MidwayConfig } from '@midwayjs/core';
export default {
typeorm: {
dataSource: {
default: {
synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true
},
},
},
} as MidwayConfig;

View File

@ -1,7 +0,0 @@
import { MidwayConfig } from '@midwayjs/core';
export default {
koa: {
port: null,
},
} as MidwayConfig;

View File

@ -1,4 +1,4 @@
import { Configuration, App } from '@midwayjs/core'; import { App, Configuration } from '@midwayjs/core';
import * as koa from '@midwayjs/koa'; import * as koa from '@midwayjs/koa';
import * as orm from '@midwayjs/typeorm'; import * as orm from '@midwayjs/typeorm';
import * as cache from '@midwayjs/cache'; import * as cache from '@midwayjs/cache';
@ -8,18 +8,12 @@ import * as staticFile from '@midwayjs/static-file';
import * as cron from './modules/plugin/cron/index.js'; import * as cron from './modules/plugin/cron/index.js';
import * as flyway from '@certd/midway-flyway-js'; import * as flyway from '@certd/midway-flyway-js';
import cors from '@koa/cors'; import cors from '@koa/cors';
import { ReportMiddleware } from './middleware/report.js';
import { GlobalExceptionMiddleware } from './middleware/global-exception.js'; import { GlobalExceptionMiddleware } from './middleware/global-exception.js';
import { PreviewMiddleware } from './middleware/preview.js'; import { PreviewMiddleware } from './middleware/preview.js';
import { AuthorityMiddleware } from './middleware/authority.js'; import { AuthorityMiddleware } from './middleware/authority.js';
import { logger } from './utils/logger.js'; import { logger } from './utils/logger.js';
import { ResetPasswdMiddleware } from './middleware/reset-passwd/middleware.js'; import { ResetPasswdMiddleware } from './middleware/reset-passwd/middleware.js';
// import { DefaultErrorFilter } from './filter/default.filter.js';
// import { NotFoundFilter } from './filter/notfound.filter.js';
import DefaultConfig from './config/config.default.js'; import DefaultConfig from './config/config.default.js';
import ProductionConfig from './config/config.production.js';
import PreviewConfig from './config/config.preview.js';
import UnittestConfig from './config/config.unittest.js';
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
console.error('未捕获的异常:', error); console.error('未捕获的异常:', error);
@ -43,9 +37,6 @@ process.on('uncaughtException', error => {
importConfigs: [ importConfigs: [
{ {
default: DefaultConfig, default: DefaultConfig,
preview: PreviewConfig,
production: ProductionConfig,
unittest: UnittestConfig,
}, },
], ],
}) })
@ -68,7 +59,6 @@ export class MainConfiguration {
//this.app.use(bodyParser()); //this.app.use(bodyParser());
//请求日志打印 //请求日志打印
this.app.useMiddleware([ this.app.useMiddleware([
ReportMiddleware,
//统一异常处理 //统一异常处理
GlobalExceptionMiddleware, GlobalExceptionMiddleware,
//预览模式限制修改id<1000的数据 //预览模式限制修改id<1000的数据

View File

@ -12,7 +12,7 @@ export class GlobalExceptionMiddleware implements IWebMiddleware {
logger.info('请求开始:', url); logger.info('请求开始:', url);
try { try {
await next(); await next();
logger.info('请求完成', url, Date.now() - startTime + 'ms'); logger.info('请求完成:', url, Date.now() - startTime + 'ms');
} catch (err) { } catch (err) {
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err); logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
ctx.status = 200; ctx.status = 200;

View File

@ -1,27 +0,0 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
ctx.logger.info(
`Report in "src/middleware/report.middleware.ts", rt = ${
Date.now() - startTime
}ms`
);
// 返回给上一个中间件的结果
return result;
};
}
static getName(): string {
return 'report';
}
}

View File

@ -1,24 +0,0 @@
import { Provide } from '@midwayjs/core';
import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa';
import { logger } from '../utils/logger.js';
@Provide()
export class ReportMiddleware implements IWebMiddleware {
resolve() {
return async (ctx: IMidwayKoaContext, next: NextFunction) => {
const { url } = ctx;
logger.info('请求开始:', url);
const startTime = Date.now();
await next();
if (ctx.status !== 200) {
logger.error(
'请求失败:',
url,
ctx.status,
Date.now() - startTime + 'ms'
);
}
logger.info('请求完成:', url, ctx.status, Date.now() - startTime + 'ms');
};
}
}

View File

@ -1,12 +1,4 @@
import { import { Provide, Controller, Post, Inject, Body, Query, ALL } from '@midwayjs/core';
Provide,
Controller,
Post,
Inject,
Body,
Query,
ALL,
} from '@midwayjs/core';
import { UserService } from '../service/user-service.js'; import { UserService } from '../service/user-service.js';
import { CrudController } from '../../../basic/crud-controller.js'; import { CrudController } from '../../../basic/crud-controller.js';
import { RoleService } from '../service/role-service.js'; import { RoleService } from '../service/role-service.js';

View File

@ -191,4 +191,17 @@ export class UserService extends BaseService<UserEntity> {
}; };
await this.update(param); await this.update(param);
} }
async delete(ids: any) {
if (typeof ids === 'string') {
ids = ids.split(',');
ids = ids.map(id => parseInt(id));
}
if (ids instanceof Array) {
if (ids.includes(1)) {
throw new CommonException('不能删除管理员');
}
}
await super.delete(ids);
}
} }

View File

@ -8,8 +8,8 @@ export class AutoRegisterCron {
@Inject() @Inject()
pipelineService: PipelineService; pipelineService: PipelineService;
@Config('preview.enabled') @Config('cron.onlyAdminUser')
private preview: boolean; private onlyAdminUser: boolean;
// @Inject() // @Inject()
// echoPlugin: EchoPlugin; // echoPlugin: EchoPlugin;
@ -19,7 +19,7 @@ export class AutoRegisterCron {
@Init() @Init()
async init() { async init() {
logger.info('加载定时trigger开始'); logger.info('加载定时trigger开始');
await this.pipelineService.onStartup(this.immediateTriggerOnce, this.preview); await this.pipelineService.onStartup(this.immediateTriggerOnce, this.onlyAdminUser);
// logger.info(this.echoPlugin, this.echoPlugin.test); // logger.info(this.echoPlugin, this.echoPlugin.test);
// logger.info('加载定时trigger完成'); // logger.info('加载定时trigger完成');
// //

View File

@ -1,10 +1,10 @@
import { MidwayEnvironmentService } from '@midwayjs/core'; import { MidwayEnvironmentService } from '@midwayjs/core';
import { Controller, Get, Inject, Provide } from '@midwayjs/core'; import { Controller, Get, Inject, Provide } from '@midwayjs/core';
import { logger } from '../utils/logger.js'; import { logger } from '../../../utils/logger.js';
import { Constants } from '../basic/constants.js'; import { Constants } from '../../../basic/constants.js';
@Provide() @Provide()
@Controller('/hello') @Controller('/home')
export class HomeController { export class HomeController {
@Inject() @Inject()
environmentService: MidwayEnvironmentService; environmentService: MidwayEnvironmentService;

View File

@ -3,7 +3,7 @@ import { Controller, Get, Inject, Provide } from '@midwayjs/core';
import { BaseController } from '../../../basic/base-controller.js'; import { BaseController } from '../../../basic/base-controller.js';
import { Constants } from '../../../basic/constants.js'; import { Constants } from '../../../basic/constants.js';
import { SysSettingsService } from '../../system/service/sys-settings-service.js'; import { SysSettingsService } from '../../system/service/sys-settings-service.js';
import { SysPublicSettings } from '../../system/service/models.js'; import { SysInstallInfo, SysPublicSettings } from '../../system/service/models.js';
export class SmsCodeReq { export class SmsCodeReq {
@Rule(RuleType.number().required()) @Rule(RuleType.number().required())
@ -32,4 +32,10 @@ export class BasicSettingsController extends BaseController {
const settings = await this.sysSettingsService.getSetting(SysPublicSettings); const settings = await this.sysSettingsService.getSetting(SysPublicSettings);
return this.ok(settings); return this.ok(settings);
} }
@Get('/install', { summary: Constants.per.guest })
public async getInstallInfo() {
const settings = await this.sysSettingsService.getSetting(SysInstallInfo);
return this.ok(settings);
}
} }

View File

@ -1,10 +1,11 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import type { EmailSend } from '@certd/pipeline'; import type { EmailSend } from '@certd/pipeline';
import { IEmailService } from '@certd/pipeline'; import { IEmailService, isPlus } from '@certd/pipeline';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import type SMTPConnection from 'nodemailer/lib/smtp-connection'; import type SMTPConnection from 'nodemailer/lib/smtp-connection';
import { logger } from '../../../utils/logger.js'; import { logger } from '../../../utils/logger.js';
import { UserSettingsService } from '../../mine/service/user-settings-service.js'; import { UserSettingsService } from '../../mine/service/user-settings-service.js';
import { PlusService } from './plus-service.js';
export type EmailConfig = { export type EmailConfig = {
host: string; host: string;
@ -19,26 +20,60 @@ export type EmailConfig = {
rejectUnauthorized: boolean; rejectUnauthorized: boolean;
}; };
sender: string; sender: string;
usePlus?: boolean;
} & SMTPConnection.Options; } & SMTPConnection.Options;
@Provide() @Provide()
@Scope(ScopeEnum.Singleton) @Scope(ScopeEnum.Singleton)
export class EmailService implements IEmailService { export class EmailService implements IEmailService {
@Inject() @Inject()
settingsService: UserSettingsService; settingsService: UserSettingsService;
@Inject()
plusService: PlusService;
async sendByPlus(email: EmailSend) {
if (!isPlus()) {
throw new Error('plus not enabled');
}
/**
* userId: number;
* subject: string;
* content: string;
* receivers: string[];
*/
await this.plusService.request({
url: '/activation/emailSend',
data: {
subject: email.subject,
text: email.content,
to: email.receivers,
},
});
}
/** /**
*/ */
async send(email: EmailSend) { async send(email: EmailSend) {
console.log('sendEmail', email); console.log('sendEmail', email);
const emailConfigEntity = await this.settingsService.getByKey( const emailConfigEntity = await this.settingsService.getByKey('email', email.userId);
'email',
email.userId
);
if (emailConfigEntity == null || !emailConfigEntity.setting) { if (emailConfigEntity == null || !emailConfigEntity.setting) {
if (isPlus()) {
//自动使用plus发邮件
return await this.sendByPlus(email);
}
throw new Error('email settings 未设置'); throw new Error('email settings 未设置');
} }
const emailConfig = JSON.parse(emailConfigEntity.setting) as EmailConfig; const emailConfig = JSON.parse(emailConfigEntity.setting) as EmailConfig;
if (emailConfig.usePlus && isPlus()) {
return await this.sendByPlus(email);
}
await this.sendByCustom(emailConfig, email);
logger.info('sendEmail complete: ', email);
}
private async sendByCustom(emailConfig: EmailConfig, email: EmailSend) {
const transporter = nodemailer.createTransport(emailConfig); const transporter = nodemailer.createTransport(emailConfig);
const mailOptions = { const mailOptions = {
from: emailConfig.sender, from: emailConfig.sender,
@ -47,7 +82,6 @@ export class EmailService implements IEmailService {
text: email.content, text: email.content,
}; };
await transporter.sendMail(mailOptions); await transporter.sendMail(mailOptions);
logger.info('sendEmail complete: ', email);
} }
async test(userId: number, receiver: string) { async test(userId: number, receiver: string) {

View File

@ -0,0 +1,79 @@
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { SysSettingsService } from '../../system/service/sys-settings-service.js';
import { SysInstallInfo } from '../../system/service/models.js';
import { AppKey, getPlusInfo, isPlus } from '@certd/pipeline';
import * as crypto from 'crypto';
import { request } from '../../../utils/http.js';
import { logger } from '../../../utils/logger.js';
@Provide()
@Scope(ScopeEnum.Singleton)
export class PlusService {
@Inject()
sysSettingsService: SysSettingsService;
@Config('plus.server.baseUrl')
plusServerBaseUrl;
async requestWithoutSign(config: any): Promise<any> {
config.baseURL = this.plusServerBaseUrl;
return await request(config);
}
async request(config: any) {
if (!isPlus()) {
throw new Error('您还不是专业版,请先激活专业版');
}
const { url, data } = config;
const timestamps = Date.now();
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
const sign = await this.sign(data, timestamps);
const requestHeader = {
subjectId: installInfo.siteId,
appKey: AppKey,
sign: sign,
timestamps: timestamps,
};
let requestHeaderStr = JSON.stringify(requestHeader);
requestHeaderStr = Buffer.from(requestHeaderStr).toString('base64');
const headers = {
'Content-Type': 'application/json',
'X-Plus-Subject': requestHeaderStr,
};
return await request({
url: url,
baseURL: this.plusServerBaseUrl,
method: 'POST',
data: data,
headers: headers,
});
}
async sign(body: any, timestamps: number) {
//content := fmt.Sprintf("%s.%d.%s", in.Params, in.Timestamps, secret)
const params = JSON.stringify(body);
const plusInfo = getPlusInfo();
const secret = plusInfo.secret;
if (!secret) {
const randomTime = Math.floor(Math.random() * 3 * 60 * 1000 + 30 * 1000);
setTimeout(() => {
process.exit();
}, randomTime);
return 'xxxxx';
}
const content = `${params}.${timestamps}.${secret}`;
// sha256
const sign = crypto.createHash('sha256').update(content).digest('base64');
logger.info('content:', content, 'sign:', sign);
return sign;
}
async active(formData: { code: any; appKey: string; subjectId: string }) {
return await this.requestWithoutSign({
url: '/activation/active',
method: 'post',
data: formData,
});
}
}

View File

@ -2,6 +2,7 @@ import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController } from '../../../basic/base-controller.js'; import { BaseController } from '../../../basic/base-controller.js';
import { Constants } from '../../../basic/constants.js'; import { Constants } from '../../../basic/constants.js';
import { UserService } from '../../authority/service/user-service.js'; import { UserService } from '../../authority/service/user-service.js';
import { getPlusInfo } from '@certd/pipeline';
/** /**
*/ */
@ -24,4 +25,12 @@ export class MineController extends BaseController {
await this.userService.changePassword(userId, body); await this.userService.changePassword(userId, body);
return this.ok({}); return this.ok({});
} }
@Post('/plusInfo', { summary: Constants.per.authOnly })
async plusInfo(@Body(ALL) body) {
const info = getPlusInfo();
return this.ok({
...info,
});
}
} }

View File

@ -63,7 +63,7 @@ export class HistoryService extends BaseService<HistoryEntity> {
return id; return id;
} }
private async clear(pipelineId: number, keepCount = 30) { private async clear(pipelineId: number, keepCount = 20) {
const count = await this.repository.count({ const count = await this.repository.count({
where: { where: {
pipelineId, pipelineId,
@ -73,13 +73,14 @@ export class HistoryService extends BaseService<HistoryEntity> {
return; return;
} }
let shouldDeleteCount = count - keepCount; let shouldDeleteCount = count - keepCount;
const deleteCountBatch = 100; const maxDeleteCountBatch = 100;
const fileStore = new FileStore({ // const fileStore = new FileStore({
rootDir: this.certdConfig.fileRootDir, // rootDir: this.certdConfig.fileRootDir,
scope: pipelineId + '', // scope: pipelineId + '',
parent: '0', // parent: '0',
}); // });
while (shouldDeleteCount > 0) { while (shouldDeleteCount > 0) {
const deleteCountBatch = maxDeleteCountBatch > shouldDeleteCount ? shouldDeleteCount : maxDeleteCountBatch;
const list = await this.repository.find({ const list = await this.repository.find({
select: { select: {
id: true, id: true,
@ -94,18 +95,16 @@ export class HistoryService extends BaseService<HistoryEntity> {
take: deleteCountBatch, take: deleteCountBatch,
}); });
for (const historyEntity of list) { // for (const historyEntity of list) {
const id = historyEntity.id; // const id = historyEntity.id;
try { // try {
fileStore.deleteByParent(pipelineId + '', id + ''); // fileStore.deleteByParent(pipelineId + '', id + '');
} catch (e) { // } catch (e) {
logger.error('删除文件失败', e); // logger.error('删除文件失败', e);
} // }
} // }
await this.repository.remove(list); const ids = list.map(item => item.id);
await this.deleteByIds(ids, null);
await this.logService.deleteByHistoryIds(list.map(item => item.id));
shouldDeleteCount -= deleteCountBatch; shouldDeleteCount -= deleteCountBatch;
} }
} }
@ -136,10 +135,13 @@ export class HistoryService extends BaseService<HistoryEntity> {
} }
async deleteByIds(ids: number[], userId: number) { async deleteByIds(ids: number[], userId: number) {
await this.repository.delete({ const condition: any = {
id: In(ids), id: In(ids),
userId, };
}); if (userId != null) {
condition.userId = userId;
}
await this.repository.delete(condition);
await this.logService.deleteByHistoryIds(ids); await this.logService.deleteByHistoryIds(ids);
} }

View File

@ -4,7 +4,7 @@ import { In, Repository } from 'typeorm';
import { BaseService } from '../../../basic/base-service.js'; import { BaseService } from '../../../basic/base-service.js';
import { PipelineEntity } from '../entity/pipeline.js'; import { PipelineEntity } from '../entity/pipeline.js';
import { PipelineDetail } from '../entity/vo/pipeline-detail.js'; import { PipelineDetail } from '../entity/vo/pipeline-detail.js';
import { Executor, Pipeline, ResultType, RunHistory } from '@certd/pipeline'; import { Executor, isPlus, Pipeline, ResultType, RunHistory } from '@certd/pipeline';
import { AccessService } from './access-service.js'; import { AccessService } from './access-service.js';
import { DbStorage } from './db-storage.js'; import { DbStorage } from './db-storage.js';
import { StorageService } from './storage-service.js'; import { StorageService } from './storage-service.js';
@ -15,9 +15,10 @@ import { HistoryLogEntity } from '../entity/history-log.js';
import { HistoryLogService } from './history-log-service.js'; import { HistoryLogService } from './history-log-service.js';
import { logger } from '../../../utils/logger.js'; import { logger } from '../../../utils/logger.js';
import { EmailService } from '../../basic/service/email-service.js'; import { EmailService } from '../../basic/service/email-service.js';
import { NeedVIPException } from '../../../basic/exception/vip-exception.js';
const runningTasks: Map<string | number, Executor> = new Map(); const runningTasks: Map<string | number, Executor> = new Map();
const freeCount = 10;
/** /**
* *
*/ */
@ -47,6 +48,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
return this.repository; return this.repository;
} }
async add(bean: PipelineEntity) {
if (!isPlus()) {
const count = await this.repository.count();
if (count >= freeCount) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
await super.add(bean);
return bean;
}
async page(query: any, page: { offset: number; limit: number }, order: any, buildQuery: any) { async page(query: any, page: { offset: number; limit: number }, order: any, buildQuery: any) {
const result = await super.page(query, page, order, buildQuery); const result = await super.page(query, page, order, buildQuery);
const pipelineIds: number[] = []; const pipelineIds: number[] = [];
@ -93,6 +105,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
async save(bean: PipelineEntity) { async save(bean: PipelineEntity) {
if (!isPlus()) {
const count = await this.repository.count();
if (count >= freeCount) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
await this.clearTriggers(bean.id); await this.clearTriggers(bean.id);
if (bean.content) { if (bean.content) {
const pipeline = JSON.parse(bean.content); const pipeline = JSON.parse(bean.content);
@ -149,10 +167,10 @@ export class PipelineService extends BaseService<PipelineEntity> {
/** /**
* *
*/ */
async onStartup(immediateTriggerOnce: boolean, preview: boolean) { async onStartup(immediateTriggerOnce: boolean, onlyAdminUser: boolean) {
logger.info('加载定时trigger开始'); logger.info('加载定时trigger开始');
await this.foreachPipeline(async entity => { await this.foreachPipeline(async entity => {
if (preview && entity.userId !== 1) { if (onlyAdminUser && entity.userId !== 1) {
return; return;
} }
const pipeline = JSON.parse(entity.content ?? '{}'); const pipeline = JSON.parse(entity.content ?? '{}');
@ -265,6 +283,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
const entity: PipelineEntity = await this.info(id); const entity: PipelineEntity = await this.info(id);
const pipeline = JSON.parse(entity.content); const pipeline = JSON.parse(entity.content);
if (!pipeline.id) {
pipeline.id = id;
}
if (!pipeline.stages || pipeline.stages.length === 0) { if (!pipeline.stages || pipeline.stages.length === 0) {
return; return;
@ -288,7 +309,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.saveHistory(history); await this.saveHistory(history);
} catch (e) { } catch (e) {
const pipelineEntity = new PipelineEntity(); const pipelineEntity = new PipelineEntity();
pipelineEntity.id = parseInt(history.pipeline.id); pipelineEntity.id = id;
pipelineEntity.status = 'error'; pipelineEntity.status = 'error';
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime; pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
await this.update(pipelineEntity); await this.update(pipelineEntity);
@ -321,7 +342,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
} }
} }
async cancel(historyId) { async cancel(historyId: number) {
const executor = runningTasks.get(historyId); const executor = runningTasks.get(historyId);
if (executor) { if (executor) {
await executor.cancel(); await executor.cancel();

View File

@ -0,0 +1,58 @@
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { SysSettingsService } from '../service/sys-settings-service.js';
import { BaseController } from '../../../basic/base-controller.js';
import { AppKey, verify } from '@certd/pipeline';
import { SysInstallInfo, SysLicenseInfo } from '../service/models.js';
import { logger } from '../../../utils/logger.js';
import { PlusService } from '../../basic/service/plus-service.js';
/**
*/
@Provide()
@Controller('/api/sys/plus')
export class SysPlusController extends BaseController {
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
plusService: PlusService;
@Post('/active', { summary: 'sys:settings:edit' })
async active(@Body(ALL) body) {
const { code } = body;
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
const formData = {
appKey: AppKey,
code,
subjectId: installInfo.siteId,
};
const res: any = await this.plusService.active(formData);
if (res.code > 0) {
logger.error('激活失败', res.message);
return this.fail(res.message, 1);
}
const license = res.data.license;
let licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo);
if (!licenseInfo) {
licenseInfo = new SysLicenseInfo();
}
licenseInfo.license = license;
await this.sysSettingsService.saveSetting(licenseInfo);
const verifyRes = await verify({
subjectId: installInfo.siteId,
license,
});
if (!verifyRes.isPlus) {
const message = verifyRes.message || '授权码校验失败';
logger.error(message);
return this.fail(message, 1);
}
return this.ok(res.data);
}
}

View File

@ -4,3 +4,4 @@ export * from './plugin-tencent/index.js';
export * from './plugin-host/index.js'; export * from './plugin-host/index.js';
export * from './plugin-huawei/index.js'; export * from './plugin-huawei/index.js';
export * from './plugin-demo/index.js'; export * from './plugin-demo/index.js';
export * from './plugin-other/index.js';

View File

@ -11,8 +11,7 @@ export class AliyunAccess {
component: { component: {
placeholder: 'accessKeyId', placeholder: 'accessKeyId',
}, },
helper: helper: '注意证书申请需要dns解析权限其他阿里云插件也需要对应的权限比如证书上传需要证书管理权限',
'注意证书申请需要dns解析权限其他阿里云插件也需要对应的权限比如证书上传需要证书管理权限',
required: true, required: true,
}) })
accessKeyId = ''; accessKeyId = '';

View File

@ -1,4 +1,4 @@
import { AbstractTaskPlugin, IAccessService, ILogger, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
// @ts-ignore // @ts-ignore
import { ROAClient } from '@alicloud/pop-core'; import { ROAClient } from '@alicloud/pop-core';
import { AliyunAccess } from '../../access/index.js'; import { AliyunAccess } from '../../access/index.js';
@ -106,13 +106,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
}) })
accessId!: string; accessId!: string;
accessService!: IAccessService; async onInstance(): Promise<void> {}
logger!: ILogger;
async onInstance(): Promise<void> {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
}
async execute(): Promise<void> { async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn'); console.log('开始部署证书到阿里云cdn');
const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this; const { regionId, ingressClass, clusterId, isPrivateIpAddress, cert } = this;
@ -121,7 +115,10 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
const kubeConfigStr = await this.getKubeConfig(client, clusterId, isPrivateIpAddress); const kubeConfigStr = await this.getKubeConfig(client, clusterId, isPrivateIpAddress);
this.logger.info('kubeconfig已成功获取'); this.logger.info('kubeconfig已成功获取');
const k8sClient = new K8sClient(kubeConfigStr,this.logger); const k8sClient = new K8sClient({
kubeConfigStr,
logger: this.logger,
});
const ingressType = ingressClass || 'qcloud'; const ingressType = ingressClass || 'qcloud';
if (ingressType === 'qcloud') { if (ingressType === 'qcloud') {
throw new Error('暂未实现'); throw new Error('暂未实现');
@ -134,7 +131,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
// await this.restartIngress({ k8sClient, props }) // await this.restartIngress({ k8sClient, props })
} }
async restartIngress(options: { k8sClient: any }) { async restartIngress(options: { k8sClient: K8sClient }) {
const { k8sClient } = options; const { k8sClient } = options;
const { namespace } = this; const { namespace } = this;
@ -147,10 +144,10 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
}; };
const ingressList = await k8sClient.getIngressList({ namespace }); const ingressList = await k8sClient.getIngressList({ namespace });
console.log('ingressList:', ingressList); console.log('ingressList:', ingressList);
if (!ingressList || !ingressList.body || !ingressList.body.items) { if (!ingressList || !ingressList.items) {
return; return;
} }
const ingressNames = ingressList.body.items const ingressNames = ingressList.items
.filter((item: any) => { .filter((item: any) => {
if (!item.spec.tls) { if (!item.spec.tls) {
return false; return false;
@ -171,7 +168,7 @@ export class DeployCertToAliyunAckIngressPlugin extends AbstractTaskPlugin {
} }
} }
async patchNginxCertSecret(options: { cert: any; k8sClient: any }) { async patchNginxCertSecret(options: { cert: CertInfo; k8sClient: K8sClient }) {
const { cert, k8sClient } = options; const { cert, k8sClient } = options;
const crt = cert.crt; const crt = cert.crt;
const key = cert.key; const key = cert.key;

View File

@ -1,11 +1,4 @@
import { import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
AbstractTaskPlugin,
IAccessService,
ILogger,
IsTaskPlugin, pluginGroups,
RunStrategy,
TaskInput,
} from '@certd/pipeline';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Core from '@alicloud/pop-core'; import Core from '@alicloud/pop-core';
import RPCClient from '@alicloud/pop-core'; import RPCClient from '@alicloud/pop-core';
@ -57,18 +50,10 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
}) })
accessId!: string; accessId!: string;
accessService!: IAccessService; async onInstance() {}
logger!: ILogger;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
}
async execute(): Promise<void> { async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn'); console.log('开始部署证书到阿里云cdn');
const access = (await this.accessService.getById( const access = (await this.accessService.getById(this.accessId)) as AliyunAccess;
this.accessId
)) as AliyunAccess;
const client = this.getClient(access); const client = this.getClient(access);
const params = await this.buildParams(); const params = await this.buildParams();
await this.doRequest(client, params); await this.doRequest(client, params);
@ -85,8 +70,7 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
} }
async buildParams() { async buildParams() {
const CertName = const CertName = (this.certName ?? 'certd') + '-' + dayjs().format('YYYYMMDDHHmmss');
(this.certName ?? 'certd') + '-' + dayjs().format('YYYYMMDDHHmmss');
const cert: any = this.cert; const cert: any = this.cert;
return { return {
RegionId: 'cn-hangzhou', RegionId: 'cn-hangzhou',
@ -103,11 +87,7 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
const requestOption = { const requestOption = {
method: 'POST', method: 'POST',
}; };
const ret: any = await client.request( const ret: any = await client.request('SetDomainServerCertificate', params, requestOption);
'SetDomainServerCertificate',
params,
requestOption
);
this.checkRet(ret); this.checkRet(ret);
this.logger.info('设置cdn证书成功:', ret.RequestId); this.logger.info('设置cdn证书成功:', ret.RequestId);
} }

View File

@ -1,8 +1,7 @@
import { AbstractTaskPlugin, IAccessService, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import Core from '@alicloud/pop-core'; import Core from '@alicloud/pop-core';
import { AliyunAccess } from '../../access/index.js'; import { AliyunAccess } from '../../access/index.js';
import { appendTimeSuffix, checkRet, ZoneOptions } from '../../utils/index.js'; import { appendTimeSuffix, checkRet, ZoneOptions } from '../../utils/index.js';
import { Logger } from 'log4js';
@IsTaskPlugin({ @IsTaskPlugin({
name: 'uploadCertToAliyun', name: 'uploadCertToAliyun',
@ -26,8 +25,7 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
title: '大区', title: '大区',
value: 'cn-hangzhou', value: 'cn-hangzhou',
component: { component: {
name: 'a-select', name: 'a-auto-complete',
mode: 'tags',
vModel: 'value', vModel: 'value',
options: ZoneOptions, options: ZoneOptions,
}, },
@ -61,13 +59,7 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
}) })
aliyunCertId!: string; aliyunCertId!: string;
accessService!: IAccessService; async onInstance() {}
logger!: Logger;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
}
async execute(): Promise<void> { async execute(): Promise<void> {
console.log('开始部署证书到阿里云cdn'); console.log('开始部署证书到阿里云cdn');

View File

@ -1,4 +1,4 @@
import { AbstractTaskPlugin, IAccessService, ILogger, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert'; import { CertInfo, CertReader } from '@certd/plugin-cert';
@IsTaskPlugin({ @IsTaskPlugin({
@ -28,8 +28,8 @@ export class CloudflareDeployToCDNPlugin extends AbstractTaskPlugin {
title: '选择框', title: '选择框',
component: { component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/select-cn //前端组件配置,具体配置见组件文档 https://www.antdv.com/components/select-cn
name: 'a-select', name: 'a-auto-complete',
mode: 'tags', vModel: 'value',
options: [ options: [
{ value: '1', label: '选项1' }, { value: '1', label: '选项1' },
{ value: '2', label: '选项2' }, { value: '2', label: '选项2' },
@ -71,13 +71,7 @@ export class CloudflareDeployToCDNPlugin extends AbstractTaskPlugin {
}) })
accessId!: string; accessId!: string;
accessService!: IAccessService; async onInstance() {}
logger!: ILogger;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
}
async execute(): Promise<void> { async execute(): Promise<void> {
const { select, text, cert, accessId } = this; const { select, text, cert, accessId } = this;
const certReader = new CertReader(cert); const certReader = new CertReader(cert);

View File

@ -1,6 +1,5 @@
import { AbstractTaskPlugin, IAccessService, ILogger, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert'; import { CertInfo, CertReader } from '@certd/plugin-cert';
import { K8sClient } from '@certd/lib-k8s';
@IsTaskPlugin({ @IsTaskPlugin({
name: 'demoTest', name: 'demoTest',
@ -28,8 +27,8 @@ export class DemoTestPlugin extends AbstractTaskPlugin {
title: '选择框', title: '选择框',
component: { component: {
//前端组件配置,具体配置见组件文档 https://www.antdv.com/components/select-cn //前端组件配置,具体配置见组件文档 https://www.antdv.com/components/select-cn
name: 'a-select', name: 'a-auto-complete',
mode: 'tags', vModel: 'value',
options: [ options: [
{ value: '1', label: '选项1' }, { value: '1', label: '选项1' },
{ value: '2', label: '选项2' }, { value: '2', label: '选项2' },
@ -55,7 +54,7 @@ export class DemoTestPlugin extends AbstractTaskPlugin {
component: { component: {
name: 'pi-output-selector', name: 'pi-output-selector',
}, },
required: true, // required: true,
}) })
cert!: CertInfo; cert!: CertInfo;
@ -67,31 +66,33 @@ export class DemoTestPlugin extends AbstractTaskPlugin {
name: 'pi-access-selector', name: 'pi-access-selector',
type: 'demo', //固定授权类型 type: 'demo', //固定授权类型
}, },
rules: [{ required: true, message: '此项必填' }], // rules: [{ required: true, message: '此项必填' }],
}) })
accessId!: string; accessId!: string;
accessService!: IAccessService; async onInstance() {}
logger!: ILogger;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
}
async execute(): Promise<void> { async execute(): Promise<void> {
const { select, text, cert, accessId } = this; const { select, text, cert, accessId } = this;
const certReader = new CertReader(cert);
const access = await this.accessService.getById(accessId); try {
this.logger.debug('access', access); const access = await this.accessService.getById(accessId);
this.logger.debug('certReader', certReader); this.logger.debug('access', access);
} catch (e) {
this.logger.error('获取授权失败', e);
}
try {
const certReader = new CertReader(cert);
this.logger.debug('certReader', certReader);
} catch (e) {
this.logger.error('读取crt失败', e);
}
this.logger.info('DemoTestPlugin execute'); this.logger.info('DemoTestPlugin execute');
this.logger.info('text:', text); this.logger.info('text:', text);
this.logger.info('select:', select); this.logger.info('select:', select);
this.logger.info('switch:', this.switch); this.logger.info('switch:', this.switch);
this.logger.info('授权id:', accessId); this.logger.info('授权id:', accessId);
//TODO 这里实现你要部署的执行方法
new K8sClient('111', null);
} }
} }
//TODO 这里实例化插件,进行注册 //TODO 这里实例化插件,进行注册

View File

@ -0,0 +1,99 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { CertInfo, CertReader } from '@certd/plugin-cert';
import * as fs from 'fs';
import { Constants } from '../../../../basic/constants.js';
import path from 'path';
@IsTaskPlugin({
name: 'CopyToLocal',
title: '复制到本机',
group: pluginGroups.host.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class CopyCertToLocalPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '证书保存路径',
helper: '需要有写入权限,路径要包含证书文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.pem',
component: {
placeholder: './tmp/cert.pem',
},
})
crtPath!: string;
@TaskInput({
title: '私钥保存路径',
helper: '需要有写入权限,路径要包含私钥文件名,文件名不能用*?!等特殊符号\n推荐使用相对路径将写入与数据库同级目录无需映射例如./tmp/cert.key',
component: {
placeholder: './tmp/cert.key',
},
})
keyPath!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
},
required: true,
})
cert!: CertInfo;
@TaskOutput({
title: '证书保存路径',
})
hostCrtPath!: string;
@TaskOutput({
title: '私钥保存路径',
})
hostKeyPath!: string;
async onInstance() {}
copyFile(srcFile: string, destFile: string) {
this.logger.info(`复制文件:${srcFile} => ${destFile}`);
const dir = path.dirname(destFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.copyFileSync(srcFile, destFile);
}
async execute(): Promise<void> {
let { crtPath, keyPath } = this;
const certReader = new CertReader(this.cert);
this.logger.info('将证书写入本地缓存文件');
const saveCrtPath = certReader.saveToFile('crt');
const saveKeyPath = certReader.saveToFile('key');
this.logger.info('本地文件写入成功');
try {
this.logger.info('复制到目标路径');
crtPath = crtPath.startsWith('/') ? crtPath : path.join(Constants.dataDir, crtPath);
keyPath = keyPath.startsWith('/') ? keyPath : path.join(Constants.dataDir, keyPath);
// crtPath = path.resolve(crtPath);
// keyPath = path.resolve(keyPath);
this.copyFile(saveCrtPath, crtPath);
this.copyFile(saveKeyPath, keyPath);
this.logger.info('证书复制成功crtPath=', crtPath, ',keyPath=', keyPath);
this.logger.info('请注意,如果使用的是相对路径,那么文件就在你的数据库同级目录下,默认是/data/certd/下面');
this.logger.info('请注意,如果使用的是绝对路径,文件在容器内的目录下,你需要给容器做目录映射才能复制到宿主机');
} catch (e) {
this.logger.error(`复制失败:${e.message}`);
throw e;
} finally {
//删除临时文件
this.logger.info('删除临时文件');
fs.unlinkSync(saveCrtPath);
fs.unlinkSync(saveKeyPath);
}
this.logger.info('执行完成');
//输出
this.hostCrtPath = crtPath;
this.hostKeyPath = keyPath;
}
}
new CopyCertToLocalPlugin();

View File

@ -1,4 +1,4 @@
import { AbstractTaskPlugin, IAccessService, ILogger, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { SshClient } from '../../lib/ssh.js'; import { SshClient } from '../../lib/ssh.js';
@IsTaskPlugin({ @IsTaskPlugin({
@ -34,12 +34,7 @@ export class HostShellExecutePlugin extends AbstractTaskPlugin {
}) })
script!: string; script!: string;
accessService!: IAccessService; async onInstance() {}
logger!: ILogger;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
}
async execute(): Promise<void> { async execute(): Promise<void> {
const { script, accessId } = this; const { script, accessId } = this;
const connectConf = await this.accessService.getById(accessId); const connectConf = await this.accessService.getById(accessId);

View File

@ -1,4 +1,4 @@
import { AbstractTaskPlugin, IAccessService, ILogger, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline'; import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { SshClient } from '../../lib/ssh.js'; import { SshClient } from '../../lib/ssh.js';
import { CertInfo, CertReader } from '@certd/plugin-cert'; import { CertInfo, CertReader } from '@certd/plugin-cert';
import * as fs from 'fs'; import * as fs from 'fs';
@ -86,13 +86,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
}) })
hostKeyPath!: string; hostKeyPath!: string;
accessService!: IAccessService; async onInstance() {}
logger!: ILogger;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
}
copyFile(srcFile: string, destFile: string) { copyFile(srcFile: string, destFile: string) {
const dir = destFile.substring(0, destFile.lastIndexOf('/')); const dir = destFile.substring(0, destFile.lastIndexOf('/'));

View File

@ -0,0 +1 @@
export * from './k8s-access.js';

Some files were not shown because too many files have changed in this diff Show More