Compare commits

...

31 Commits

Author SHA1 Message Date
xiaojunnuo
0c130f9596 v1.29.3 2025-01-05 01:11:06 +08:00
xiaojunnuo
f156f4cb4e build: prepare to build 2025-01-05 01:09:09 +08:00
xiaojunnuo
fa3bfa2ea8 chore: 2025-01-05 01:07:04 +08:00
xiaojunnuo
ab5c7bb75a chore: 2025-01-05 01:02:41 +08:00
xiaojunnuo
81b322cd60 chore: 2025-01-04 20:17:08 +08:00
xiaojunnuo
e6dd7cd54a perf: 优化站点证书检查页面,检查增加3次重试 2025-01-04 20:10:00 +08:00
xiaojunnuo
aa1da7c11a chore: 2025-01-04 01:46:49 +08:00
xiaojunnuo
3f74d4d9e5 perf: http校验方式,支持七牛云oss、阿里云oss、腾讯云cos 2025-01-04 01:45:24 +08:00
xiaojunnuo
297d09c5ad docs: 增加支付配置说明 2025-01-03 16:50:16 +08:00
xiaojunnuo
07e1dbb4cc chore: 2025-01-03 16:12:37 +08:00
xiaojunnuo
3c6618b4fc chore: 2025-01-03 09:27:51 +08:00
xiaojunnuo
54db744282 perf: 优化acme sdk 2025-01-03 01:17:20 +08:00
xiaojunnuo
03b751fa13 chore: 2025-01-03 00:12:15 +08:00
xiaojunnuo
ec342708b2 chore: 2025-01-02 17:48:54 +08:00
xiaojunnuo
405591c5d0 perf: 支持http校验方式申请证书 2025-01-02 00:28:13 +08:00
xiaojunnuo
67af67b92d chore: 2024-12-27 22:40:07 +08:00
xiaojunnuo
8644348fc4 fix: 修复系统级授权无法查看密钥的bug 2024-12-26 23:15:35 +08:00
xiaojunnuo
00dc226bd2 chore: auto-upgrade 2024-12-26 16:14:08 +08:00
xiaojunnuo
b6b7c3e2e0 chore: storage存储的数据量优化,去掉logs信息 2024-12-26 13:48:55 +08:00
xiaojunnuo
246ef348d3 chore: mysql text 改成longtext 2024-12-26 13:26:10 +08:00
xiaojunnuo
3e9ba1a30a docs: 2024-12-26 09:02:04 +08:00
xiaojunnuo
598cde4865 build: publish 2024-12-26 01:56:08 +08:00
xiaojunnuo
fc4a716b4e build: trigger build image 2024-12-26 01:55:50 +08:00
xiaojunnuo
ed5634ff83 v1.29.2 2024-12-26 01:53:32 +08:00
xiaojunnuo
884af1ea62 build: prepare to build 2024-12-26 01:51:48 +08:00
xiaojunnuo
01ad62df16 build: prepare to build 2024-12-26 01:49:48 +08:00
xiaojunnuo
512a667e44 Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2024-12-26 01:47:50 +08:00
xiaojunnuo
d0e841f7de build: publish 2024-12-26 01:43:52 +08:00
xiaojunnuo
c04641d835 build: trigger build image 2024-12-26 01:43:35 +08:00
xiaojunnuo
66fb9e5f49 fix: 修复套餐关闭状态下,仍然限制用户流水线数量的bug 2024-12-25 11:42:42 +08:00
xiaojunnuo
a323f3aa2c chore: 2024-12-25 10:38:48 +08:00
107 changed files with 1880 additions and 350 deletions

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
### Bug Fixes
* 修复系统级授权无法查看密钥的bug ([8644348](https://github.com/certd/certd/commit/8644348fc41ae2e1672f946ca37e5d3a674e0218))
### Performance Improvements
* 优化站点证书检查页面检查增加3次重试 ([e6dd7cd](https://github.com/certd/certd/commit/e6dd7cd54a3e23897031b5df6e0c3cdc0545d35a))
* 优化acme sdk ([54db744](https://github.com/certd/certd/commit/54db74428259de64d12230c2ab7353ae11197bbc))
* 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a))
* http校验方式支持七牛云oss、阿里云oss、腾讯云cos ([3f74d4d](https://github.com/certd/certd/commit/3f74d4d9e5f5d0e629b44cff1895b3f7a8fbcafc))
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
### Bug Fixes
* 修复套餐关闭状态下仍然限制用户流水线数量的bug ([66fb9e5](https://github.com/certd/certd/commit/66fb9e5f49491f9c159363b48af14720a37673b1))
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
### Bug Fixes

View File

@@ -10,6 +10,7 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
* 全自动申请证书(支持所有注册商注册的域名)
* 全自动部署更新证书目前支持部署到主机、阿里云、腾讯云等目前已支持40+部署插件)
* 支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式
* 支持通配符域名/泛域名支持多个域名打到一个证书上支持pem、pfx、der、jks等多种证书格式
* 邮件通知、webhook通知
* 私有化部署数据保存本地授权信息加密存储镜像由Github Actions构建过程公开透明
@@ -20,6 +21,11 @@ Certd 是一个免费全自动申请和自动部署更新SSL证书的管理系
> 流水线数量现已调整为无限制,欢迎大家使用
>
> 关于证书续期:
>* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
>* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。
## 二、在线体验
官方Demo地址自助注册后体验
@@ -88,11 +94,13 @@ https://certd.handfree.work/
## 五、 升级
如果使用固定版本号
### docker-compose方式部署
#### 1. 如果使用固定版本号
1. 修改`docker-compose.yaml`中的镜像版本号
2. 运行`docker compose up -d` 即可
如果需要使用最新版本
#### 2. 如果需要使用最新版本
```shell
#重新拉取镜像
docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
@@ -100,7 +108,9 @@ docker pull registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
docker compose down
docker compose up -d
```
关于自动升级(仅限尝鲜建议非生产使用)
> 数据默认存在`/data/certd`目录下,不用担心数据丢失
### 自动升级(仅限尝鲜建议非生产使用)
```yaml
version: '3.3'
services:
@@ -113,16 +123,8 @@ services:
ports:
- "7001:7001"
- "7002:7002"
# 如果需要修改系统配置,可以通过环境变量传递;初次运行请保持默认配置
environment:
- certd_system_resetAdminPasswd=false
# 如果需要切换数据库类型可以在此处设置为mysql或postgres
# - certd_typeorm_dataSource_default_type=mysql
# - certd_typeorm_dataSource_default_host=localhost
# - certd_typeorm_dataSource_default_port=3306
# - certd_typeorm_dataSource_default_username=root
# - certd_typeorm_dataSource_default_password=123456
# - certd_typeorm_dataSource_default_database=certd
labels:
com.centurylinklabs.watchtower.enable: "true"
@@ -139,27 +141,21 @@ services:
- WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
- WATCHTOWER_POLL_INTERVAL=300 # 每 5 分钟检查一次更新
# 如果需要支持 IPv6请取消以下注释
# networks:
# ip6net:
# enable_ipv6: true
# ipam:
# config:
# - subnet: 2001:db8::/64
```
> 数据默认存在`/data/certd`目录下,不用担心数据丢失
### 其他部署方式升级方法
请参考 https://certd.docmirror.cn/guide/install/upgrade.html
更新日志: [CHANGELOG](./CHANGELOG.md)
### 更新日志:
[CHANGELOG](./CHANGELOG.md)
## 六、一些说明
* 本项目ssl证书提供商为letencrypt/Google/ZeroSSL
* 申请过程遵循acme协议
* 需要验证域名所有权一般有两种方式目前本项目仅支持dns-01
* http-01 在网站根目录下放置一份txt文件
* dns-01 需要给域名添加txt解析记录通配符域名只能用这种方式
* 证书续期:
* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。

View File

@@ -1 +1 @@
01:06
01:55

View File

@@ -1,7 +1,7 @@
version: '3.3' # 兼容旧版docker-compose
services:
certd:
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号
# 镜像 # ↓↓↓↓↓ ---- 镜像版本号,建议改成固定版本号,例如certd:1.29.0
image: registry.cn-shenzhen.aliyuncs.com/handsfree/certd:latest
container_name: certd # 容器名
restart: unless-stopped # 自动重启
@@ -25,14 +25,19 @@ services:
# - 8.8.4.4
# extra_hosts:
# # ↓↓↓↓ -------------------------------------------------------- 这里可以配置自定义hosts外网域名可以指向本地局域网ip地址
# - "localdomain.comm:192.168.1.3"
# - "localdomain.com:192.168.1.3"
labels:
com.centurylinklabs.watchtower.enable: "true"
# ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络还需要把下面networks的注释放开
# networks:
# - ip6net
environment:
# 设置环境变量即可自定义certd配置
# 配置项见: packages/ui/certd-server/src/config/config.default.ts
# 配置规则: certd_ + 配置项, 点号用_代替
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false
- certd_system_resetAdminPasswd=false
# 默认使用sqlite文件数据库如果需要使用其他数据库请设置以下环境变量
# 注意: 选定使用一种数据库之后,不支持更换数据库。
# 数据库迁移方法1、使用新数据库重新部署一套然后将旧数据同步过去注意flyway_history表的数据不要同步
@@ -54,13 +59,22 @@ services:
# - certd_typeorm_dataSource_default_password=yourpasswd # 密码
# - certd_typeorm_dataSource_default_database=certd # 数据库名
# ↓↓↓↓ --------------------------------------------------------- 自动升级上面certd的版本号要保持为latest
# certd-updater: # 添加 Watchtower 服务
# image: containrrr/watchtower:latest
# container_name: certd-updater
# restart: unless-stopped
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock
# # 配置 自动更新
# environment:
# - WATCHTOWER_CLEANUP=true # 自动清理旧版本容器
# - WATCHTOWER_INCLUDE_STOPPED=false # 不更新已停止的容器
# - WATCHTOWER_LABEL_ENABLE=true # 根据容器标签进行更新
# - WATCHTOWER_POLL_INTERVAL=600 # 每 10 分钟检查一次更新
# #↓↓↓↓ ------------------------------------------------------------- 启用ipv6网络
# networks:
# - ip6net
# ↓↓↓↓ -------------------------------------------------------------- 启用ipv6网络还需要把上面networks的注释放开
#networks:
# ip6net:
# enable_ipv6: true

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
### Bug Fixes
* 修复套餐关闭状态下仍然限制用户流水线数量的bug ([66fb9e5](https://github.com/certd/certd/commit/66fb9e5f49491f9c159363b48af14720a37673b1))
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
### Bug Fixes
* 免费套餐支持购买 ([f5ec987](https://github.com/certd/certd/commit/f5ec9870fd6af1f0c9099852bbdb4d07813ccce8))
* 修复某处金额转换丢失精度的bug ([d2d6f12](https://github.com/certd/certd/commit/d2d6f12218cbe7bd55f4ae082b93084be85f0a7b))
* 修复新版本小红点显示错误问题 ([fe4786e](https://github.com/certd/certd/commit/fe4786e168afe03a5243dd67971476c348339809))
### Performance Improvements
* 用户创建证书流水线没有购买套餐或者超限时提前报错 ([472f06c](https://github.com/certd/certd/commit/472f06c2d190d0ae48e8b53c18bc278437656a1c))
* 优化插件名称显示 ([26adf7d](https://github.com/certd/certd/commit/26adf7d437e674385f26a8f92fded6521a620671))
# [1.29.0](https://github.com/certd/certd/compare/v1.28.4...v1.29.0) (2024-12-24)
### Bug Fixes

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.29.1"
"version": "1.29.3"
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/publishlab/node-acme-client/compare/v1.29.2...v1.29.3) (2025-01-04)
### Performance Improvements
* 优化acme sdk ([54db744](https://github.com/publishlab/node-acme-client/commit/54db74428259de64d12230c2ab7353ae11197bbc))
## [1.29.2](https://github.com/publishlab/node-acme-client/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/acme-client
## [1.29.1](https://github.com/publishlab/node-acme-client/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/acme-client

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.29.1",
"version": "1.29.3",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.29.1",
"@certd/basic": "^1.29.3",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.7.2",
@@ -65,5 +65,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -99,31 +99,14 @@ export default async (client, userOpts) => {
return;
}
const keyAuthorizationGetter = async (challenge) => {
return await client.getChallengeKeyAuthorization(challenge);
}
try {
/* Select challenge based on priority */
const challenge = authz.challenges.sort((a, b) => {
const aidx = opts.challengePriority.indexOf(a.type);
const bidx = opts.challengePriority.indexOf(b.type);
if (aidx === -1) return 1;
if (bidx === -1) return -1;
return aidx - bidx;
}).slice(0, 1)[0];
if (!challenge) {
throw new Error(`Unable to select challenge for ${d}, no challenge found`);
}
log(`[auto] [${d}] Found ${authz.challenges.length} challenges, selected type: ${challenge.type}`);
/* Trigger challengeCreateFn() */
log(`[auto] [${d}] Trigger challengeCreateFn()`);
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
try {
const { recordReq, recordRes, dnsProvider } = await opts.challengeCreateFn(authz, challenge, keyAuthorization);
log(`[auto] [${d}] challengeCreateFn success`);
log(`[auto] [${d}] add challengeRemoveFn()`);
const { recordReq, recordRes, dnsProvider,challenge ,keyAuthorization} = await opts.challengeCreateFn(authz, keyAuthorizationGetter);
clearTasks.push(async () => {
/* Trigger challengeRemoveFn(), suppress errors */
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
@@ -141,7 +124,7 @@ export default async (client, userOpts) => {
await wait(60 * 1000);
}
else {
log(`[auto] [${d}] Running challenge verification`);
log(`[auto] [${d}] Running challenge verification, type = ${challenge.type}`);
try {
await client.verifyChallenge(authz, challenge);
}

View File

@@ -5,3 +5,5 @@ export class CancelError extends Error {
}
}

View File

@@ -4,6 +4,8 @@
import { AxiosInstance } from 'axios';
import * as rfc8555 from './rfc8555';
import {CancelError} from '../src/error.js'
export * from '../src/error.js'
export type PrivateKeyBuffer = Buffer;
export type PublicKeyBuffer = Buffer;
@@ -56,7 +58,7 @@ export interface ClientExternalAccountBindingOptions {
export interface ClientAutoOptions {
csr: CsrBuffer | CsrString;
challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<{recordReq:any,recordRes:any,dnsProvider:any}>;
challengeCreateFn: (authz: Authorization, keyAuthorization: (challenge:rfc8555.Challenge)=>Promise<string>) => Promise<{recordReq?:any,recordRes?:any,dnsProvider?:any,challenge: rfc8555.Challenge,keyAuthorization:string}>;
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any) => Promise<any>;
email?: string;
termsOfServiceAgreed?: boolean;
@@ -202,4 +204,4 @@ export function setLogger(fn: (message: any, ...args: any[]) => void): void;
export function walkTxtRecord(record: any): Promise<string[]>;
export const CancelError: Error;
export const CancelError: typeof CancelError;

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
**Note:** Version bump only for package @certd/basic
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/basic
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
### Bug Fixes

View File

@@ -1 +1 @@
01:39
01:09

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.29.1",
"version": "1.29.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -44,5 +44,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
### Performance Improvements
* 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a))
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/pipeline
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.29.1",
"version": "1.29.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,8 +16,8 @@
"test": "mocha --loader=ts-node/esm"
},
"dependencies": {
"@certd/basic": "^1.29.1",
"@certd/plus-core": "^1.29.1",
"@certd/basic": "^1.29.3",
"@certd/plus-core": "^1.29.3",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -43,5 +43,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -109,7 +109,13 @@ export class Executor {
} finally {
clearInterval(intervalFlushLogId);
await this.onChanged(this.runtime);
await this.pipelineContext.setObj("lastRuntime", this.runtime);
//保存之前移除logs
const lastRuntime: any = {
...this.runtime,
};
delete lastRuntime.logs;
delete lastRuntime._loggers;
await this.pipelineContext.setObj("lastRuntime", lastRuntime);
this.logger.info(`pipeline.${this.pipeline.id} end`);
}
}

View File

@@ -18,6 +18,7 @@ export type CnameRecord = {
status: string;
commonDnsProvider?: any;
};
export type ICnameProxyService = {
getByDomain: (domain: string) => Promise<CnameRecord>;
};

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
**Note:** Version bump only for package @certd/lib-huawei
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/lib-huawei
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.29.1",
"version": "1.29.3",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -21,5 +21,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
**Note:** Version bump only for package @certd/lib-iframe
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/lib-iframe
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/lib-iframe

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.29.1",
"version": "1.29.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -30,5 +30,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
**Note:** Version bump only for package @certd/lib-k8s
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/lib-k8s
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.29.1",
"version": "1.29.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -16,7 +16,7 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/basic": "^1.29.1",
"@certd/basic": "^1.29.3",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
### Bug Fixes
* 修复系统级授权无法查看密钥的bug ([8644348](https://github.com/certd/certd/commit/8644348fc41ae2e1672f946ca37e5d3a674e0218))
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/lib-server
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/lib-server

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.29.1",
"version": "1.29.3",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -27,10 +27,10 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.29.1",
"@certd/basic": "^1.29.1",
"@certd/pipeline": "^1.29.1",
"@certd/plus-core": "^1.29.1",
"@certd/acme-client": "^1.29.3",
"@certd/basic": "^1.29.3",
"@certd/pipeline": "^1.29.3",
"@certd/plus-core": "^1.29.3",
"@midwayjs/cache": "~3.14.0",
"@midwayjs/core": "~3.17.1",
"@midwayjs/i18n": "~3.17.3",
@@ -61,5 +61,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -112,6 +112,17 @@ export class SysSecretBackup extends BaseSettings {
encryptSecret?: string;
}
/**
* 不要修改
*/
export class SysSecret extends BaseSettings {
static __title__ = '密钥信息';
static __key__ = 'sys.secret';
static __access__ = 'private';
siteId?: string;
encryptSecret?: string;
}
export class SysSiteEnv {
agent?: {
enabled?: boolean;

View File

@@ -1,25 +1,22 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { SysSettingsEntity } from '../entity/sys-settings.js';
import { CacheManager } from '@midwayjs/cache';
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecretBackup } from './models.js';
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
import * as _ from 'lodash-es';
import { BaseService } from '../../../basic/index.js';
import { logger, setGlobalProxy } from '@certd/basic';
import { cache, logger, setGlobalProxy } from '@certd/basic';
import * as dns from 'node:dns';
/**
* 设置
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@Scope(ScopeEnum.Singleton)
export class SysSettingsService extends BaseService<SysSettingsEntity> {
@InjectEntityModel(SysSettingsEntity)
repository: Repository<SysSettingsEntity>;
@Inject()
cache: CacheManager; // 依赖注入CacheManager
getRepository() {
return this.repository;
}
@@ -72,7 +69,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
async getSetting<T>(type: any): Promise<T> {
const key = type.__key__;
const cacheKey = type.getCacheKey();
const settings: T = await this.cache.get(cacheKey);
const settings: T = cache.get(cacheKey);
if (settings) {
return settings;
}
@@ -80,7 +77,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
const savedSettings = await this.getSettingByKey(key);
newSetting = _.merge(newSetting, savedSettings);
await this.saveSetting(newSetting);
await this.cache.set(cacheKey, newSetting);
cache.set(cacheKey, newSetting);
return newSetting;
}
@@ -93,6 +90,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
if (entity) {
entity.setting = JSON.stringify(bean);
entity.access = type.__access__;
if (key === SysSecretBackup.__key__ || key === SysSecret.__key__) {
//备份密钥不允许更新
return;
}
await this.repository.save(entity);
} else {
const newEntity = new SysSettingsEntity();
@@ -103,7 +106,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
await this.repository.save(newEntity);
}
await this.cache.set(cacheKey, bean);
cache.set(cacheKey, bean);
}
async getPublicSettings(): Promise<SysPublicSettings> {
@@ -146,7 +149,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
} else {
throw new Error('该设置不存在');
}
await this.cache.del(`settings.${key}`);
cache.delete(`settings.${key}`);
}
async backupSecret() {
@@ -173,4 +176,20 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}
}
}
async getSecret() {
const sysSecret = await this.getSetting<SysSecret>(SysSecret);
if (sysSecret.encryptSecret) {
return sysSecret;
}
//从备份中读取
const settings = await this.getSettingByKey(SysSecretBackup.__key__);
if (settings == null || !settings.encryptSecret) {
throw new Error('密钥备份不存在');
}
sysSecret.siteId = settings.siteId;
sysSecret.encryptSecret = settings.encryptSecret;
await this.saveSetting(sysSecret);
logger.info('密钥恢复成功');
return sysSecret;
}
}

View File

@@ -1,6 +1,6 @@
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import crypto from 'crypto';
import { SysPrivateSettings, SysSettingsService } from '../../../system/index.js';
import { SysSecret, SysSettingsService } from '../../../system/index.js';
/**
* 授权
@@ -15,8 +15,8 @@ export class EncryptService {
@Init()
async init() {
const privateInfo: SysPrivateSettings = await this.sysSettingService.getSetting(SysPrivateSettings);
this.secretKey = Buffer.from(privateInfo.encryptSecret, 'base64');
const secret: SysSecret = await this.sysSettingService.getSecret();
this.secretKey = Buffer.from(secret.encryptSecret, 'base64');
}
// 加密函数

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.29.1",
"version": "1.29.3",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
### Performance Improvements
* 优化acme sdk ([54db744](https://github.com/certd/certd/commit/54db74428259de64d12230c2ab7353ae11197bbc))
* 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a))
* http校验方式支持七牛云oss、阿里云oss、腾讯云cos ([3f74d4d](https://github.com/certd/certd/commit/3f74d4d9e5f5d0e629b44cff1895b3f7a8fbcafc))
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/plugin-cert
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/plugin-cert

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.29.1",
"version": "1.29.3",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -15,9 +15,10 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "^1.29.1",
"@certd/basic": "^1.29.1",
"@certd/pipeline": "^1.29.1",
"@certd/acme-client": "^1.29.3",
"@certd/basic": "^1.29.3",
"@certd/pipeline": "^1.29.3",
"@certd/plugin-lib": "^1.29.3",
"@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
@@ -40,5 +41,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -6,23 +6,37 @@ import { Challenge } from "@certd/acme-client/types/rfc8555";
import { IContext } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
import { IDnsProvider, parseDomain } from "../../dns-provider/index.js";
import { HttpChallengeUploader } from "./uploads/api.js";
export type CnameVerifyPlan = {
type?: string;
domain: string;
fullRecord: string;
dnsProvider: IDnsProvider;
};
export type HttpVerifyPlan = {
type: string;
domain: string;
httpUploader: HttpChallengeUploader;
};
export type DomainVerifyPlan = {
domain: string;
type: "cname" | "dns";
type: "cname" | "dns" | "http";
dnsProvider?: IDnsProvider;
cnameVerifyPlan?: Record<string, CnameVerifyPlan>;
httpVerifyPlan?: Record<string, HttpVerifyPlan>;
};
export type DomainsVerifyPlan = {
[key: string]: DomainVerifyPlan;
};
export type Providers = {
dnsProvider?: IDnsProvider;
domainsVerifyPlan?: DomainsVerifyPlan;
httpUploader?: HttpChallengeUploader;
};
export type CertInfo = {
crt: string; //fullchain证书
key: string; //私钥
@@ -155,58 +169,37 @@ export class AcmeService {
return key.toString();
}
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider, domainsVerifyPlan: DomainsVerifyPlan) {
async challengeCreateFn(authz: any, keyAuthorizationGetter: (challenge: Challenge) => Promise<string>, providers: Providers) {
this.logger.info("Triggered challengeCreateFn()");
/* http-01 */
const fullDomain = authz.identifier.value;
if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
let domain = parseDomain(fullDomain);
this.logger.info("主域名为:" + domain);
const getChallenge = (type: string) => {
return authz.challenges.find((c: any) => c.type === type);
};
const doHttpVerify = async (challenge: any, httpUploader: HttpChallengeUploader) => {
const keyAuthorization = await keyAuthorizationGetter(challenge);
this.logger.info("http校验");
const filePath = `.well-known/acme-challenge/${challenge.token}`;
const fileContents = keyAuthorization;
this.logger.info(`校验 ${fullDomain} ,准备上传文件:${filePath}`);
await httpUploader.upload(filePath, Buffer.from(fileContents));
this.logger.info(`上传文件【${filePath}】成功`);
return {
challenge,
keyAuthorization,
httpUploader,
};
};
this.logger.info(`Creating challenge response for ${fullDomain} at path: ${filePath}`);
const doDnsVerify = async (challenge: any, fullRecord: string, dnsProvider: IDnsProvider) => {
this.logger.info("dns校验");
const keyAuthorization = await keyAuthorizationGetter(challenge);
/* Replace this */
this.logger.info(`Would write "${fileContents}" to path "${filePath}"`);
// await fs.writeFileAsync(filePath, fileContents);
} else if (challenge.type === "dns-01") {
/* dns-01 */
let fullRecord = `_acme-challenge.${fullDomain}`;
const recordValue = keyAuthorization;
this.logger.info(`Creating TXT record for ${fullDomain}: ${fullRecord}`);
/* Replace this */
this.logger.info(`Would create TXT record "${fullRecord}" with value "${recordValue}"`);
let domain = parseDomain(fullDomain);
this.logger.info("解析到域名domain=" + domain);
if (domainsVerifyPlan) {
//按照计划执行
const domainVerifyPlan = domainsVerifyPlan[domain];
if (domainVerifyPlan) {
if (domainVerifyPlan.type === "dns") {
dnsProvider = domainVerifyPlan.dnsProvider;
} else if (domainVerifyPlan.type === "cname") {
const cnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
if (cnameVerifyPlan) {
const cname = cnameVerifyPlan[fullDomain];
if (cname) {
dnsProvider = cname.dnsProvider;
domain = parseDomain(cname.domain);
fullRecord = cname.fullRecord;
}
} else {
this.logger.error("未找到域名Cname校验计划使用默认的dnsProvider");
}
} else {
this.logger.error("不支持的校验类型", domainVerifyPlan.type);
}
} else {
this.logger.info("未找到域名校验计划使用默认的dnsProvider");
}
}
let hostRecord = fullRecord.replace(`${domain}`, "");
if (hostRecord.endsWith(".")) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
@@ -226,8 +219,54 @@ export class AcmeService {
recordReq,
recordRes,
dnsProvider,
challenge,
keyAuthorization,
};
};
let dnsProvider = providers.dnsProvider;
let fullRecord = `_acme-challenge.${fullDomain}`;
if (providers.domainsVerifyPlan) {
//按照计划执行
const domainVerifyPlan = providers.domainsVerifyPlan[domain];
if (domainVerifyPlan) {
if (domainVerifyPlan.type === "dns") {
dnsProvider = domainVerifyPlan.dnsProvider;
} else if (domainVerifyPlan.type === "cname") {
const cnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
if (cnameVerifyPlan) {
const cname = cnameVerifyPlan[fullDomain];
if (cname) {
dnsProvider = cname.dnsProvider;
domain = parseDomain(cname.domain);
fullRecord = cname.fullRecord;
}
} else {
this.logger.error("未找到域名Cname校验计划使用默认的dnsProvider");
}
} else if (domainVerifyPlan.type === "http") {
const httpVerifyPlan = domainVerifyPlan.httpVerifyPlan;
if (httpVerifyPlan) {
const httpChallenge = getChallenge("http-01");
if (httpChallenge == null) {
throw new Error("该域名不支持http-01方式校验");
}
const plan = httpVerifyPlan[fullDomain];
return await doHttpVerify(httpChallenge, plan.httpUploader);
} else {
throw new Error("未找到域名【" + fullDomain + "】的http校验配置");
}
} else {
throw new Error("不支持的校验类型", domainVerifyPlan.type);
}
} else {
this.logger.info("未找到域名校验计划使用默认的dnsProvider");
}
}
const dnsChallenge = getChallenge("dns-01");
return await doDnsVerify(dnsChallenge, fullRecord, dnsProvider);
}
/**
@@ -239,22 +278,28 @@ export class AcmeService {
* @param recordReq
* @param recordRes
* @param dnsProvider dnsProvider
* @param httpUploader
* @returns {Promise}
*/
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordReq: any, recordRes: any, dnsProvider: IDnsProvider) {
this.logger.info("Triggered challengeRemoveFn()");
async challengeRemoveFn(
authz: any,
challenge: any,
keyAuthorization: string,
recordReq: any,
recordRes: any,
dnsProvider?: IDnsProvider,
httpUploader?: HttpChallengeUploader
) {
this.logger.info("执行清理");
/* http-01 */
const fullDomain = authz.identifier.value;
if (challenge.type === "http-01") {
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
this.logger.info(`Removing challenge response for ${fullDomain} at path: ${filePath}`);
/* Replace this */
this.logger.info(`Would remove file on path "${filePath}"`);
// await fs.unlinkAsync(filePath);
const filePath = `.well-known/acme-challenge/${challenge.token}`;
this.logger.info(`Removing challenge response for ${fullDomain} at file: ${filePath}`);
await httpUploader.remove(filePath);
this.logger.info(`删除文件【${filePath}】成功`);
} else if (challenge.type === "dns-01") {
this.logger.info(`删除 TXT 解析记录:${JSON.stringify(recordReq)} ,recordRes = ${JSON.stringify(recordRes)}`);
try {
@@ -275,11 +320,12 @@ export class AcmeService {
domains: string | string[];
dnsProvider?: any;
domainsVerifyPlan?: DomainsVerifyPlan;
httpUploader?: any;
csrInfo: any;
isTest?: boolean;
privateKeyType?: string;
}): Promise<CertInfo> {
const { email, isTest, domains, csrInfo, dnsProvider, domainsVerifyPlan } = options;
const { email, isTest, domains, csrInfo, dnsProvider, domainsVerifyPlan, httpUploader } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest);
/* Create CSR */
@@ -319,22 +365,27 @@ export class AcmeService {
privateKey
);
if (dnsProvider == null && domainsVerifyPlan == null) {
throw new Error("dnsProvider 、 domainsVerifyPlan 不能都为空");
if (dnsProvider == null && domainsVerifyPlan == null && httpUploader == null) {
throw new Error("dnsProvider 、 domainsVerifyPlan 、 httpUploader不能都为空");
}
const providers: Providers = {
dnsProvider,
domainsVerifyPlan,
httpUploader,
};
/* 自动申请证书 */
const crt = await client.auto({
csr,
email: email,
termsOfServiceAgreed: true,
skipChallengeVerification: this.skipLocalVerify,
challengePriority: ["dns-01"],
challengePriority: ["dns-01", "http-01"],
challengeCreateFn: async (
authz: acme.Authorization,
challenge: Challenge,
keyAuthorization: string
): Promise<{ recordReq: any; recordRes: any; dnsProvider: any }> => {
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider, domainsVerifyPlan);
keyAuthorizationGetter: (challenge: Challenge) => Promise<string>
): Promise<{ recordReq?: any; recordRes?: any; dnsProvider?: any; challenge: Challenge; keyAuthorization: string }> => {
return await this.challengeCreateFn(authz, keyAuthorizationGetter, providers);
},
challengeRemoveFn: async (
authz: acme.Authorization,
@@ -344,7 +395,7 @@ export class AcmeService {
recordRes: any,
dnsProvider: IDnsProvider
): Promise<any> => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider, httpUploader);
},
signal: this.options.signal,
});

View File

@@ -17,6 +17,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
vModel: "value",
mode: "tags",
open: false,
placeholder: "foo.com / *.foo.com / *.bar.com",
tokenSeparators: [",", " ", "", "、", "|"],
},
rules: [{ type: "domains" }],
@@ -26,9 +27,9 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
},
order: -999,
helper:
"1、支持通配符域名,例如: *.foo.comfoo.com*.test.handsfree.work\n" +
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上域名必须是在同一个DNS提供商解析\n" +
"3、多级子域名要分成多个域名输入*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com\n" +
"1、支持多个域名打到一个证书上,例如: foo.com*.foo.com*.bar.com\n" +
"2、子域名被通配符包含的不要填写例如www.foo.com已经被*.foo.com包含不要填写www.foo.com\n" +
"3、泛域名只能通配*号那一级*.foo.com的证书不能用于xxx.yyy.foo.com、不能用于foo.com\n" +
"4、输入一个空格之后再输入下一个",
})
domains!: string[];
@@ -262,11 +263,12 @@ cert.jksjks格式证书文件java服务器使用
this.logger.info("输入参数变更,准备申请新证书");
return null;
} else {
this.logger.info("输入参数未变更,不需要更新证书");
this.logger.info("输入参数未变更,检查证书是否过期");
}
let oldCert: CertReader | undefined = undefined;
try {
this.logger.info("读取上次证书");
oldCert = await this.readLastCert();
} catch (e) {
this.logger.warn("读取cert失败", e);
@@ -304,6 +306,7 @@ cert.jksjks格式证书文件java服务器使用
async readLastCert(): Promise<CertReader | undefined> {
const cert = this.lastStatus?.status?.output?.cert;
if (cert == null) {
this.logger.info("没有找到上次的证书");
return undefined;
}
return new CertReader(cert);
@@ -321,7 +324,7 @@ cert.jksjks格式证书文件java服务器使用
// 检查有效期
const leftDays = dayjs(expires).diff(dayjs(), "day");
return {
isWillExpire: leftDays < maxDays,
isWillExpire: leftDays <= maxDays,
leftDays,
};
}

View File

@@ -147,10 +147,11 @@ export class CertReader {
tmpOnePath,
});
} catch (err) {
logger.error("处理失败", err);
throw err;
} finally {
//删除临时文件
logger.info("删除临时文件");
logger.info("清理临时文件");
function removeFile(filepath?: string) {
if (filepath) {
fs.unlinkSync(filepath);

View File

@@ -1,7 +1,7 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { utils } from "@certd/basic";
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import type { CertInfo, CnameVerifyPlan, DomainsVerifyPlan, HttpVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import { AcmeService } from "./acme.js";
import * as _ from "lodash-es";
import { createDnsProvider, DnsProviderContext, IDnsProvider } from "../../dns-provider/index.js";
@@ -9,7 +9,7 @@ import { CertReader } from "./cert-reader.js";
import { CertApplyBasePlugin } from "./base.js";
import { GoogleClient } from "../../libs/google.js";
import { EabAccess } from "../../access";
import { CancelError } from "@certd/pipeline";
import { httpChallengeUploaderFactory } from "./uploads/factory.js";
export type { CertInfo };
export * from "./cert-reader.js";
@@ -17,12 +17,20 @@ export type CnameRecordInput = {
id: number;
status: string;
};
export type HttpRecordInput = {
domain: string;
httpUploaderType: string;
httpUploaderAccess: number;
httpUploadRootDir: string;
};
export type DomainVerifyPlanInput = {
domain: string;
type: "cname" | "dns";
type: "cname" | "dns" | "http";
dnsProviderType?: string;
dnsProviderAccessId?: number;
cnameVerifyPlan?: Record<string, CnameRecordInput>;
httpVerifyPlan?: Record<string, HttpRecordInput>;
};
export type DomainsVerifyPlanInput = {
[key: string]: DomainVerifyPlanInput;
@@ -54,11 +62,13 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
options: [
{ value: "dns", label: "DNS直接验证" },
{ value: "cname", label: "CNAME代理验证" },
{ value: "http", label: "HTTP文件验证" },
],
},
required: true,
helper:
"DNS直接验证域名是在阿里云、腾讯云、华为云、Cloudflare、NameSilo、西数注册的选它。\nCNAME代理验证支持任何注册商注册的域名但第一次需要手动添加CNAME记录",
helper: `DNS直接验证域名是在阿里云、腾讯云、华为云、Cloudflare、NameSilo、西数注册的选它
CNAME代理验证支持任何注册商注册的域名但第一次需要手动添加CNAME记录
HTTP文件验证不支持泛域名需要配置网站文件上传`,
})
challengeType!: string;
@@ -122,9 +132,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
component: {
name: "domains-verify-plan-editor",
},
rules: [{ type: "checkCnameVerifyPlan" }],
rules: [{ type: "checkDomainVerifyPlan" }],
required: true,
helper: "如果选择CNAME方式请按照上面的显示给要申请证书的域名添加CNAME记录添加后点击验证验证成功后不要删除记录申请和续期证书会一直用它",
col: {
span: 24,
},
@@ -132,10 +141,20 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
component:{
domains: ctx.compute(({form})=>{
return form.domains
}),
defaultType: ctx.compute(({form})=>{
return form.challengeType || 'cname'
})
},
show: ctx.compute(({form})=>{
return form.challengeType === 'cname'
return form.challengeType === 'cname' || form.challengeType === 'http'
}),
helper: ctx.compute(({form})=>{
if(form.challengeType === 'cname' ){
return '请按照上面的提示给要申请证书的域名添加CNAME记录添加后点击验证验证成功后不要删除记录申请和续期证书会一直用它'
}else if (form.challengeType === 'http'){
return '请按照上面的提示,给每个域名设置文件上传配置,证书申请过程中会上传校验文件到网站根目录下'
}
})
}
`,
@@ -320,9 +339,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
);
this.logger.info("开始申请证书,", email, domains);
let dnsProvider: any = null;
let dnsProvider: IDnsProvider = null;
let domainsVerifyPlan: DomainsVerifyPlan = null;
if (this.challengeType === "cname") {
if (this.challengeType === "cname" || this.challengeType === "http") {
domainsVerifyPlan = await this.createDomainsVerifyPlan();
} else {
const dnsProviderType = this.dnsProviderType;
@@ -370,10 +389,11 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
const domainVerifyPlan = this.domainsVerifyPlan[domain];
let dnsProvider = null;
const cnameVerifyPlan: Record<string, CnameVerifyPlan> = {};
const httpVerifyPlan: Record<string, HttpVerifyPlan> = {};
if (domainVerifyPlan.type === "dns") {
const access = await this.ctx.accessService.getById(domainVerifyPlan.dnsProviderAccessId);
dnsProvider = await this.createDnsProvider(domainVerifyPlan.dnsProviderType, access);
} else {
} else if (domainVerifyPlan.type === "cname") {
for (const key in domainVerifyPlan.cnameVerifyPlan) {
const cnameRecord = await this.ctx.cnameProxyService.getByDomain(key);
let dnsProvider = cnameRecord.commonDnsProvider;
@@ -381,17 +401,44 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
dnsProvider = await this.createDnsProvider(cnameRecord.cnameProvider.dnsProviderType, cnameRecord.cnameProvider.access);
}
cnameVerifyPlan[key] = {
type: "cname",
domain: cnameRecord.cnameProvider.domain,
fullRecord: cnameRecord.recordValue,
dnsProvider,
};
}
} else if (domainVerifyPlan.type === "http") {
const httpUploaderContext = {
accessService: this.ctx.accessService,
logger: this.logger,
utils,
};
for (const key in domainVerifyPlan.httpVerifyPlan) {
const httpRecord = domainVerifyPlan.httpVerifyPlan[key];
const access = await this.ctx.accessService.getById(httpRecord.httpUploaderAccess);
let rootDir = httpRecord.httpUploadRootDir;
if (!rootDir.endsWith("/") && !rootDir.endsWith("\\")) {
rootDir = rootDir + "/";
}
this.logger.info("上传方式", httpRecord.httpUploaderType);
const httpUploader = await httpChallengeUploaderFactory.createUploaderByType(httpRecord.httpUploaderType, {
access,
rootDir: rootDir,
ctx: httpUploaderContext,
});
httpVerifyPlan[key] = {
type: "http",
domain: key,
httpUploader,
};
}
}
plan[domain] = {
domain,
type: domainVerifyPlan.type,
dnsProvider,
cnameVerifyPlan,
httpVerifyPlan,
};
}
return plan;

View File

@@ -0,0 +1,35 @@
import { IAccessService } from "@certd/pipeline";
import { ILogger, utils } from "@certd/basic";
export type HttpChallengeUploader = {
upload: (fileName: string, fileContent: Buffer) => Promise<void>;
remove: (fileName: string) => Promise<void>;
};
export type HttpChallengeUploadContext = {
accessService: IAccessService;
logger: ILogger;
utils: typeof utils;
};
export abstract class BaseHttpChallengeUploader<A> implements HttpChallengeUploader {
rootDir: string;
access: A = null;
logger: ILogger;
utils: typeof utils;
ctx: HttpChallengeUploadContext;
protected constructor(opts: { rootDir: string; access: A }) {
this.rootDir = opts.rootDir;
this.access = opts.access;
}
async setCtx(ctx: any) {
// set context
this.ctx = ctx;
this.logger = ctx.logger;
this.utils = ctx.utils;
}
abstract remove(fileName: string): Promise<void>;
abstract upload(fileName: string, fileContent: Buffer): Promise<void>;
}

View File

@@ -0,0 +1,35 @@
import { HttpChallengeUploadContext } from "./api";
export class HttpChallengeUploaderFactory {
async getClassByType(type: string) {
if (type === "alioss") {
const module = await import("./impls/alioss.js");
return module.AliossHttpChallengeUploader;
} else if (type === "ssh") {
const module = await import("./impls/ssh.js");
return module.SshHttpChallengeUploader;
} else if (type === "ftp") {
const module = await import("./impls/ftp.js");
return module.FtpHttpChallengeUploader;
} else if (type === "tencentcos") {
const module = await import("./impls/tencentcos.js");
return module.TencentCosHttpChallengeUploader;
} else if (type === "qiniuoss") {
const module = await import("./impls/qiniuoss.js");
return module.QiniuOssHttpChallengeUploader;
} else {
throw new Error(`暂不支持此文件上传方式: ${type}`);
}
}
async createUploaderByType(type: string, opts: { rootDir: string; access: any; ctx: HttpChallengeUploadContext }) {
const cls = await this.getClassByType(type);
if (cls) {
// @ts-ignore
const instance = new cls(opts);
await instance.setCtx(opts.ctx);
return instance;
}
}
}
export const httpChallengeUploaderFactory = new HttpChallengeUploaderFactory();

View File

@@ -0,0 +1,39 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { AliossAccess, AliyunAccess } from "@certd/plugin-lib";
import { AliossClient } from "@certd/plugin-lib";
export class AliossHttpChallengeUploader extends BaseHttpChallengeUploader<AliossAccess> {
async upload(filePath: string, fileContent: Buffer) {
const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId);
const client = new AliossClient({
access: aliyunAccess,
bucket: this.access.bucket,
region: this.access.region,
});
const key = this.rootDir + filePath;
this.logger.info(`开始上传文件: ${key}`);
await client.uploadFile(key, fileContent);
this.logger.info(`校验文件上传成功: ${filePath}`);
}
async remove(filePath: string) {
const key = this.rootDir + filePath;
// remove file from alioss
const client = await this.getAliossClient();
await client.removeFile(key);
this.logger.info(`文件删除成功: ${key}`);
}
private async getAliossClient() {
const aliyunAccess = await this.ctx.accessService.getById<AliyunAccess>(this.access.accessId);
const client = new AliossClient({
access: aliyunAccess,
bucket: this.access.bucket,
region: this.access.region,
});
await client.init();
return client;
}
}

View File

@@ -0,0 +1,41 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { FtpAccess, FtpClient } from "@certd/plugin-lib";
import path from "path";
import os from "os";
import fs from "fs";
export class FtpHttpChallengeUploader extends BaseHttpChallengeUploader<FtpAccess> {
async upload(filePath: string, fileContent: Buffer) {
const client = new FtpClient({
access: this.access,
logger: this.logger,
});
await client.connect(async (client) => {
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath);
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
try {
// Write file to temp path
const path = this.rootDir + filePath;
await client.upload(path, tmpFilePath);
} finally {
// Remove temp file
fs.unlinkSync(tmpFilePath);
}
});
}
async remove(filePath: string) {
const client = new FtpClient({
access: this.access,
logger: this.logger,
});
await client.connect(async (client) => {
const path = this.rootDir + filePath;
await client.client.remove(path);
});
}
}

View File

@@ -0,0 +1,31 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { QiniuOssAccess, QiniuClient, QiniuAccess } from "@certd/plugin-lib";
export class QiniuOssHttpChallengeUploader extends BaseHttpChallengeUploader<QiniuOssAccess> {
async upload(filePath: string, fileContent: Buffer) {
const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId);
const client = new QiniuClient({
access: qiniuAccess,
logger: this.logger,
http: this.ctx.utils.http,
});
if (this.rootDir.endsWith("/")) {
this.rootDir = this.rootDir.slice(0, -1);
}
await client.uploadFile(this.access.bucket, this.rootDir + filePath, fileContent);
}
async remove(filePath: string) {
const qiniuAccess = await this.ctx.accessService.getById<QiniuAccess>(this.access.accessId);
const client = new QiniuClient({
access: qiniuAccess,
logger: this.logger,
http: this.ctx.utils.http,
});
if (this.rootDir.endsWith("/")) {
this.rootDir = this.rootDir.slice(0, -1);
}
await client.removeFile(this.access.bucket, this.rootDir + filePath);
}
}

View File

@@ -0,0 +1,45 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { SshAccess, SshClient } from "@certd/plugin-lib";
import path from "path";
import os from "os";
import fs from "fs";
export class SshHttpChallengeUploader extends BaseHttpChallengeUploader<SshAccess> {
async upload(filePath: string, fileContent: Buffer) {
const tmpFilePath = path.join(os.tmpdir(), "cert", "http", filePath);
// Write file to temp path
const dir = path.dirname(tmpFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(tmpFilePath, fileContent);
const key = this.rootDir + filePath;
try {
const client = new SshClient(this.logger);
await client.uploadFiles({
connectConf: this.access,
mkdirs: true,
transports: [
{
localPath: tmpFilePath,
remotePath: key,
},
],
});
} finally {
// Remove temp file
fs.unlinkSync(tmpFilePath);
}
}
async remove(filePath: string) {
const client = new SshClient(this.logger);
const key = this.rootDir + filePath;
await client.removeFiles({
connectConf: this.access,
files: [key],
});
}
}

View File

@@ -0,0 +1,28 @@
import { BaseHttpChallengeUploader } from "../api.js";
import { TencentAccess, TencentCosAccess, TencentCosClient } from "@certd/plugin-lib";
export class TencentCosHttpChallengeUploader extends BaseHttpChallengeUploader<TencentCosAccess> {
async upload(filePath: string, fileContent: Buffer) {
const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId);
const client = new TencentCosClient({
access: access,
logger: this.logger,
region: this.access.region,
bucket: this.access.bucket,
});
const key = this.rootDir + filePath;
await client.uploadFile(key, fileContent);
}
async remove(filePath: string) {
const access = await this.ctx.accessService.getById<TencentAccess>(this.access.accessId);
const client = new TencentCosClient({
access: access,
logger: this.logger,
region: this.access.region,
bucket: this.access.bucket,
});
const key = this.rootDir + filePath;
await client.removeFile(key);
}
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
### Performance Improvements
* 优化acme sdk ([54db744](https://github.com/certd/certd/commit/54db74428259de64d12230c2ab7353ae11197bbc))
* 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a))
* http校验方式支持七牛云oss、阿里云oss、腾讯云cos ([3f74d4d](https://github.com/certd/certd/commit/3f74d4d9e5f5d0e629b44cff1895b3f7a8fbcafc))
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/plugin-lib
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
**Note:** Version bump only for package @certd/plugin-lib

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.29.1",
"version": "1.29.3",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -16,18 +16,22 @@
},
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@certd/basic": "^1.29.1",
"@certd/pipeline": "^1.29.1",
"@certd/plugin-cert": "^1.29.1",
"@certd/basic": "^1.29.3",
"@certd/pipeline": "^1.29.3",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.21.0",
"basic-ftp": "^5.0.5",
"cos-nodejs-sdk-v5": "^2.14.6",
"dayjs": "^1.11.7",
"iconv-lite": "^0.6.3",
"lodash-es": "^4.17.21",
"qiniu": "^7.12.0",
"rimraf": "^5.0.5",
"socks": "^2.8.3",
"socks-proxy-agent": "^8.0.4",
"ssh2": "^1.15.0",
"strip-ansi": "^7.1.0"
"strip-ansi": "^7.1.0",
"tencentcloud-sdk-nodejs": "^4.0.1005"
},
"devDependencies": {
"@types/chai": "^4.3.3",
@@ -44,5 +48,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "36993cb6f8244f4a183d64fcdf5194282140d888"
"gitHead": "ed5634ff83405ad0eb13a8456f59270ed4218734"
}

View File

@@ -0,0 +1,71 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "alioss",
title: "阿里云OSS授权",
desc: "包含地域和Bucket",
icon: "ant-design:aliyun-outlined",
})
export class AliossAccess extends BaseAccess {
@AccessInput({
title: "阿里云授权",
component: {
name: "access-selector",
vModel: "modelValue",
type: "aliyun",
},
helper: "请选择阿里云授权",
required: true,
})
accessId = "";
@AccessInput({
title: "大区",
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "oss-cn-hangzhou", label: "华东1杭州" },
{ value: "oss-cn-shanghai", label: "华东2上海" },
{ value: "oss-cn-nanjing", label: "华东5南京-本地地域)" },
{ value: "oss-cn-fuzhou", label: "华东6福州-本地地域)" },
{ value: "oss-cn-wuhan-lr", label: "华中1武汉-本地地域)" },
{ value: "oss-cn-qingdao", label: "华北1青岛" },
{ value: "oss-cn-beijing", label: "华北2北京" },
{ value: "oss-cn-zhangjiakou", label: "华北 3张家口" },
{ value: "oss-cn-huhehaote", label: "华北5呼和浩特" },
{ value: "oss-cn-wulanchabu", label: "华北6乌兰察布" },
{ value: "oss-cn-shenzhen", label: "华南1深圳" },
{ value: "oss-cn-heyuan", label: "华南2河源" },
{ value: "oss-cn-guangzhou", label: "华南3广州" },
{ value: "oss-cn-chengdu", label: "西南1成都" },
{ value: "oss-cn-hongkong", label: "中国香港" },
{ value: "oss-us-west-1", label: "美国(硅谷)①" },
{ value: "oss-us-east-1", label: "美国(弗吉尼亚)①" },
{ value: "oss-ap-northeast-1", label: "日本(东京)①" },
{ value: "oss-ap-northeast-2", label: "韩国(首尔)" },
{ value: "oss-ap-southeast-1", label: "新加坡①" },
{ value: "oss-ap-southeast-2", label: "澳大利亚(悉尼)①" },
{ value: "oss-ap-southeast-3", label: "马来西亚(吉隆坡)①" },
{ value: "oss-ap-southeast-5", label: "印度尼西亚(雅加达)①" },
{ value: "oss-ap-southeast-6", label: "菲律宾(马尼拉)" },
{ value: "oss-ap-southeast-7", label: "泰国(曼谷)" },
{ value: "oss-eu-central-1", label: "德国(法兰克福)①" },
{ value: "oss-eu-west-1", label: "英国(伦敦)" },
{ value: "oss-me-east-1", label: "阿联酋(迪拜)①" },
{ value: "oss-rg-china-mainland", label: "无地域属性(中国内地)" },
],
},
required: true,
})
region!: string;
@AccessInput({
title: "Bucket",
helper: "存储桶名称",
required: true,
})
bucket!: string;
}
new AliossAccess();

View File

@@ -1 +1,2 @@
export * from './aliyun-access.js';
export * from "./aliyun-access.js";
export * from "./alioss-access.js";

View File

@@ -1,2 +1,3 @@
export * from "./base-client.js";
export * from "./ssl-client.js";
export * from "./oss-client.js";

View File

@@ -0,0 +1,64 @@
import { AliyunAccess } from "../access";
export class AliossClient {
access: AliyunAccess;
region: string;
bucket: string;
client: any;
constructor(opts: { access: AliyunAccess; bucket: string; region: string }) {
this.access = opts.access;
this.bucket = opts.bucket;
this.region = opts.region;
}
async init() {
if (this.client) {
return;
}
// @ts-ignore
const OSS = await import("ali-oss");
const ossClient = new OSS.default({
accessKeyId: this.access.accessKeyId,
accessKeySecret: this.access.accessKeySecret,
// yourRegion填写Bucket所在地域。以华东1杭州为例Region填写为oss-cn-hangzhou。
region: this.region,
//@ts-ignore
authorizationV4: true,
// yourBucketName填写Bucket名称。
bucket: this.bucket,
});
// oss
this.client = ossClient;
}
async doRequest(bucket: string, xml: string, params: any) {
await this.init();
params = this.client._bucketRequestParams("POST", bucket, {
...params,
});
params.content = xml;
params.mime = "xml";
params.successStatuses = [200];
const res = await this.client.request(params);
this.checkRet(res);
return res;
}
checkRet(ret: any) {
if (ret.code != null) {
throw new Error("执行失败:" + ret.Message);
}
}
async uploadFile(filePath: string, content: Buffer) {
await this.init();
return await this.client.put(filePath, content);
}
async removeFile(filePath: string) {
await this.init();
return await this.client.delete(filePath);
}
}

View File

@@ -1,8 +1,11 @@
import { ILogger } from "@certd/basic";
import { AliyunAccess } from "../access/index.js";
import { AliyunClient } from "./index.js";
import { CertInfo } from "@certd/plugin-cert";
export type AliyunCertInfo = {
crt: string; //fullchain证书
key: string; //私钥
};
export type AliyunSslClientOpts = {
access: AliyunAccess;
logger: ILogger;
@@ -23,7 +26,7 @@ export type AliyunSslCreateDeploymentJobReq = {
export type AliyunSslUploadCertReq = {
name: string;
cert: CertInfo;
cert: AliyunCertInfo;
};
export class AliyunSslClient {

View File

@@ -0,0 +1,77 @@
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: "ftp",
title: "FTP授权",
desc: "",
icon: "mdi:folder-upload-outline",
})
export class FtpAccess extends BaseAccess {
@AccessInput({
title: "host",
component: {
placeholder: "ip / 域名",
name: "a-input",
vModel: "value",
},
helper: "FTP地址",
required: true,
})
host!: string;
@AccessInput({
title: "host",
value: 21,
component: {
placeholder: "21",
name: "a-input-number",
vModel: "value",
},
helper: "FTP端口",
required: true,
})
port!: string;
@AccessInput({
title: "user",
component: {
placeholder: "用户名",
},
helper: "FTP用户名",
required: true,
})
user!: string;
@AccessInput({
title: "password",
component: {
placeholder: "密码",
component: {
name: "a-input-password",
vModel: "value",
},
},
encrypt: true,
helper: "FTP密码",
required: true,
})
password!: string;
@AccessInput({
title: "secure",
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "是否使用SSL",
required: true,
})
secure?: boolean = false;
}
new FtpAccess();

View File

@@ -0,0 +1,47 @@
import { FtpAccess } from "./access";
import { ILogger } from "@certd/basic";
import path from "node:path";
export class FtpClient {
access: FtpAccess = null;
logger: ILogger = null;
client: any;
constructor(opts: { access: FtpAccess; logger: ILogger }) {
this.access = opts.access;
this.logger = opts.logger;
}
async connect(callback: (client: FtpClient) => Promise<void>) {
const ftp = await import("basic-ftp");
const Client = ftp.Client;
const client = new Client();
client.ftp.verbose = true;
this.logger.info("开始连接FTP");
await client.access(this.access as any);
this.logger.info("FTP连接成功");
this.client = client;
try {
await callback(this);
} finally {
if (client) {
client.close();
}
}
}
async upload(filePath: string, remotePath: string): Promise<void> {
if (!remotePath) {
return;
}
const dirname = path.dirname(remotePath);
this.logger.info(`确保目录存在:${dirname}`);
await this.client.ensureDir(dirname);
this.logger.info(`开始上传文件${filePath} -> ${remotePath}`);
await this.client.uploadFrom(filePath, remotePath);
}
async remove(filePath: string): Promise<void> {
this.logger.info(`开始删除文件${filePath}`);
await this.client.remove(filePath, true);
}
}

View File

@@ -0,0 +1,2 @@
export * from "./access.js";
export * from "./client.js";

View File

@@ -1,3 +1,6 @@
export * from "./ssh/index.js";
export * from "./aliyun/index.js";
export * from "./common/index.js";
export * from "./ftp/index.js";
export * from "./tencent/index.js";
export * from "./qiniu/index.js";

View File

@@ -0,0 +1,31 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "qiniuoss",
title: "七牛OSS授权",
desc: "",
icon: "svg:icon-qiniuyun",
input: {},
})
export class QiniuOssAccess extends BaseAccess {
@AccessInput({
title: "七牛云授权",
component: {
name: "access-selector",
vModel: "modelValue",
type: "qiniu",
},
helper: "请选择七牛云授权",
required: true,
})
accessId = "";
@AccessInput({
title: "Bucket",
helper: "存储桶名称",
required: true,
})
bucket = "";
}
new QiniuOssAccess();

View File

@@ -0,0 +1,25 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "qiniu",
title: "七牛云授权",
desc: "",
icon: "svg:icon-qiniuyun",
input: {},
})
export class QiniuAccess extends BaseAccess {
@AccessInput({
title: "AccessKey",
rules: [{ required: true, message: "此项必填" }],
helper: "AK前往[密钥管理](https://portal.qiniu.com/developer/user/key)获取",
})
accessKey!: string;
@AccessInput({
title: "SecretKey",
encrypt: true,
helper: "SK",
})
secretKey!: string;
}
new QiniuAccess();

View File

@@ -0,0 +1,3 @@
export * from "./access.js";
export * from "./access-oss.js";
export * from "./lib/sdk.js";

View File

@@ -0,0 +1,142 @@
import { HttpClient, ILogger } from "@certd/basic";
import { QiniuAccess } from "../access.js";
export type QiniuCertInfo = {
key: string;
crt: string;
};
export class QiniuClient {
http: HttpClient;
access: QiniuAccess;
logger: ILogger;
constructor(opts: { http: HttpClient; access: QiniuAccess; logger: ILogger }) {
this.http = opts.http;
this.access = opts.access;
this.logger = opts.logger;
}
async uploadCert(cert: QiniuCertInfo, certName?: string) {
const url = "https://api.qiniu.com/sslcert";
const body = {
name: certName,
common_name: "certd",
pri: cert.key,
ca: cert.crt,
};
const res = await this.doRequest(url, "post", body);
return res.certID;
}
async bindCert(body: { certid: string; domain: string }) {
const url = "https://api.qiniu.com/cert/bind";
return await this.doRequest(url, "post", body);
}
async getCertBindings() {
const url = "https://api.qiniu.com/cert/bindings";
const res = await this.doRequest(url, "get");
return res;
}
async doRequest(url: string, method: string, body?: any) {
const { generateAccessToken } = await import("qiniu/qiniu/util.js");
const token = generateAccessToken(this.access, url);
const res = await this.http.request({
url,
method: method,
headers: {
Authorization: token,
},
data: body,
logRes: false,
});
if (res && res.error) {
if (res.error.includes("domaintype")) {
throw new Error("请求失败:" + res.error + ",该域名属于CDN域名请使用部署到七牛云CDN插件");
}
throw new Error("请求失败:" + res.error);
}
console.log("res", res);
return res;
}
async doRequestV2(opts: { url: string; method: string; body?: any; contentType: string }) {
const { HttpClient } = await import("qiniu/qiniu/httpc/client.js");
const { QiniuAuthMiddleware } = await import("qiniu/qiniu/httpc/middleware/qiniuAuth.js");
// X-Qiniu-Date: 20060102T150405Z
const auth = new QiniuAuthMiddleware({
mac: {
...this.access,
options: {},
},
});
const http = new HttpClient({ timeout: 10000, middlewares: [auth] });
console.log("http", http);
return new Promise((resolve, reject) => {
try {
http.get({
url: opts.url,
headers: {
"Content-Type": opts.contentType,
},
callback: (nullable, res) => {
console.log("nullable", nullable, "res", res);
if (res?.error) {
reject(res);
} else {
resolve(res);
}
},
});
} catch (e) {
reject(e);
}
});
}
async uploadFile(bucket: string, key: string, content: Buffer) {
const sdk = await import("qiniu");
const qiniu = sdk.default;
const mac = new qiniu.auth.digest.Mac(this.access.accessKey, this.access.secretKey);
const options = {
scope: bucket,
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const config = new qiniu.conf.Config();
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();
// 文件上传
const { data, resp } = await formUploader.put(uploadToken, key, content, putExtra);
if (resp.statusCode === 200) {
this.logger.info("文件上传成功:" + key);
return data;
} else {
console.log(resp.statusCode);
throw new Error("上传失败:" + JSON.stringify(resp));
}
}
async removeFile(bucket: string, key: string) {
const sdk = await import("qiniu");
const qiniu = sdk.default;
const mac = new qiniu.auth.digest.Mac(this.access.accessKey, this.access.secretKey);
const config = new qiniu.conf.Config();
config.useHttpsDomain = true;
const bucketManager = new qiniu.rs.BucketManager(mac, config);
const { resp } = await bucketManager.delete(bucket, key);
if (resp.statusCode === 200) {
this.logger.info("文件删除成功:" + key);
return;
} else {
throw new Error("删除失败:" + JSON.stringify(resp));
}
}
}

View File

@@ -7,6 +7,7 @@ import { SshAccess } from "./ssh-access.js";
import stripAnsi from "strip-ansi";
import { SocksClient } from "socks";
import { SocksProxy, SocksProxyType } from "socks/typings/common/constants.js";
export type TransportItem = { localPath: string; remotePath: string };
export class AsyncSsh2Client {
conn: ssh2.Client;
@@ -95,6 +96,21 @@ export class AsyncSsh2Client {
});
}
async unlink(options: { sftp: any; remotePath: string }) {
const { sftp, remotePath } = options;
return new Promise((resolve, reject) => {
this.logger.info(`开始删除远程文件:${remotePath}`);
sftp.unlink(remotePath, (err: Error) => {
if (err) {
reject(err);
return;
}
this.logger.info(`删除文件成功:${remotePath}`);
resolve({});
});
});
}
async exec(
script: string,
opts: {
@@ -239,7 +255,7 @@ export class SshClient {
}
* @param options
*/
async uploadFiles(options: { connectConf: SshAccess; transports: any; mkdirs: boolean }) {
async uploadFiles(options: { connectConf: SshAccess; transports: TransportItem[]; mkdirs: boolean }) {
const { connectConf, transports, mkdirs } = options;
await this._call({
connectConf,
@@ -272,6 +288,24 @@ export class SshClient {
});
}
async removeFiles(opts: { connectConf: SshAccess; files: string[] }) {
const { connectConf, files } = opts;
await this._call({
connectConf,
callable: async (conn: AsyncSsh2Client) => {
const sftp = await conn.getSftp();
this.logger.info("开始删除");
for (const file of files) {
await conn.unlink({
sftp,
remotePath: file,
});
}
this.logger.info("文件全部删除成功");
},
});
}
async isCmd(conn: AsyncSsh2Client) {
const spec = await conn.exec("echo %COMSPEC% ");
if (spec.toString().trim() === "%COMSPEC%") {

View File

@@ -0,0 +1,65 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "tencentcos",
title: "腾讯云COS授权",
icon: "svg:icon-tencentcloud",
desc: "腾讯云对象存储授权,包含地域和存储桶",
})
export class TencentCosAccess extends BaseAccess {
@AccessInput({
title: "腾讯云授权",
component: {
name: "access-selector",
vModel: "modelValue",
type: "tencent",
},
helper: "请选择腾讯云授权",
required: true,
})
accessId = "";
@AccessInput({
title: "所在地域",
helper: "存储桶所在地域",
component: {
name: "a-auto-complete",
vModel: "value",
options: [
{ value: "", label: "--------中国大陆地区-------", disabled: true },
{ value: "ap-beijing-1", label: "北京1区" },
{ value: "ap-beijing", label: "北京" },
{ value: "ap-nanjing", label: "南京" },
{ value: "ap-shanghai", label: "上海" },
{ value: "ap-guangzhou", label: "广州" },
{ value: "ap-chengdu", label: "成都" },
{ value: "ap-chongqing", label: "重庆" },
{ value: "ap-shenzhen-fsi", label: "深圳金融" },
{ value: "ap-shanghai-fsi", label: "上海金融" },
{ value: "ap-beijing-fsi", label: "北京金融" },
{ value: "", label: "--------中国香港及境外-------", disabled: true },
{ value: "ap-hongkong", label: "中国香港" },
{ value: "ap-singapore", label: "新加坡" },
{ value: "ap-mumbai", label: "孟买" },
{ value: "ap-jakarta", label: "雅加达" },
{ value: "ap-seoul", label: "首尔" },
{ value: "ap-bangkok", label: "曼谷" },
{ value: "ap-tokyo", label: "东京" },
{ value: "na-siliconvalley", label: "硅谷" },
{ value: "na-ashburn", label: "弗吉尼亚" },
{ value: "sa-saopaulo", label: "圣保罗" },
{ value: "eu-frankfurt", label: "法兰克福" },
],
},
})
region!: string;
@AccessInput({
title: "Bucket",
helper: "存储桶名称",
required: true,
})
bucket = "";
}
new TencentCosAccess();

View File

@@ -0,0 +1,28 @@
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
@IsAccess({
name: "tencent",
title: "腾讯云",
icon: "svg:icon-tencentcloud",
})
export class TencentAccess extends BaseAccess {
@AccessInput({
title: "secretId",
helper:
"使用对应的插件需要有对应的权限,比如上传证书,需要证书管理权限;部署到clb需要clb相关权限\n前往[密钥管理](https://console.cloud.tencent.com/cam/capi)进行创建",
component: {
placeholder: "secretId",
},
rules: [{ required: true, message: "该项必填" }],
})
secretId = "";
@AccessInput({
title: "secretKey",
component: {
placeholder: "secretKey",
},
encrypt: true,
rules: [{ required: true, message: "该项必填" }],
})
secretKey = "";
}

View File

@@ -0,0 +1,3 @@
export * from "./access.js";
export * from "./access-cos.js";
export * from "./lib/index.js";

View File

@@ -0,0 +1,69 @@
import { TencentAccess } from "../access.js";
import { ILogger } from "@certd/basic";
export class TencentCosClient {
access: TencentAccess;
logger: ILogger;
region: string;
bucket: string;
constructor(opts: { access: TencentAccess; logger: ILogger; region: string; bucket: string }) {
this.access = opts.access;
this.logger = opts.logger;
this.bucket = opts.bucket;
this.region = opts.region;
}
async getCosClient() {
const sdk = await import("cos-nodejs-sdk-v5");
const clientConfig = {
SecretId: this.access.secretId,
SecretKey: this.access.secretKey,
};
return new sdk.default(clientConfig);
}
async uploadFile(key: string, file: Buffer) {
const cos = await this.getCosClient();
return new Promise((resolve, reject) => {
cos.putObject(
{
Bucket: this.bucket /* 必须 */,
Region: this.region /* 必须 */,
Key: key /* 必须 */,
Body: file, // 上传文件对象
onProgress: function (progressData) {
console.log(JSON.stringify(progressData));
},
},
function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
}
);
});
}
async removeFile(key: string) {
const cos = await this.getCosClient();
return new Promise((resolve, reject) => {
cos.deleteObject(
{
Bucket: this.bucket,
Region: this.region,
Key: key,
},
function (err, data) {
if (err) {
reject(err);
return;
}
resolve(data);
}
);
});
}
}

View File

@@ -0,0 +1,2 @@
export * from "./ssl-client.js";
export * from "./cos-client.js";

View File

@@ -1,6 +1,10 @@
import { TencentAccess } from '@certd/plugin-plus';
import { CertInfo } from '@certd/plugin-cert';
import { ILogger } from '@certd/basic';
import { ILogger } from "@certd/basic";
import { TencentAccess } from "../access.js";
export type TencentCertInfo = {
key: string;
crt: string;
};
export class TencentSslClient {
access: TencentAccess;
logger: ILogger;
@@ -11,7 +15,7 @@ export class TencentSslClient {
this.region = opts.region;
}
async getSslClient(): Promise<any> {
const sdk = await import('tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js');
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
const SslClient = sdk.v20191205.Client;
const clientConfig = {
@@ -22,7 +26,7 @@ export class TencentSslClient {
region: this.region,
profile: {
httpProfile: {
endpoint: 'ssl.tencentcloudapi.com',
endpoint: "ssl.tencentcloudapi.com",
},
},
};
@@ -32,11 +36,11 @@ export class TencentSslClient {
checkRet(ret: any) {
if (!ret || ret.Error) {
throw new Error('请求失败:' + ret.Error.Code + ',' + ret.Error.Message);
throw new Error("请求失败:" + ret.Error.Code + "," + ret.Error.Message);
}
}
async uploadToTencent(opts: { certName: string; cert: CertInfo }): Promise<string> {
async uploadToTencent(opts: { certName: string; cert: TencentCertInfo }): Promise<string> {
const client = await this.getSslClient();
const params = {
CertificatePublicKey: opts.cert.crt,
@@ -45,7 +49,7 @@ export class TencentSslClient {
};
const ret = await client.UploadCertificate(params);
this.checkRet(ret);
this.logger.info('证书上传成功tencentCertId=', ret.CertificateId);
this.logger.info("证书上传成功tencentCertId=", ret.CertificateId);
return ret.CertificateId;
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
### Performance Improvements
* 优化站点证书检查页面检查增加3次重试 ([e6dd7cd](https://github.com/certd/certd/commit/e6dd7cd54a3e23897031b5df6e0c3cdc0545d35a))
* 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a))
* http校验方式支持七牛云oss、阿里云oss、腾讯云cos ([3f74d4d](https://github.com/certd/certd/commit/3f74d4d9e5f5d0e629b44cff1895b3f7a8fbcafc))
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
**Note:** Version bump only for package @certd/ui-client
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.29.1",
"version": "1.29.3",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -66,8 +66,8 @@
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@certd/lib-iframe": "^1.29.1",
"@certd/pipeline": "^1.29.1",
"@certd/lib-iframe": "^1.29.3",
"@certd/pipeline": "^1.29.3",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -9,6 +9,12 @@ export type CnameRecord = {
recordValue?: string;
};
export type DomainGroupItem = {
domain: string;
domains?: string[];
keySubDomains?: string[];
};
export async function GetList() {
return await request({
url: apiPrefix + "/list",

View File

@@ -24,10 +24,7 @@ defineOptions({
name: "CnameVerifyPlan"
});
const emit = defineEmits<{
"update:modelValue": any;
change: Record<string, any>;
}>();
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: Record<string, any>;

View File

@@ -0,0 +1,106 @@
<template>
<table class="http-verify-plan">
<thead>
<tr>
<td style="width: 160px">网站域名</td>
<td style="width: 100px; text-align: center">上传方式</td>
<td style="width: 150px">上传授权</td>
<td style="width: 200px">网站根目录路径</td>
</tr>
</thead>
<tbody v-if="records" class="http-record-body">
<template v-for="(item, key) of records" :key="key">
<tr>
<td class="domain">
{{ item.domain }}
</td>
<td>
<fs-dict-select v-model:value="item.httpUploaderType" :dict="uploaderTypeDict" @change="onRecordChange"></fs-dict-select>
</td>
<td>
<access-selector v-model="item.httpUploaderAccess" :type="item.httpUploaderType" @change="onRecordChange"></access-selector>
</td>
<td>
<a-input v-model:value="item.httpUploadRootDir" placeholder="网站根目录,如:/www/wwwroot" @change="onRecordChange"></a-input>
</td>
</tr>
</template>
</tbody>
</table>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from "vue";
import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type";
import { dict } from "@fast-crud/fast-crud";
defineOptions({
name: "HttpVerifyPlan"
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: Record<string, any>;
}>();
const records: Ref<Record<string, HttpRecord>> = ref({});
watch(
() => {
return props.modelValue;
},
(value: any) => {
if (value) {
records.value = {
...value
};
}
},
{
immediate: true
}
);
function onRecordChange() {
emit("update:modelValue", records.value);
emit("change", records.value);
}
const uploaderTypeDict = dict({
data: [
{ label: "SFTP/SSH", value: "ssh" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" }
]
});
</script>
<style lang="less">
.http-verify-plan {
width: 100%;
table-layout: fixed;
tbody tr td {
border-top: 1px solid #e8e8e8 !important;
}
tr {
td {
border: 0 !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.center {
text-align: center;
}
}
//&:last-child {
// td {
// border-bottom: 0 !important;
// }
//}
}
}
</style>

View File

@@ -13,7 +13,7 @@
<table class="plan-table">
<thead>
<tr>
<th>域名</th>
<th style="min-width: 100px">域名</th>
<th>验证方式</th>
<th>验证计划</th>
</tr>
@@ -58,6 +58,9 @@
<div v-if="item.type === 'cname'" class="plan-cname">
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
</div>
<div v-if="item.type === 'http'" class="plan-http">
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
</div>
</div>
</td>
</tr>
@@ -76,10 +79,12 @@ import { ref, watch } from "vue";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CnameVerifyPlan from "./cname-verify-plan.vue";
import HttpVerifyPlan from "./http-verify-plan.vue";
//@ts-ignore
import psl from "psl";
import { Form } from "ant-design-vue";
import { DomainsVerifyPlanInput } from "./type";
import { CnameRecord } from "./api";
import { CnameRecord, DomainGroupItem } from "./api";
defineOptions({
name: "DomainsVerifyPlanEditor"
});
@@ -92,12 +97,17 @@ const challengeTypeOptions = ref<any[]>([
{
label: "CNAME验证",
value: "cname"
},
{
label: "HTTP验证",
value: "http"
}
]);
const props = defineProps<{
modelValue?: DomainsVerifyPlanInput;
domains?: string[];
defaultType?: string;
}>();
const emit = defineEmits<{
@@ -127,23 +137,17 @@ function showError(error: string) {
errorMessageRef.value = error;
}
type DomainGroup = Record<
string,
{
[key: string]: CnameRecord;
}
>[];
type DomainGroup = Record<string, DomainGroupItem>;
function onDomainsChanged(domains: string[]) {
console.log("域名变化", domains);
if (domains == null) {
return;
}
const domainGroups: DomainGroup = {};
for (let domain of domains) {
domain = domain.replace("*.", "");
const parsed = psl.parse(domain);
const keyDomain = domain.replace("*.", "");
const parsed = psl.parse(keyDomain);
if (parsed.error) {
showError(`域名${domain}解析失败: ${JSON.stringify(parsed.error)}`);
continue;
@@ -154,39 +158,83 @@ function onDomainsChanged(domains: string[]) {
}
let group = domainGroups[mainDomain];
if (!group) {
group = {};
group = {
domain: mainDomain,
domains: [],
keySubDomains: []
} as DomainGroupItem;
domainGroups[mainDomain] = group;
}
group[domain] = {
id: 0
};
group.domains.push(domain);
group.keySubDomains.push(keyDomain);
}
for (const domain in domainGroups) {
let planItem = planRef.value[domain];
const subDomains = domainGroups[domain];
const domainGroupItem = domainGroups[domain];
if (!planItem) {
planItem = {
domain,
type: "cname",
cnameVerifyPlan: {
...subDomains
}
//@ts-ignore
type: props.defaultType || "cname",
//@ts-ignore
cnameVerifyPlan: {},
//@ts-ignore
httpVerifyPlan: {}
};
planRef.value[domain] = planItem;
} else {
const cnamePlan = planItem.cnameVerifyPlan;
for (const subDomain in subDomains) {
if (!cnamePlan[subDomain]) {
cnamePlan[subDomain] = {
id: 0
};
}
}
planItem.domains = domainGroupItem.domains;
const cnameOrigin = planItem.cnameVerifyPlan;
const httpOrigin = planItem.httpVerifyPlan;
planItem.cnameVerifyPlan = {};
planItem.httpVerifyPlan = {};
const cnamePlan = planItem.cnameVerifyPlan;
const httpPlan = planItem.httpVerifyPlan;
for (const subDomain of domainGroupItem.keySubDomains) {
if (!cnameOrigin[subDomain]) {
//@ts-ignore
planItem.cnameVerifyPlan[subDomain] = {
id: 0
};
} else {
planItem.cnameVerifyPlan[subDomain] = cnameOrigin[subDomain];
}
for (const subDomain of Object.keys(cnamePlan)) {
if (!subDomains[subDomain]) {
delete cnamePlan[subDomain];
}
if (!cnamePlan[subDomain]) {
//@ts-ignore
cnamePlan[subDomain] = {
id: 0
};
}
if (!httpOrigin[subDomain]) {
//@ts-ignore
planItem.httpVerifyPlan[subDomain] = {
domain: subDomain
};
} else {
planItem.httpVerifyPlan[subDomain] = httpOrigin[subDomain];
}
if (!httpPlan[subDomain]) {
//@ts-ignore
httpPlan[subDomain] = {
domain: subDomain
};
}
}
for (const subDomain of Object.keys(cnamePlan)) {
if (!domainGroupItem.keySubDomains.includes(subDomain)) {
delete cnamePlan[subDomain];
}
}
for (const subDomain of Object.keys(httpPlan)) {
if (!domainGroupItem.keySubDomains.includes(subDomain)) {
delete httpPlan[subDomain];
}
}
}
@@ -200,10 +248,10 @@ function onDomainsChanged(domains: string[]) {
watch(
() => {
return props.domains;
return props.domains && props.defaultType;
},
(domains: string[]) => {
onDomainsChanged(domains);
() => {
onDomainsChanged(props.domains);
},
{
immediate: true,
@@ -277,12 +325,15 @@ watch(
padding: 10px 6px;
}
td {
border-bottom: 1px solid #e8e8e8;
border-bottom: 2px solid #d8d8d8;
border-left: 1px solid #e8e8e8;
padding: 6px 6px;
}
.plan {
td {
border-right: 1px solid #e8e8e8 !important;
}
font-size: 14px;
.ant-select {
width: 100%;

View File

@@ -1,11 +1,20 @@
import { CnameRecord } from "@certd/pipeline";
export type HttpRecord = {
domain: string;
httpUploaderType: string;
httpUploaderAccess: number;
httpUploadRootDir: string;
};
export type DomainVerifyPlanInput = {
domain: string;
type: "cname" | "dns";
domains: string[];
type: "cname" | "dns" | "http";
dnsProviderType?: string;
dnsProviderAccessId?: number;
cnameVerifyPlan?: Record<string, CnameRecord>;
httpVerifyPlan?: Record<string, HttpRecord>;
};
export type DomainsVerifyPlanInput = {
[key: string]: DomainVerifyPlanInput;

View File

@@ -1,12 +1,13 @@
import Validator from "async-validator";
import { DomainsVerifyPlanInput } from "./type";
function checkCnameVerifyPlan(rule, value: DomainsVerifyPlanInput) {
function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
if (value == null) {
return true;
}
for (const domain in value) {
if (value[domain].type === "cname") {
const type = value[domain].type;
if (type === "cname") {
const subDomains = Object.keys(value[domain].cnameVerifyPlan);
if (subDomains.length > 0) {
for (const subDomain of subDomains) {
@@ -16,8 +17,32 @@ function checkCnameVerifyPlan(rule, value: DomainsVerifyPlanInput) {
}
}
}
} else {
if (value[domain].dnsProviderType == null || value[domain].dnsProviderAccessId == null) {
} else if (type === "http") {
const domains = value[domain].domains || [];
for (const item of domains) {
//如果有通配符域名则不允许使用http校验
if (item.startsWith("*.")) {
throw new Error(`域名${item}为通配符域名不支持HTTP校验`);
}
}
const subDomains = Object.keys(value[domain].httpVerifyPlan);
if (subDomains.length > 0) {
for (const subDomain of subDomains) {
const plan = value[domain].httpVerifyPlan[subDomain];
if (!plan.httpUploaderType) {
throw new Error(`域名${subDomain}的上传方式必须填写`);
}
if (!plan.httpUploaderAccess) {
throw new Error(`域名${subDomain}的上传授权信息必须填写`);
}
if (!plan.httpUploadRootDir) {
throw new Error(`域名${subDomain}的网站根路径必须填写`);
}
}
}
} else if (type === "dns") {
if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) {
throw new Error(`DNS模式下域名${domain}的DNS类型和授权信息必须填写`);
}
}
@@ -25,4 +50,4 @@ function checkCnameVerifyPlan(rule, value: DomainsVerifyPlanInput) {
return true;
}
// 注册自定义验证器
Validator.register("checkCnameVerifyPlan", checkCnameVerifyPlan);
Validator.register("checkDomainVerifyPlan", checkDomainVerifyPlan);

View File

@@ -238,7 +238,7 @@ function openUpgrade() {
title: "专业版",
desc: "开源需要您的赞助支持",
type: "plus",
privilege: ["可加VIP群您需求将优先实现", "站点证书监控无限制", "更多通知方式", "更多强大部署插件宝塔、群晖、1Panel等"],
privilege: ["可加VIP群需求将优先实现", "站点证书监控无限制", "更多通知方式", "更多强大部署插件宝塔、群晖、1Panel等"],
trial: {
title: "点击获取7天试用",
click: () => {

View File

@@ -145,7 +145,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
search: {
show: true
},
type: "text",
type: "copyable",
form: {
rules: [
{ required: true, message: "请输入域名" },
@@ -154,8 +154,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
]
},
column: {
width: 160,
sorter: true
width: 200,
sorter: true,
cellRender({ value }) {
return (
<a-tooltip title={value} placement="left">
<fs-copyable modelValue={value}></fs-copyable>
</a-tooltip>
);
}
}
},
httpsPort: {
@@ -185,7 +192,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
width: 200,
sorter: true,
show: true
show: true,
cellRender({ value }) {
return (
<a-tooltip title={value} placement="left">
{value}
</a-tooltip>
);
}
}
},
certProvider: {
@@ -199,7 +213,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
width: 200,
sorter: true
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
}
}
},
certStatus: {
@@ -256,7 +273,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
show: false
},
column: {
sorter: true
sorter: true,
width: 155
}
},
checkStatus: {
@@ -268,6 +286,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
dict: dict({
data: [
{ label: "正常", value: "ok", color: "green" },
{ label: "检查中", value: "checking", color: "blue" },
{ label: "异常", value: "error", color: "red" }
]
}),
@@ -291,7 +310,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
width: 200,
sorter: true
sorter: true,
cellRender({ value }) {
return <a-tooltip title={value}>{value}</a-tooltip>;
}
}
},
pipelineId: {
@@ -336,7 +358,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
value: false
},
column: {
width: 100,
width: 90,
sorter: true
}
}

View File

@@ -43,6 +43,8 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
}
}
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
return {
crudOptions: {
form: {
@@ -94,6 +96,7 @@ export default function (certPlugins: any[], formWrapperRef: any): CreateCrudOpt
title: "定时触发",
type: "text",
form: {
value: `0 ${randomMin} ${randomHour} * * *`,
component: {
name: "cron-editor",
vModel: "modelValue",

View File

@@ -5,7 +5,7 @@ import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue";
import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue";
import dayjs from "dayjs";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
@@ -286,7 +286,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
component: {
name: "expires-time-text",
vModel: "value",
mode: "tag"
mode: "tag",
title: compute(({ value }) => {
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
})
}
}
},

View File

@@ -14,6 +14,7 @@
</a-form-item>
<a-form-item v-if="formState.alipay.enabled" label="支付宝配置" :name="['alipay', 'accessId']" :required="true">
<access-selector v-model="formState.alipay.accessId" type="alipay" from="sys" />
<div class="helper">需要开通电脑网站支付</div>
</a-form-item>
<a-form-item label="微信支付" :name="['wxpay', 'enabled']" :required="true">
@@ -21,6 +22,7 @@
</a-form-item>
<a-form-item v-if="formState.wxpay.enabled" label="微信支付配置" :name="['wxpay', 'accessId']" :required="true">
<access-selector v-model="formState.wxpay.accessId" type="wxpay" from="sys" />
<div class="helper">需要开通Native支付</div>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">

View File

@@ -7,6 +7,7 @@ import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
import createCrudOptionsUser from "/@/views/sys/authority/user/crud";
import UserSuiteStatus from "/@/views/certd/suite/mine/user-suite-status.vue";
import SuiteDurationSelector from "../setting/suite-duration-selector.vue";
import dayjs from "dayjs";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const api = sysUserSuiteApi;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -156,7 +157,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
]
},
valueResolve({ form, value }) {
debugger;
if (value && value.productId) {
form.productId = value.productId;
form.duration = value.duration;
@@ -345,7 +345,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
component: {
name: "expires-time-text",
vModel: "value",
mode: "tag"
mode: "tag",
title: compute(({ value }) => {
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
})
}
}
},

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.29.3](https://github.com/certd/certd/compare/v1.29.2...v1.29.3) (2025-01-04)
### Bug Fixes
* 修复系统级授权无法查看密钥的bug ([8644348](https://github.com/certd/certd/commit/8644348fc41ae2e1672f946ca37e5d3a674e0218))
### Performance Improvements
* 优化站点证书检查页面检查增加3次重试 ([e6dd7cd](https://github.com/certd/certd/commit/e6dd7cd54a3e23897031b5df6e0c3cdc0545d35a))
* 支持http校验方式申请证书 ([405591c](https://github.com/certd/certd/commit/405591c5d08fa1a3b228ee3980199e7731cfec4a))
* http校验方式支持七牛云oss、阿里云oss、腾讯云cos ([3f74d4d](https://github.com/certd/certd/commit/3f74d4d9e5f5d0e629b44cff1895b3f7a8fbcafc))
## [1.29.2](https://github.com/certd/certd/compare/v1.29.1...v1.29.2) (2024-12-25)
### Bug Fixes
* 修复套餐关闭状态下仍然限制用户流水线数量的bug ([66fb9e5](https://github.com/certd/certd/commit/66fb9e5f49491f9c159363b48af14720a37673b1))
## [1.29.1](https://github.com/certd/certd/compare/v1.29.0...v1.29.1) (2024-12-25)
### Performance Improvements

View File

@@ -0,0 +1,4 @@
ALTER TABLE `pi_history` MODIFY COLUMN `pipeline` longtext NULL;
ALTER TABLE `pi_storage` MODIFY COLUMN `value` longtext NULL;
ALTER TABLE `pi_history_log` MODIFY COLUMN `logs` longtext NULL;

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.29.1",
"version": "1.29.3",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -34,18 +34,18 @@
"@aws-sdk/client-acm": "^3.699.0",
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.29.1",
"@certd/basic": "^1.29.1",
"@certd/commercial-core": "^1.29.1",
"@certd/lib-huawei": "^1.29.1",
"@certd/lib-k8s": "^1.29.1",
"@certd/lib-server": "^1.29.1",
"@certd/midway-flyway-js": "^1.29.1",
"@certd/pipeline": "^1.29.1",
"@certd/plugin-cert": "^1.29.1",
"@certd/plugin-lib": "^1.29.1",
"@certd/plugin-plus": "^1.29.1",
"@certd/plus-core": "^1.29.1",
"@certd/acme-client": "^1.29.3",
"@certd/basic": "^1.29.3",
"@certd/commercial-core": "^1.29.3",
"@certd/lib-huawei": "^1.29.3",
"@certd/lib-k8s": "^1.29.3",
"@certd/lib-server": "^1.29.3",
"@certd/midway-flyway-js": "^1.29.3",
"@certd/pipeline": "^1.29.3",
"@certd/plugin-cert": "^1.29.3",
"@certd/plugin-lib": "^1.29.3",
"@certd/plugin-plus": "^1.29.3",
"@certd/plus-core": "^1.29.3",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
"@koa/cors": "^5.0.0",

View File

@@ -40,7 +40,7 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
const res = await this.service.add(bean);
await this.service.check(res.id);
await this.service.check(res.id, false, 0);
return this.ok(res);
}
@@ -49,7 +49,7 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
await this.service.checkUserId(bean.id, this.getUserId());
delete bean.userId;
await this.service.update(bean);
await this.service.check(bean.id);
await this.service.check(bean.id, false, 0);
return this.ok();
}
@Post('/info', { summary: Constants.per.authOnly })
@@ -67,14 +67,14 @@ export class SiteInfoController extends CrudController<SiteInfoService> {
@Post('/check', { summary: Constants.per.authOnly })
async check(@Body('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
await this.service.check(id, false);
await this.service.check(id, false, 0);
return this.ok();
}
@Post('/checkAll', { summary: Constants.per.authOnly })
async checkAll() {
const userId = this.getUserId();
this.service.checkAll(userId);
await this.service.checkAllByUsers(userId);
return this.ok();
}
}

View File

@@ -1,5 +1,5 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { AccessService } from '@certd/lib-server';
import { AccessService, Constants } from '@certd/lib-server';
import { AccessController } from '../../pipeline/access-controller.js';
import { checkComm } from '@certd/plus-core';
@@ -55,6 +55,12 @@ export class SysAccessController extends AccessController {
return await super.define(type);
}
@Post('/getSecretPlain', { summary: Constants.per.authOnly })
async getSecretPlain(@Body(ALL) body: { id: number; key: string }) {
const value = await this.service.getById(body.id, 0);
return this.ok(value[body.key]);
}
@Post('/accessTypeDict', { summary: 'sys:settings:view' })
async getAccessTypeDict() {
return await super.getAccessTypeDict();

View File

@@ -45,6 +45,9 @@ export class AutoAInitSite {
await this.sysSettingsService.backupSecret();
//加载一次密钥
await this.sysSettingsService.getSecret();
await this.sysSettingsService.reloadPrivateSettings();
// 授权许可

View File

@@ -14,8 +14,6 @@ export class AutoCRegisterCron {
@Config('cron.onlyAdminUser')
private onlyAdminUser: boolean;
// @Inject()
// echoPlugin: EchoPlugin;
@Config('cron.immediateTriggerOnce')
private immediateTriggerOnce = false;
@@ -61,13 +59,7 @@ export class AutoCRegisterCron {
break;
}
offset += records.length;
for (const record of records) {
try {
await this.siteInfoService.doCheck(record, true);
} catch (e) {
logger.error(`站点${record.name}检查出错:`, e);
}
}
await this.siteInfoService.checkList(records);
}
logger.info('站点证书检查完成');

View File

@@ -45,7 +45,8 @@ export class AutoZPrint {
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
}
setInterval(() => {
logger.info(`heapUsed: ${format(process.memoryUsage().heapUsed)}`);
const mu = process.memoryUsage();
logger.info(`rss:${format(mu.rss)},heapUsed: ${format(mu.heapUsed)},heapTotal: ${format(mu.heapTotal)},external: ${format(mu.external)}`);
}, 60000);
}

View File

@@ -59,4 +59,13 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
await this.addOrUpdate(bean);
}
async deleteByPipelineId(id: number) {
if (!id) {
return;
}
await this.repository.delete({
pipelineId: id,
});
}
}

View File

@@ -1,11 +1,11 @@
import { Inject, Provide } from '@midwayjs/core';
import { BaseService, NeedSuiteException, NeedVIPException, SysSettingsService, SysSuiteSetting } from '@certd/lib-server';
import { BaseService, NeedSuiteException, NeedVIPException, SysSettingsService } from '@certd/lib-server';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { SiteInfoEntity } from '../entity/site-info.js';
import { siteTester } from './site-tester.js';
import dayjs from 'dayjs';
import { logger } from '@certd/basic';
import { logger, utils } from '@certd/basic';
import { PeerCertificate } from 'tls';
import { NotificationService } from '../../pipeline/service/notification-service.js';
import { isComm, isPlus } from '@certd/plus-core';
@@ -42,7 +42,7 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
}
if (isComm()) {
const suiteSetting = await this.sysSettingsService.getSetting<SysSuiteSetting>(SysSuiteSetting);
const suiteSetting = await this.userSuiteService.getSuiteSetting();
if (suiteSetting.enabled) {
const userSuite = await this.userSuiteService.getMySuiteDetail(data.userId);
if (userSuite.monitorCount.max != -1 && userSuite.monitorCount.max <= userSuite.monitorCount.used) {
@@ -76,23 +76,35 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
* 检查站点证书过期时间
* @param site
* @param notify
* @param retryTimes
*/
async doCheck(site: SiteInfoEntity, notify = true) {
async doCheck(site: SiteInfoEntity, notify = true, retryTimes = 3) {
if (!site?.domain) {
throw new Error('站点域名不能为空');
}
try {
await this.update({
id: site.id,
checkStatus: 'checking',
lastCheckTime: dayjs,
});
const res = await siteTester.test({
host: site.domain,
port: site.httpsPort,
retryTimes,
});
const certi: PeerCertificate = res.certificate;
if (!certi) {
return;
throw new Error('没有发现证书');
}
const expires = certi.valid_to;
const domains = [certi.subject?.CN, ...certi.subjectaltname?.replaceAll('DNS:', '').split(',')];
const allDomains = certi.subjectaltname?.replaceAll('DNS:', '').split(',');
const mainDomain = certi.subject?.CN;
let domains = allDomains;
if (!allDomains.includes(mainDomain)) {
domains = [mainDomain, ...allDomains];
}
const issuer = `${certi.issuer.O}<${certi.issuer.CN}>`;
const isExpired = dayjs().valueOf() > dayjs(expires).valueOf();
const status = isExpired ? 'expired' : 'ok';
@@ -139,13 +151,14 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
* 检查,但不发邮件
* @param id
* @param notify
* @param retryTimes
*/
async check(id: number, notify = false) {
async check(id: number, notify = false, retryTimes = 3) {
const site = await this.info(id);
if (!site) {
throw new Error('站点不存在');
}
return await this.doCheck(site, notify);
return await this.doCheck(site, notify, retryTimes);
}
async sendCheckErrorNotify(site: SiteInfoEntity) {
@@ -206,15 +219,22 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
}
async checkAll(userId: any) {
async checkAllByUsers(userId: any) {
if (!userId) {
throw new Error('userId is required');
}
const sites = await this.repository.find({
where: { userId },
});
this.checkList(sites);
}
async checkList(sites: SiteInfoEntity[]) {
for (const site of sites) {
await this.doCheck(site);
this.doCheck(site).catch(e => {
logger.error(`检查站点证书失败,${site.domain}`, e.message);
});
await utils.sleep(200);
}
}
}

View File

@@ -1,4 +1,4 @@
import { logger } from '@certd/basic';
import { logger, utils } from '@certd/basic';
import { merge } from 'lodash-es';
import https from 'https';
import { PeerCertificate } from 'tls';
@@ -6,6 +6,7 @@ export type SiteTestReq = {
host: string; // 只用域名部分
port?: number;
method?: string;
retryTimes?: number;
};
export type SiteTestRes = {
@@ -14,6 +15,28 @@ export type SiteTestRes = {
export class SiteTester {
async test(req: SiteTestReq): Promise<SiteTestRes> {
logger.info('测试站点:', JSON.stringify(req));
const maxRetryTimes = req.retryTimes ?? 3;
let tryCount = 0;
let result: SiteTestRes = {};
while (true) {
try {
result = await this.doTestOnce(req);
return result;
} catch (e) {
tryCount++;
if (tryCount > maxRetryTimes) {
logger.error(`测试站点出错,重试${maxRetryTimes}次。`, e.message);
throw e;
}
//指数退避
const time = 2 ** tryCount;
logger.error(`测试站点出错,${time}s后重试`, e);
await utils.sleep(time * 1000);
}
}
}
async doTestOnce(req: SiteTestReq): Promise<SiteTestRes> {
const agent = new https.Agent({ keepAlive: false });
const options: any = merge(

View File

@@ -204,13 +204,16 @@ export class PipelineService extends BaseService<PipelineEntity> {
// }
if (isComm()) {
//校验pipelineCount
const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId);
if (userSuite?.pipelineCount.max != -1 && userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
}
const suiteSetting = await this.userSuiteService.getSuiteSetting();
if (suiteSetting.enabled) {
const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId);
if (userSuite?.pipelineCount.max != -1 && userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
}
if (userSuite.domainCount.max != -1 && userSuite.domainCount.used + domains.length > userSuite.domainCount.max) {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
if (userSuite.domainCount.max != -1 && userSuite.domainCount.used + domains.length > userSuite.domainCount.max) {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
}
}
}
@@ -345,6 +348,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
await super.delete([id]);
await this.historyService.deleteByPipelineId(id);
await this.historyLogService.deleteByPipelineId(id);
await this.certInfoService.deleteByPipelineId(id);
}
async clearTriggers(id: number) {

View File

@@ -1,6 +1,5 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { CertInfo, CertReader, CertReaderHandleContext } from '@certd/plugin-cert';
import * as fs from 'fs';
import dayjs from 'dayjs';
import { SshAccess, SshClient } from '@certd/plugin-lib';
@@ -243,27 +242,32 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
})
hostJksPath!: string;
@TaskOutput({
title: '一体证书保存路径',
})
hostOnePath!: string;
async onInstance() {}
copyFile(srcFile: string, destFile: string) {
if (!srcFile || !destFile) {
this.logger.warn(`srcFile:${srcFile} 或 destFile:${destFile} 为空,不复制`);
return;
}
const dir = destFile.substring(0, destFile.lastIndexOf('/'));
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.copyFileSync(srcFile, destFile);
this.logger.info(`复制文件:${srcFile} => ${destFile}`);
}
// copyFile(srcFile: string, destFile: string) {
// if (!srcFile || !destFile) {
// this.logger.warn(`srcFile:${srcFile} 或 destFile:${destFile} 为空,不复制`);
// return;
// }
// const dir = destFile.substring(0, destFile.lastIndexOf('/'));
// if (!fs.existsSync(dir)) {
// fs.mkdirSync(dir, { recursive: true });
// }
// fs.copyFileSync(srcFile, destFile);
// this.logger.info(`复制文件:${srcFile} => ${destFile}`);
// }
async execute(): Promise<void> {
const { cert, accessId } = this;
let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath } = this;
let { crtPath, keyPath, icPath, pfxPath, derPath, jksPath, onePath } = this;
const certReader = new CertReader(cert);
const handle = async (opts: CertReaderHandleContext) => {
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpJksPath, tmpPfxPath, tmpIcPath } = opts;
const { tmpCrtPath, tmpKeyPath, tmpDerPath, tmpJksPath, tmpPfxPath, tmpIcPath, tmpOnePath } = opts;
// if (this.copyToThisHost) {
// this.logger.info('复制到目标路径');
// this.copyFile(tmpCrtPath, crtPath);
@@ -336,6 +340,16 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
});
this.logger.info(`上传jks证书到主机${jksPath}`);
}
if (this.onePath) {
this.logger.info(`上传一体证书到主机:${this.onePath}`);
onePath = this.onePath.trim();
transports.push({
localPath: tmpOnePath,
remotePath: this.onePath,
});
}
this.logger.info('开始上传文件到服务器');
await sshClient.uploadFiles({
connectConf,
@@ -350,6 +364,7 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
this.hostPfxPath = pfxPath;
this.hostDerPath = derPath;
this.hostJksPath = jksPath;
this.hostOnePath = onePath;
};
await certReader.readCertFile({
@@ -376,6 +391,8 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
env['HOST_IC_PATH'] = this.hostIcPath || '';
env['HOST_PFX_PATH'] = this.hostPfxPath || '';
env['HOST_DER_PATH'] = this.hostDerPath || '';
env['HOST_JKS_PATH'] = this.hostJksPath || '';
env['HOST_ONE_PATH'] = this.hostOnePath || '';
}
const scripts = this.script.split('\n');

View File

@@ -1,8 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine, QiniuAccess, QiniuClient } from '@certd/plugin-lib';
import { CertInfo } from '@certd/plugin-cert';
import { optionsUtils } from '@certd/basic/dist/utils/util.options.js';
import { QiniuAccess, QiniuClient } from '@certd/plugin-plus';
@IsTaskPlugin({
name: 'QiniuDeployCertToCDN',

View File

@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, TaskOutput } from '@certd/pipeline';
import { QiniuAccess, QiniuClient } from '@certd/plugin-plus';
import { CertInfo } from '@certd/plugin-cert';
import { QiniuAccess, QiniuClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'QiniuCertUpload',
@@ -51,7 +51,7 @@ export class QiniuCertUpload extends AbstractTaskPlugin {
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始上传证书到七牛云');
const access = (await this.accessService.getById(this.accessId)) as QiniuAccess;
const access = await this.accessService.getById<QiniuAccess>(this.accessId);
const qiniuClient = new QiniuClient({
http: this.ctx.http,
access,

View File

@@ -1,7 +1,7 @@
import { Autowire } from '@certd/pipeline';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { TencentAccess } from '@certd/plugin-plus';
import { TencentAccess } from '@certd/plugin-lib';
@IsDnsProvider({
name: 'tencent',

View File

@@ -1,8 +1,8 @@
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { AbstractPlusTaskPlugin, TencentAccess } from '@certd/plugin-plus';
import { TencentSslClient } from '../../lib/index.js';
import { AbstractPlusTaskPlugin } from '@certd/plugin-plus';
import dayjs from 'dayjs';
import { remove } from 'lodash-es';
import { TencentAccess, TencentSslClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'TencentDeleteExpiringCert',

View File

@@ -1,9 +1,7 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { TencentAccess } from '@certd/plugin-plus';
import { CertInfo } from '@certd/plugin-cert';
import { TencentSslClient } from '../../lib/index.js';
import { createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { TencentAccess, TencentSslClient } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'TencentDeployCertToCDNv2',
title: '腾讯云-部署到CDN-v2',

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