Compare commits

...

73 Commits

Author SHA1 Message Date
xiaojunnuo
031df8fc35 v1.21.2 2024-07-08 15:58:51 +08:00
xiaojunnuo
62419a8212 build: prepare to build 2024-07-08 15:57:13 +08:00
xiaojunnuo
09d93af853 build: prepare to build 2024-07-08 15:47:51 +08:00
xiaojunnuo
b6ab178de9 chore: 1 2024-07-08 15:47:36 +08:00
xiaojunnuo
5a08f27d2c build: prepare to build 2024-07-08 15:36:18 +08:00
xiaojunnuo
fe91d94090 perf: 申请证书时可以选择跳过本地dns校验 2024-07-08 15:35:58 +08:00
xiaojunnuo
56ab3269d2 chore: 1 2024-07-08 12:05:56 +08:00
xiaojunnuo
c8243c573f chore: 1 2024-07-08 12:04:59 +08:00
xiaojunnuo
0b769a1c86 v1.21.1 2024-07-08 11:58:09 +08:00
xiaojunnuo
a8189a974d build: prepare to build 2024-07-08 11:46:13 +08:00
xiaojunnuo
322875d241 chore: 1 2024-07-08 11:45:31 +08:00
xiaojunnuo
ed8a54a2bc chore: 1 2024-07-08 11:29:11 +08:00
xiaojunnuo
5ba9831ed1 perf: 上传到主机,支持设置不mkdirs 2024-07-08 11:19:02 +08:00
xiaojunnuo
f9c9fce581 docs: 1 2024-07-08 11:10:08 +08:00
xiaojunnuo
bc650a32cd docs: 1 2024-07-08 10:59:19 +08:00
xiaojunnuo
c6d3e3fe5b docs: 1 2024-07-08 10:57:42 +08:00
xiaojunnuo
73acc62af1 docs: 1 2024-07-08 10:56:19 +08:00
xiaojunnuo
0d4491f3a0 docs: 1 2024-07-08 10:55:55 +08:00
xiaojunnuo
85248044ab docs: 1 2024-07-08 10:53:55 +08:00
xiaojunnuo
c532449102 docs: 1 2024-07-05 10:56:29 +08:00
xiaojunnuo
970c7fd8a0 perf: 说明优化,默认值优化 2024-07-04 02:22:52 +08:00
xiaojunnuo
4656019898 v1.21.0 2024-07-04 01:15:51 +08:00
xiaojunnuo
7eceabb2d8 build: prepare to build 2024-07-04 01:14:33 +08:00
xiaojunnuo
eade2c2b68 feat: 支持zero ssl 2024-07-04 01:14:09 +08:00
xiaojunnuo
6ec950818c v1.20.17 2024-07-03 23:45:57 +08:00
xiaojunnuo
7058b20df4 build: prepare to build 2024-07-03 23:43:55 +08:00
xiaojunnuo
a09b0e48c1 perf: 文件上传提示由cert.crt改为cert.pem 2024-07-03 23:39:12 +08:00
xiaojunnuo
664bb66a91 Merge branch 'client_sync' into v2
# Conflicts:
#	packages/ui/certd-client/CHANGELOG.md
#	packages/ui/certd-client/package.json
2024-07-03 23:37:08 +08:00
xiaojunnuo
eba333de7a perf: 优化cname verify 2024-07-03 23:36:06 +08:00
xiaojunnuo
f47b35f6d5 perf: 创建dns解析后,强制等待60s 2024-07-03 23:27:35 +08:00
Greper
c04707c0f7 fix: 修复对Windows powershell 的支持
修复对Windows powershell 的支持 #73
2024-07-03 23:22:29 +08:00
ltxhhz
22ebcd4dd1 fixed #73 2024-07-03 18:30:38 +08:00
xiaojunnuo
d46dab4fdd v1.20.16 2024-07-02 00:36:55 +08:00
xiaojunnuo
d44849c53c build: prepare to build 2024-07-02 00:35:41 +08:00
xiaojunnuo
dbc5a3c6b3 docs: 1 2024-07-02 00:33:21 +08:00
xiaojunnuo
4a5fa767ed fix: 修复配置了cdn cname后申请失败的bug 2024-07-02 00:18:28 +08:00
xiaojunnuo
1b7debc6a4 Merge remote-tracking branch 'origin/acme_sync' into v2
# Conflicts:
#	packages/core/acme-client/package.json
#	packages/core/acme-client/src/auto.js
#	packages/core/acme-client/src/axios.js
#	packages/core/acme-client/src/http.js
2024-07-01 23:09:57 +08:00
xiaojunnuo
19a6b94680 v1.20.15 2024-06-28 16:06:55 +08:00
xiaojunnuo
65a72b8d60 build: prepare to build 2024-06-28 14:27:19 +08:00
xiaojunnuo
7f61cab101 perf: 支持windows文件上传 2024-06-27 16:38:43 +08:00
GitHub Actions Bot
692e2b5b96 🔱: [client] sync upgrade with 2 commits [trident-sync]
perf: 增加示例,FsInDrawer
2024-06-26 19:24:03 +00:00
xiaojunnuo
37caef38ad chore: 1 2024-06-26 19:07:37 +08:00
xiaojunnuo
9cc01db1d5 fix: 修复无法强制取消任务的bug 2024-06-26 19:05:35 +08:00
xiaojunnuo
9172440f79 chore: 1 2024-06-26 18:37:36 +08:00
xiaojunnuo
e0eb3a4413 perf: 腾讯云dns provider 支持腾讯云的accessId 2024-06-26 18:36:11 +08:00
xiaojunnuo
ae0f16bf35 chore: doc 2024-06-26 13:58:17 +08:00
xiaojunnuo
6c9ed162e3 chore: doc 2024-06-26 13:48:22 +08:00
xiaojunnuo
3849b52cdf chore: ssh优化 2024-06-25 12:28:37 +08:00
xiaojunnuo
9ecfcb5814 chore: ssh优化 2024-06-25 12:25:57 +08:00
xiaojunnuo
54ad09f755 chore: 1 2024-06-25 11:27:13 +08:00
xiaojunnuo
6ee4dc165b chore: 1 2024-06-25 11:22:11 +08:00
xiaojunnuo
8e2eb89696 chore: 1 2024-06-25 11:22:02 +08:00
xiaojunnuo
9d397cc8be chore: 1 2024-06-25 11:02:29 +08:00
xiaojunnuo
cbfb0755b3 chore: 1 2024-06-25 10:52:58 +08:00
xiaojunnuo
d8d127ee9d chore: 1 2024-06-24 09:59:14 +08:00
xiaojunnuo
0ed5430e80 chore: 1 2024-06-24 09:54:41 +08:00
xiaojunnuo
878c1f52fa chore: 1 2024-06-24 09:53:21 +08:00
GitHub Actions Bot
586d23fc55 🔱: [client] sync upgrade with 6 commits [trident-sync]
build: publish success
fix: 修复多级表头时列设置的问题

https://github.com/fast-crud/fast-crud/issues/175
chore: 1
fix: 修复element示例中远程搜索下拉框label不显示的bug

https://github.com/fast-crud/fast-crud/issues/422
fix: 修复独立使用对话框 openDialog方法await无返回值的bug
2024-06-23 19:23:47 +00:00
xiaojunnuo
6900452b49 v1.20.14 2024-06-24 01:05:48 +08:00
xiaojunnuo
b97e0e512d build: prepare to build 2024-06-24 01:04:25 +08:00
xiaojunnuo
f740ff517f fix: 修复修改密码功能异常问题 2024-06-24 01:04:03 +08:00
xiaojunnuo
e5989fe023 v1.20.13 2024-06-19 00:56:42 +08:00
xiaojunnuo
4323156fbe build: prepare to build 2024-06-19 00:54:13 +08:00
xiaojunnuo
3c721901c5 chore: 1 2024-06-19 00:53:53 +08:00
xiaojunnuo
5c2c50839a perf: ssh登录支持openssh格式私钥、支持私钥密码 2024-06-19 00:21:13 +08:00
xiaojunnuo
fd54c2ffac perf: 增加警告,修复一些样式错乱问题 2024-06-19 00:20:35 +08:00
xiaojunnuo
7e483e6091 fix: 修复logo问题 2024-06-19 00:19:48 +08:00
xiaojunnuo
80c48e9acd Merge remote-tracking branch 'origin/v2' into v2 2024-06-18 23:50:04 +08:00
xiaojunnuo
b98f1c0dd0 fix: 修复邮箱设置页面SMTP拼写错误的问题
https://github.com/certd/certd/issues/69
2024-06-18 23:09:32 +08:00
Greper
b53874a0b8 fix: 日志高度越界
fix: 日志高度越界
2024-06-18 23:07:55 +08:00
shenan
c4c9adb8bf fix: 日志高度越界 2024-06-18 11:46:54 +08:00
GitHub Actions Bot
162e10909b 🔱: [acme] sync upgrade with 7 commits [trident-sync]
Small crypto docs fix 2
Small crypto docs fix
Bump v5.3.1
Discourage use of cert subject common name, examples and docs
Style refactor docs and examples
Bump dependencies
2024-05-23 19:24:12 +00:00
GitHub Actions Bot
0f1ae6ccd9 🔱: [acme] sync upgrade with 3 commits [trident-sync]
Clean up eslintrc, style refactor and formatting fixes
Update auto.js

see https://github.com/publishlab/node-acme-client/issues/88#issuecomment-2105255828
2024-05-22 19:24:07 +00:00
113 changed files with 1941 additions and 975 deletions

View File

@@ -3,6 +3,69 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08)
### Performance Improvements
* 申请证书时可以选择跳过本地dns校验 ([fe91d94](https://github.com/certd/certd/commit/fe91d94090d22ed0a3ea753ba74dfaa1bf057c17))
## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08)
### Performance Improvements
* 上传到主机支持设置不mkdirs ([5ba9831](https://github.com/certd/certd/commit/5ba9831ed1aa6ec6057df246f1035b36b9c41d2e))
* 说明优化,默认值优化 ([970c7fd](https://github.com/certd/certd/commit/970c7fd8a0f557770e973d8462ee5684ef742810))
# [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03)
### Features
* 支持zero ssl ([eade2c2](https://github.com/certd/certd/commit/eade2c2b681569f03e9cd466e7d5bcd6703ed492))
## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03)
### Performance Improvements
* 创建dns解析后强制等待60s ([f47b35f](https://github.com/certd/certd/commit/f47b35f6d5bd7d675005c3e286b7e9a029201f8b))
* 文件上传提示由cert.crt改为cert.pem ([a09b0e4](https://github.com/certd/certd/commit/a09b0e48c176f3ed763791bd50322c29729f7c1c))
* 优化cname verify ([eba333d](https://github.com/certd/certd/commit/eba333de7a5b5ef4b0b7eaa904f578720102fa61))
## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01)
### Bug Fixes
* 修复配置了cdn cname后申请失败的bug ([4a5fa76](https://github.com/certd/certd/commit/4a5fa767edc347d03d29a467e86c9a4d70b0220c))
## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28)
### Bug Fixes
* 修复无法强制取消任务的bug ([9cc01db](https://github.com/certd/certd/commit/9cc01db1d569a5c45bb3e731f35d85df324a8e62))
### Performance Improvements
* 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/certd/certd/commit/e0eb3a441384d474fe2923c69b25318264bdc9df))
* 支持windows文件上传 ([7f61cab](https://github.com/certd/certd/commit/7f61cab101fa13b4e88234e9ad47434e6130fed2))
## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23)
### Bug Fixes
* 修复修改密码功能异常问题 ([f740ff5](https://github.com/certd/certd/commit/f740ff517f521dce361284c2c54bccc68aee0ea2))
## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18)
### Bug Fixes
* 日志高度越界 ([c4c9adb](https://github.com/certd/certd/commit/c4c9adb8bfd513f57252e523794e3799a9b220f8))
* 修复邮箱设置页面SMTP拼写错误的问题 ([b98f1c0](https://github.com/certd/certd/commit/b98f1c0dd0bc6c6b4f814c578692afdf6d90b88d))
* 修复logo问题 ([7e483e6](https://github.com/certd/certd/commit/7e483e60913d509b113148c735fe13ba1d72dddf))
### Performance Improvements
* 增加警告,修复一些样式错乱问题 ([fd54c2f](https://github.com/certd/certd/commit/fd54c2ffac492222e85ff2f5f49a9ee5cfc73588))
* ssh登录支持openssh格式私钥、支持私钥密码 ([5c2c508](https://github.com/certd/certd/commit/5c2c50839a9076004f9034d754ac6deb531acdfb))
## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17)
### Bug Fixes

View File

@@ -1,13 +1,15 @@
# CertD
CertD 是一个免费全自动申请和部署SSL证书的工具。
CertD 是一个免费全自动申请和自动部署更新SSL证书的工具。
后缀D取自linux守护进程的命名风格意为证书守护进程。
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签
## 一、特性
本项目不仅支持证书申请过程自动化,还可以自动化部署证书,让你的证书永不过期。
本项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。
* 全自动申请证书支持阿里云、腾讯云、华为云、Cloudflare注册的域名
* 全自动部署证书(目前支持服务器上传部署、部署到阿里云、腾讯云等)
* 全自动部署更新证书(目前支持服务器上传部署、部署到阿里云、腾讯云等)
* 支持通配符域名
* 支持多个域名打到一个证书上
* 邮件通知
@@ -49,9 +51,8 @@ https://certd.handsfree.work/
1.2 安装docker
https://docs.docker.com/engine/install/
选择对应的操作系统,按照官方文档执行命令即可
1.3 安装docker-compose
https://docs.docker.com/compose/install/linux/
### 2. 下载docker-compose.yaml文件
@@ -82,12 +83,12 @@ https://github.com/certd/certd/releases
# 如果docker compose是插件化安装
export CERTD_VERSION=latest
docker compose up -d
#如果docker compose是独立安装
export CERTD_VERSION=latest
docker-compose up -d
```
如果提示 没有compose命令,请安装docker-compose
https://docs.docker.com/compose/install/linux/
### 4. 访问
http://your_server_ip:7001
@@ -102,21 +103,27 @@ http://your_server_ip:7001
## 五、一些说明及问题处理
### 1. 一些说明
## 五、一些说明
* 本项目ssl证书提供商为letencrypt
* 申请过程遵循acme协议
* 需要验证域名所有权一般有两种方式目前本项目仅支持dns-01
* http-01 在网站根目录下放置一份txt文件
* dns-01 需要给域名添加txt解析记录通配符域名只能用这种方式
* 证书续期:
* 实际上acme并没有续期概念
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书。
* 实际上没有办法不改变证书文件本身情况下直接续期或者续签
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去
* 免费证书过期时间90天以后可能还会缩短所以自动化部署必不可少
* 设置每天自动运行当证书过期前20天会自动重新申请证书并部署
### 2. 问题处理
#### 2.1 忘记管理员密码
## 六、不同平台的设置说明
* [Cloudflare](./doc/cf/cf.md)
* [腾讯云](./doc/tencent/tencent.md)
* [windows主机](./doc/host/host.md)
## 七、问题处理
### 7.1 忘记管理员密码
解决方法如下:
1. 修改docker-compose.yaml文件将环境变量`certd_system_resetAdminPassword`改为`true`
```yaml
@@ -138,7 +145,7 @@ docker compose up -d
```
5. 使用admin/123456登录系统请及时修改管理员密码
## 、联系作者
## 、联系作者
如有疑问欢迎加入群聊请备注certd
* QQ群141236433
* 微信群:
@@ -150,7 +157,7 @@ docker compose up -d
<img height="230" src="./doc/images/me.png">
</p>
## 捐赠
## 九、捐赠
媳妇儿说:“一天到晚搞开源,也不管管老婆孩子!😡😡😡”
拜托各位捐赠支持一下,让媳妇儿开心开心,我也能有更多时间进行开源项目,感谢🙏🙏🙏
<p align="center">
@@ -158,12 +165,16 @@ docker compose up -d
</p>
## 、贡献代码
## 、贡献代码
[贡献插件教程](./plugin.md)
## 、我的其他项目
## 十一、我的其他项目
* [袖手GPT](https://ai.handsfree.work/) ChatGPT国内可用无需FQ每日免费额度
* [fast-crud](https://gitee.com/fast-crud/fast-crud/) 基于vue3的crud快速开发框架
* [dev-sidecar](https://github.com/docmirror/dev-sidecar/) 直连访问github工具无需FQ解决github无法访问的问题
## 十二、版本更新日志
https://github.com/certd/certd/blob/v2/CHANGELOG.md

View File

@@ -1,6 +1,5 @@
import http from 'axios'
import fs from 'fs'
//读取 packages/core/pipline/package.json的版本号
import {default as packageJson} from './packages/core/pipeline/package.json' assert { type: "json" };

15
doc/cf/cf.md Normal file
View File

@@ -0,0 +1,15 @@
# Cloudflare
## CF Token申请
### 申请地址:
https://dash.cloudflare.com/profile/api-tokens
### 权限设置:
需要设置权限和资源范围
权限包括Zone.Zone.edit, Zone.DNS.edit
资源范围要包含对应域名推荐直接设置为All Zones
最终效果如下,可以切换语言为英文对比如下图检查
![](./cf_token.png)

BIN
doc/cf/cf_token.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

24
doc/host/host.md Normal file
View File

@@ -0,0 +1,24 @@
# 远程主机
远程主机基于ssh协议通过ssh连接远程主机执行命令。
## windows开启OpenSSH Server
1. 安装OpenSSH Server
请前往Microsoft官方文档查看如何开启openSSH
https://learn.microsoft.com/zh-cn/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui#install-openssh-for-windows
2. 启动OpenSSH Server服务
```
win+R 弹出运行对话框,输入 services.msc 打开服务管理器
找到 OpenSSH SSH Server
启动ssh server服务并且设置为自动启动
```
3. 测试ssh登录
使用你常用的ssh客户端连接你的windows主机进行测试
```cmd
# 如何确定你用户名
C:\Users\xiaoj>
↑↑↑↑---------这个就是windows ssh的登录用户名
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

16
doc/tencent/tencent.md Normal file
View File

@@ -0,0 +1,16 @@
# 腾讯云
## DNSPOD 授权设置
目前腾讯云管理的域名的dns暂时只支持从DNSPOD进行设置
打开 https://console.dnspod.cn/account/token/apikey
然后按如下方式获取DNSPOD的授权
![](./dnspod-token.png)
## 腾讯云API密钥设置
腾讯云其他部署需要API密钥需要在腾讯云控制台进行设置
打开 https://console.cloud.tencent.com/cam/capi
然后按如下方式获取腾讯云的API密钥
![](./tencent-access.png)

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.20.12"
"version": "1.21.2"
}

View File

@@ -14,9 +14,10 @@
"i-all": "lerna link && lerna exec npm install ",
"publish": "npm run prepublishOnly1 && lerna publish --conventional-commits && npm run afterpublishOnly && npm run deploy1",
"afterpublishOnly": "",
"prepublishOnly1": "npm run before-build && lerna run build ",
"prepublishOnly1": "npm run check && npm run before-build && lerna run build ",
"before-build": "cd ./packages/core/acme-client && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
"deploy1": "node --experimental-json-modules deploy.js "
"deploy1": "node --experimental-json-modules deploy.js ",
"check": "node --experimental-json-modules publish-check.js"
},
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -9,15 +9,8 @@ env:
rules:
indent: [2, 4, { SwitchCase: 1, VariableDeclarator: 1 }]
brace-style: [2, 'stroustrup', { allowSingleLine: true }]
space-before-function-paren: [2, { anonymous: 'never', named: 'never' }]
func-names: 0
prefer-destructuring: 0
object-curly-newline: 0
class-methods-use-this: 0
wrap-iife: [2, 'inside']
no-param-reassign: 0
comma-dangle: [2, 'never']
max-len: [1, 200, 2, { ignoreUrls: true, ignoreComments: false }]
no-multiple-empty-lines: [2, { max: 2, maxBOF: 0, maxEOF: 0 }]
prefer-object-spread: 0
import/no-useless-path-segments: 0

View File

@@ -1,4 +1,3 @@
---
name: test
on: [push, pull_request]
@@ -12,7 +11,6 @@ jobs:
node: [16, 18, 20, 22]
eab: [0, 1]
#
# Environment
#
@@ -41,7 +39,6 @@ jobs:
ACME_HTTP_PORT: 5002
ACME_HTTPS_PORT: 5003
#
# Pipeline
#

View File

@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.2](https://github.com/publishlab/node-acme-client/compare/v1.21.1...v1.21.2) (2024-07-08)
**Note:** Version bump only for package @certd/acme-client
## [1.21.1](https://github.com/publishlab/node-acme-client/compare/v1.21.0...v1.21.1) (2024-07-08)
**Note:** Version bump only for package @certd/acme-client
# [1.21.0](https://github.com/publishlab/node-acme-client/compare/v1.20.17...v1.21.0) (2024-07-03)
### Features
* 支持zero ssl ([eade2c2](https://github.com/publishlab/node-acme-client/commit/eade2c2b681569f03e9cd466e7d5bcd6703ed492))
## [1.20.17](https://github.com/publishlab/node-acme-client/compare/v1.20.16...v1.20.17) (2024-07-03)
### Performance Improvements
* 创建dns解析后强制等待60s ([f47b35f](https://github.com/publishlab/node-acme-client/commit/f47b35f6d5bd7d675005c3e286b7e9a029201f8b))
* 优化cname verify ([eba333d](https://github.com/publishlab/node-acme-client/commit/eba333de7a5b5ef4b0b7eaa904f578720102fa61))
## [1.20.16](https://github.com/publishlab/node-acme-client/compare/v1.20.15...v1.20.16) (2024-07-01)
### Bug Fixes
* 修复配置了cdn cname后申请失败的bug ([4a5fa76](https://github.com/publishlab/node-acme-client/commit/4a5fa767edc347d03d29a467e86c9a4d70b0220c))
## [1.20.15](https://github.com/publishlab/node-acme-client/compare/v1.20.14...v1.20.15) (2024-06-28)
### Performance Improvements
* 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/publishlab/node-acme-client/commit/e0eb3a441384d474fe2923c69b25318264bdc9df))
## [1.20.14](https://github.com/publishlab/node-acme-client/compare/v1.20.13...v1.20.14) (2024-06-23)
**Note:** Version bump only for package @certd/acme-client
## [1.20.13](https://github.com/publishlab/node-acme-client/compare/v1.20.12...v1.20.13) (2024-06-18)
**Note:** Version bump only for package @certd/acme-client
## [1.20.12](https://github.com/publishlab/node-acme-client/compare/v1.20.10...v1.20.12) (2024-06-17)
### Bug Fixes
@@ -41,7 +82,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# Changelog
## v5.3.1
## v5.3.1 (2024-05-22)
* `fixed` Allow `client.auto()` being called with an empty CSR common name
* `fixed` Bug when calling `updateAccountKey()` with external account binding
@@ -78,7 +119,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
* `fixed` Upgrade `axios@0.26.1`
* `fixed` Upgrade `node-forge@1.3.0` - [CVE-2022-24771](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24771), [CVE-2022-24772](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24772), [CVE-2022-24773](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24773)
## 4.2.4 (2022-03-19)
## v4.2.4 (2022-03-19)
* `fixed` Use SHA-256 when signing CSRs

View File

@@ -9,13 +9,13 @@ This module is written to handle communication with a Boulder/Let's Encrypt-styl
## Compatibility
| acme-client | Node.js | |
| ------------- | --------- | ----------------------------------------- |
| v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) |
| v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) |
| v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) |
| v2.x | >= v4 | [Changelog](CHANGELOG.md#v200-2018-04-02) |
| v1.x | >= v4 | [Changelog](CHANGELOG.md#v100-2017-10-20) |
| acme-client | Node.js | |
| ----------- | ------- | ----------------------------------------- |
| v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) |
| v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) |
| v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) |
| v2.x | >= v4 | [Changelog](CHANGELOG.md#v200-2018-04-02) |
| v1.x | >= v4 | [Changelog](CHANGELOG.md#v100-2017-10-20) |
## Table of contents
@@ -49,7 +49,7 @@ const accountPrivateKey = '<PEM encoded private key>';
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: accountPrivateKey
accountKey: accountPrivateKey,
});
```
@@ -75,8 +75,8 @@ const client = new acme.Client({
accountKey: accountPrivateKey,
externalAccountBinding: {
kid: 'YOUR-EAB-KID',
hmacKey: 'YOUR-EAB-HMAC-KEY'
}
hmacKey: 'YOUR-EAB-HMAC-KEY',
},
});
```
@@ -90,7 +90,7 @@ In some cases, for example with some EAB providers, this account creation step m
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: accountPrivateKey,
accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678'
accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678',
});
```
@@ -113,8 +113,7 @@ const privateRsaKey = await acme.crypto.createPrivateRsaKey();
const privateEcdsaKey = await acme.crypto.createPrivateEcdsaKey();
const [certificateKey, certificateCsr] = await acme.crypto.createCsr({
commonName: '*.example.com',
altNames: ['example.com']
altNames: ['example.com', '*.example.com'],
});
```
@@ -139,7 +138,7 @@ const autoOpts = {
email: 'test@example.com',
termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {},
};
const certificate = await client.auto(autoOpts);
@@ -156,7 +155,7 @@ To modify challenge priority, provide a list of challenge types in `challengePri
```js
await client.auto({
...,
challengePriority: ['http-01', 'dns-01']
challengePriority: ['http-01', 'dns-01'],
});
```
@@ -171,7 +170,7 @@ To completely disable `acme-client`s internal challenge verification, enable `sk
```js
await client.auto({
...,
skipChallengeVerification: true
skipChallengeVerification: true,
});
```
@@ -185,14 +184,14 @@ For more fine-grained control you can interact with the ACME API using the metho
```js
const account = await client.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:test@example.com']
contact: ['mailto:test@example.com'],
});
const order = await client.createOrder({
identifiers: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: '*.example.com' }
]
{ type: 'dns', value: '*.example.com' },
],
});
```
@@ -207,7 +206,7 @@ const acme = require('acme-client');
acme.axios.defaults.proxy = {
host: '127.0.0.1',
port: 9000
port: 9000,
};
```

View File

@@ -1 +1 @@
23:08
15:57

View File

@@ -63,7 +63,7 @@ Create ACME client instance
```js
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: 'Private key goes here'
accountKey: 'Private key goes here',
});
```
**Example**
@@ -75,7 +75,7 @@ const client = new acme.Client({
accountUrl: 'Optional account URL goes here',
backoffAttempts: 10,
backoffMin: 5000,
backoffMax: 30000
backoffMax: 30000,
});
```
**Example**
@@ -86,8 +86,8 @@ const client = new acme.Client({
accountKey: 'Private key goes here',
externalAccountBinding: {
kid: 'YOUR-EAB-KID',
hmacKey: 'YOUR-EAB-HMAC-KEY'
}
hmacKey: 'YOUR-EAB-HMAC-KEY',
},
});
```
<a name="AcmeClient+getTermsOfServiceUrl"></a>
@@ -145,7 +145,7 @@ https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
Create a new account
```js
const account = await client.createAccount({
termsOfServiceAgreed: true
termsOfServiceAgreed: true,
});
```
**Example**
@@ -153,7 +153,7 @@ Create a new account with contact info
```js
const account = await client.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:test@example.com']
contact: ['mailto:test@example.com'],
});
```
<a name="AcmeClient+updateAccount"></a>
@@ -174,7 +174,7 @@ https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
Update existing account
```js
const account = await client.updateAccount({
contact: ['mailto:foo@example.com']
contact: ['mailto:foo@example.com'],
});
```
<a name="AcmeClient+updateAccountKey"></a>
@@ -218,8 +218,8 @@ Create a new order
const order = await client.createOrder({
identifiers: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: 'test.example.com' }
]
{ type: 'dns', value: 'test.example.com' },
],
});
```
<a name="AcmeClient+getOrder"></a>
@@ -452,7 +452,7 @@ Revoke certificate with reason
```js
const certificate = { ... }; // Previously created certificate
const result = await client.revokeCertificate(certificate, {
reason: 4
reason: 4,
});
```
<a name="AcmeClient+auto"></a>
@@ -479,7 +479,7 @@ Auto mode
Order a certificate using auto mode
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com'
altNames: ['test.example.com'],
});
const certificate = await client.auto({
@@ -491,14 +491,14 @@ const certificate = await client.auto({
},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
// Clean up challenge here
}
},
});
```
**Example**
Order a certificate using auto mode with preferred chain
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com'
altNames: ['test.example.com'],
});
const certificate = await client.auto({
@@ -507,7 +507,7 @@ const certificate = await client.auto({
termsOfServiceAgreed: true,
preferredChain: 'DST Root CA X3',
challengeCreateFn: async () => {},
challengeRemoveFn: async () => {}
challengeRemoveFn: async () => {},
});
```
<a name="Client"></a>

View File

@@ -239,29 +239,30 @@ Create a Certificate Signing Request
Create a Certificate Signing Request
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com'
altNames: ['test.example.com'],
});
```
**Example**
Certificate Signing Request with both common and alternative names
> *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
keySize: 4096,
commonName: 'test.example.com',
altNames: ['foo.example.com', 'bar.example.com']
altNames: ['foo.example.com', 'bar.example.com'],
});
```
**Example**
Certificate Signing Request with additional information
```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com',
altNames: ['test.example.com'],
country: 'US',
state: 'California',
locality: 'Los Angeles',
organization: 'The Company Inc.',
organizationUnit: 'IT Department',
emailAddress: 'contact@example.com'
emailAddress: 'contact@example.com',
});
```
**Example**
@@ -270,8 +271,9 @@ Certificate Signing Request with ECDSA private key
const certificateKey = await acme.crypto.createPrivateEcdsaKey();
const [, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com'
altNames: ['test.example.com'],
}, certificateKey);
```
<a name="createAlpnCertificate"></a>
## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ <code>Promise.&lt;Array.&lt;buffer&gt;&gt;</code>
@@ -298,6 +300,7 @@ Create a ALPN certificate with ECDSA private key
```js
const alpnKey = await acme.crypto.createPrivateEcdsaKey();
const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
```
<a name="isAlpnCertificateAuthorizationValid"></a>
## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ <code>boolean</code>

View File

@@ -222,29 +222,30 @@ Create a Certificate Signing Request
Create a Certificate Signing Request
```js
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
commonName: 'test.example.com'
altNames: ['test.example.com'],
});
```
**Example**
Certificate Signing Request with both common and alternative names
> *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
```js
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
keySize: 4096,
commonName: 'test.example.com',
altNames: ['foo.example.com', 'bar.example.com']
altNames: ['foo.example.com', 'bar.example.com'],
});
```
**Example**
Certificate Signing Request with additional information
```js
const [certificateKey, certificateRequest] = await acme.forge.createCsr({
commonName: 'test.example.com',
altNames: ['test.example.com'],
country: 'US',
state: 'California',
locality: 'Los Angeles',
organization: 'The Company Inc.',
organizationUnit: 'IT Department',
emailAddress: 'contact@example.com'
emailAddress: 'contact@example.com',
});
```
**Example**
@@ -253,5 +254,5 @@ Certificate Signing Request with predefined private key
const certificateKey = await acme.forge.createPrivateKey();
const [, certificateRequest] = await acme.forge.createCsr({
commonName: 'test.example.com'
altNames: ['test.example.com'],
}, certificateKey);

View File

@@ -8,7 +8,6 @@ function log(m) {
process.stdout.write(`${m}\n`);
}
/**
* Function used to satisfy an ACME challenge
*
@@ -25,7 +24,6 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
log(keyAuthorization);
}
/**
* Function used to remove an ACME challenge response
*
@@ -41,30 +39,29 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
log(keyAuthorization);
}
/**
* Main
*/
module.exports = async function() {
module.exports = async () => {
/* Init client */
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey()
accountKey: await acme.crypto.createPrivateKey(),
});
/* Register account */
await client.createAccount({
termsOfServiceAgreed: true,
contact: ['mailto:test@example.com']
contact: ['mailto:test@example.com'],
});
/* Place new order */
const order = await client.createOrder({
identifiers: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: '*.example.com' }
]
{ type: 'dns', value: '*.example.com' },
],
});
/**
@@ -138,8 +135,7 @@ module.exports = async function() {
/* Finalize order */
const [key, csr] = await acme.crypto.createCsr({
commonName: '*.example.com',
altNames: ['example.com']
altNames: ['example.com', '*.example.com'],
});
const finalized = await client.finalizeOrder(order, csr);

View File

@@ -9,7 +9,6 @@ function log(m) {
process.stdout.write(`${m}\n`);
}
/**
* Function used to satisfy an ACME challenge
*
@@ -47,7 +46,6 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
}
}
/**
* Function used to remove an ACME challenge response
*
@@ -80,25 +78,24 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
/* Replace this */
log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
// await dnsProvider.removeRecord(dnsRecord, 'TXT');
// await dnsProvider.removeRecord(dnsRecord, 'TXT', recordValue);
}
}
/**
* Main
*/
module.exports = async function() {
module.exports = async () => {
/* Init client */
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey()
accountKey: await acme.crypto.createPrivateKey(),
});
/* Create CSR */
const [key, csr] = await acme.crypto.createCsr({
commonName: 'example.com'
altNames: ['example.com'],
});
/* Certificate */
@@ -107,7 +104,7 @@ module.exports = async function() {
email: 'test@example.com',
termsOfServiceAgreed: true,
challengeCreateFn,
challengeRemoveFn
challengeRemoveFn,
});
/* Done */

View File

@@ -19,7 +19,6 @@ function log(m) {
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
}
/**
* Main
*/
@@ -33,18 +32,16 @@ function log(m) {
log('Initializing ACME client');
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey()
accountKey: await acme.crypto.createPrivateKey(),
});
/**
* Order wildcard certificate
*/
log(`Creating CSR for ${WILDCARD_DOMAIN}`);
const [key, csr] = await acme.crypto.createCsr({
commonName: WILDCARD_DOMAIN,
altNames: [`*.${WILDCARD_DOMAIN}`]
altNames: [WILDCARD_DOMAIN, `*.${WILDCARD_DOMAIN}`],
});
log(`Ordering certificate for ${WILDCARD_DOMAIN}`);
@@ -60,12 +57,11 @@ function log(m) {
challengeRemoveFn: (authz, challenge, keyAuthorization) => {
/* TODO: Implement this */
log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
}
},
});
log(`Certificate for ${WILDCARD_DOMAIN} created successfully`);
/**
* HTTPS server
*/
@@ -78,7 +74,7 @@ function log(m) {
const httpsServer = https.createServer({
key,
cert
cert,
}, requestListener);
httpsServer.listen(HTTPS_SERVER_PORT, () => {

View File

@@ -23,7 +23,6 @@ function log(m) {
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
}
/**
* On-demand certificate generation using http-01
*/
@@ -52,7 +51,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
/* Create CSR */
log(`Creating CSR for ${servername}`);
const [key, csr] = await acme.crypto.createCsr({
commonName: servername
altNames: [servername],
});
/* Order certificate */
@@ -67,7 +66,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
},
challengeRemoveFn: (authz, challenge) => {
delete challengeResponses[challenge.token];
}
},
});
/* Done, store certificate */
@@ -77,7 +76,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
return certificateStore[servername];
}
/**
* Main
*/
@@ -91,10 +89,9 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log('Initializing ACME client');
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey()
accountKey: await acme.crypto.createPrivateKey(),
});
/**
* HTTP server
*/
@@ -129,7 +126,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`HTTP server listening on port ${HTTP_SERVER_PORT}`);
});
/**
* HTTPS server
*/
@@ -158,7 +154,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`[ERROR] ${e.message}`);
cb(e.message);
}
}
},
}, requestListener);
httpsServer.listen(HTTPS_SERVER_PORT, () => {

View File

@@ -22,7 +22,6 @@ function log(m) {
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
}
/**
* On-demand certificate generation using tls-alpn-01
*/
@@ -51,7 +50,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
/* Create CSR */
log(`Creating CSR for ${servername}`);
const [key, csr] = await acme.crypto.createCsr({
commonName: servername
altNames: [servername],
});
/* Order certificate */
@@ -66,7 +65,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
},
challengeRemoveFn: (authz) => {
delete alpnResponses[authz.identifier.value];
}
},
});
/* Done, store certificate */
@@ -76,7 +75,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
return certificateStore[servername];
}
/**
* Main
*/
@@ -90,10 +88,9 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log('Initializing ACME client');
const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey()
accountKey: await acme.crypto.createPrivateKey(),
});
/**
* ALPN responder
*/
@@ -118,14 +115,14 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`Found ALPN certificate for ${servername}, serving secure context`);
cb(null, tls.createSecureContext({
key: alpnResponses[servername][0],
cert: alpnResponses[servername][1]
cert: alpnResponses[servername][1],
}));
}
catch (e) {
log(`[ERROR] ${e.message}`);
cb(e.message);
}
}
},
});
/* Terminate once TLS handshake has been established */
@@ -137,7 +134,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`);
});
/**
* HTTPS server
*/
@@ -166,7 +162,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`[ERROR] ${e.message}`);
cb(e.message);
}
}
},
}, requestListener);
httpsServer.listen(HTTPS_SERVER_PORT, () => {

View File

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

View File

@@ -4,7 +4,6 @@
const util = require('./util');
/**
* AcmeApi
*
@@ -18,7 +17,6 @@ class AcmeApi {
this.accountUrl = accountUrl;
}
/**
* Get account URL
*
@@ -34,7 +32,6 @@ class AcmeApi {
return this.accountUrl;
}
/**
* ACME API request
*
@@ -59,7 +56,6 @@ class AcmeApi {
return resp;
}
/**
* ACME API request by resource name helper
*
@@ -78,7 +74,6 @@ class AcmeApi {
return this.apiRequest(resourceUrl, payload, validStatusCodes, { includeJwsKid, includeExternalAccountBinding });
}
/**
* Get Terms of Service URL if available
*
@@ -91,7 +86,6 @@ class AcmeApi {
return this.http.getMetaField('termsOfService');
}
/**
* Create new account
*
@@ -104,7 +98,7 @@ class AcmeApi {
async createAccount(data) {
const resp = await this.apiResourceRequest('newAccount', data, [200, 201], {
includeJwsKid: false,
includeExternalAccountBinding: (data.onlyReturnExisting !== true)
includeExternalAccountBinding: (data.onlyReturnExisting !== true),
});
/* Set account URL */
@@ -115,7 +109,6 @@ class AcmeApi {
return resp;
}
/**
* Update account
*
@@ -129,7 +122,6 @@ class AcmeApi {
return this.apiRequest(this.getAccountUrl(), data, [200, 202]);
}
/**
* Update account key
*
@@ -143,7 +135,6 @@ class AcmeApi {
return this.apiResourceRequest('keyChange', data, [200]);
}
/**
* Create new order
*
@@ -157,7 +148,6 @@ class AcmeApi {
return this.apiResourceRequest('newOrder', data, [201]);
}
/**
* Get order
*
@@ -171,7 +161,6 @@ class AcmeApi {
return this.apiRequest(url, null, [200]);
}
/**
* Finalize order
*
@@ -186,7 +175,6 @@ class AcmeApi {
return this.apiRequest(url, data, [200]);
}
/**
* Get identifier authorization
*
@@ -200,7 +188,6 @@ class AcmeApi {
return this.apiRequest(url, null, [200]);
}
/**
* Update identifier authorization
*
@@ -215,7 +202,6 @@ class AcmeApi {
return this.apiRequest(url, data, [200]);
}
/**
* Complete challenge
*
@@ -230,7 +216,6 @@ class AcmeApi {
return this.apiRequest(url, data, [200]);
}
/**
* Revoke certificate
*
@@ -245,6 +230,5 @@ class AcmeApi {
}
}
/* Export API */
module.exports = AcmeApi;

View File

@@ -4,6 +4,7 @@
const { readCsrDomains } = require('./crypto');
const { log } = require('./logger');
const { wait } = require('./wait');
const defaultOpts = {
csr: null,
@@ -13,10 +14,9 @@ const defaultOpts = {
skipChallengeVerification: false,
challengePriority: ['http-01', 'dns-01'],
challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()'); },
challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); }
challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); },
};
/**
* ACME client auto mode
*
@@ -25,8 +25,8 @@ const defaultOpts = {
* @returns {Promise<buffer>} Certificate
*/
module.exports = async function(client, userOpts) {
const opts = Object.assign({}, defaultOpts, userOpts);
module.exports = async (client, userOpts) => {
const opts = { ...defaultOpts, ...userOpts };
const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed };
if (!Buffer.isBuffer(opts.csr)) {
@@ -36,7 +36,9 @@ module.exports = async function(client, userOpts) {
if (opts.email) {
accountPayload.contact = [`mailto:${opts.email}`];
}
if (opts.externalAccountBinding) {
accountPayload.externalAccountBinding = opts.externalAccountBinding;
}
/**
* Register account
@@ -53,7 +55,6 @@ module.exports = async function(client, userOpts) {
await client.createAccount(accountPayload);
}
/**
* Parse domains from CSR
*/
@@ -64,7 +65,6 @@ module.exports = async function(client, userOpts) {
log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`);
/**
* Place order
*/
@@ -76,7 +76,6 @@ module.exports = async function(client, userOpts) {
log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`);
/**
* Resolve and satisfy challenges
*/
@@ -118,11 +117,23 @@ module.exports = async function(client, userOpts) {
let recordItem = null;
try {
recordItem = await opts.challengeCreateFn(authz, challenge, keyAuthorization);
log(`[auto] [${d}] challengeCreateFn success`);
log(`[auto] [${d}] add challengeRemoveFn()`);
clearTasks.push(async () => {
/* Trigger challengeRemoveFn(), suppress errors */
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
try {
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem);
}
catch (e) {
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
}
});
// throw new Error('测试异常');
/* Challenge verification */
if (opts.skipChallengeVerification === true) {
log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`);
log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=truewait 60s`);
await wait(60 * 1000);
}
else {
log(`[auto] [${d}] Running challenge verification`);
@@ -140,19 +151,6 @@ module.exports = async function(client, userOpts) {
log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`);
throw e;
}
finally {
log(`[auto] [${d}] add challengeRemoveFn()`);
clearTasks.push(async () => {
/* Trigger challengeRemoveFn(), suppress errors */
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
try {
await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem);
}
catch (e) {
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
}
});
}
}
catch (e) {
/* Deactivate pending authz when unable to complete challenge */
@@ -177,7 +175,6 @@ module.exports = async function(client, userOpts) {
await challengeFunc(authz);
});
function runAllPromise(tasks) {
let promise = Promise.resolve();
tasks.forEach((task) => {
@@ -186,14 +183,20 @@ module.exports = async function(client, userOpts) {
return promise;
}
// function runPromisePa(tasks) {
// return Promise.all(tasks.map((task) => task()));
// }
async function runPromisePa(tasks) {
const results = [];
// eslint-disable-next-line no-await-in-loop,no-restricted-syntax
for (const task of tasks) {
results.push(task());
// eslint-disable-next-line no-await-in-loop
await wait(10000);
}
return Promise.all(results);
}
try {
log('开始challenge');
await runAllPromise(challengePromises);
await runPromisePa(challengePromises);
log('challenge结束');
@@ -210,11 +213,18 @@ module.exports = async function(client, userOpts) {
}
catch (e) {
log('证书申请失败');
throw e;
log(e);
throw new Error(`证书申请失败:${e.message}`);
}
finally {
log(`清理challenge痕迹length:${clearTasks.length}`);
await runAllPromise(clearTasks);
try {
await runAllPromise(clearTasks);
}
catch (e) {
log('清理challenge失败');
log(e);
}
}
// try {

View File

@@ -5,7 +5,6 @@
const axios = require('axios');
const pkg = require('./../package.json');
/**
* Instance
*/
@@ -19,9 +18,8 @@ instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version
instance.defaults.acmeSettings = {
httpChallengePort: 80,
httpsChallengePort: 443,
tlsAlpnChallengePort: 443
tlsAlpnChallengePort: 443,
};
// instance.defaults.proxy = {
// host: '192.168.34.139',
// port: 10811
@@ -35,7 +33,6 @@ instance.defaults.acmeSettings = {
instance.defaults.adapter = 'http';
/**
* Export instance
*/

View File

@@ -13,7 +13,6 @@ const verify = require('./verify');
const util = require('./util');
const auto = require('./auto');
/**
* ACME states
*
@@ -24,7 +23,6 @@ const validStates = ['ready', 'valid'];
const pendingStates = ['pending', 'processing'];
const invalidStates = ['invalid'];
/**
* Default options
*
@@ -38,10 +36,9 @@ const defaultOpts = {
externalAccountBinding: {},
backoffAttempts: 10,
backoffMin: 5000,
backoffMax: 30000
backoffMax: 30000,
};
/**
* AcmeClient
*
@@ -61,7 +58,7 @@ const defaultOpts = {
* ```js
* const client = new acme.Client({
* directoryUrl: acme.directory.letsencrypt.staging,
* accountKey: 'Private key goes here'
* accountKey: 'Private key goes here',
* });
* ```
*
@@ -73,7 +70,7 @@ const defaultOpts = {
* accountUrl: 'Optional account URL goes here',
* backoffAttempts: 10,
* backoffMin: 5000,
* backoffMax: 30000
* backoffMax: 30000,
* });
* ```
*
@@ -84,8 +81,8 @@ const defaultOpts = {
* accountKey: 'Private key goes here',
* externalAccountBinding: {
* kid: 'YOUR-EAB-KID',
* hmacKey: 'YOUR-EAB-HMAC-KEY'
* }
* hmacKey: 'YOUR-EAB-HMAC-KEY',
* },
* });
* ```
*/
@@ -96,19 +93,17 @@ class AcmeClient {
opts.accountKey = Buffer.from(opts.accountKey);
}
this.opts = Object.assign({}, defaultOpts, opts);
this.opts = { ...defaultOpts, ...opts };
this.backoffOpts = {
attempts: this.opts.backoffAttempts,
min: this.opts.backoffMin,
max: this.opts.backoffMax
max: this.opts.backoffMax,
};
this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey, this.opts.externalAccountBinding);
this.api = new AcmeApi(this.http, this.opts.accountUrl);
}
/**
* Get Terms of Service URL if available
*
@@ -128,7 +123,6 @@ class AcmeClient {
return this.api.getTermsOfServiceUrl();
}
/**
* Get current account URL
*
@@ -150,7 +144,6 @@ class AcmeClient {
return this.api.getAccountUrl();
}
/**
* Create a new account
*
@@ -162,7 +155,7 @@ class AcmeClient {
* @example Create a new account
* ```js
* const account = await client.createAccount({
* termsOfServiceAgreed: true
* termsOfServiceAgreed: true,
* });
* ```
*
@@ -170,7 +163,7 @@ class AcmeClient {
* ```js
* const account = await client.createAccount({
* termsOfServiceAgreed: true,
* contact: ['mailto:test@example.com']
* contact: ['mailto:test@example.com'],
* });
* ```
*/
@@ -196,7 +189,6 @@ class AcmeClient {
}
}
/**
* Update existing account
*
@@ -208,7 +200,7 @@ class AcmeClient {
* @example Update existing account
* ```js
* const account = await client.updateAccount({
* contact: ['mailto:foo@example.com']
* contact: ['mailto:foo@example.com'],
* });
* ```
*/
@@ -236,7 +228,6 @@ class AcmeClient {
return resp.data;
}
/**
* Update account private key
*
@@ -282,7 +273,6 @@ class AcmeClient {
return resp.data;
}
/**
* Create a new order
*
@@ -296,8 +286,8 @@ class AcmeClient {
* const order = await client.createOrder({
* identifiers: [
* { type: 'dns', value: 'example.com' },
* { type: 'dns', value: 'test.example.com' }
* ]
* { type: 'dns', value: 'test.example.com' },
* ],
* });
* ```
*/
@@ -314,7 +304,6 @@ class AcmeClient {
return resp.data;
}
/**
* Refresh order object from CA
*
@@ -376,7 +365,6 @@ class AcmeClient {
return resp.data;
}
/**
* Get identifier authorizations from order
*
@@ -406,7 +394,6 @@ class AcmeClient {
}));
}
/**
* Deactivate identifier authorization
*
@@ -427,10 +414,7 @@ class AcmeClient {
throw new Error('Unable to deactivate identifier authorization, URL not found');
}
const data = {
status: 'deactivated'
};
const data = { status: 'deactivated' };
const resp = await this.api.updateAuthorization(authz.url, data);
/* Add URL to response */
@@ -438,7 +422,6 @@ class AcmeClient {
return resp.data;
}
/**
* Get key authorization for ACME challenge
*
@@ -480,7 +463,6 @@ class AcmeClient {
throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
}
/**
* Verify that ACME challenge is satisfied
*
@@ -515,7 +497,6 @@ class AcmeClient {
return util.retry(verifyFn, this.backoffOpts);
}
/**
* Notify CA that challenge has been completed
*
@@ -536,7 +517,6 @@ class AcmeClient {
return resp.data;
}
/**
* Wait for ACME provider to verify status on a order, authorization or challenge
*
@@ -593,7 +573,6 @@ class AcmeClient {
return util.retry(verifyFn, this.backoffOpts);
}
/**
* Get certificate from ACME order
*
@@ -640,7 +619,6 @@ class AcmeClient {
return resp.data;
}
/**
* Revoke certificate
*
@@ -660,7 +638,7 @@ class AcmeClient {
* ```js
* const certificate = { ... }; // Previously created certificate
* const result = await client.revokeCertificate(certificate, {
* reason: 4
* reason: 4,
* });
* ```
*/
@@ -671,7 +649,6 @@ class AcmeClient {
return resp.data;
}
/**
* Auto mode
*
@@ -689,7 +666,7 @@ class AcmeClient {
* @example Order a certificate using auto mode
* ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com'
* altNames: ['test.example.com'],
* });
*
* const certificate = await client.auto({
@@ -701,14 +678,14 @@ class AcmeClient {
* },
* challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
* // Clean up challenge here
* }
* },
* });
* ```
*
* @example Order a certificate using auto mode with preferred chain
* ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com'
* altNames: ['test.example.com'],
* });
*
* const certificate = await client.auto({
@@ -717,7 +694,7 @@ class AcmeClient {
* termsOfServiceAgreed: true,
* preferredChain: 'DST Root CA X3',
* challengeCreateFn: async () => {},
* challengeRemoveFn: async () => {}
* challengeRemoveFn: async () => {},
* });
* ```
*/
@@ -727,6 +704,5 @@ class AcmeClient {
}
}
/* Export client */
module.exports = AcmeClient;

View File

@@ -13,7 +13,6 @@ const forge = require('node-forge');
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
/**
* Attempt to parse forge object from PEM encoded string
*
@@ -54,7 +53,6 @@ function forgeObjectFromPem(input) {
return result;
}
/**
* Parse domain names from a certificate or CSR
*
@@ -93,11 +91,10 @@ function parseDomains(obj) {
return {
commonName,
altNames
altNames,
};
}
/**
* Generate a private RSA key
*
@@ -123,7 +120,6 @@ async function createPrivateKey(size = 2048) {
exports.createPrivateKey = createPrivateKey;
/**
* Create public key from a private RSA key
*
@@ -136,14 +132,13 @@ exports.createPrivateKey = createPrivateKey;
* ```
*/
exports.createPublicKey = async function(key) {
exports.createPublicKey = async (key) => {
const privateKey = forge.pki.privateKeyFromPem(key);
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
const pemKey = forge.pki.publicKeyToPem(publicKey);
return Buffer.from(pemKey);
};
/**
* Parse body of PEM encoded object from buffer or string
* If multiple objects are chained, the first body will be returned
@@ -157,7 +152,6 @@ exports.getPemBody = (str) => {
return forge.util.encode64(msg.body);
};
/**
* Split chain of PEM encoded objects from buffer or string into array
*
@@ -167,7 +161,6 @@ exports.getPemBody = (str) => {
exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
/**
* Get modulus
*
@@ -182,7 +175,7 @@ exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
* ```
*/
exports.getModulus = async function(input) {
exports.getModulus = async (input) => {
if (!Buffer.isBuffer(input)) {
input = Buffer.from(input);
}
@@ -191,7 +184,6 @@ exports.getModulus = async function(input) {
return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary');
};
/**
* Get public exponent
*
@@ -206,7 +198,7 @@ exports.getModulus = async function(input) {
* ```
*/
exports.getPublicExponent = async function(input) {
exports.getPublicExponent = async (input) => {
if (!Buffer.isBuffer(input)) {
input = Buffer.from(input);
}
@@ -215,7 +207,6 @@ exports.getPublicExponent = async function(input) {
return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary');
};
/**
* Read domains from a Certificate Signing Request
*
@@ -231,7 +222,7 @@ exports.getPublicExponent = async function(input) {
* ```
*/
exports.readCsrDomains = async function(csr) {
exports.readCsrDomains = async (csr) => {
if (!Buffer.isBuffer(csr)) {
csr = Buffer.from(csr);
}
@@ -240,7 +231,6 @@ exports.readCsrDomains = async function(csr) {
return parseDomains(obj);
};
/**
* Read information from a certificate
*
@@ -260,7 +250,7 @@ exports.readCsrDomains = async function(csr) {
* ```
*/
exports.readCertificateInfo = async function(cert) {
exports.readCertificateInfo = async (cert) => {
if (!Buffer.isBuffer(cert)) {
cert = Buffer.from(cert);
}
@@ -270,15 +260,14 @@ exports.readCertificateInfo = async function(cert) {
return {
issuer: {
commonName: issuerCn ? issuerCn.value : null
commonName: issuerCn ? issuerCn.value : null,
},
domains: parseDomains(obj),
notAfter: obj.validity.notAfter,
notBefore: obj.validity.notBefore
notBefore: obj.validity.notBefore,
};
};
/**
* Determine ASN.1 type for CSR subject short name
* Note: https://datatracker.ietf.org/doc/html/rfc5280
@@ -299,7 +288,6 @@ function getCsrValueTagClass(shortName) {
}
}
/**
* Create array of short names and values for Certificate Signing Request subjects
*
@@ -319,7 +307,6 @@ function createCsrSubject(subjectObj) {
}, []);
}
/**
* Create array of alt names for Certificate Signing Requests
* Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454
@@ -336,7 +323,6 @@ function formatCsrAltNames(altNames) {
});
}
/**
* Create a Certificate Signing Request
*
@@ -356,29 +342,30 @@ function formatCsrAltNames(altNames) {
* @example Create a Certificate Signing Request
* ```js
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
* commonName: 'test.example.com'
* altNames: ['test.example.com'],
* });
* ```
*
* @example Certificate Signing Request with both common and alternative names
* > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
* ```js
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
* keySize: 4096,
* commonName: 'test.example.com',
* altNames: ['foo.example.com', 'bar.example.com']
* altNames: ['foo.example.com', 'bar.example.com'],
* });
* ```
*
* @example Certificate Signing Request with additional information
* ```js
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({
* commonName: 'test.example.com',
* altNames: ['test.example.com'],
* country: 'US',
* state: 'California',
* locality: 'Los Angeles',
* organization: 'The Company Inc.',
* organizationUnit: 'IT Department',
* emailAddress: 'contact@example.com'
* emailAddress: 'contact@example.com',
* });
* ```
*
@@ -387,11 +374,11 @@ function formatCsrAltNames(altNames) {
* const certificateKey = await acme.forge.createPrivateKey();
*
* const [, certificateRequest] = await acme.forge.createCsr({
* commonName: 'test.example.com'
* altNames: ['test.example.com'],
* }, certificateKey);
*/
exports.createCsr = async function(data, key = null) {
exports.createCsr = async (data, key = null) => {
if (!key) {
key = await createPrivateKey(data.keySize);
}
@@ -423,7 +410,7 @@ exports.createCsr = async function(data, key = null) {
L: data.locality,
O: data.organization,
OU: data.organizationUnit,
E: data.emailAddress
E: data.emailAddress,
});
csr.setSubject(subject);
@@ -434,8 +421,8 @@ exports.createCsr = async function(data, key = null) {
name: 'extensionRequest',
extensions: [{
name: 'subjectAltName',
altNames: formatCsrAltNames(data.altNames)
}]
altNames: formatCsrAltNames(data.altNames),
}],
}]);
}

View File

@@ -22,7 +22,6 @@ const subjectAltNameOID = '2.5.29.17';
/* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
/**
* Determine key type and info by attempting to derive public key
*
@@ -35,7 +34,7 @@ function getKeyInfo(keyPem) {
const result = {
isRSA: false,
isECDSA: false,
publicKey: crypto.createPublicKey(keyPem)
publicKey: crypto.createPublicKey(keyPem),
};
if (result.publicKey.asymmetricKeyType === 'rsa') {
@@ -51,7 +50,6 @@ function getKeyInfo(keyPem) {
return result;
}
/**
* Generate a private RSA key
*
@@ -74,8 +72,8 @@ async function createPrivateRsaKey(modulusLength = 2048) {
modulusLength,
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
format: 'pem',
},
});
return Buffer.from(pair.privateKey);
@@ -83,7 +81,6 @@ async function createPrivateRsaKey(modulusLength = 2048) {
exports.createPrivateRsaKey = createPrivateRsaKey;
/**
* Alias of `createPrivateRsaKey()`
*
@@ -92,7 +89,6 @@ exports.createPrivateRsaKey = createPrivateRsaKey;
exports.createPrivateKey = createPrivateRsaKey;
/**
* Generate a private ECDSA key
*
@@ -115,14 +111,13 @@ exports.createPrivateEcdsaKey = async (namedCurve = 'P-256') => {
namedCurve,
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
format: 'pem',
},
});
return Buffer.from(pair.privateKey);
};
/**
* Get a public key derived from a RSA or ECDSA key
*
@@ -140,13 +135,12 @@ exports.getPublicKey = (keyPem) => {
const publicKey = info.publicKey.export({
type: info.isECDSA ? 'spki' : 'pkcs1',
format: 'pem'
format: 'pem',
});
return Buffer.from(publicKey);
};
/**
* Get a JSON Web Key derived from a RSA or ECDSA key
*
@@ -163,7 +157,7 @@ exports.getPublicKey = (keyPem) => {
function getJwk(keyPem) {
const jwk = crypto.createPublicKey(keyPem).export({
format: 'jwk'
format: 'jwk',
});
/* Sort keys */
@@ -175,7 +169,6 @@ function getJwk(keyPem) {
exports.getJwk = getJwk;
/**
* Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
*
@@ -191,7 +184,7 @@ async function getWebCryptoKeyPair(keyPem) {
/* Signing algorithm */
const sigalg = {
name: 'RSASSA-PKCS1-v1_5',
hash: { name: 'SHA-256' }
hash: { name: 'SHA-256' },
};
if (info.isECDSA) {
@@ -215,7 +208,6 @@ async function getWebCryptoKeyPair(keyPem) {
return [{ privateKey, publicKey }, sigalg];
}
/**
* Split chain of PEM encoded objects from string into array
*
@@ -235,7 +227,6 @@ function splitPemChain(chainPem) {
exports.splitPemChain = splitPemChain;
/**
* Parse body of PEM encoded object and return a Base64URL string
* If multiple objects are chained, the first body will be returned
@@ -256,7 +247,6 @@ exports.getPemBodyAsB64u = (pem) => {
return Buffer.from(dec).toString('base64url');
};
/**
* Parse domains from a certificate or CSR
*
@@ -277,11 +267,10 @@ function parseDomains(input) {
return {
commonName,
altNames
altNames,
};
}
/**
* Read domains from a Certificate Signing Request
*
@@ -307,7 +296,6 @@ exports.readCsrDomains = (csrPem) => {
return parseDomains(csr);
};
/**
* Read information from a certificate
* If multiple certificates are chained, the first will be read
@@ -338,15 +326,14 @@ exports.readCertificateInfo = (certPem) => {
return {
issuer: {
commonName: cert.issuerName.getField('CN').pop() || null
commonName: cert.issuerName.getField('CN').pop() || null,
},
domains: parseDomains(cert),
notBefore: cert.notBefore,
notAfter: cert.notAfter
notAfter: cert.notAfter,
};
};
/**
* Determine ASN.1 character string type for CSR subject field name
*
@@ -369,7 +356,6 @@ function getCsrAsn1CharStringType(field) {
}
}
/**
* Create array of subject fields for a Certificate Signing Request
*
@@ -391,7 +377,6 @@ function createCsrSubject(input) {
}, []);
}
/**
* Create x509 subject alternate name extension
*
@@ -409,7 +394,6 @@ function createSubjectAltNameExtension(altNames) {
}));
}
/**
* Create a Certificate Signing Request
*
@@ -429,29 +413,30 @@ function createSubjectAltNameExtension(altNames) {
* @example Create a Certificate Signing Request
* ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com'
* altNames: ['test.example.com'],
* });
* ```
*
* @example Certificate Signing Request with both common and alternative names
* > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
* ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* keySize: 4096,
* commonName: 'test.example.com',
* altNames: ['foo.example.com', 'bar.example.com']
* altNames: ['foo.example.com', 'bar.example.com'],
* });
* ```
*
* @example Certificate Signing Request with additional information
* ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com',
* altNames: ['test.example.com'],
* country: 'US',
* state: 'California',
* locality: 'Los Angeles',
* organization: 'The Company Inc.',
* organizationUnit: 'IT Department',
* emailAddress: 'contact@example.com'
* emailAddress: 'contact@example.com',
* });
* ```
*
@@ -460,8 +445,9 @@ function createSubjectAltNameExtension(altNames) {
* const certificateKey = await acme.crypto.createPrivateEcdsaKey();
*
* const [, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com'
* altNames: ['test.example.com'],
* }, certificateKey);
* ```
*/
exports.createCsr = async (data, keyPem = null) => {
@@ -489,7 +475,7 @@ exports.createCsr = async (data, keyPem = null) => {
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
createSubjectAltNameExtension(data.altNames)
createSubjectAltNameExtension(data.altNames),
];
/* Create CSR */
@@ -504,8 +490,8 @@ exports.createCsr = async (data, keyPem = null) => {
L: data.locality,
O: data.organization,
OU: data.organizationUnit,
E: data.emailAddress
})
E: data.emailAddress,
}),
});
/* Done */
@@ -513,7 +499,6 @@ exports.createCsr = async (data, keyPem = null) => {
return [keyPem, Buffer.from(pem)];
};
/**
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
*
@@ -533,6 +518,7 @@ exports.createCsr = async (data, keyPem = null) => {
* ```js
* const alpnKey = await acme.crypto.createPrivateEcdsaKey();
* const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
* ```
*/
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
@@ -564,7 +550,7 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
createSubjectAltNameExtension([commonName])
createSubjectAltNameExtension([commonName]),
];
/* ALPN extension */
@@ -581,8 +567,8 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
notBefore: now,
notAfter: now,
name: createCsrSubject({
CN: commonName
})
CN: commonName,
}),
});
/* Done */
@@ -590,7 +576,6 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
return [keyPem, Buffer.from(pem)];
};
/**
* Validate that a ALPN certificate contains the expected key authorization
*

View File

@@ -40,7 +40,6 @@ class HttpClient {
this.jwk = null;
}
/**
* HTTP request
*
@@ -70,7 +69,6 @@ class HttpClient {
return resp;
}
/**
* Ensure provider directory exists
*
@@ -95,7 +93,6 @@ class HttpClient {
}
}
/**
* Get JSON Web Key
*
@@ -110,7 +107,6 @@ class HttpClient {
return this.jwk;
}
/**
* Get nonce from directory API endpoint
*
@@ -130,7 +126,6 @@ class HttpClient {
return resp.headers['replay-nonce'];
}
/**
* Get URL for a directory resource
*
@@ -148,7 +143,6 @@ class HttpClient {
return this.directory[resource];
}
/**
* Get directory meta field
*
@@ -166,7 +160,6 @@ class HttpClient {
return null;
}
/**
* Prepare HTTP request body for signature
*
@@ -199,11 +192,10 @@ class HttpClient {
/* Body */
return {
payload: payload ? Buffer.from(JSON.stringify(payload)).toString('base64url') : '',
protected: Buffer.from(JSON.stringify(header)).toString('base64url')
protected: Buffer.from(JSON.stringify(header)).toString('base64url'),
};
}
/**
* Create JWS HTTP request body using HMAC
*
@@ -226,7 +218,6 @@ class HttpClient {
return result;
}
/**
* Create JWS HTTP request body using RSA or ECC
*
@@ -267,13 +258,12 @@ class HttpClient {
result.signature = signer.sign({
key: this.accountKey,
padding: RSA_PKCS1_PADDING,
dsaEncoding: 'ieee-p1363'
dsaEncoding: 'ieee-p1363',
}, 'base64url');
return result;
}
/**
* Signed HTTP request
*
@@ -309,7 +299,7 @@ class HttpClient {
const data = this.createSignedBody(url, payload, { nonce, kid });
const resp = await this.request(url, 'post', { data });
/* Retry on bad nonce - https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-10#section-6.4 */
/* Retry on bad nonce - https://datatracker.ietf.org/doc/html/rfc8555#section-6.5 */
if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
nonce = resp.headers['replay-nonce'] || null;
attempts += 1;
@@ -323,6 +313,5 @@ class HttpClient {
}
}
/* Export client */
module.exports = HttpClient;

View File

@@ -4,7 +4,6 @@
exports.Client = require('./client');
/**
* Directory URLs
*/
@@ -12,18 +11,18 @@ exports.Client = require('./client');
exports.directory = {
buypass: {
staging: 'https://api.test4.buypass.no/acme/directory',
production: 'https://api.buypass.com/acme/directory'
production: 'https://api.buypass.com/acme/directory',
},
letsencrypt: {
staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
production: 'https://acme-v02.api.letsencrypt.org/directory'
production: 'https://acme-v02.api.letsencrypt.org/directory',
},
zerossl: {
production: 'https://acme.zerossl.com/v2/DV90'
}
staging: 'https://acme.zerossl.com/v2/DV90',
production: 'https://acme.zerossl.com/v2/DV90',
},
};
/**
* Crypto
*/
@@ -31,14 +30,12 @@ exports.directory = {
exports.crypto = require('./crypto');
exports.forge = require('./crypto/forge');
/**
* Axios
*/
exports.axios = require('./axios');
/**
* Logger
*/

View File

@@ -6,7 +6,6 @@ const debug = require('debug')('acme-client');
let logger = () => {};
/**
* Set logger function
*
@@ -17,11 +16,10 @@ exports.setLogger = (fn) => {
logger = fn;
};
/**
* Log message
*
* @param {string} Message
* @param {string} msg Message
*/
exports.log = (msg) => {

View File

@@ -7,7 +7,6 @@ const dns = require('dns').promises;
const { readCertificateInfo, splitPemChain } = require('./crypto');
const { log } = require('./logger');
/**
* Exponential backoff
*
@@ -26,7 +25,6 @@ class Backoff {
this.attempts = 0;
}
/**
* Get backoff duration
*
@@ -40,7 +38,6 @@ class Backoff {
}
}
/**
* Retry promise
*
@@ -70,7 +67,6 @@ async function retryPromise(fn, attempts, backoff) {
}
}
/**
* Retry promise
*
@@ -87,7 +83,6 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
return retryPromise(fn, attempts, backoff);
}
/**
* Parse URLs from link header
*
@@ -107,7 +102,6 @@ function parseLinkHeader(header, rel = 'alternate') {
return results.filter((r) => r);
}
/**
* Find certificate chain with preferred issuer common name
* - If issuer is found in multiple chains, the closest to root wins
@@ -157,7 +151,6 @@ function findCertificateChainForIssuer(chains, issuer) {
return chains[0];
}
/**
* Find and format error in response object
*
@@ -178,7 +171,6 @@ function formatResponseError(resp) {
return result.replace(/\n/g, '');
}
/**
* Resolve root domain name by looking for SOA record
*
@@ -204,7 +196,6 @@ async function resolveDomainBySoaRecord(recordName) {
}
}
/**
* Get DNS resolver using domains authoritative NS records
*
@@ -245,7 +236,6 @@ async function getAuthoritativeDnsResolver(recordName) {
return resolver;
}
/**
* Attempt to retrieve TLS ALPN certificate from peer
*
@@ -267,7 +257,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
port,
servername: host,
rejectUnauthorized: false,
ALPNProtocols: ['acme-tls/1']
ALPNProtocols: ['acme-tls/1'],
});
socket.setTimeout(timeout);
@@ -299,7 +289,6 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
});
}
/**
* Export utils
*/
@@ -310,5 +299,5 @@ module.exports = {
findCertificateChainForIssuer,
formatResponseError,
getAuthoritativeDnsResolver,
retrieveTlsAlpnCertificate
retrieveTlsAlpnCertificate,
};

View File

@@ -9,7 +9,6 @@ const axios = require('./axios');
const util = require('./util');
const { isAlpnCertificateAuthorizationValid } = require('./crypto');
/**
* Verify ACME HTTP challenge
*
@@ -43,25 +42,24 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
return true;
}
/**
* Walk DNS until TXT records are found
*/
async function walkDnsChallengeRecord(recordName, resolver = dns) {
/* Resolve CNAME record first */
try {
log(`Checking name for CNAME records: ${recordName}`);
const cnameRecords = await resolver.resolveCname(recordName);
if (cnameRecords.length) {
log(`CNAME record found at ${recordName}, new challenge record name: ${cnameRecords[0]}`);
return walkDnsChallengeRecord(cnameRecords[0]);
}
}
catch (e) {
log(`No CNAME records found for name: ${recordName}`);
}
// try {
// log(`Checking name for CNAME records: ${recordName}`);
// const cnameRecords = await resolver.resolveCname(recordName);
//
// if (cnameRecords.length) {
// log(`CNAME record found at ${recordName}, new challenge record name: ${cnameRecords[0]}`);
// return walkDnsChallengeRecord(cnameRecords[0]);
// }
// }
// catch (e) {
// log(`No CNAME records found for name: ${recordName}`);
// }
/* Resolve TXT records */
try {
@@ -81,7 +79,6 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) {
throw new Error(`No TXT records found for name: ${recordName}`);
}
/**
* Verify ACME DNS challenge
*
@@ -121,7 +118,6 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
return true;
}
/**
* Verify ACME TLS ALPN challenge
*
@@ -149,7 +145,6 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
return true;
}
/**
* Export API
*/
@@ -157,5 +152,5 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
module.exports = {
'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge
'tls-alpn-01': verifyTlsAlpnChallenge,
};

View File

@@ -0,0 +1,9 @@
async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
module.exports = {
wait
};

View File

@@ -16,7 +16,6 @@ const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
describe('pebble', () => {
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
@@ -39,18 +38,16 @@ describe('pebble', () => {
const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`;
const testTlsAlpn01ChallengeValue = uuid();
/**
* Pebble CTS required
*/
before(function() {
before(function () {
if (!cts.isEnabled()) {
this.skip();
}
});
/**
* DNS mocking
*/
@@ -92,7 +89,6 @@ describe('pebble', () => {
});
});
/**
* HTTP-01 challenge response
*/
@@ -118,7 +114,6 @@ describe('pebble', () => {
});
});
/**
* HTTPS-01 challenge response
*/
@@ -143,7 +138,7 @@ describe('pebble', () => {
/* Assert HTTP 302 */
const resp = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, {
maxRedirects: 0,
validateStatus: null
validateStatus: null,
});
assert.strictEqual(resp.status, 302);
@@ -165,7 +160,6 @@ describe('pebble', () => {
});
});
/**
* DNS-01 challenge response
*/
@@ -188,7 +182,6 @@ describe('pebble', () => {
});
});
/**
* TLS-ALPN-01 challenge response
*/

View File

@@ -9,7 +9,6 @@ const axios = require('./../src/axios');
const HttpClient = require('./../src/http');
const pkg = require('./../package.json');
describe('http', () => {
let testClient;
@@ -20,7 +19,6 @@ describe('http', () => {
const defaultUaEndpoint = `http://${uuid()}.example.com`;
const customUaEndpoint = `http://${uuid()}.example.com`;
/**
* HTTP mocking
*/
@@ -43,7 +41,6 @@ describe('http', () => {
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
});
/**
* Initialize
*/
@@ -52,7 +49,6 @@ describe('http', () => {
testClient = new HttpClient();
});
/**
* HTTP verbs
*/
@@ -65,7 +61,6 @@ describe('http', () => {
assert.strictEqual(resp.data, 'ok');
});
/**
* User-Agent
*/

View File

@@ -5,7 +5,6 @@
const { assert } = require('chai');
const logger = require('./../src/logger');
describe('logger', () => {
let lastLogMessage = null;
@@ -13,7 +12,6 @@ describe('logger', () => {
lastLogMessage = msg;
}
/**
* Logger
*/
@@ -23,7 +21,6 @@ describe('logger', () => {
assert.isNull(lastLogMessage);
});
it('should log with custom logger', () => {
logger.setLogger(customLoggerFn);

View File

@@ -9,7 +9,6 @@ const verify = require('./../src/verify');
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
describe('verify', () => {
const challengeTypes = ['http-01', 'dns-01'];
@@ -30,18 +29,16 @@ describe('verify', () => {
const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
const testTlsAlpn01Key = uuid();
/**
* Pebble CTS required
*/
before(function() {
before(function () {
if (!cts.isEnabled()) {
this.skip();
}
});
/**
* API
*/
@@ -50,7 +47,6 @@ describe('verify', () => {
assert.containsAllKeys(verify, challengeTypes);
});
/**
* http-01
*/
@@ -81,7 +77,6 @@ describe('verify', () => {
});
});
/**
* https-01
*/
@@ -102,7 +97,6 @@ describe('verify', () => {
});
});
/**
* dns-01
*/
@@ -133,7 +127,6 @@ describe('verify', () => {
});
});
/**
* tls-alpn-01
*/

View File

@@ -9,10 +9,9 @@ const spec = require('./spec');
const forge = require('./../src/crypto/forge');
const cryptoEngines = {
forge
forge,
};
describe('crypto-legacy', () => {
let testPemKey;
let testCert;
@@ -28,7 +27,6 @@ describe('crypto-legacy', () => {
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
/**
* Fixtures
*/
@@ -50,7 +48,6 @@ describe('crypto-legacy', () => {
});
});
/**
* Engines
*/
@@ -62,7 +59,6 @@ describe('crypto-legacy', () => {
let testNonCnCsr;
let testNonAsciiCsr;
/**
* Key generation
*/
@@ -83,14 +79,13 @@ describe('crypto-legacy', () => {
publicKeyStore.push(key.toString().replace(/[\r\n]/gm, ''));
});
/**
* Certificate Signing Request
*/
it('should generate a csr', async () => {
const [key, csr] = await engine.createCsr({
commonName: testCsrDomain
commonName: testCsrDomain,
});
assert.isTrue(Buffer.isBuffer(key));
@@ -102,7 +97,7 @@ describe('crypto-legacy', () => {
it('should generate a san csr', async () => {
const [key, csr] = await engine.createCsr({
commonName: testSanCsrDomains[0],
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length)
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length),
});
assert.isTrue(Buffer.isBuffer(key));
@@ -113,7 +108,7 @@ describe('crypto-legacy', () => {
it('should generate a csr without common name', async () => {
const [key, csr] = await engine.createCsr({
altNames: testSanCsrDomains
altNames: testSanCsrDomains,
});
assert.isTrue(Buffer.isBuffer(key));
@@ -126,7 +121,7 @@ describe('crypto-legacy', () => {
const [key, csr] = await engine.createCsr({
commonName: testCsrDomain,
organization: '大安區',
organizationUnit: '中文部門'
organizationUnit: '中文部門',
});
assert.isTrue(Buffer.isBuffer(key));
@@ -167,7 +162,6 @@ describe('crypto-legacy', () => {
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
});
/**
* Certificate
*/
@@ -188,7 +182,6 @@ describe('crypto-legacy', () => {
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
});
/**
* PEM utils
*/
@@ -214,7 +207,6 @@ describe('crypto-legacy', () => {
});
});
/**
* Modulus and exponent
*/
@@ -246,7 +238,6 @@ describe('crypto-legacy', () => {
});
});
/**
* Verify identical results
*/

View File

@@ -50,7 +50,6 @@ dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0
-----END TEST-----
`;
describe('crypto', () => {
const testCsrDomain = 'example.com';
const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com'];
@@ -58,7 +57,6 @@ describe('crypto', () => {
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
/**
* Key types
*/
@@ -68,24 +66,23 @@ describe('crypto', () => {
createKeyFns: {
s1024: () => crypto.createPrivateRsaKey(1024),
s2048: () => crypto.createPrivateRsaKey(),
s4096: () => crypto.createPrivateRsaKey(4096)
s4096: () => crypto.createPrivateRsaKey(4096),
},
jwkSpecFn: spec.jwk.rsa
jwkSpecFn: spec.jwk.rsa,
},
ecdsa: {
createKeyFns: {
p256: () => crypto.createPrivateEcdsaKey(),
p384: () => crypto.createPrivateEcdsaKey('P-384'),
p521: () => crypto.createPrivateEcdsaKey('P-521')
p521: () => crypto.createPrivateEcdsaKey('P-521'),
},
jwkSpecFn: spec.jwk.ecdsa
}
jwkSpecFn: spec.jwk.ecdsa,
},
}).forEach(([name, { createKeyFns, jwkSpecFn }]) => {
describe(name, () => {
const testPrivateKeys = {};
const testPublicKeys = {};
/**
* Iterate through all generator variations
*/
@@ -97,7 +94,6 @@ describe('crypto', () => {
let testNonAsciiCsr;
let testAlpnCertificate;
/**
* Keys and JWK
*/
@@ -132,14 +128,13 @@ describe('crypto', () => {
jwkSpecFn(jwk);
});
/**
* Certificate Signing Request
*/
it(`${n}/should generate a csr`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
commonName: testCsrDomain,
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
@@ -151,7 +146,7 @@ describe('crypto', () => {
it(`${n}/should generate a san csr`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testSanCsrDomains[0],
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length)
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length),
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
@@ -162,7 +157,7 @@ describe('crypto', () => {
it(`${n}/should generate a csr without common name`, async () => {
const [key, csr] = await crypto.createCsr({
altNames: testSanCsrDomains
altNames: testSanCsrDomains,
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
@@ -175,7 +170,7 @@ describe('crypto', () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain,
organization: '大安區',
organizationUnit: '中文部門'
organizationUnit: '中文部門',
}, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key));
@@ -186,7 +181,7 @@ describe('crypto', () => {
it(`${n}/should generate a csr with key as string`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
commonName: testCsrDomain,
}, testPrivateKeys[n].toString());
assert.isTrue(Buffer.isBuffer(key));
@@ -195,11 +190,10 @@ describe('crypto', () => {
it(`${n}/should throw with invalid key`, async () => {
await assert.isRejected(crypto.createCsr({
commonName: testCsrDomain
commonName: testCsrDomain,
}, testPublicKeys[n]));
});
/**
* Domain and info resolver
*/
@@ -243,7 +237,6 @@ describe('crypto', () => {
});
});
/**
* ALPN
*/
@@ -284,7 +277,6 @@ describe('crypto', () => {
});
});
/**
* Common functionality
*/
@@ -294,7 +286,6 @@ describe('crypto', () => {
let testCert;
let testSanCert;
it('should read private key fixture', async () => {
testPemKey = await fs.readFile(testKeyPath);
assert.isTrue(Buffer.isBuffer(testPemKey));
@@ -310,21 +301,19 @@ describe('crypto', () => {
assert.isTrue(Buffer.isBuffer(testSanCert));
});
/**
* CSR with auto-generated key
*/
it('should generate a csr with default key', async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
commonName: testCsrDomain,
});
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
});
/**
* Certificate
*/
@@ -352,7 +341,6 @@ describe('crypto', () => {
});
});
/**
* ALPN
*/
@@ -365,7 +353,6 @@ describe('crypto', () => {
assert.isTrue(Buffer.isBuffer(cert));
});
/**
* PEM utils
*/

View File

@@ -20,35 +20,32 @@ const clientOpts = {
directoryUrl,
backoffAttempts: 5,
backoffMin: 1000,
backoffMax: 5000
backoffMax: 5000,
};
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
clientOpts.externalAccountBinding = {
kid: process.env.ACME_EAB_KID,
hmacKey: process.env.ACME_EAB_HMAC_KEY
hmacKey: process.env.ACME_EAB_HMAC_KEY,
};
}
describe('client', () => {
const testDomain = `${uuid()}.${domainName}`;
const testDomainAlpn = `${uuid()}.${domainName}`;
const testDomainWildcard = `*.${testDomain}`;
const testContact = `mailto:test-${uuid()}@nope.com`;
/**
* Pebble CTS required
*/
before(function() {
before(function () {
if (!cts.isEnabled()) {
this.skip();
}
});
/**
* Key types
*/
@@ -58,18 +55,18 @@ describe('client', () => {
createKeyFn: () => acme.crypto.createPrivateRsaKey(),
createKeyAltFns: {
s1024: () => acme.crypto.createPrivateRsaKey(1024),
s4096: () => acme.crypto.createPrivateRsaKey(4096)
s4096: () => acme.crypto.createPrivateRsaKey(4096),
},
jwkSpecFn: spec.jwk.rsa
jwkSpecFn: spec.jwk.rsa,
},
ecdsa: {
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
createKeyAltFns: {
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
p521: () => acme.crypto.createPrivateEcdsaKey('P-521')
p521: () => acme.crypto.createPrivateEcdsaKey('P-521'),
},
jwkSpecFn: spec.jwk.ecdsa
}
jwkSpecFn: spec.jwk.ecdsa,
},
}).forEach(([name, { createKeyFn, createKeyAltFns, jwkSpecFn }]) => {
describe(name, () => {
let testIssuers;
@@ -97,7 +94,6 @@ describe('client', () => {
let testCertificateAlpn;
let testCertificateWildcard;
/**
* Fixtures
*/
@@ -114,11 +110,11 @@ describe('client', () => {
it('should generate certificate signing request', async () => {
[, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn());
[, testCsrAlpn] = await acme.crypto.createCsr({ commonName: testDomainAlpn }, await createKeyFn());
[, testCsrWildcard] = await acme.crypto.createCsr({ commonName: testDomainWildcard }, await createKeyFn());
[, testCsrAlpn] = await acme.crypto.createCsr({ altNames: [testDomainAlpn] }, await createKeyFn());
[, testCsrWildcard] = await acme.crypto.createCsr({ altNames: [testDomainWildcard] }, await createKeyFn());
});
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
@@ -134,7 +130,6 @@ describe('client', () => {
});
});
/**
* Initialize clients
*/
@@ -142,7 +137,7 @@ describe('client', () => {
it('should initialize client', () => {
testClient = new acme.Client({
...clientOpts,
accountKey: testAccountKey
accountKey: testAccountKey,
});
});
@@ -151,12 +146,11 @@ describe('client', () => {
jwkSpecFn(jwk);
});
/**
* Terms of Service
*/
it('should produce tos url [ACME_CAP_META_TOS_FIELD]', async function() {
it('should produce tos url [ACME_CAP_META_TOS_FIELD]', async function () {
if (!capMetaTosField) {
this.skip();
}
@@ -165,7 +159,7 @@ describe('client', () => {
assert.isString(tos);
});
it('should not produce tos url [!ACME_CAP_META_TOS_FIELD]', async function() {
it('should not produce tos url [!ACME_CAP_META_TOS_FIELD]', async function () {
if (capMetaTosField) {
this.skip();
}
@@ -174,12 +168,11 @@ describe('client', () => {
assert.isNull(tos);
});
/**
* Create account
*/
it('should refuse account creation without tos [ACME_CAP_META_TOS_FIELD]', async function() {
it('should refuse account creation without tos [ACME_CAP_META_TOS_FIELD]', async function () {
if (!capMetaTosField) {
this.skip();
}
@@ -187,7 +180,7 @@ describe('client', () => {
await assert.isRejected(testClient.createAccount());
});
it('should refuse account creation without eab [ACME_CAP_EAB_ENABLED]', async function() {
it('should refuse account creation without eab [ACME_CAP_EAB_ENABLED]', async function () {
if (!capEabEnabled) {
this.skip();
}
@@ -195,17 +188,17 @@ describe('client', () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey,
externalAccountBinding: null
externalAccountBinding: null,
});
await assert.isRejected(client.createAccount({
termsOfServiceAgreed: true
termsOfServiceAgreed: true,
}));
});
it('should create an account', async () => {
testAccount = await testClient.createAccount({
termsOfServiceAgreed: true
termsOfServiceAgreed: true,
});
spec.rfc8555.account(testAccount);
@@ -217,7 +210,6 @@ describe('client', () => {
assert.isString(testAccountUrl);
});
/**
* Create account with alternate key sizes
*/
@@ -226,11 +218,11 @@ describe('client', () => {
it(`should create account with key=${k}`, async () => {
const client = new acme.Client({
...clientOpts,
accountKey: await altKeyFn()
accountKey: await altKeyFn(),
});
const account = await client.createAccount({
termsOfServiceAgreed: true
termsOfServiceAgreed: true,
});
spec.rfc8555.account(account);
@@ -238,7 +230,6 @@ describe('client', () => {
});
});
/**
* Find existing account using secondary client
*/
@@ -246,22 +237,22 @@ describe('client', () => {
it('should throw when trying to find account using invalid account key', async () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountSecondaryKey
accountKey: testAccountSecondaryKey,
});
await assert.isRejected(client.createAccount({
onlyReturnExisting: true
onlyReturnExisting: true,
}));
});
it('should find existing account using account key', async () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey
accountKey: testAccountKey,
});
const account = await client.createAccount({
onlyReturnExisting: true
onlyReturnExisting: true,
});
spec.rfc8555.account(account);
@@ -269,7 +260,6 @@ describe('client', () => {
assert.deepStrictEqual(account.key, testAccount.key);
});
/**
* Account URL
*/
@@ -278,7 +268,7 @@ describe('client', () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey,
accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1'
accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1',
});
await assert.isRejected(client.updateAccount());
@@ -288,11 +278,11 @@ describe('client', () => {
const client = new acme.Client({
...clientOpts,
accountKey: testAccountKey,
accountUrl: testAccountUrl
accountUrl: testAccountUrl,
});
const account = await client.createAccount({
onlyReturnExisting: true
onlyReturnExisting: true,
});
spec.rfc8555.account(account);
@@ -300,7 +290,6 @@ describe('client', () => {
assert.deepStrictEqual(account.key, testAccount.key);
});
/**
* Update account contact info
*/
@@ -316,12 +305,11 @@ describe('client', () => {
assert.include(account.contact, testContact);
});
/**
* Change account private key
*/
it('should change account private key [ACME_CAP_UPDATE_ACCOUNT_KEY]', async function() {
it('should change account private key [ACME_CAP_UPDATE_ACCOUNT_KEY]', async function () {
if (!capUpdateAccountKey) {
this.skip();
}
@@ -329,7 +317,7 @@ describe('client', () => {
await testClient.updateAccountKey(testAccountSecondaryKey);
const account = await testClient.createAccount({
onlyReturnExisting: true
onlyReturnExisting: true,
});
spec.rfc8555.account(account);
@@ -337,7 +325,6 @@ describe('client', () => {
assert.notDeepEqual(account.key, testAccount.key);
});
/**
* Create new certificate order
*/
@@ -357,7 +344,6 @@ describe('client', () => {
});
});
/**
* Get status of existing certificate order
*/
@@ -371,7 +357,6 @@ describe('client', () => {
}));
});
/**
* Get identifier authorization
*/
@@ -401,7 +386,6 @@ describe('client', () => {
});
});
/**
* Generate challenge key authorization
*/
@@ -418,7 +402,6 @@ describe('client', () => {
[testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
});
/**
* Deactivate identifier authorization
*/
@@ -427,8 +410,8 @@ describe('client', () => {
const order = await testClient.createOrder({
identifiers: [
{ type: 'dns', value: `${uuid()}.${domainName}` },
{ type: 'dns', value: `${uuid()}.${domainName}` }
]
{ type: 'dns', value: `${uuid()}.${domainName}` },
],
});
const authzCollection = await testClient.getAuthorizations(order);
@@ -445,7 +428,6 @@ describe('client', () => {
});
});
/**
* Verify satisfied challenge
*/
@@ -460,7 +442,6 @@ describe('client', () => {
await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard);
});
/**
* Complete challenge
*/
@@ -474,7 +455,6 @@ describe('client', () => {
}));
});
/**
* Wait for valid challenge
*/
@@ -483,7 +463,6 @@ describe('client', () => {
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
});
/**
* Finalize order
*/
@@ -500,7 +479,6 @@ describe('client', () => {
assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url);
});
/**
* Wait for valid order
*/
@@ -509,7 +487,6 @@ describe('client', () => {
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
});
/**
* Get certificate
*/
@@ -525,7 +502,7 @@ describe('client', () => {
});
});
it('should get alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
it('should get alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
@@ -539,7 +516,7 @@ describe('client', () => {
}));
});
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
@@ -551,7 +528,6 @@ describe('client', () => {
assert.strictEqual(testIssuers[0], info.issuer.commonName);
});
/**
* Revoke certificate
*/
@@ -568,7 +544,6 @@ describe('client', () => {
await assert.isRejected(testClient.getCertificate(testOrderWildcard));
});
/**
* Deactivate account
*/
@@ -581,7 +556,6 @@ describe('client', () => {
assert.strictEqual(account.status, 'deactivated');
});
/**
* Verify that no new orders can be made
*/

View File

@@ -18,17 +18,16 @@ const clientOpts = {
directoryUrl,
backoffAttempts: 5,
backoffMin: 1000,
backoffMax: 5000
backoffMax: 5000,
};
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
clientOpts.externalAccountBinding = {
kid: process.env.ACME_EAB_KID,
hmacKey: process.env.ACME_EAB_HMAC_KEY
hmacKey: process.env.ACME_EAB_HMAC_KEY,
};
}
describe('client.auto', () => {
const testDomain = `${uuid()}.${domainName}`;
const testHttpDomain = `${uuid()}.${domainName}`;
@@ -40,21 +39,19 @@ describe('client.auto', () => {
const testSanDomains = [
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`
`${uuid()}.${domainName}`,
];
/**
* Pebble CTS required
*/
before(function() {
before(function () {
if (!cts.isEnabled()) {
this.skip();
}
});
/**
* Key types
*/
@@ -64,16 +61,16 @@ describe('client.auto', () => {
createKeyFn: () => acme.crypto.createPrivateRsaKey(),
createKeyAltFns: {
s1024: () => acme.crypto.createPrivateRsaKey(1024),
s4096: () => acme.crypto.createPrivateRsaKey(4096)
}
s4096: () => acme.crypto.createPrivateRsaKey(4096),
},
},
ecdsa: {
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
createKeyAltFns: {
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
p521: () => acme.crypto.createPrivateEcdsaKey('P-521')
}
}
p521: () => acme.crypto.createPrivateEcdsaKey('P-521'),
},
},
}).forEach(([name, { createKeyFn, createKeyAltFns }]) => {
describe(name, () => {
let testIssuers;
@@ -82,12 +79,11 @@ describe('client.auto', () => {
let testSanCertificate;
let testWildcardCertificate;
/**
* Fixtures
*/
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
@@ -103,7 +99,6 @@ describe('client.auto', () => {
});
});
/**
* Initialize client
*/
@@ -111,31 +106,30 @@ describe('client.auto', () => {
it('should initialize client', async () => {
testClient = new acme.Client({
...clientOpts,
accountKey: await createKeyFn()
accountKey: await createKeyFn(),
});
});
/**
* Invalid challenge response
*/
it('should throw on invalid challenge response', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
await assert.isRejected(testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeNoopFn,
challengeRemoveFn: cts.challengeNoopFn
challengeRemoveFn: cts.challengeNoopFn,
}), /^authorization not found/i);
});
it('should throw on invalid challenge response with opts.skipChallengeVerification=true', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
await assert.isRejected(testClient.auto({
@@ -143,38 +137,37 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
skipChallengeVerification: true,
challengeCreateFn: cts.challengeNoopFn,
challengeRemoveFn: cts.challengeNoopFn
challengeRemoveFn: cts.challengeNoopFn,
}));
});
/**
* Challenge function exceptions
*/
it('should throw on challengeCreate exception', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
await assert.isRejected(testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeThrowFn,
challengeRemoveFn: cts.challengeNoopFn
challengeRemoveFn: cts.challengeNoopFn,
}), /^oops$/);
});
it('should not throw on challengeRemove exception', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeThrowFn
challengeRemoveFn: cts.challengeThrowFn,
});
assert.isString(cert);
@@ -188,8 +181,8 @@ describe('client.auto', () => {
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`
]
`${uuid()}.${domainName}`,
],
}, await createKeyFn());
await assert.isRejected(testClient.auto({
@@ -205,28 +198,27 @@ describe('client.auto', () => {
results.push(true);
return cts.challengeCreateFn(...args);
},
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
}));
assert.strictEqual(results.length, 5);
assert.deepStrictEqual(results, [false, false, false, true, true]);
});
/**
* Order certificates
*/
it('should order certificate', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testDomain
commonName: testDomain,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
@@ -235,7 +227,7 @@ describe('client.auto', () => {
it('should order certificate using http-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testHttpDomain
commonName: testHttpDomain,
}, await createKeyFn());
const cert = await testClient.auto({
@@ -243,7 +235,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertHttpChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['http-01']
challengePriority: ['http-01'],
});
assert.isString(cert);
@@ -251,7 +243,7 @@ describe('client.auto', () => {
it('should order certificate using https-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testHttpsDomain
commonName: testHttpsDomain,
}, await createKeyFn());
const cert = await testClient.auto({
@@ -259,7 +251,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertHttpsChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['http-01']
challengePriority: ['http-01'],
});
assert.isString(cert);
@@ -267,7 +259,7 @@ describe('client.auto', () => {
it('should order certificate using dns-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testDnsDomain
commonName: testDnsDomain,
}, await createKeyFn());
const cert = await testClient.auto({
@@ -275,7 +267,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertDnsChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['dns-01']
challengePriority: ['dns-01'],
});
assert.isString(cert);
@@ -283,7 +275,7 @@ describe('client.auto', () => {
it('should order certificate using tls-alpn-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testAlpnDomain
commonName: testAlpnDomain,
}, await createKeyFn());
const cert = await testClient.auto({
@@ -291,7 +283,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['tls-alpn-01']
challengePriority: ['tls-alpn-01'],
});
assert.isString(cert);
@@ -299,14 +291,14 @@ describe('client.auto', () => {
it('should order san certificate', async () => {
const [, csr] = await acme.crypto.createCsr({
altNames: testSanDomains
altNames: testSanDomains,
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
@@ -315,15 +307,14 @@ describe('client.auto', () => {
it('should order wildcard certificate', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testWildcardDomain,
altNames: [`*.${testWildcardDomain}`]
altNames: [testWildcardDomain, `*.${testWildcardDomain}`],
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
@@ -332,7 +323,7 @@ describe('client.auto', () => {
it('should order certificate with opts.skipChallengeVerification=true', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
@@ -340,20 +331,20 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
skipChallengeVerification: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
});
it('should order alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
it('should order alternate certificate chain [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
await Promise.all(testIssuers.map(async (issuer) => {
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
@@ -361,7 +352,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
preferredChain: issuer,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
});
const rootCert = acme.crypto.splitPemChain(cert).pop();
@@ -371,13 +362,13 @@ describe('client.auto', () => {
}));
});
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function() {
it('should get default chain with invalid preference [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
if (!capAlternateCertRoots) {
this.skip();
}
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`
commonName: `${uuid()}.${domainName}`,
}, await createKeyFn());
const cert = await testClient.auto({
@@ -385,7 +376,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true,
preferredChain: uuid(),
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
});
const rootCert = acme.crypto.splitPemChain(cert).pop();
@@ -394,7 +385,6 @@ describe('client.auto', () => {
assert.strictEqual(testIssuers[0], info.issuer.commonName);
});
/**
* Order certificate with alternate key sizes
*/
@@ -402,21 +392,20 @@ describe('client.auto', () => {
Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => {
it(`should order certificate with key=${k}`, async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testDomain
commonName: testDomain,
}, await altKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn
challengeRemoveFn: cts.challengeRemoveFn,
});
assert.isString(cert);
});
});
/**
* Read certificates
*/

View File

@@ -8,7 +8,6 @@ const axios = require('./../src/axios');
const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null;
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
/**
* Send request
*/
@@ -21,20 +20,18 @@ async function request(apiPath, data = {}) {
await axios.request({
url: `${apiBaseUrl}/${apiPath}`,
method: 'post',
data
data,
});
return true;
}
/**
* State
*/
exports.isEnabled = () => !!apiBaseUrl;
/**
* DNS
*/
@@ -42,7 +39,6 @@ exports.isEnabled = () => !!apiBaseUrl;
exports.addDnsARecord = async (host, addresses) => request('add-a', { host, addresses });
exports.setDnsCnameRecord = async (host, target) => request('set-cname', { host, target });
/**
* Challenge response
*/
@@ -55,7 +51,7 @@ async function addHttps01ChallengeResponse(token, content, targetHostname) {
await addHttp01ChallengeResponse(token, content);
return request('add-redirect', {
path: `/.well-known/acme-challenge/${token}`,
targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`
targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`,
});
}
@@ -72,7 +68,6 @@ exports.addHttps01ChallengeResponse = addHttps01ChallengeResponse;
exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse;
/**
* Challenge response mock functions
*/

View File

@@ -7,7 +7,6 @@ const util = require('./../src/util');
const pebbleManagementUrl = process.env.ACME_PEBBLE_MANAGEMENT_URL || null;
/**
* Pebble
*/
@@ -26,7 +25,6 @@ async function getPebbleCertIssuers() {
return info.map((i) => i.issuer.commonName);
}
/**
* Get certificate issuers
*/

View File

@@ -7,14 +7,12 @@ const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const axios = require('./../src/axios');
/**
* Add promise support to Chai
*/
chai.use(chaiAsPromised);
/**
* Challenge test server ports
*/
@@ -31,7 +29,6 @@ if (process.env.ACME_TLSALPN_PORT) {
axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT;
}
/**
* External account binding
*/

View File

@@ -7,7 +7,6 @@ const { assert } = require('chai');
const spec = {};
module.exports = spec;
/**
* ACME
*/
@@ -120,7 +119,6 @@ spec.rfc8555.challenge = (obj) => {
}
};
/**
* Crypto
*/
@@ -150,7 +148,6 @@ spec.crypto.certificateInfo = (obj) => {
assert.strictEqual(Object.prototype.toString.call(obj.notAfter), '[object Date]');
};
/**
* JWK
*/

View File

@@ -15,7 +15,6 @@ export type PublicKeyString = string;
export type CertificateString = string;
export type CsrString = string;
/**
* Augmented ACME interfaces
*/
@@ -28,7 +27,6 @@ export interface Authorization extends rfc8555.Authorization {
url: string;
}
/**
* Client
*/
@@ -80,7 +78,6 @@ export class Client {
auto(opts: ClientAutoOptions): Promise<string>;
}
/**
* Directory URLs
*/
@@ -95,11 +92,11 @@ export const directory: {
production: string
},
zerossl: {
staging: string,
production: string
}
};
/**
* Crypto
*/
@@ -177,14 +174,12 @@ export interface CryptoLegacyInterface {
export const forge: CryptoLegacyInterface;
/**
* Axios
*/
export const axios: AxiosInstance;
/**
* Logger
*/

View File

@@ -4,7 +4,6 @@
import * as acme from 'acme-client';
(async () => {
/* Client */
const accountKey = await acme.crypto.createPrivateKey();

View File

@@ -27,7 +27,6 @@ export interface AccountUpdateRequest {
termsOfServiceAgreed?: boolean;
}
/**
* Order
*
@@ -53,7 +52,6 @@ export interface OrderCreateRequest {
notAfter?: string;
}
/**
* Authorization
*
@@ -73,7 +71,6 @@ export interface Identifier {
value: string;
}
/**
* Challenge
*
@@ -102,7 +99,6 @@ export interface DnsChallenge extends ChallengeAbstract {
export type Challenge = HttpChallenge | DnsChallenge;
/**
* Certificate
*

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08)
**Note:** Version bump only for package @certd/pipeline
## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08)
**Note:** Version bump only for package @certd/pipeline
# [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03)
**Note:** Version bump only for package @certd/pipeline
## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03)
**Note:** Version bump only for package @certd/pipeline
## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01)
**Note:** Version bump only for package @certd/pipeline
## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28)
**Note:** Version bump only for package @certd/pipeline
## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23)
**Note:** Version bump only for package @certd/pipeline
## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18)
**Note:** Version bump only for package @certd/pipeline
## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.20.12",
"version": "1.21.2",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
@@ -23,7 +23,7 @@
"qs": "^6.11.2"
},
"devDependencies": {
"@certd/acme-client": "workspace:^1.20.12",
"@certd/acme-client": "workspace:^1.21.2",
"@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@@ -52,6 +52,8 @@ export function createAxiosService({ logger }: { logger: Logger }) {
// }
logger.error(`请求出错url:${error?.response?.config.url},method:${error.response.config.method},status:${error?.response?.status}`);
logger.info("返回数据:", JSON.stringify(error?.response?.data));
delete error.config;
delete error.response;
return Promise.reject(error);
}
);

View File

@@ -3,6 +3,50 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08)
### Performance Improvements
* 申请证书时可以选择跳过本地dns校验 ([fe91d94](https://github.com/certd/certd/commit/fe91d94090d22ed0a3ea753ba74dfaa1bf057c17))
## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08)
### Performance Improvements
* 说明优化,默认值优化 ([970c7fd](https://github.com/certd/certd/commit/970c7fd8a0f557770e973d8462ee5684ef742810))
# [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03)
### Features
* 支持zero ssl ([eade2c2](https://github.com/certd/certd/commit/eade2c2b681569f03e9cd466e7d5bcd6703ed492))
## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03)
### Performance Improvements
* 优化cname verify ([eba333d](https://github.com/certd/certd/commit/eba333de7a5b5ef4b0b7eaa904f578720102fa61))
## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01)
### Bug Fixes
* 修复配置了cdn cname后申请失败的bug ([4a5fa76](https://github.com/certd/certd/commit/4a5fa767edc347d03d29a467e86c9a4d70b0220c))
## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28)
### Performance Improvements
* 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/certd/certd/commit/e0eb3a441384d474fe2923c69b25318264bdc9df))
## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23)
**Note:** Version bump only for package @certd/plugin-cert
## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18)
**Note:** Version bump only for package @certd/plugin-cert
## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17)
### Bug Fixes

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.20.12",
"version": "1.21.2",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
@@ -17,8 +17,8 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "workspace:^1.20.12",
"@certd/pipeline": "workspace:^1.20.12",
"@certd/acme-client": "workspace:^1.21.2",
"@certd/pipeline": "workspace:^1.21.2",
"jszip": "^3.10.1",
"node-forge": "^0.10.0",
"psl": "^1.9.0"

View File

@@ -0,0 +1,29 @@
import { IsAccess, AccessInput } from "@certd/pipeline";
@IsAccess({
name: "eab",
title: "EAB授权",
desc: "ZeroSSL证书申请需要EAB授权",
})
export class EabAccess {
@AccessInput({
title: "KID",
component: {
placeholder: "kid",
},
helper: "EAB KID",
required: true,
})
kid = "";
@AccessInput({
title: "HMACKey",
component: {
placeholder: "HMAC Key",
},
helper: "EAB HMAC Key",
required: true,
})
hmacKey = "";
}
new EabAccess();

View File

@@ -0,0 +1 @@
export * from "./eab-access";

View File

@@ -1,2 +1,3 @@
export * from "./plugin";
export * from "./dns-provider";
export * from "./access";

View File

@@ -6,18 +6,32 @@ import { Logger } from "log4js";
import { IContext } from "@certd/pipeline";
import { IDnsProvider } from "../../dns-provider";
import psl from "psl";
import { ClientExternalAccountBindingOptions } from "@certd/acme-client";
export type CertInfo = {
crt: string;
key: string;
csr: string;
};
export type SSLProvider = "letsencrypt" | "buypass" | "zerossl";
export class AcmeService {
userContext: IContext;
logger: Logger;
constructor(options: { userContext: IContext; logger: Logger }) {
sslProvider: SSLProvider;
skipLocalVerify = true;
eab?: ClientExternalAccountBindingOptions;
constructor(options: {
userContext: IContext;
logger: Logger;
sslProvider: SSLProvider;
eab?: ClientExternalAccountBindingOptions;
skipLocalVerify?: boolean;
}) {
this.userContext = options.userContext;
this.logger = options.logger;
this.sslProvider = options.sslProvider || "letsencrypt";
this.eab = options.eab;
this.skipLocalVerify = options.skipLocalVerify ?? false;
acme.setLogger((text: string) => {
this.logger.info(text);
});
@@ -28,7 +42,7 @@ export class AcmeService {
}
buildAccountKey(email: string) {
return "acme.config." + email;
return `acme.config.${this.sslProvider}.${email}`;
}
async saveAccountConfig(email: string, conf: any) {
@@ -41,11 +55,18 @@ export class AcmeService {
conf.key = await this.createNewKey();
await this.saveAccountConfig(email, conf);
}
let directoryUrl = "";
if (isTest) {
directoryUrl = acme.directory[this.sslProvider].staging;
} else {
directoryUrl = acme.directory[this.sslProvider].production;
}
const client = new acme.Client({
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
directoryUrl: directoryUrl,
accountKey: conf.key,
accountUrl: conf.accountUrl,
backoffAttempts: 20,
externalAccountBinding: this.eab,
backoffAttempts: 30,
backoffMin: 5000,
backoffMax: 10000,
});
@@ -54,6 +75,7 @@ export class AcmeService {
const accountPayload = {
termsOfServiceAgreed: true,
contact: [`mailto:${email}`],
externalAccountBinding: this.eab,
};
await client.createAccount(accountPayload);
conf.accountUrl = client.getAccountUrl();
@@ -178,6 +200,7 @@ export class AcmeService {
csr,
email: email,
termsOfServiceAgreed: true,
skipChallengeVerification: this.skipLocalVerify,
challengePriority: ["dns-01"],
challengeCreateFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string): Promise<any> => {
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider);

View File

@@ -1,6 +1,6 @@
import { AbstractTaskPlugin, Decorator, HttpClient, IAccessService, IContext, IsTaskPlugin, RunStrategy, Step, TaskInput, TaskOutput } from "@certd/pipeline";
import dayjs from "dayjs";
import { AcmeService, CertInfo } from "./acme";
import { AcmeService, CertInfo, SSLProvider } from "./acme";
import _ from "lodash";
import { Logger } from "log4js";
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider";
@@ -38,10 +38,10 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
span: 24,
},
helper:
"支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
"支持多个域名、多个子域名、多个通配符域名打到一个证书上域名必须是在同一个DNS提供商解析\n" +
"多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com\n" +
"输入一个回车之后,再输入下一个",
"1、支持通配符域名,例如: *.foo.com、foo.com、*.test.handsfree.work\n" +
"2、支持多个域名、多个子域名、多个通配符域名打到一个证书上域名必须是在同一个DNS提供商解析\n" +
"3、多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com、foo.com\n" +
"4、输入一个回车之后,再输入下一个",
})
domains!: string;
@@ -56,6 +56,32 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
})
email!: string;
@TaskInput({
title: "证书提供商",
default: "letsencrypt",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt" },
// { value: "buypass", label: "Buypass" },
{ value: "zerossl", label: "ZeroSSL" },
],
},
required: true,
})
sslProvider!: SSLProvider;
@TaskInput({
title: "EAB授权",
component: {
name: "pi-access-selector",
type: "eab",
},
helper: "如果使用ZeroSSL证书需要提供EAB授权 请前往 https://app.zerossl.com/developer 生成 'EAB Credentials for ACME Clients' ",
})
eabAccessId!: number;
@TaskInput({
title: "DNS提供商",
component: {
@@ -83,6 +109,17 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
})
dnsProviderAccess!: string;
@TaskInput({
title: "跳过本地校验DNS",
default: false,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "如果重试多次出现Authorization not found TXT record导致无法申请成功请尝试开启此选项",
})
skipLocalVerify = false;
@TaskInput({
title: "更新天数",
component: {
@@ -90,7 +127,7 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
vModel: "value",
},
required: true,
helper: "到期前多少天后更新证书",
helper: "到期前多少天后更新证书,注意:流水线默认不会自动运行,请设置定时器,每天定时运行本流水线",
})
renewDays!: number;
@@ -106,9 +143,16 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
@TaskInput({
title: "CsrInfo",
helper: "暂时没有用",
})
csrInfo!: string;
@TaskInput({
title: "配置说明",
helper: "运行策略请选择总是运行,其他证书部署任务请选择成功后跳过;当证书快到期前将会自动重新申请证书,然后会清空后续任务的成功状态,部署任务将会重新运行",
})
intro!: string;
acme!: AcmeService;
logger!: Logger;
userContext!: IContext;
@@ -128,7 +172,17 @@ export class CertApplyPlugin extends AbstractTaskPlugin {
this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step;
this.acme = new AcmeService({ userContext: this.userContext, logger: this.logger });
let eab: any = null;
if (this.eabAccessId) {
eab = await this.ctx.accessService.getById(this.eabAccessId);
}
this.acme = new AcmeService({
userContext: this.userContext,
logger: this.logger,
sslProvider: this.sslProvider,
eab,
skipLocalVerify: this.skipLocalVerify,
});
}
async execute(): Promise<void> {

View File

@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08)
**Note:** Version bump only for package @certd/plugin-util
## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08)
**Note:** Version bump only for package @certd/plugin-util
# [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03)
**Note:** Version bump only for package @certd/plugin-util
## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03)
**Note:** Version bump only for package @certd/plugin-util
## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01)
**Note:** Version bump only for package @certd/plugin-util
## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28)
**Note:** Version bump only for package @certd/plugin-util
## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23)
**Note:** Version bump only for package @certd/plugin-util
## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18)
**Note:** Version bump only for package @certd/plugin-util
## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17)
**Note:** Version bump only for package @certd/plugin-util

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-util",
"private": false,
"version": "1.20.12",
"version": "1.21.2",
"main": "./src/index.ts",
"module": "./src/index.ts",
"types": "./src/index.ts",
@@ -21,7 +21,7 @@
"shelljs": "^0.8.5"
},
"devDependencies": {
"@certd/pipeline": "workspace:^1.20.12",
"@certd/pipeline": "workspace:^1.21.2",
"@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@@ -3,6 +3,8 @@ VITE_APP_API=/api
VITE_APP_PM_ENABLED=true
VITE_APP_TITLE=Certd
VITE_APP_SLOGAN=让你的证书永不过期
VITE_APP_COPYRIGHT=Copyright &copy; 2021-2024 handsfree.work
VITE_APP_COPYRIGHT_YEAR=2021-2024
VITE_APP_COPYRIGHT_NAME=handsfree.work
VITE_APP_COPYRIGHT_URL=https://certd.handsfree.work
VITE_APP_LOGO_PATH=./images/logo/logo.svg
VITE_APP_PROJECT_PATH=https://github.com/certd/certd

View File

@@ -3,6 +3,56 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.2](https://github.com/certd/certd/compare/v1.21.1...v1.21.2) (2024-07-08)
**Note:** Version bump only for package @certd/ui-client
## [1.21.1](https://github.com/certd/certd/compare/v1.21.0...v1.21.1) (2024-07-08)
### Performance Improvements
* 说明优化,默认值优化 ([970c7fd](https://github.com/certd/certd/commit/970c7fd8a0f557770e973d8462ee5684ef742810))
# [1.21.0](https://github.com/certd/certd/compare/v1.20.17...v1.21.0) (2024-07-03)
**Note:** Version bump only for package @certd/ui-client
## [1.20.17](https://github.com/certd/certd/compare/v1.20.16...v1.20.17) (2024-07-03)
**Note:** Version bump only for package @certd/ui-client
## [1.20.16](https://github.com/certd/certd/compare/v1.20.15...v1.20.16) (2024-07-01)
**Note:** Version bump only for package @certd/ui-client
## [1.20.15](https://github.com/certd/certd/compare/v1.20.14...v1.20.15) (2024-06-28)
### Bug Fixes
* 修复无法强制取消任务的bug ([9cc01db](https://github.com/certd/certd/commit/9cc01db1d569a5c45bb3e731f35d85df324a8e62))
### Performance Improvements
* 支持windows文件上传 ([7f61cab](https://github.com/certd/certd/commit/7f61cab101fa13b4e88234e9ad47434e6130fed2))
## [1.20.14](https://github.com/certd/certd/compare/v1.20.13...v1.20.14) (2024-06-23)
### Bug Fixes
* 修复修改密码功能异常问题 ([f740ff5](https://github.com/certd/certd/commit/f740ff517f521dce361284c2c54bccc68aee0ea2))
## [1.20.13](https://github.com/certd/certd/compare/v1.20.12...v1.20.13) (2024-06-18)
### Bug Fixes
* 日志高度越界 ([c4c9adb](https://github.com/certd/certd/commit/c4c9adb8bfd513f57252e523794e3799a9b220f8))
* 修复邮箱设置页面SMTP拼写错误的问题 ([b98f1c0](https://github.com/certd/certd/commit/b98f1c0dd0bc6c6b4f814c578692afdf6d90b88d))
* 修复logo问题 ([7e483e6](https://github.com/certd/certd/commit/7e483e60913d509b113148c735fe13ba1d72dddf))
### Performance Improvements
* 增加警告,修复一些样式错乱问题 ([fd54c2f](https://github.com/certd/certd/commit/fd54c2ffac492222e85ff2f5f49a9ee5cfc73588))
## [1.20.12](https://github.com/certd/certd/compare/v1.20.10...v1.20.12) (2024-06-17)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.20.12",
"version": "1.21.2",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -26,10 +26,10 @@
"@ant-design/icons-vue": "^7.0.1",
"@aws-sdk/client-s3": "^3.535.0",
"@aws-sdk/s3-request-presigner": "^3.535.0",
"@fast-crud/fast-crud": "^1.21.0",
"@fast-crud/fast-extends": "^1.21.0",
"@fast-crud/ui-antdv4": "^1.21.0",
"@fast-crud/ui-interface": "^1.21.0",
"@fast-crud/fast-crud": "^1.21.1",
"@fast-crud/fast-extends": "^1.21.1",
"@fast-crud/ui-antdv4": "^1.21.1",
"@fast-crud/ui-interface": "^1.21.1",
"@iconify/vue": "^4.1.1",
"@soerenmartius/vue3-clipboard": "^0.1.2",
"ant-design-vue": "^4.1.2",
@@ -59,7 +59,7 @@
"vuedraggable": "^2.24.3"
},
"devDependencies": {
"@certd/pipeline": "^1.20.12",
"@certd/pipeline": "^1.21.2",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -1,106 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="210mm"
height="210mm"
viewBox="0 0 210 210"
version="1.1"
id="svg8"
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="500" height="500" viewBox="0 0 500.000000 500.000000"
>
<g id="layer1" style="display:inline">
<path
style="fill:#002255;stroke:none;stroke-width:0.625348"
d="M 35.587501,69.766667 V 59.766664 h 70.000109 69.99991 v 10.000003 9.999997 H 105.58761 35.587501 Z"
id="path12" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2"
width="32.244232"
height="20"
x="71.506088"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-8"
width="32.244232"
height="20"
x="107.42467"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-5-8"
width="32.244232"
height="20"
x="143.34325"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-4"
width="32.244232"
height="20"
x="71.506088"
y="129.82079" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-8-3"
width="32.244232"
height="20"
x="107.42467"
y="129.82079" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-5-8-2"
width="32.244232"
height="20"
x="143.34325"
y="129.82079" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-7"
width="32.244232"
height="20"
x="35.587502"
y="106.64581" />
<rect
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-4-0"
width="32.244232"
height="20"
x="35.587502"
y="129.82079" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-9"
width="32.244232"
height="20"
x="71.506088"
y="82.941666" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-8-37"
width="32.244232"
height="20"
x="107.42467"
y="82.941666" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-8-5-8-4"
width="32.244232"
height="20"
x="143.34325"
y="82.941666" />
<rect
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
id="rect22-2-7-1"
width="32.244232"
height="20"
x="35.587502"
y="82.941666" />
</g>
<polygon
points="75.3,174.4 103.1,103.6 79.8,103.6 112.6,41.3 156.4,41.3 129.9,90.5 148.1,90.5 "
fill="#f6cc00"
id="polygon276"
transform="matrix(1.0930933,0,0,0.99853202,-17.517362,-0.52287941)" />
</svg>
<path d="M28.34 56.68h28.34V36.12H28.34a7.79 7.79 0 1 1 0-15.58h19.84v9.05h8.5V12H28.34a16.29 16.29 0 0 0 0 32.58h19.84v3.56H28.34a19.84 19.84 0 0 1 0-39.68h28.34V0H28.34a28.34 28.34 0 0 0 0 56.68z"
transform="translate(70, 76) scale(6,6)"
></path>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 397 B

View File

@@ -19,7 +19,9 @@
<a href="_self">隐私</a>
<a href="_self">条款</a>
</div>
<div class="copyright">{{ envRef.COPYRIGHT }}</div>
<div class="copyright">
Copyright &copy; {{ envRef.COPYRIGHT_YEAR }} <a :href="envRef.COPYRIGHT_URL" target="_blank">{{ envRef.COPYRIGHT_NAME }}</a>
</div>
</div>
</div>
</div>

View File

@@ -65,7 +65,6 @@ function install(app: App, options: any = {}) {
view: { type: "link", text: null, icon: "ion:eye-outline" },
copy: { show: true, type: "link", text: null, icon: "ion:copy-outline" },
edit: { type: "link", text: null, icon: "ion:create-outline" },
copy: {show:true,type: "link", text: null, icon: "ion:copy-outline"},
remove: { type: "link", style: { color: "red" }, text: null, icon: "ion:trash-outline" }
},
dropdown: {
@@ -126,16 +125,17 @@ function install(app: App, options: any = {}) {
}
},
columns: {
createdAt: {
title: "创建时间",
type: "datetime",
form: {
show: false
},
column: {
order: 1000
}
}
// createdAt: {
// title: "创建时间",
// type: "datetime",
// form: {
// show: false
// },
// column: {
// show: false,
// order: 1000
// }
// }
}
};

View File

@@ -765,6 +765,12 @@ export const crudResources = [
path: "/crud/advanced/in-dialog",
component: "/crud/advanced/in-dialog/index.vue"
},
{
title: "抽屉中显示crud",
name: "AdvancedInDrawer",
path: "/crud/advanced/in-drawer",
component: "/crud/advanced/in-drawer/index.vue"
},
{
title: "大量数据",
name: "AdvancedBigData",

View File

@@ -65,7 +65,9 @@ h1, h2, h3, h4, h5, h6 {
.mt-10{
margin-top:10px;
}
.mb-10{
margin-bottom: 10px;
}
.m-10{
margin:10px;
}

View File

@@ -11,7 +11,9 @@ export class EnvConfig {
STORAGE: string;
TITLE: string;
SLOGAN: string;
COPYRIGHT: string;
COPYRIGHT_YEAR: string;
COPYRIGHT_NAME: string;
COPYRIGHT_URL: string;
LOGO_PATH: string;
PM_ENABLED: string;
constructor() {

View File

@@ -5,7 +5,7 @@
<fs-icon class="cd-icon-button" icon="ion:close-circle-outline" @click="clear"></fs-icon>
</span>
<span v-else class="mlr-5 gray">请选择</span>
<a-button @click="chooseForm.open">选择</a-button>
<a-button class="ml-5" @click="chooseForm.open">选择</a-button>
<a-form-item-rest v-if="chooseForm.show">
<a-modal v-model:open="chooseForm.show" title="选择授权提供者" width="700px" @ok="chooseForm.ok">
<div style="height: 400px; position: relative">
@@ -46,9 +46,9 @@ export default defineComponent({
}
}
function clear(){
function clear() {
selectedId.value = "";
target.value = null
target.value = null;
ctx.emit("update:modelValue", selectedId.value);
}
@@ -99,7 +99,6 @@ export default defineComponent({
}
});
return {
clear,
target,

View File

@@ -97,5 +97,6 @@ async function changePassword() {
const formOptions = buildFormOptions(passwordFormOptions);
formOptions.newInstance = true; //新实例打开
passwordFormRef.value = await openDialog(formOptions);
console.log(passwordFormRef.value);
}
</script>

View File

@@ -1,7 +1,7 @@
import { compute } from "@fast-crud/fast-crud";
import { compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from "@fast-crud/fast-crud";
import { Dicts } from "./dicts";
export default function () {
export default function (): CreateCrudOptionsRet {
return {
crudOptions: {
form: {
@@ -12,7 +12,7 @@ export default function () {
columns: {
domains: {
title: "域名",
type: ["dict-select"],
type: "dict-select",
search: {
show: true,
component: {
@@ -58,6 +58,30 @@ export default function () {
rules: [{ required: true, type: "email", message: "请填写邮箱" }]
}
},
blank: {
title: "占位",
type: "text",
form: {
blank: true
}
},
sslProvider: {
title: "证书提供商",
type: "dict-select",
dict: Dicts.sslProviderDict
},
eabAccess: {
title: "EAB授权",
type: "dict-select",
form: {
component: {
name: "PiAccessSelector",
type: "eab",
vModel: "modelValue"
},
helper: "如果是ZeroSSL需要配置EAB授权https://app.zerossl.com/developer 生成 'EAB' "
}
},
dnsProviderType: {
title: "DNS提供商",
type: "dict-select",

View File

@@ -1,7 +1,12 @@
import { dict } from "@fast-crud/fast-crud";
export const Dicts = {
certIssuerDict: dict({ data: [{ value: "letencrypt", label: "LetEncrypt" }] }),
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Lets Encrypt" },
{ value: "zerossl", label: "ZeroSSL" }
]
}),
challengeTypeDict: dict({ data: [{ value: "dns", label: "DNS校验" }] }),
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict"

View File

@@ -9,7 +9,7 @@ import { ref } from "vue";
import _ from "lodash-es";
export default {
name: "PiCertdForm",
setup(props:any, ctx:any) {
setup(props: any, ctx: any) {
// 自定义表单配置
const { buildFormOptions } = useColumns();
//使用crudOptions结构来构建自定义表单配置
@@ -18,7 +18,7 @@ export default {
const formOptions = buildFormOptions(
_.merge(crudOptions, {
form: {
doSubmit({ form }:any) {
doSubmit({ form }: any) {
// 创建certd 的pipeline
doSubmitRef.value({ form });
}
@@ -29,7 +29,7 @@ export default {
const formWrapperRef = ref();
const formWrapperOptions = ref();
formWrapperOptions.value = formOptions;
function open(doSubmit:any) {
function open(doSubmit: any) {
doSubmitRef.value = doSubmit;
formWrapperRef.value.open(formWrapperOptions.value);
}

View File

@@ -5,7 +5,12 @@
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-right>
<span style="margin-left: 10px">出现<a-tag>Promise rejected attempt #18,retrying in 10000ms:No TXT recordsfound for name</a-tag>属于正常现象多重试几次</span>
<!-- <span style="margin-left: 10px">出现<a-tag>Promise rejected attempt #18,retrying in 10000ms:No TXT recordsfound for name</a-tag>属于正常现象多重试几次</span>-->
</template>
<template #form-bottom>
<div>
申请证书
</div>
</template>
<pi-certd-form ref="certdFormRef"></pi-certd-form>
</fs-crud>

View File

@@ -28,6 +28,9 @@ export default defineComponent({
return {};
}
},
historyId: {
type: Number
},
type: {
type: String,
default: "icon"
@@ -59,7 +62,7 @@ export default defineComponent({
okText: "确认",
cancelText: "取消",
onOk: async () => {
await api.Cancel(props.runnable.id);
await api.Cancel(props.historyId);
notification.success({
message: "任务取消成功"
});

View File

@@ -1,7 +1,7 @@
<template>
<a-drawer v-model:open="stepDrawerVisible" placement="right" :closable="true" width="600px" @after-open-change="stepDrawerOnAfterVisibleChange">
<template #title>
编辑任务
编辑步骤
<a-button v-if="editMode" @click="stepDelete()">
<template #icon><DeleteOutlined /></template>
</a-button>
@@ -64,11 +64,11 @@
name: 'a-select',
vModel: 'value',
options: [
{ value: 0, label: '正常运行' },
{ value: 1, label: '成功后跳过' }
{ value: 0, label: '正常运行(证书申请任务请选择它)' },
{ value: 1, label: '成功后跳过(其他任务请选择它)' }
]
},
helper:'该任务运行成功一次之后下次运行是否跳过,证书申请任务务必选择正常运行',
helper: '该任务运行成功一次之后下次运行是否跳过,保持默认即可',
rules: [{ required: true, message: '此项必填' }]
}"
:get-context-fn="blankFn"
@@ -90,10 +90,10 @@ import { message, Modal } from "ant-design-vue";
import { computed, inject, Ref, ref } from "vue";
import _ from "lodash-es";
import { nanoid } from "nanoid";
import {CopyOutlined} from "@ant-design/icons-vue";
import { CopyOutlined } from "@ant-design/icons-vue";
export default {
name: "PiStepForm",
components:{CopyOutlined},
components: { CopyOutlined },
props: {
editMode: {
type: Boolean,
@@ -173,7 +173,7 @@ export default {
stepDrawerShow();
};
const stepAdd = (emit: any,stepDef:any) => {
const stepAdd = (emit: any, stepDef: any) => {
mode.value = "add";
const step: any = {
id: nanoid(),
@@ -183,7 +183,7 @@ export default {
input: {},
status: null
};
_.merge(step,stepDef)
_.merge(step, stepDef);
stepOpen(step, emit);
};
@@ -222,6 +222,10 @@ export default {
);
}
}
//设置初始值
if (input.default != null && currentStep.value.input[key] == null) {
currentStep.value.input[key] = input.default ?? input.value;
}
}
}

View File

@@ -71,11 +71,11 @@ import _ from "lodash-es";
import { nanoid } from "nanoid";
import PiStepForm from "../step-form/index.vue";
import { message, Modal } from "ant-design-vue";
import {CopyOutlined} from "@ant-design/icons-vue";
import { CopyOutlined } from "@ant-design/icons-vue";
export default {
name: "PiTaskForm",
components: {CopyOutlined, PiStepForm },
components: { CopyOutlined, PiStepForm },
props: {
editMode: {
type: Boolean,
@@ -88,7 +88,7 @@ export default {
const stepFormRef: Ref<any> = ref(null);
const currentStepIndex = ref(0);
provide("currentStepIndex", currentStepIndex);
const stepAdd = (task: any,stepDef:any) => {
const stepAdd = (task: any, stepDef: any) => {
currentStepIndex.value = task.steps.length;
stepFormRef.value.stepAdd((type: any, value: any) => {
if (type === "save") {
@@ -97,14 +97,14 @@ export default {
task.title = value.title;
}
}
},stepDef);
}, stepDef);
};
const stepCopy = (task: any, step: any, stepIndex: any) => {
step = _.cloneDeep(step)
step.id = nanoid()
step.title = step.title +"_copy"
stepAdd(task,step)
step = _.cloneDeep(step);
step.id = nanoid();
step.title = step.title + "_copy";
stepAdd(task, step);
};
const stepEdit = (task: any, step: any, stepIndex: any) => {
currentStepIndex.value = stepIndex;
@@ -135,7 +135,7 @@ export default {
});
};
return { stepAdd, stepEdit,stepCopy, stepDelete, stepFormRef };
return { stepAdd, stepEdit, stepCopy, stepDelete, stepFormRef };
}
/**
@@ -177,10 +177,10 @@ export default {
taskDrawerShow();
};
const taskAdd = (emit: any,taskMerge:any) => {
const taskAdd = (emit: any, taskMerge: any) => {
mode.value = "add";
const blankTask = { id: nanoid(), title: "新任务", steps: [], status: null }
const task: any = _.merge(blankTask,taskMerge) ;
const blankTask = { id: nanoid(), title: "新任务", steps: [], status: null };
const task: any = _.merge(blankTask, taskMerge);
taskOpen(task, emit);
};
@@ -234,7 +234,7 @@ export default {
taskSave,
taskDelete,
rules,
blankFn,
blankFn
};
}
return {
@@ -252,5 +252,8 @@ export default {
.steps {
margin: 0 50px 0 50px;
}
.ant-list .ant-list-item .ant-list-item-meta .ant-list-item-meta-title {
margin: 0;
}
}
</style>

View File

@@ -8,7 +8,7 @@
<pi-status-show :status="item.node.status?.result" type="icon"></pi-status-show>
</div>
</template>
<pre class="pi-task-view-logs"><template v-for="(text, index) of item.logs" :key="index">{{ text }}</template></pre>
<pre class="pi-task-view-logs" style="overflow: auto;"><template v-for="(text, index) of item.logs" :key="index">{{ text }}</template></pre>
</a-tab-pane>
</a-tabs>
</a-modal>

View File

@@ -81,11 +81,11 @@
<fs-icon v-if="editMode" class="add-stage-btn" title="添加新阶段" icon="ion:add-circle" @click="stageAdd(index)"></fs-icon>
</div>
<div class="task">
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
{{ task.title }}
<pi-status-show :status="task.status?.result"></pi-status-show>
</a-button>
<fs-icon class="copy" v-if="editMode" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
<a-button shape="round" @click="taskEdit(stage, index, task, taskIndex)">
{{ task.title }}
<pi-status-show :status="task.status?.result"></pi-status-show>
</a-button>
<fs-icon v-if="editMode" class="copy" title="复制" icon="ion:copy-outline" @click="taskCopy(stage, index, task)"></fs-icon>
</div>
</div>
<div v-if="editMode" class="task-container is-add">
@@ -99,7 +99,6 @@
并行任务
</a-button>
</a-tooltip>
</div>
</div>
</div>
@@ -188,7 +187,14 @@
<a-page-header title="运行历史" sub-title="点任务可查看日志" class="logs-block">
<a-timeline class="mt-10">
<template v-for="item of histories" :key="item.id">
<pi-history-timeline-item :runnable="item.pipeline" :is-current="currentHistory?.id === item.id" :edit-mode="editMode" @view="historyView(item)" @cancel="historyCancel()"></pi-history-timeline-item>
<pi-history-timeline-item
:runnable="item.pipeline"
:history-id="item.id"
:is-current="currentHistory?.id === item.id"
:edit-mode="editMode"
@view="historyView(item)"
@cancel="historyCancel()"
></pi-history-timeline-item>
</template>
<a-empty v-if="histories.length === 0"> </a-empty>
</a-timeline>
@@ -373,7 +379,7 @@ export default defineComponent({
const taskView = useTaskView();
const taskAdd = (stage: any, stageIndex: number, onSuccess?: any,taskDef?:any) => {
const taskAdd = (stage: any, stageIndex: number, onSuccess?: any, taskDef?: any) => {
currentStageIndex.value = stageIndex;
currentTaskIndex.value = stage.tasks.length;
taskFormRef.value.taskAdd((type: any, value: any) => {
@@ -383,17 +389,17 @@ export default defineComponent({
onSuccess();
}
}
},taskDef);
}, taskDef);
};
const taskCopy = (stage: any, stageIndex: number, task: any ) => {
task = _.cloneDeep(task)
task.id = nanoid()
task.title= task.title+"_copy"
const taskCopy = (stage: any, stageIndex: number, task: any) => {
task = _.cloneDeep(task);
task.id = nanoid();
task.title = task.title + "_copy";
for (const step of task.steps) {
step.id = nanoid()
step.id = nanoid();
}
taskAdd(stage,stageIndex,null,task)
taskAdd(stage, stageIndex, null, task);
};
const taskEdit = (stage: any, stageIndex: number, task: any, taskIndex: number, onSuccess?: any) => {
@@ -423,7 +429,7 @@ export default defineComponent({
}
};
return { taskAdd, taskEdit, taskCopy,taskFormRef, ...taskView };
return { taskAdd, taskEdit, taskCopy, taskFormRef, ...taskView };
}
function useStage(useTaskRet: any) {
@@ -760,10 +766,10 @@ export default defineComponent({
height: 100%;
z-index: 2;
.copy{
.copy {
position: absolute;
right:60px;
top:18px;
right: 60px;
top: 18px;
cursor: pointer;
&:hover {
color: #1890ff;

View File

@@ -36,6 +36,13 @@ const StatusEnum: StatusEnumType = {
spin: true,
icon: "ant-design:sync-outlined"
},
canceled: {
value: "canceled",
label: "已取消",
color: "yellow",
spin: true,
icon: "ant-design:minus-circle-twotone"
},
none: {
value: "none",
label: "未运行",

View File

@@ -5,11 +5,11 @@
</template>
<div class="email-form">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
<a-form-item label="STMP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
<a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
<a-input v-model:value="formState.host" />
</a-form-item>
<a-form-item label="STMP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
<a-input v-model:value="formState.port" />
</a-form-item>
@@ -18,12 +18,14 @@
</a-form-item>
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model:value="formState.auth.pass" />
<div class="helper">如果是qq邮箱需要到qq邮箱的设置里面申请授权码作为密码</div>
</a-form-item>
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
<a-input v-model:value="formState.sender" />
</a-form-item>
<a-form-item label="是否ssl" name="secure">
<a-switch v-model:checked="formState.secure" />
<div class="helper">ssl和非ssl的smtp端口是不一样的注意修改端口</div>
</a-form-item>
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
@@ -124,5 +126,12 @@ async function onTestSend() {
width: 500px;
margin: 20px;
}
.helper{
padding:1px;
margin:0px;
color: #999;
font-size: 10px;
}
}
</style>

View File

@@ -0,0 +1,43 @@
//@ts-ignore
import { requestForMock } from "/src/api/service";
const request = requestForMock;
const apiPrefix = "/mock/AdvancedInDrawer";
export function GetList(query: any) {
return request({
url: apiPrefix + "/page",
method: "get",
data: query
});
}
export function AddObj(obj: any) {
return request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export function UpdateObj(obj: any) {
return request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export function DelObj(id: any) {
return request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export function GetObj(id: any) {
return request({
url: apiPrefix + "/get",
method: "get",
params: { id }
});
}

View File

@@ -0,0 +1,76 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
//从context中获取子组件的ref
const drawerClassTimeRef = context.drawerClassTimeRef;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
if (form.id == null) {
form.id = row.id;
}
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
rowHandle: {
width: 300,
buttons: {
editClassTime: {
text: "录入课时",
click: ({ row }) => {
//调用子组件暴露的open方法打开抽屉对话框
drawerClassTimeRef.value.open({
textbook: row
});
}
}
}
},
columns: {
textbookCategory: {
title: "教材分类",
type: "text", //虽然不写也能正确显示组件,但不建议省略它
search: { show: true }
},
textbookVersion: {
title: "教材版本",
type: "text"
},
textbookName: {
title: "教材名称",
type: "text"
},
totalWords: {
title: "总词汇数",
type: "number"
},
classTimeNumber: {
title: "课时数量",
type: "number",
column: {
cellRender({ value }) {
return `${value}课时`;
}
}
}
}
}
};
}

View File

@@ -0,0 +1,43 @@
//@ts-ignore
import { requestForMock } from "/src/api/service";
const request = requestForMock;
const apiPrefix = "/mock/AdvancedInDrawerClassTime";
export function GetList(query: any) {
return request({
url: apiPrefix + "/page",
method: "get",
data: query
});
}
export function AddObj(obj: any) {
return request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export function UpdateObj(obj: any) {
return request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export function DelObj(id: any) {
return request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export function GetObj(id: any) {
return request({
url: apiPrefix + "/get",
method: "get",
params: { id }
});
}

View File

@@ -0,0 +1,89 @@
import * as api from "./api";
import { computed } from "vue";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const textbookRef = context.textbookRef;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
if (form.id == null) {
form.id = row.id;
}
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
columns: {
textbookId: {
title: "教材ID",
type: "text",
search: {
show: true
},
form: {
value: computed(() => {
//动态设置初始值
return textbookRef.value.id;
})
}
},
textbookCategory: {
title: "教材分类",
type: "text",
form: { show: false },
column: {
//本字段禁止条件render因为此字段没有值是从父组件传过来显示的
conditionalRender: false,
cellRender() {
return textbookRef.value.textbookCategory;
}
}
},
textbookVersion: {
title: "教材版本",
type: "text",
form: { show: false },
column: {
//本字段禁止条件render因为此字段没有值是从父组件传过来显示的
conditionalRender: false,
cellRender() {
return textbookRef.value.textbookVersion;
}
}
},
textbookName: {
title: "教材名称",
type: "text",
form: { show: false },
column: {
//本字段禁止条件render因为此字段没有值是从父组件传过来显示的
conditionalRender: false,
cellRender() {
return textbookRef.value.textbookName;
}
}
},
classTimeName: {
title: "课时名称",
type: "text"
}
}
}
};
}

View File

@@ -0,0 +1,35 @@
<template>
<a-drawer v-model:open="drawerOpened" width="1000px">
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, nextTick } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
const textbookRef = ref();
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { textbookRef } });
//课时子表组件名称定义
defineOptions({
name: "AdvancedInDrawerClassTime"
});
const drawerOpened = ref(false);
//定义抽屉打开方法
const open = async ({ textbook }) => {
textbookRef.value = textbook;
drawerOpened.value = true;
await nextTick(); //等待crud初始化完成
//设置查询条件只查询当前选中的教材id
crudExpose.setSearchFormData({ form: { textbookId: textbook.id } });
//刷新课时表
await crudExpose.doRefresh();
};
//暴露出去父组件通过ref可以调用open方法
defineExpose({
open
});
</script>

View File

@@ -0,0 +1,61 @@
// @ts-ignore
import mockUtil from "/src/mock/base";
const options: any = {
name: "AdvancedInDrawerClassTime",
idGenerator: 0
};
const list = [
{
textbookId: 1,
classTimeName: "1",
order: 1
},
{
textbookId: 1,
classTimeName: "2",
order: 1
},
{
textbookId: 1,
classTimeName: "3",
order: 1
},
{
textbookId: 1,
classTimeName: "4",
order: 1
},
{
textbookId: 1,
classTimeName: "5",
order: 1
},
{
textbookId: 2,
classTimeName: "1",
order: 1
},
{
textbookId: 2,
classTimeName: "2",
order: 1
},
{
textbookId: 2,
classTimeName: "3",
order: 1
},
{
textbookId: 2,
classTimeName: "4",
order: 1
},
{
textbookId: 2,
classTimeName: "5",
order: 1
}
];
options.list = list;
const mock = mockUtil.buildMock(options);
export default mock;

View File

@@ -0,0 +1,26 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding" />
<advanced-in-drawer-class-time ref="drawerClassTimeRef" />
</fs-page>
</template>
<script lang="ts" setup>
import { defineComponent, onMounted, ref } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import AdvancedInDrawerClassTime from "./drawer-class-time/index.vue";
//保留子组件的ref引用
const drawerClassTimeRef = ref();
//通过context传递到crud.tsx中
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { drawerClassTimeRef } });
defineOptions({
name: "AdvancedInDrawer"
});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,25 @@
// @ts-ignore
import mockUtil from "/src/mock/base";
const options: any = {
name: "AdvancedInDrawer",
idGenerator: 0
};
const list = [
{
textbookCategory: "初中英语",
textbookVersion: "初中人教版",
textbookName: "初一上学期",
totalWords: 200,
classTimeNumber: 40
},
{
textbookCategory: "初中英语",
textbookVersion: "初中人教版",
textbookName: "初一上学期",
totalWords: 200,
classTimeNumber: 40
}
];
options.list = list;
const mock = mockUtil.buildMock(options);
export default mock;

View File

@@ -160,6 +160,26 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
width: 300,
valueChange({ value, getComponentRef }) {
console.log("value changed:", value, getComponentRef("radio"));
},
editable: {
async updateCell(opts) {
const { row, key, value } = opts;
//如果是添加,需要返回{[rowKey]:xxx},比如:{id:2}
await api.UpdateCell(row.id, key, value);
//同时修改 updateCellLink
await api.UpdateCell(row.id, "updateCellLink", value);
//修改联动本地列
row.updateCellLink = value;
}
}
}
},
updateCellLink: {
title: "状态联动",
type: "text",
column: {
editable: {
disabled: true
}
}
},

View File

@@ -1,19 +1,29 @@
<template>
<div class="d2-page-cover">
<div class="d2-page-cover__title" @click="$open('https://github.com/certd/certd')">
<div class="title-line">Certd v{{ version }}</div>
<div class="title-line">
<span>Certd v{{ version }}</span>
</div>
</div>
<p class="d2-page-cover__sub-title">让你的证书永不过期</p>
<div class="warnning">
<a-alert type="warning" show-icon>
<template #description>
<div class="flex">
证书和授权为敏感信息不要使用来历不明的在线CertD服务以免泄露请务必私有化部署使用认准官方版本发布渠道
<a class="ml-5 flex" href="https://gitee.com/certd/certd" target="_blank">gitee</a>
<a class="ml-5 flex" href="https://github.com/certd/certd" target="_blank">github</a>
</div>
</template>
</a-alert>
</div>
<div class="content">
<img src="/images/preview.png" class="preview_img" />
</div>
<div class="footer_box">
<div class="left"></div>
<div class="right">
<div>如果觉得好用请不要吝啬你的star哟</div>
<a href="https://gitee.com/certd/certd" target="_blank"><img src="https://gitee.com/certd/certd/badge/star.svg?theme=dark" alt="star" /></a>
<a href="https://github.com/certd/certd" target="_blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/certd/certd?logo=github" /></a>
</div>
<div>如果觉得好用请不要吝啬你的star哟</div>
<a href="https://gitee.com/certd/certd" target="_blank"><img src="https://gitee.com/certd/certd/badge/star.svg?theme=dark" alt="star" /></a>
<a href="https://github.com/certd/certd" target="_blank"><img alt="GitHub stars" src="https://img.shields.io/github/stars/certd/certd?logo=github" /></a>
</div>
</div>
</template>
@@ -73,7 +83,7 @@ export default defineComponent({
.content {
padding: 20px;
width: 80%;
width: 70%;
.preview_img {
width: 100%;
border: 1px solid #eee;

View File

@@ -65,7 +65,7 @@ export default defineComponent({
//处理过,无需再次处理
return;
}
value.class="is-twig"
value.class = "is-twig";
if (value.children != null && value.children.length > 0) {
return;
}
@@ -83,13 +83,13 @@ export default defineComponent({
}
// 所有的子节点都没有children
parent.class = "is-twig"; // 连接叶子节点的末梢枝杈节点
let i = 0
let i = 0;
for (const child of parent.children) {
child.class = "is-leaf";
if(i !== 0){
if (i !== 0) {
child.class += " leaf-after";
}
i++
i++;
}
});
return [
@@ -135,11 +135,10 @@ export default defineComponent({
<style lang="less">
.fs-permission-tree {
.ant-tree-list-holder-inner{
.ant-tree-list-holder-inner {
flex-direction: row !important;
flex-wrap: wrap;
.is-twig{
.is-twig {
width: 100%;
}
@@ -149,8 +148,8 @@ export default defineComponent({
display: none;
}
&.leaf-after{
.ant-tree-indent-unit{
&.leaf-after {
.ant-tree-indent-unit {
display: none;
}
}
@@ -159,7 +158,6 @@ export default defineComponent({
border-bottom: 1px solid #ddd;
}
}
}
//.is-twig ul {
// display: flex;

View File

@@ -3,6 +3,54 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.2](https://github.com/fast-crud/fast-server-js/compare/v1.21.1...v1.21.2) (2024-07-08)
**Note:** Version bump only for package @certd/ui-server
## [1.21.1](https://github.com/fast-crud/fast-server-js/compare/v1.21.0...v1.21.1) (2024-07-08)
### Performance Improvements
* 上传到主机支持设置不mkdirs ([5ba9831](https://github.com/fast-crud/fast-server-js/commit/5ba9831ed1aa6ec6057df246f1035b36b9c41d2e))
* 说明优化,默认值优化 ([970c7fd](https://github.com/fast-crud/fast-server-js/commit/970c7fd8a0f557770e973d8462ee5684ef742810))
# [1.21.0](https://github.com/fast-crud/fast-server-js/compare/v1.20.17...v1.21.0) (2024-07-03)
**Note:** Version bump only for package @certd/ui-server
## [1.20.17](https://github.com/fast-crud/fast-server-js/compare/v1.20.16...v1.20.17) (2024-07-03)
### Performance Improvements
* 文件上传提示由cert.crt改为cert.pem ([a09b0e4](https://github.com/fast-crud/fast-server-js/commit/a09b0e48c176f3ed763791bd50322c29729f7c1c))
## [1.20.16](https://github.com/fast-crud/fast-server-js/compare/v1.20.15...v1.20.16) (2024-07-01)
### Bug Fixes
* 修复配置了cdn cname后申请失败的bug ([4a5fa76](https://github.com/fast-crud/fast-server-js/commit/4a5fa767edc347d03d29a467e86c9a4d70b0220c))
## [1.20.15](https://github.com/fast-crud/fast-server-js/compare/v1.20.14...v1.20.15) (2024-06-28)
### Bug Fixes
* 修复无法强制取消任务的bug ([9cc01db](https://github.com/fast-crud/fast-server-js/commit/9cc01db1d569a5c45bb3e731f35d85df324a8e62))
### Performance Improvements
* 腾讯云dns provider 支持腾讯云的accessId ([e0eb3a4](https://github.com/fast-crud/fast-server-js/commit/e0eb3a441384d474fe2923c69b25318264bdc9df))
* 支持windows文件上传 ([7f61cab](https://github.com/fast-crud/fast-server-js/commit/7f61cab101fa13b4e88234e9ad47434e6130fed2))
## [1.20.14](https://github.com/fast-crud/fast-server-js/compare/v1.20.13...v1.20.14) (2024-06-23)
**Note:** Version bump only for package @certd/ui-server
## [1.20.13](https://github.com/fast-crud/fast-server-js/compare/v1.20.12...v1.20.13) (2024-06-18)
### Performance Improvements
* ssh登录支持openssh格式私钥、支持私钥密码 ([5c2c508](https://github.com/fast-crud/fast-server-js/commit/5c2c50839a9076004f9034d754ac6deb531acdfb))
## [1.20.12](https://github.com/fast-crud/fast-server-js/compare/v1.20.10...v1.20.12) (2024-06-17)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.20.12",
"version": "1.21.2",
"description": "fast-server base midway",
"private": true,
"scripts": {
@@ -24,10 +24,10 @@
"@alicloud/cs20151215": "^3.0.3",
"@alicloud/openapi-client": "^0.4.0",
"@alicloud/pop-core": "^1.7.10",
"@certd/acme-client": "^1.20.12",
"@certd/pipeline": "^1.20.12",
"@certd/plugin-cert": "^1.20.12",
"@certd/plugin-util": "^1.20.12",
"@certd/acme-client": "^1.21.2",
"@certd/pipeline": "^1.21.2",
"@certd/plugin-cert": "^1.21.2",
"@certd/plugin-util": "^1.21.2",
"@koa/cors": "^3.4.3",
"@midwayjs/bootstrap": "^3.15.0",
"@midwayjs/cache": "^3.14.0",
@@ -46,6 +46,7 @@
"dayjs": "^1.11.7",
"glob": "^7.2.3",
"https-proxy-agent": "^7.0.4",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"koa-send": "^5.0.1",
@@ -62,6 +63,7 @@
"ssh2": "^0.8.9",
"svg-captcha": "^1.4.0",
"tencentcloud-sdk-nodejs": "^4.0.44",
"tencentcloud-sdk-nodejs-dnspod": "^4.0.866",
"typeorm": "^0.3.11"
},
"devDependencies": {

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