Compare commits

...

53 Commits

Author SHA1 Message Date
xiaojunnuo
e1daaf07ce v1.37.2 2025-10-14 22:50:33 +08:00
xiaojunnuo
cd21f2d1d7 build: prepare to build 2025-10-14 22:48:36 +08:00
xiaojunnuo
836e41064f build: prepare to build 2025-10-14 22:43:55 +08:00
xiaojunnuo
4658e4c739 build: prepare to build 2025-10-14 22:41:32 +08:00
xiaojunnuo
7993a7cdb0 perf: 优化start.sh脚本,去掉删除非ui目录的操作及提示 2025-10-14 22:41:03 +08:00
xiaojunnuo
567cb7d737 perf: start.sh脚本支持根据当前系统判断是否使用sudo 2025-10-14 22:35:24 +08:00
xiaojunnuo
985128b537 chore: 1 2025-10-14 22:25:42 +08:00
xiaojunnuo
cd35568e04 perf: 证书监控支持设置证书即将过期天数 2025-10-14 22:25:04 +08:00
xiaojunnuo
f612509cac perf: 支持新网代理方式 2025-10-14 12:05:31 +08:00
xiaojunnuo
f415190483 perf: dns支持新网互联 2025-10-14 10:55:10 +08:00
xiaojunnuo
e00733a346 fix: aliyunoss 选择证书接入点选择新加坡无法上传的bug 2025-10-13 23:16:03 +08:00
xiaojunnuo
aafafa0e73 chore: 1 2025-10-12 23:57:17 +08:00
xiaojunnuo
c87c9af12e chore: 1 2025-10-12 23:56:14 +08:00
xiaojunnuo
622215715f chore: 删除无用依赖 2025-10-11 19:08:14 +08:00
xiaojunnuo
c87250c028 docs: ipv6地址提示 2025-10-11 18:52:57 +08:00
xiaojunnuo
d6b6d700a5 docs: ipv6地址提示 2025-10-11 18:21:17 +08:00
xiaojunnuo
9d4e2c98a3 Merge remote-tracking branch 'origin/v2-dev' into v2-dev 2025-10-11 17:00:00 +08:00
xiaojunnuo
08094c2660 docs: ipv6地址提示 2025-10-11 16:59:28 +08:00
xiaojunnuo
fda82c82b0 chore: 1 2025-10-07 21:54:50 +08:00
xiaojunnuo
f0eabd4ea0 build: vscode auto compile 2025-10-07 21:45:09 +08:00
xiaojunnuo
5a4d812146 perf: 增加飞牛证书id选择的提示 2025-10-07 21:05:28 +08:00
xiaojunnuo
bf156a13bd fix: 修复飞牛证书部署后无法生效的bug 2025-10-07 21:01:19 +08:00
xiaojunnuo
53d276a8fc chore: 官方email服务支持发送附件 2025-10-05 22:44:59 +08:00
xiaojunnuo
978fa54518 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2025-10-05 08:05:59 +00:00
xiaojunnuo
31f82e58b5 chore: test优化 2025-10-05 07:59:56 +00:00
xiaojunnuo
5967f66e6d chore: 1 2025-10-05 15:01:35 +08:00
xiaojunnuo
bea81b54ca chore: 1 2025-10-05 14:52:25 +08:00
xiaojunnuo
6fd403bdca Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2025-10-05 14:47:45 +08:00
xiaojunnuo
3d673d9d40 chroe: remote 2025-10-05 14:47:40 +08:00
xiaojunnuo
cac949de56 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2025-10-05 05:57:03 +00:00
xiaojunnuo
dffa152698 chore: nettest 2025-10-05 13:10:03 +08:00
xiaojunnuo
7796298fca 1 2025-10-01 02:11:03 +08:00
xiaojunnuo
5291bfe8d4 chore: 网络测试 2025-09-30 18:03:16 +00:00
xiaojunnuo
b364313297 chore: linux 网络测试命令验证 2025-09-30 18:01:49 +00:00
xiaojunnuo
2bef608e07 perf: 支持网络测试 2025-09-30 23:27:31 +08:00
xiaojunnuo
aee13ad909 docs 2025-09-29 21:11:43 +08:00
xiaojunnuo
9d82eba599 docs: 2025-09-29 21:00:28 +08:00
xiaojunnuo
4852beb390 1.37.1 2025-09-29 20:48:34 +08:00
xiaojunnuo
522c2f61c0 build: publish 2025-09-29 20:37:20 +08:00
xiaojunnuo
d331396afe build: trigger build image 2025-09-29 20:37:03 +08:00
xiaojunnuo
c725cee044 v1.37.1 2025-09-29 20:35:30 +08:00
xiaojunnuo
367ef4ecb2 build: prepare to build 2025-09-29 20:32:54 +08:00
xiaojunnuo
c3a64facd5 chore 2025-09-29 20:32:31 +08:00
xiaojunnuo
2671781e1b fix: 修复某些情况下cname申请证书报错主域名不一致的bug 2025-09-29 18:58:19 +08:00
xiaojunnuo
9291fa68aa perf: dns解析支持阿里esa 2025-09-28 23:29:56 +08:00
xiaojunnuo
6ebb3659f4 perf: cname主域名校验提示优化,显示不一致的两方便于排查问题 2025-09-28 16:18:39 +08:00
xiaojunnuo
109696e965 fix: 修复版本比较bug 2025-09-28 12:49:53 +08:00
xiaojunnuo
b86bbd370c chore: deploy 1.37.0 2025-09-28 12:44:02 +08:00
xiaojunnuo
1575a4fb1a chore: deploy 1.37.0 2025-09-28 12:42:54 +08:00
xiaojunnuo
e2f500be90 chore: deploy 1.37.0 2025-09-28 12:37:22 +08:00
xiaojunnuo
284b00a826 chore: 2025-09-28 12:21:42 +08:00
xiaojunnuo
66180e19b5 build: publish 2025-09-28 12:18:46 +08:00
xiaojunnuo
1531462d22 build: trigger build image 2025-09-28 12:18:30 +08:00
99 changed files with 2442 additions and 694 deletions

View File

@@ -3,7 +3,7 @@ on:
push:
branches: ['v2-dev']
paths:
- "trigger/build1.trigger"
- "trigger/build.trigger"
# schedule:
# - # 国际时间 19:17 执行北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间

View File

@@ -1,9 +1,9 @@
name: deploy-demo
on:
# push:
# branches: ['v2-dev']
# paths:
# - "trigger/deploy.trigger"
push:
branches: ['v2-dev']
paths:
- "trigger/deploy.trigger"
workflow_run:
workflows: [ "build-image" ]
types:
@@ -55,14 +55,3 @@ jobs:
retry-count: 3
retry-delay: 5000
- name: deploy-certd-doc
uses: tyrrrz/action-http-request@master
with:
url: http://flow-openapi.aliyun.com/pipeline/webhook/IiSxLDp9aOhgDUxJPytv
method: POST
body: |
{}
headers: |
Content-Type: application/json
retry-count: 3
retry-delay: 5000

View File

@@ -1,11 +1,11 @@
name: build-image-for-release
on:
# push:
# branches: ['v2-dev']
# paths:
# - "builder/release.trigger"
push:
branches: ['v2-dev']
paths:
- "trigger/release.trigger"
# workflow_run:
# workflows: [ "build-image" ]
# workflows: [ "deploy-demo" ]
# types:
# - completed
@@ -117,3 +117,15 @@ jobs:
# registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}}
# greper/certd-agent:latest
# greper/certd-agent:${{steps.get_certd_version.outputs.result}}
- name: deploy-certd-doc
uses: tyrrrz/action-http-request@master
with:
url: http://flow-openapi.aliyun.com/pipeline/webhook/IiSxLDp9aOhgDUxJPytv
method: POST
body: |
{}
headers: |
Content-Type: application/json
retry-count: 3
retry-delay: 5000

4
.npmrc
View File

@@ -1,2 +1,6 @@
link-workspace-packages=deep
prefer-workspace-packages=true
better_sqlite3_binary_host=https://registry.npmmirror.com/-/binary/better-sqlite3
better_sqlite3_binary_host_mirror=https://registry.npmmirror.com/-/binary/better-sqlite3
better-sqlite3_binary_host=https://registry.npmmirror.com/-/binary/better-sqlite3
better-sqlite3_binary_host_mirror=https://registry.npmmirror.com/-/binary/better-sqlite3

8
.vscode/launch.json vendored
View File

@@ -10,8 +10,8 @@
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/packages/ui/certd-client",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"runtimeExecutable": "pnpm",
"runtimeArgs": ["dev"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
@@ -20,8 +20,8 @@
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/packages/ui/certd-server",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"runtimeExecutable": "pnpm",
"runtimeArgs": ["dev"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},

View File

@@ -1,4 +1,5 @@
{
"eslint.debug": false,
"eslint.format.enable": true
"eslint.format.enable": true,
"typescript.tsc.autoDetect": "watch"
}

View File

@@ -3,6 +3,35 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
### Bug Fixes
* 修复飞牛证书部署后无法生效的bug ([bf156a1](https://github.com/certd/certd/commit/bf156a13bd443cdadb73c9dff79bbef7231b4401))
* aliyunoss 选择证书接入点选择新加坡无法上传的bug ([e00733a](https://github.com/certd/certd/commit/e00733a34644c23ffe926486b15dc96bf2fa4b57))
### Performance Improvements
* 优化start.sh脚本去掉删除非ui目录的操作及提示 ([7993a7c](https://github.com/certd/certd/commit/7993a7cdb01885535950c63187e3f67d67ba2f75))
* 增加飞牛证书id选择的提示 ([5a4d812](https://github.com/certd/certd/commit/5a4d8121462b1afe921d028465687be8c9679814))
* 证书监控支持设置证书即将过期天数 ([cd35568](https://github.com/certd/certd/commit/cd35568e042e6ab928685efad51cdbed823d2d4f))
* 支持网络测试 ([2bef608](https://github.com/certd/certd/commit/2bef608e07ceb56d52007f290667e0afef401b22))
* 支持新网代理方式 ([f612509](https://github.com/certd/certd/commit/f612509cac87b859e81a7a52fe94b2eaccad22f9))
* dns支持新网互联 ([f415190](https://github.com/certd/certd/commit/f41519048326d971acd9e0a30462231f77a299a6))
* start.sh脚本支持根据当前系统判断是否使用sudo ([567cb7d](https://github.com/certd/certd/commit/567cb7d737023e26ec58403c6f28f109e212d379))
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Bug Fixes
* 修复版本比较bug ([109696e](https://github.com/certd/certd/commit/109696e965d68c50c8627ffd40203edd1d2daea5))
* 修复某些情况下cname申请证书报错主域名不一致的bug ([2671781](https://github.com/certd/certd/commit/2671781e1bb0838981728d85eacf0e1a25a0fa48))
### Performance Improvements
* cname主域名校验提示优化显示不一致的两方便于排查问题 ([6ebb365](https://github.com/certd/certd/commit/6ebb3659f42155e4e8da600c493fb5227cd08137))
* dns解析支持阿里esa ([9291fa6](https://github.com/certd/certd/commit/9291fa68aa7a88a05c2f888bf3048df36a8fbde3))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
### Features

View File

@@ -47,6 +47,8 @@ services:
# 配置规则: certd_ + 配置项, 点号用_代替
# #↓↓↓↓ ----------------------------- 如果忘记管理员密码可以设置为truedocker compose up -d 重建容器之后管理员密码将改成123456然后请及时修改回false
- certd_system_resetAdminPasswd=false
# ↓↓↓ 要使用ipv6将此配置修改为::
- certd_koa_hostname=0.0.0.0
# 默认使用sqlite文件数据库如果需要使用其他数据库请设置以下环境变量
# 注意: 选定使用一种数据库之后,不支持更换数据库。

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Bug Fixes
* 修复版本比较bug ([109696e](https://github.com/certd/certd/commit/109696e965d68c50c8627ffd40203edd1d2daea5))
* 修复某些情况下cname申请证书报错主域名不一致的bug ([2671781](https://github.com/certd/certd/commit/2671781e1bb0838981728d85eacf0e1a25a0fa48))
### Performance Improvements
* cname主域名校验提示优化显示不一致的两方便于排查问题 ([6ebb365](https://github.com/certd/certd/commit/6ebb3659f42155e4e8da600c493fb5227cd08137))
* dns解析支持阿里esa ([9291fa6](https://github.com/certd/certd/commit/9291fa68aa7a88a05c2f888bf3048df36a8fbde3))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
### Features
* @certd/ui-server module import报错的问题 ([0c61d4c](https://github.com/certd/certd/commit/0c61d4c9788677c83c567db5381b9e257ec90bba))
* dist打包前检查 ([8f6e5bd](https://github.com/certd/certd/commit/8f6e5bd24b3b65fbfcba36c08f532a3abad2d606))
## [1.36.25](https://github.com/certd/certd/compare/v1.36.24...v1.36.25) (2025-09-27)
### Bug Fixes

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -5,6 +5,9 @@ Certd 是一款开源、免费、全自动申请和部署更新SSL证书的工
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签、证书管理工具
![首页](../images/start/home.png)
## 1、关于证书续期
>* 实际上没有办法不改变证书文件本身情况下直接续期或者续签。
>* 我们所说的续期,其实就是按照全套流程重新申请一份新证书,然后重新部署上去。
@@ -15,7 +18,7 @@ Certd 是一款开源、免费、全自动申请和部署更新SSL证书的工
本项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。
* 全自动申请证书支持所有注册商注册的域名支持DNS-01、HTTP-01、CNAME代理等多种域名验证方式
* 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等70+部署插件)
* 全自动部署更新证书(目前支持部署到主机、阿里云、腾讯云等100+部署插件)
* 支持通配符域名/泛域名支持多个域名打到一个证书上支持pem、pfx、der、jks等多种证书格式
* 邮件通知、webhook通知、企微、钉钉、飞书、anpush等多种通知方式
* 私有化部署,数据保存本地,安装升级非常简单快捷

View File

@@ -11,9 +11,12 @@
git clone https://github.com/certd/certd --depth=1
# git checkout v1.x.x # 当v2主干分支代码无法正常启动时可以尝试此命令1.x.x换成最新版本号
cd certd
# 启动服务
./start.sh
```
>如果是windows请先安装`git for windows` ,然后右键,选择`open git bash here`打开终端,再执行`./start.sh`命令

View File

@@ -19,7 +19,7 @@
"detail": too many certificates (5) already issued for this exact set of idantifiers in the last 168hm0s
```
## ssl.com报错 CAA record does not include ssl.com which is required to issue the certificate
## 4. ssl.com报错 CAA record does not include ssl.com which is required to issue the certificate
ssl.com申请证书要求必须设置CAA记录表示允许ssl.com为该域名颁发证书
请按如下格式添加CAA记录
@@ -29,5 +29,18 @@ ssl.com申请证书要求必须设置CAA记录表示允许ssl.com为该域名
| 一级泛域名 | CAA | * | 0 | issue/issuewild | "ssl.com" |
| 固定子域名 | CAA | sub | 0 | issue |"ssl.com" |
## 5. address family not supported
启动时出现此错误是由于您的服务器不支持绑定ipv6地址
请配置环境变量 certd_koa_hostname=0.0.0.0
在docker-compose.yml中添加如下配置
```yaml
service:
certd:
environment:
certd_koa_hostname: 0.0.0.0
```

View File

@@ -7,10 +7,16 @@
https://certd.handsfree.work/
> 注意数据将不定期清理,不定期停止定时任务,生产使用请自行部署
注册 -> 创建证书流水线 -> 添加部署任务 -> 测试运行
> 注意demo的数据将不定期清理生产使用请自行部署
> 包含敏感信息,务必自己本地部署进行生产使用
![首页](../images/start/home-2.png)
## 二、私有化部署
由于证书、授权信息等属于高度敏感数据,请务必私有化部署,保障数据安全

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

View File

@@ -24,13 +24,13 @@ features:
- title: 全自动申请证书
details: 支持所有注册商注册的域名
- title: 全自动部署证书
details: 支持部署到主机、阿里云、腾讯云等,目前已支持60+部署插件
details: 支持部署到主机、阿里云、腾讯云等,目前已支持100+部署插件
- title: 多域名、泛域名打到一个证书上
details: 支持通配符域名/泛域名,支持多个域名打到一个证书上
- title: 多证书格式支持
details: 支持pem、pfx、der、jks等多种证书格式支持Google、Letsencrypt、ZeroSSL证书颁发机构
- title: 支持私有化部署
details: 授权数据加密存储,保障数据安全
- title: 多数据库支持
details: 支持SQLite、Postgresql、MySQL数据库
- title: 私有化部署,数据安全
details: 授权数据加密存储,保障数据安全支持SQLite、Postgresql、MySQL多种数据库
- title: 无痛升级
details: 有手就行,向下兼容,无需担心数据作废
---

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.37.0"
"version": "1.37.2"
}

View File

@@ -18,7 +18,7 @@
"devb": "lerna run dev-build",
"i-all": "lerna link && lerna exec npm install ",
"publish": "npm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits --create-release github && npm run afterpublishOnly && npm run commitAll",
"afterpublishOnly": "npm run copylogs && time /t >build.trigger && git add ./build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && git push",
"afterpublishOnly": "npm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && git push",
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
"commitAll": "git add . && git commit -m \"build: publish\" && git push && npm run commitPro",
"commitPro": "cd ./packages/pro/ && git add . && git commit -m \"build: publish\" && git push",
@@ -33,7 +33,8 @@
"docs:dev": "vitepress dev docs",
"docs:build": "npm run copylogs && vitepress build docs",
"docs:preview": "vitepress preview docs",
"pub": "echo 1"
"pub": "echo 1",
"dev": "pnpm run -r --parallel compile "
},
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/publishlab/node-acme-client/compare/v1.37.1...v1.37.2) (2025-10-14)
### Bug Fixes
* aliyunoss 选择证书接入点选择新加坡无法上传的bug ([e00733a](https://github.com/publishlab/node-acme-client/commit/e00733a34644c23ffe926486b15dc96bf2fa4b57))
## [1.37.1](https://github.com/publishlab/node-acme-client/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/acme-client
# [1.37.0](https://github.com/publishlab/node-acme-client/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/acme-client

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.37.0",
"version": "1.37.2",
"type": "module",
"module": "scr/index.js",
"main": "src/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.37.0",
"@certd/basic": "^1.37.2",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.7.2",
@@ -52,7 +52,8 @@
"lint-types": "tsd",
"prepublishOnly": "npm run build-docs",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"pub": "npm publish"
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"repository": {
"type": "git",
@@ -69,5 +70,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -255,7 +255,7 @@ export default async (client, userOpts) => {
await wait(waitDnsDiffuseTime * 1000)
}
log("开始向提供商请求挑战验证");
log("开始向提供商请求检查验证");
await runPromisePa(completeChallengeTasks, 1000);
} catch (e) {
log(`证书申请失败${e.message}`);

View File

@@ -502,7 +502,7 @@ class AcmeClient {
await verify[challenge.type](authz, challenge, keyAuthorization);
};
log('Waiting for ACME challenge verification等待ACME挑战验证)');
log('Waiting for ACME challenge verification等待ACME检查验证)');
return util.retry(verifyFn, this.backoffOpts);
}
@@ -570,7 +570,7 @@ class AcmeClient {
const resp = await this.api.apiRequest(item.url, null, [200]);
/* Verify status */
log(`[${d}] Item has status挑战状态): ${resp.data.status}`);
log(`[${d}] Item has status检查状态): ${resp.data.status}`);
if (invalidStates.includes(resp.data.status)) {
abort();

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
### Performance Improvements
* 支持网络测试 ([2bef608](https://github.com/certd/certd/commit/2bef608e07ceb56d52007f290667e0afef401b22))
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/basic
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/basic

View File

@@ -1 +1 @@
12:14
22:48

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.37.0",
"version": "1.37.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -13,7 +13,8 @@
"dev-build": "npm run build",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"pub": "npm publish"
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"axios": "^1.7.2",
@@ -45,5 +46,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -1,8 +1,10 @@
//转换为import
import childProcess from 'child_process';
import { safePromise } from './util.promise.js';
import { ILogger, logger } from './util.log.js';
import iconv from 'iconv-lite';
//@ts-ignore
import childProcess from "child_process";
import { safePromise } from "./util.promise.js";
import { ILogger, logger } from "./util.log.js";
//@ts-ignore
import iconv from "iconv-lite";
export type ExecOption = {
cmd: string | string[];
env: any;
@@ -11,12 +13,12 @@ export type ExecOption = {
};
async function exec(opts: ExecOption): Promise<string> {
let cmd = '';
let cmd = "";
const log = opts.logger || logger;
if (opts.cmd instanceof Array) {
for (const item of opts.cmd) {
if (cmd) {
cmd += ' && ' + item;
cmd += " && " + item;
} else {
cmd = item;
}
@@ -28,17 +30,18 @@ async function exec(opts: ExecOption): Promise<string> {
cmd,
{
env: {
//@ts-ignore
...process.env,
...opts.env,
},
...opts.options,
},
(error, stdout, stderr) => {
(error: any, stdout: { toString: (arg0: string) => any }, stderr: any) => {
if (error) {
log.error(`exec error: ${error}`);
reject(error);
} else {
const res = stdout.toString('utf-8');
const res = stdout.toString("utf-8");
log.info(`stdout: ${res}`);
resolve(res);
}
@@ -57,11 +60,12 @@ export type SpawnOption = {
};
function isWindows() {
return process.platform === 'win32';
// @ts-ignore
return process.platform === "win32";
}
function convert(buffer: any) {
if (isWindows()) {
const decoded = iconv.decode(buffer, 'GBK');
const decoded = iconv.decode(buffer, "GBK");
// 检查是否有有效字符
return decoded && decoded.trim().length > 0 ? decoded : buffer.toString();
} else {
@@ -74,12 +78,12 @@ function convert(buffer: any) {
// }
async function spawn(opts: SpawnOption): Promise<string> {
let cmd = '';
let cmd = "";
const log = opts.logger || logger;
if (opts.cmd instanceof Array) {
for (const item of opts.cmd) {
if (cmd) {
cmd += ' && ' + item;
cmd += " && " + item;
} else {
cmd = item;
}
@@ -88,37 +92,47 @@ async function spawn(opts: SpawnOption): Promise<string> {
cmd = opts.cmd;
}
log.info(`执行命令: ${cmd}`);
let stdout = '';
let stderr = '';
let stdout = "";
let stderr = "";
return safePromise((resolve, reject) => {
const ls = childProcess.spawn(cmd, {
shell: true,
env: {
//@ts-ignore
...process.env,
...opts.env,
},
...opts.options,
});
ls.stdout.on('data', data => {
ls.stdout.on("data", (data: string) => {
data = convert(data);
log.info(`stdout: ${data}`);
stdout += data;
});
ls.stderr.on('data', data => {
ls.stderr.on("data", (data: string) => {
data = convert(data);
log.warn(`stderr: ${data}`);
stderr += data;
});
ls.on('error', error => {
ls.on("error", (error: any) => {
log.error(`child process error: ${error}`);
//@ts-ignore
error.stderr = stderr;
//@ts-ignore
error.stdout = stdout;
reject(error);
});
ls.on('close', (code: number) => {
ls.on("close", (code: number) => {
if (code !== 0) {
log.error(`child process exited with code ${code}`);
reject(new Error(stderr));
const e = new Error(stderr || "return " + code);
//@ts-ignore
e.stderr = stderr;
//@ts-ignore
e.stdout = stdout;
reject(e);
} else {
resolve(stdout);
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/pipeline
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/pipeline
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/pipeline

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.37.0",
"version": "1.37.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,11 +14,12 @@
"build3": "rollup -c",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"pub": "npm publish"
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.37.0",
"@certd/plus-core": "^1.37.0",
"@certd/basic": "^1.37.2",
"@certd/plus-core": "^1.37.2",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -44,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/lib-huawei
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/lib-huawei
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/lib-huawei

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.37.0",
"version": "1.37.2",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/lib-iframe
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/lib-iframe
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/lib-iframe

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.37.0",
"version": "1.37.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/jdcloud
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/jdcloud
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/jdcloud

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.37.0",
"version": "1.37.2",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -61,5 +61,5 @@
"fetch"
]
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/lib-k8s
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/lib-k8s
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.37.0",
"version": "1.37.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -17,7 +17,7 @@
"pub": "npm publish"
},
"dependencies": {
"@certd/basic": "^1.37.0",
"@certd/basic": "^1.37.2",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/lib-server
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/lib-server
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.37.0",
"version": "1.37.2",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -17,7 +17,8 @@
"lint": "mwts check",
"lint:fix": "mwts fix",
"prepublish": "npm run build",
"pub": "npm publish"
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"keywords": [],
"author": "greper",
@@ -27,11 +28,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.37.0",
"@certd/basic": "^1.37.0",
"@certd/pipeline": "^1.37.0",
"@certd/plugin-lib": "^1.37.0",
"@certd/plus-core": "^1.37.0",
"@certd/acme-client": "^1.37.2",
"@certd/basic": "^1.37.2",
"@certd/pipeline": "^1.37.2",
"@certd/plugin-lib": "^1.37.2",
"@certd/plus-core": "^1.37.2",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -63,5 +64,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -3,7 +3,7 @@ import { AppKey, PlusRequestService } from '@certd/plus-core';
import { cache, http, HttpRequestConfig, logger } from '@certd/basic';
import { SysInstallInfo, SysLicenseInfo, SysSettingsService } from '../../settings/index.js';
import { merge } from 'lodash-es';
import fs from 'fs';
@Provide("plusService")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class PlusService {
@@ -85,12 +85,31 @@ export class PlusService {
async sendEmail(email: any) {
const plusRequestService = await this.getPlusRequestService();
let attachments = email.attachments || [];
if (attachments.length > 0) {
const newAttachments: any[] = [];
attachments.forEach((item: any) => {
const name = item.filename || item.path.split('/').pop();
const body = item.content || fs.readFileSync(item.path);
const bodyBase64 = Buffer.from(body).toString('base64');
item = {
name,
body: bodyBase64,
};
newAttachments.push(item);
});
attachments = newAttachments;
}
await plusRequestService.request({
url: '/activation/emailSend',
data: {
subject: email.subject,
text: email.content,
to: email.receivers,
text: email.content,
html: email.html,
attachments,
},
});
}

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
**Note:** Version bump only for package @certd/midway-flyway-js
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.37.0",
"version": "1.37.2",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
### Performance Improvements
* 证书监控支持设置证书即将过期天数 ([cd35568](https://github.com/certd/certd/commit/cd35568e042e6ab928685efad51cdbed823d2d4f))
* 支持新网代理方式 ([f612509](https://github.com/certd/certd/commit/f612509cac87b859e81a7a52fe94b2eaccad22f9))
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Performance Improvements
* cname主域名校验提示优化显示不一致的两方便于排查问题 ([6ebb365](https://github.com/certd/certd/commit/6ebb3659f42155e4e8da600c493fb5227cd08137))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/plugin-cert

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.37.0",
"version": "1.37.2",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,13 +13,14 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"pub": "npm publish"
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/acme-client": "^1.37.0",
"@certd/basic": "^1.37.0",
"@certd/pipeline": "^1.37.0",
"@certd/plugin-lib": "^1.37.0",
"@certd/acme-client": "^1.37.2",
"@certd/basic": "^1.37.2",
"@certd/pipeline": "^1.37.2",
"@certd/plugin-lib": "^1.37.2",
"@google-cloud/publicca": "^1.3.0",
"dayjs": "^1.11.7",
"jszip": "^3.10.1",
@@ -31,7 +32,6 @@
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/mocha": "^10.0.0",
"@types/psl": "^1.1.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
@@ -43,5 +43,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -117,11 +117,11 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
],
},
required: true,
helper: `1. <b>DNS直接验证</b>域名dns解析是在阿里云/腾讯云/华为云/CF/NameSilo/西数/火山/dns.la/京东云/51dns的选它
2. <b>CNAME代理验证</b>:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)建议将DNS服务器修改为阿里云/腾讯云的然后使用DNS直接验证
helper: `1. <b>DNS直接验证</b>域名dns解析已被本系统支持时即下方DNS解析服务商选项中可选推荐选择此方式
2. <b>CNAME代理验证</b>:支持任何注册商的域名,第一次需要手动添加[CNAME记录](#/certd/cname/record)如果经常申请失败,建议将DNS服务器修改为阿里云/腾讯云的然后使用DNS直接验证
3. <b>HTTP文件验证</b>:不支持泛域名,需要配置网站文件上传
4. <b>多DNS提供商</b>每个域名可以选择独立的DNS提供商
5. <b>自动匹配</b>:需要在[域名管理](#/certd/cert/domain)中事先配置好校验方式
5. <b>自动匹配</b>此处无需选择校验方式,需要在[域名管理](#/certd/cert/domain)中提前配置好校验方式
`,
})
challengeType!: string;
@@ -133,9 +133,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "icon-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt", icon: "simple-icons:letsencrypt" },
{ value: "google", label: "Google", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL", icon: "emojione:digit-zero" },
{ value: "letsencrypt", label: "Let's Encrypt(免费,新手推荐)", icon: "simple-icons:letsencrypt" },
{ value: "google", label: "Google(免费)", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL(免费)", icon: "emojione:digit-zero" },
{ value: "sslcom", label: "SSL.com仅主域名和www免费", icon: "la:expeditedssl" },
],
},
@@ -635,8 +635,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}
// 主域名异常
if (cnameRecord.mainDomain !== mainDomain) {
throw new Error(`CNAME记录${domain}的域名与配置的主域名不一致请确认是否在流水线创建之后修改了子域名托管您需要重新校验CNAME记录的校验状态`);
if (cnameRecord.mainDomain && mainDomain && cnameRecord.mainDomain !== mainDomain) {
throw new Error(`CNAME记录${domain}的域名与配置的主域名不一致${cnameRecord.mainDomain}${mainDomain}请确认是否在流水线创建之后修改了子域名托管您需要重新校验CNAME记录的校验状态`);
}
let dnsProvider = cnameRecord.commonDnsProvider;

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
**Note:** Version bump only for package @certd/plugin-lib
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Performance Improvements
* dns解析支持阿里esa ([9291fa6](https://github.com/certd/certd/commit/9291fa68aa7a88a05c2f888bf3048df36a8fbde3))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/plugin-lib

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.37.0",
"version": "1.37.2",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,7 +13,8 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"pub": "npm publish"
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@alicloud/openapi-client": "^0.4.14",
@@ -21,8 +22,8 @@
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.10",
"@aws-sdk/client-s3": "^3.787.0",
"@certd/basic": "^1.37.0",
"@certd/pipeline": "^1.37.0",
"@certd/basic": "^1.37.2",
"@certd/pipeline": "^1.37.2",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -41,7 +42,6 @@
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/mocha": "^10.0.0",
"@types/psl": "^1.1.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
@@ -53,5 +53,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "6b43007c44e1b8ccdb87081b0cf3f99fc2c09d53"
"gitHead": "c725cee0445dbe1ebd1b6588373bde31697113da"
}

View File

@@ -0,0 +1,45 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
@IsAccess({
name: "aliesa",
title: "阿里云ESA授权",
desc: "",
icon: "ant-design:aliyun-outlined",
order: 0,
})
export class AliesaAccess extends BaseAccess {
@AccessInput({
title: "阿里云授权",
component: {
name: "access-selector",
vModel: "modelValue",
type: "aliyun",
},
helper: "请选择阿里云授权",
required: true,
})
accessId = "";
@AccessInput({
title: "地区",
component: {
name: "a-select",
vModel: "value",
options: [
{
label: "杭州",
value: "cn-hangzhou",
},
{
label: "新加坡",
value: "ap-southeast-1",
},
],
},
helper: "请选择ESA地区",
required: true,
})
region = "";
}
new AliesaAccess();

View File

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

View File

@@ -0,0 +1,2 @@
#登录与权限开启
VITE_APP_PM_ENABLED=false

View File

@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
### Performance Improvements
* 证书监控支持设置证书即将过期天数 ([cd35568](https://github.com/certd/certd/commit/cd35568e042e6ab928685efad51cdbed823d2d4f))
* 支持网络测试 ([2bef608](https://github.com/certd/certd/commit/2bef608e07ceb56d52007f290667e0afef401b22))
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Bug Fixes
* 修复版本比较bug ([109696e](https://github.com/certd/certd/commit/109696e965d68c50c8627ffd40203edd1d2daea5))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
**Note:** Version bump only for package @certd/ui-client

View File

@@ -1,11 +1,12 @@
{
"name": "@certd/ui-client",
"version": "1.37.0",
"version": "1.37.2",
"private": true,
"scripts": {
"dev": "vite --open",
"dev:pm": "vite --mode pm",
"dev:force": "vite --force",
"remote": "vite --mode remote --open",
"debug": "vite --mode debug --open",
"debug:pm": "vite --mode debugpm",
"debug:force": "vite --force --mode debug",
@@ -105,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.37.0",
"@certd/pipeline": "^1.37.0",
"@certd/lib-iframe": "^1.37.2",
"@certd/pipeline": "^1.37.2",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -175,6 +175,7 @@ export default {
suiteSetting: "Suite Settings",
orderManager: "Order Management",
userSuites: "User Suites",
netTest: "Network Test",
},
certificateRepo: {
title: "Certificate Repository",
@@ -280,6 +281,8 @@ export default {
cronTrigger: "Scheduled trigger for monitoring",
dnsServer: "DNS Server",
dnsServerHelper: "Use a custom domain name resolution server, such as: 1.1.1.1 , support multiple",
certValidDays: "Certificate Valid Days",
certValidDaysHelper: "Number of days before expiration to send a notification",
},
},
checkStatus: {
@@ -447,6 +450,7 @@ export default {
description: "Description",
createTime: "Creation Time",
updateTime: "Update Time",
mainDomain: "Main Domain",
edit: "Edit",
groupName: "Group Name",
enterGroupName: "Please enter group name",

View File

@@ -181,6 +181,7 @@ export default {
suiteSetting: "套餐设置",
orderManager: "订单管理",
userSuites: "用户套餐",
netTest: "网络测试",
},
certificateRepo: {
title: "证书仓库",
@@ -285,6 +286,8 @@ export default {
cronTrigger: "定时触发监控",
dnsServer: "DNS服务器",
dnsServerHelper: "使用自定义的域名解析服务器1.1.1.1 , 支持多个",
certValidDays: "证书到期前天数",
certValidDaysHelper: "证书到期前多少天发送通知",
},
},
checkStatus: {
@@ -453,6 +456,7 @@ export default {
description: "说明",
createTime: "创建时间",
updateTime: "更新时间",
mainDomain: "主域名",
edit: "编辑",
groupName: "分组名称",
enterGroupName: "请输入分组名称",

View File

@@ -14,7 +14,7 @@ import { usePreferences } from "/@/vben/preferences";
import { LocalStorage } from "/@/utils/util.storage";
import { FsEditorCode } from "@fast-crud/editor-code";
import "@fast-crud/editor-code/dist/style.css"
import "@fast-crud/editor-code/dist/style.css";
class ColumnSizeSaver {
save: (key: string, size: number) => void;

View File

@@ -249,6 +249,17 @@ export const sysResources = [
},
],
},
{
title: "certd.sysResources.netTest",
name: "NetTest",
path: "/sys/nettest",
component: "/sys/nettest/index.vue",
meta: {
icon: "ion:build-outline",
auth: true,
keepAlive: true,
},
},
],
},
];

View File

@@ -19,6 +19,10 @@ div#app {
height: 100%;
}
pre.pre{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
h1,
h2,
h3,

View File

@@ -54,8 +54,8 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
);
},
{
inheritAttrs: false,
name: "VbenParentModal",
inheritAttrs: false,
}
);
return [Modal, extendedApi as ExtendedModalApi] as const;
@@ -104,8 +104,8 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
);
},
{
inheritAttrs: false,
name: "VbenModal",
inheritAttrs: false,
}
);
injectData.extendApi?.(extendedApi);

View File

@@ -57,4 +57,3 @@ export async function DeleteBatch(ids: any[]) {
data: { ids },
});
}

View File

@@ -189,7 +189,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
column: {
width: 120,
align: "center",
align: "left",
cellRender({ value, row }) {
async function resetStatus() {
Modal.confirm({
@@ -202,7 +202,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
}
return (
<div class={"flex flex-center"}>
<div class={"flex flex-left"}>
<fs-values-format modelValue={value} dict={dictRef}></fs-values-format>
{row.error && (
<a-tooltip title={row.error}>
@@ -268,8 +268,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
mainDomain: {
title: t("certd.mainDomain"),
type: "text",
form: {
show: false,
},
},
createTime: {
title: t("certd.create_time"),
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
@@ -281,7 +288,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
updateTime: {
title: t("certd.update_time"),
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,

View File

@@ -2,6 +2,7 @@
import { useI18n } from "/src/locales";
import { AddReq, ColumnCompositionProps, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import * as settingApi from "./setting/api";
import dayjs from "dayjs";
import { Modal, notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings";
@@ -9,6 +10,7 @@ import { mySuiteApi } from "/@/views/certd/suite/mine/api";
import { mitter } from "/@/utils/util.mitt";
import { useSiteIpMonitor } from "./ip/use";
import { useSiteImport } from "/@/views/certd/monitor/site/use";
import { ref } from "vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -47,6 +49,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const { openSiteIpMonitorDialog } = useSiteIpMonitor();
const { openSiteImportDialog } = useSiteImport();
const certValidDaysRef = ref(10);
async function loadSetting() {
const setting = await settingApi.SiteMonitorSettingsGet();
certValidDaysRef.value = setting?.certValidDays || 10;
}
loadSetting();
function checkAll() {
Modal.confirm({
title: t("certd.monitor.confirmTitle"), // "确认"
@@ -385,10 +395,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
column: {
conditionalRender: false,
cellRender({ row }) {
const {
certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row || {};
const certValidDays = certValidDaysRef.value;
const { certEffectiveTime: effectiveTime, certExpiresTime: expiresTime } = row || {};
if (!expiresTime) {
return "-";
}
@@ -400,7 +408,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const effectiveDays = Math.max(90, dayjs(expiresTime).diff(applyDate, "day"));
// 距离失效时间剩余天数
const leftDays = dayjs(expiresTime).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d";
const color = leftDays < certValidDays ? "red" : "#389e0d";
const percent = (leftDays / effectiveDays) * 100;
// console.log('cellRender', 'effectiveDays', effectiveDays, 'expiresTime', expiresTime, 'applyTime', applyTime, 'percent', percent, row)
return <a-progress title={expireDate + t("certd.monitor.expired")} percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}${t("certd.monitor.days")}`} />;

View File

@@ -6,6 +6,7 @@ export type UserSiteMonitorSetting = {
retryTimes?: number;
cron?: string;
dnsServer?: string[];
certValidDays?: number;
};
export async function SiteMonitorSettingsGet() {

View File

@@ -17,6 +17,12 @@
</div>
<div class="helper">{{ t("certd.monitor.setting.monitorRetryTimes") }}</div>
</a-form-item>
<a-form-item :label="t('certd.monitor.setting.certValidDays')" :name="['certValidDays']">
<div class="flex">
<a-input-number v-model:value="formState.certValidDays" />
</div>
<div class="helper">{{ t("certd.monitor.setting.certValidDaysHelper") }}</div>
</a-form-item>
<a-form-item :label="t('certd.monitor.setting.dnsServer')" :name="['dnsServer']">
<div class="flex">
<a-select v-model:value="formState.dnsServer" mode="tags" :open="false" />

View File

@@ -366,10 +366,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
},
column: {
cellRender({ row }) {
const {
certEffectiveTime: effectiveTime,
certExpiresTime: expiresTime,
} = row?.lastVars || {};
const { certEffectiveTime: effectiveTime, certExpiresTime: expiresTime } = row?.lastVars || {};
if (!expiresTime) {
return "-";
}

View File

@@ -177,6 +177,8 @@ function isNewVersion(version: string, latestVersion: string) {
for (let i = 0; i < current.length; i++) {
if (parseInt(latest[i]) > parseInt(current[i])) {
return true;
} else if (parseInt(latest[i]) < parseInt(current[i])) {
return false;
}
}
return false;
@@ -191,7 +193,6 @@ async function loadLatestVersion() {
const minVersion = settingsStore.productInfo?.app?.minVersion;
if (minVersion) {
//
if (isNewVersion(version.value, minVersion)) {
notification.error({
message: settingsStore.productInfo?.app?.minVersionTip ?? "版本过低,为了您的数据安全,请尽快升级",

View File

@@ -0,0 +1,288 @@
<template>
<div class="domain-test-card">
<div class="card-header flex flex-wrap justify-start">
<div v-if="title">{{ title }}</div>
<a-form v-if="editing" layout="inline" :model="formData">
<a-form-item label="域名">
<a-input v-model:value="formData.domain" placeholder="请输入要测试的域名或IP" style="width: 240px" />
</a-form-item>
<a-form-item label="端口">
<a-input-number v-model:value="formData.port" placeholder="请输入端口" :min="1" :max="65535" style="width: 120px" />
</a-form-item>
</a-form>
<div v-else class="domain-info">
<span>域名: {{ formData.domain }}</span>
<span>端口: {{ formData.port }}</span>
</div>
<a-button :disabled="!formData.domain" size="small" type="primary" :loading="loading" @click="runAllTests"> 开始测试 </a-button>
</div>
<div class="card-content">
<div class="test-results">
<!-- 域名解析结果 -->
<test-case ref="domainResolveRef" title="域名解析" :test-method="() => createDomainResolveMethod()" :disabled="!getCurrentDomain()" />
<!-- Ping测试结果 -->
<test-case ref="pingTestRef" title="Ping测试" :test-method="() => createPingTestMethod()" :disabled="!getCurrentDomain()" />
<!-- Telnet测试结果 -->
<test-case ref="telnetTestRef" title="Telnet测试" :port="getCurrentPort()" :test-method="() => createTelnetTestMethod()" :disabled="!getCurrentDomain() || !getCurrentPort()" />
</div>
<div class="summary">
<a-alert :message="testSummary.title" :type="testSummary.status === 'success' ? 'success' : testSummary.status === 'failed' ? 'error' : 'warning'" show-icon :closable="false">
<template v-if="testSummary.text" #description>
<pre class="summary-text pre">{{ testSummary.text }}</pre>
</template>
</a-alert>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted, watch } from "vue";
import { message } from "ant-design-vue";
import { DomainResolve, PingTest, TelnetTest } from "./api";
import TestCase from "./TestCase.vue";
// 组件属性
const props = defineProps<{
title?: string;
domain?: string;
port?: number;
autoStart?: boolean;
}>();
const editing = ref(!props.domain);
// 测试组件的引用
const domainResolveRef = ref();
const pingTestRef = ref();
const telnetTestRef = ref();
// 表单数据
const formData = reactive({
domain: props.domain || "",
port: props.port || 443,
});
// 加载状态
const loading = ref(false);
// 创建域名解析测试方法
const createDomainResolveMethod = async () => {
const domain = getCurrentDomain();
return DomainResolve(domain);
};
// 创建Ping测试方法
const createPingTestMethod = async () => {
const domain = getCurrentDomain();
return PingTest(domain);
};
// 创建Telnet测试方法
const createTelnetTestMethod = async () => {
const domain = getCurrentDomain();
const port = getCurrentPort();
return TelnetTest(domain, port);
};
// 获取当前使用的域名
const getCurrentDomain = () => {
return formData.domain;
};
// 获取当前使用的端口
const getCurrentPort = () => {
return formData.port;
};
// 获取各测试用例的状态
const getTestStatus = (testRef: any) => {
const result = testRef?.getResult();
if (!result) {
return null;
}
const isNetTestResult = typeof result === "object" && result !== null && "success" in result && "message" in result;
return {
success: isNetTestResult ? result.success : false,
message: isNetTestResult ? result.message : "测试失败",
};
};
// 生成测试总结
const testSummary = computed(() => {
if (loading.value) {
return { status: "waiting", title: "测试中请稍后..." };
}
// 通过computed获取各测试结果
const domainResolveResult = getTestStatus(domainResolveRef.value);
const pingTestResult = getTestStatus(pingTestRef.value);
const telnetTestResult = getTestStatus(telnetTestRef.value);
// 检查是否有测试结果
const testDone = domainResolveResult != null && pingTestResult != null && telnetTestResult != null;
if (!testDone) {
return { status: "waiting", title: '请点击"开始测试"按钮进行网络测试' };
}
// 详细分析不同的测试结果组合
// 1. 三个测试都失败
if (domainResolveResult?.success === false && pingTestResult?.success === false && telnetTestResult?.success === false) {
return {
status: "failed",
title: "所有测试均未通过",
text: `这表明应用容器内的网络可能完全不通。建议:\n1. 检查宿主机的网络连接状态\n2. 确认容器网络配置是否正确\n3. 检查防火墙设置是否阻止了网络访问`,
};
}
// 2. 域名解析成功但Ping不通
if (domainResolveResult?.success === true && pingTestResult?.success === false) {
return {
status: "partial",
title: "域名解析成功但Ping不通",
text: `可能原因:\n1. DNS被劫持解析到了错误的IP地址\n2. 目标服务器禁止了Ping请求\n3. 目标服务器IP被墙\n4. 目标服务器网络不通或已下线`,
};
}
// 3. 域名解析和Ping都成功但Telnet连接失败
if (domainResolveResult?.success === true && pingTestResult?.success === true && telnetTestResult?.success === false) {
return {
status: "partial",
title: "域名解析和Ping测试均通过但Telnet连接失败",
text: `可能原因:\n1. 端口号输入错误,请确认目标服务使用的正确端口\n2. 目标服务器上该端口未开放或服务未启动\n3. 防火墙或安全组限制了该端口的访问\n4. 目标网站被墙`,
};
}
// 4. 域名解析失败,但其他测试可能成功或未执行
if (domainResolveResult?.success === false) {
return {
status: "partial",
title: "域名解析失败",
text: `可能原因:\n1. 域名输入错误或不存在\n2. DNS服务器配置问题\n3. 本地网络DNS解析故障\n4. 域名已过期或被注销`,
};
}
// 5. 所有测试都成功
if (domainResolveResult?.success === true && pingTestResult?.success === true && telnetTestResult?.success === true) {
return {
status: "success",
title: "所有测试均通过",
text: `域名${formData.domain}解析正常能够正常Ping通且端口${formData.port}可访问。`,
};
}
// 6. 其他部分成功的情况
return {
status: "partial",
title: "部分测试未通过",
text: `请结合具体测试结果进行分析:\n- 域名解析:${domainResolveResult ? (domainResolveResult.success ? "成功" : "失败") : "未执行"}\n- Ping测试${pingTestResult ? (pingTestResult.success ? "成功" : "失败") : "未执行"}\n- Telnet测试${telnetTestResult ? (telnetTestResult.success ? "成功" : "失败") : "未执行"}`,
};
});
// 运行全部测试
async function runAllTests() {
const domain = getCurrentDomain();
// 检查是否有域名
if (!domain) {
message.error("请输入域名");
return;
}
loading.value = true;
// 通过组件引用调用测试方法
try {
await Promise.allSettled([domainResolveRef.value?.test(), pingTestRef.value?.test(), telnetTestRef.value?.test()]);
} catch (error) {
message.error("部分测试执行失败请查看详细结果");
} finally {
loading.value = false;
}
}
onMounted(() => {
if (props.autoStart) {
runAllTests();
}
});
</script>
<style lang="less">
.domain-test-card {
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
background-color: #fff;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.card-content {
padding: 16px;
}
.input-form {
margin-bottom: 12px;
padding: 12px;
background-color: #fafafa;
border-radius: 4px;
}
.domain-info {
padding: 5.5px 12px;
background-color: #f0f0f0;
border-radius: 4px;
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
}
.test-buttons {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.test-results {
margin-top: 0px;
}
.summary {
margin-top: 16px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 4px;
.summary-text {
}
}
/* 调整按钮大小 */
.ant-btn {
font-size: 12px;
padding: 2px 8px;
height: 24px;
}
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<a-card title="服务端信息" class="server-info-card">
<template #extra>
<a-button size="small" :loading="loading" @click="refreshServerInfo">
<template #icon>
<a-icon type="sync" :spin="loading" />
</template>
刷新
</a-button>
</template>
<div v-if="loading" class="loading">
<a-spin size="small" />
<span style="margin-left: 8px">加载中...</span>
</div>
<div v-else-if="error" class="error">
<a-alert message="获取服务器信息失败" :description="error" type="error" show-icon />
</div>
<div v-else class="server-info-grid">
<!-- 本地IP -->
<div class="info-item">
<div class="info-label">本地IP:</div>
<div v-if="serverInfo.localIP && serverInfo.localIP.length > 0" class="info-value">
<a-tag v-for="ip in serverInfo.localIP" :key="ip" type="info" color="blue">{{ ip }}</a-tag>
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
<!-- 外网IP -->
<div class="info-item">
<div class="info-label">外网IP:</div>
<div v-if="serverInfo.publicIP && serverInfo.publicIP.length > 0" class="info-value">
<a-tag v-for="ip in serverInfo.publicIP" :key="ip" type="info" color="green">{{ ip }}</a-tag>
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
<!-- DNS服务器 -->
<div class="info-item">
<div class="info-label">DNS服务器:</div>
<div v-if="serverInfo.dnsServers && serverInfo.dnsServers.length > 0" class="info-value">
<a-tag v-for="dns in serverInfo.dnsServers" :key="dns" type="info" color="cyan">{{ dns }}</a-tag>
</div>
<div v-else class="info-empty">暂无信息</div>
</div>
</div>
</a-card>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { message } from "ant-design-vue";
import { GetServerInfo } from "./api";
// 服务器信息类型
interface ServerInfo {
localIP?: string[];
publicIP?: string[];
dnsServers?: string[];
}
const loading = ref(false);
const error = ref<string | null>(null);
const serverInfo = ref<ServerInfo>({});
// 加载服务器信息
const loadServerInfo = async () => {
loading.value = true;
error.value = null;
try {
serverInfo.value = await GetServerInfo();
} catch (e) {
error.value = e instanceof Error ? e.message : String(e);
message.error("获取服务器信息失败");
} finally {
loading.value = false;
}
};
// 刷新服务器信息
const refreshServerInfo = () => {
loadServerInfo();
};
// 组件挂载时加载数据
onMounted(() => {
loadServerInfo();
});
</script>
<style lang="less">
.server-info-card {
margin-bottom: 16px;
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
}
.error {
margin: 0;
}
.server-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.info-item {
background-color: #fafafa;
border-radius: 4px;
padding: 12px;
.info-label {
font-size: 14px;
font-weight: 500;
color: #666;
margin-bottom: 8px;
}
.info-value {
font-size: 14px;
color: #333;
.ant-list-item {
padding: 4px 0;
}
}
.info-empty {
font-size: 14px;
color: #999;
font-style: italic;
}
}
}
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="test-case" :class="{ loading }">
<div class="case-header">
<span class="flex items-center">
<fs-button size="small" type="text" icon="ion:play-circle" :loading="loading" :disabled="disabled" class="test-button" @click="runTest" />
<a-tag color="blue" class="case-title">
{{ title }}
</a-tag>
<span v-if="port" class="port-info">{{ port }}</span>
</span>
<span v-if="result && isNetTestResult" class="result-status flex-1" :style="{ color: isSuccess ? 'green' : 'red' }">
<span>
{{ isSuccess ? "✓" : "✗" }}
</span>
<span class="ml-2">
{{ result.message }}
</span>
</span>
</div>
<div v-if="result" class="result-content">
<div v-if="error" class="error-message">
<span style="color: red">{{ error }}</span>
</div>
<div v-else-if="isNetTestResult">
<div v-if="resultTestLog" class="test-log">
<pre>{{ resultTestLog }}</pre>
</div>
</div>
<div v-else-if="typeof result === 'object'" class="object-result">
<pre>{{ JSON.stringify(result, null, 2) }}</pre>
</div>
<div v-else class="text-result">
<pre>{{ result }}</pre>
</div>
</div>
<div v-else class="no-result">
<p>暂无结果</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { message } from "ant-design-vue";
// 组件属性
const props = defineProps<{
title: string;
port?: number | string;
testMethod: () => Promise<any>;
disabled?: boolean;
}>();
// 内部状态
const loading = ref(false);
const result = ref<any>(null);
const error = ref<string | null>(null);
// 运行测试
const runTest = async () => {
loading.value = true;
error.value = null;
result.value = null;
try {
const testResult = await props.testMethod();
// 如果结果有 data 属性,则使用 data否则使用整个结果
result.value = testResult.data || testResult;
} catch (err: any) {
result.value = null;
error.value = err.message || "测试失败";
message.error(`${props.title} 测试失败: ${error.value}`);
} finally {
loading.value = false;
}
};
// 暴露方法给父组件
defineExpose({
test: runTest,
getResult: () => result.value,
});
// 辅助计算属性,用于模板中显示结果
const isNetTestResult = computed(() => {
return typeof result.value === "object" && result.value !== null && "success" in result.value && "message" in result.value && "testLog" in result.value;
});
const isSuccess = computed(() => {
return isNetTestResult.value && result.value.success;
});
const resultMessage = computed(() => {
return isNetTestResult.value ? result.value.message : "";
});
const resultTestLog = computed(() => {
return isNetTestResult.value ? result.value.testLog : "";
});
const resultError = computed(() => {
return isNetTestResult.value ? result.value.error : "";
});
</script>
<style lang="less" scoped>
.test-case {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
position: relative;
&:last-child {
border-bottom: none;
}
&.loading {
opacity: 0.7;
}
}
.case-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.result-status {
font-size: 14px;
color: #999;
margin-right: 10px;
}
}
.case-title {
font-weight: 500;
font-size: 14px;
}
.port-info {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
margin-right: 8px;
}
.test-button {
color: #1890ff;
font-size: 12px;
margin-right: 5px;
}
.result-content {
.error-message,
.object-result,
.text-result {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
}
pre {
margin: 0;
font-size: 12px;
line-height: 1.4;
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
white-space: pre-wrap;
word-wrap: break-word;
}
.test-log {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
}
}
.no-result {
padding: 12px 0;
text-align: center;
color: #999;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,33 @@
import { request } from "/@/api/service";
export async function DomainResolve(domain: string) {
return await request({
url: "/sys/nettest/domainResolve",
method: "post",
data: { domain },
});
}
export async function PingTest(domain: string) {
return await request({
url: "/sys/nettest/ping",
method: "post",
data: { domain },
});
}
export async function TelnetTest(domain: string, port: number) {
return await request({
url: "/sys/nettest/telnet",
method: "post",
data: { domain, port },
});
}
// 获取服务器信息包括本地IP、外网IP和DNS服务器
export async function GetServerInfo() {
return await request({
url: "/sys/nettest/serverInfo",
method: "post",
});
}

View File

@@ -0,0 +1,46 @@
<template>
<fs-page class="page-sys-nettest">
<template #header>
<div class="title">
网络测试
<span class="sub">测试您的服务器容器网络连接是否正常</span>
</div>
</template>
<div class="nettest-container">
<!-- 服务端信息 -->
<server-info-card />
<!-- 测试区域 -->
<div class="test-areas flex-wrap md:flex-nowrap">
<!-- 百度域名测试 (用于对比) -->
<domain-test-card class="test-card" :domain="'baidu.com'" :port="443" :auto-start="true" />
<!-- 用户输入域名测试 -->
<domain-test-card class="test-card" :title="'自定义域名测试'" />
</div>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import DomainTestCard from "./DomainTestCard.vue";
import ServerInfoCard from "./ServerInfoCard.vue";
</script>
<style lang="less">
.page-sys-nettest {
.nettest-container {
padding: 16px;
background-color: #fff;
}
.test-areas {
display: flex;
gap: 16px;
margin-top: 16px;
}
.test-card {
min-width: 50%;
}
}
</style>

View File

@@ -55,15 +55,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
groups: {
base: {
header: t("certd.basicInfo"),
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"]
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"],
},
content: {
header: t("certd.packageContent"),
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"]
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"],
},
price: {
header: t("certd.price"),
columns: ["durationPrices"]
columns: ["durationPrices"],
},
},
},

View File

@@ -84,6 +84,7 @@ export default ({ command, mode }) => {
host: "0.0.0.0",
port: 3008,
fs: devServerFs,
allowedHosts: ["localhost", "127.0.0.1", "yfy.docmirror.cn"],
proxy: {
// with options
"/api": {

View File

@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.37.2](https://github.com/certd/certd/compare/v1.37.1...v1.37.2) (2025-10-14)
### Bug Fixes
* 修复飞牛证书部署后无法生效的bug ([bf156a1](https://github.com/certd/certd/commit/bf156a13bd443cdadb73c9dff79bbef7231b4401))
* aliyunoss 选择证书接入点选择新加坡无法上传的bug ([e00733a](https://github.com/certd/certd/commit/e00733a34644c23ffe926486b15dc96bf2fa4b57))
### Performance Improvements
* 增加飞牛证书id选择的提示 ([5a4d812](https://github.com/certd/certd/commit/5a4d8121462b1afe921d028465687be8c9679814))
* 证书监控支持设置证书即将过期天数 ([cd35568](https://github.com/certd/certd/commit/cd35568e042e6ab928685efad51cdbed823d2d4f))
* 支持网络测试 ([2bef608](https://github.com/certd/certd/commit/2bef608e07ceb56d52007f290667e0afef401b22))
* 支持新网代理方式 ([f612509](https://github.com/certd/certd/commit/f612509cac87b859e81a7a52fe94b2eaccad22f9))
* dns支持新网互联 ([f415190](https://github.com/certd/certd/commit/f41519048326d971acd9e0a30462231f77a299a6))
## [1.37.1](https://github.com/certd/certd/compare/v1.37.0...v1.37.1) (2025-09-29)
### Bug Fixes
* 修复某些情况下cname申请证书报错主域名不一致的bug ([2671781](https://github.com/certd/certd/commit/2671781e1bb0838981728d85eacf0e1a25a0fa48))
### Performance Improvements
* dns解析支持阿里esa ([9291fa6](https://github.com/certd/certd/commit/9291fa68aa7a88a05c2f888bf3048df36a8fbde3))
# [1.37.0](https://github.com/certd/certd/compare/v1.36.25...v1.37.0) (2025-09-28)
### Features

View File

@@ -5,9 +5,8 @@ const { Bootstrap } = require('@midwayjs/bootstrap');
const DirectoryFileDetector = require('@midwayjs/core').DirectoryFileDetector;
const baseDir = process.cwd();
const pipelineDir = baseDir + './node_modules/@certd/pipeline/dist';
const customFileDetector = new DirectoryFileDetector({
loadDir: [baseDir, pipelineDir],
loadDir: [baseDir],
});
module.exports = async () => {

View File

@@ -1,12 +1,14 @@
{
"name": "@certd/ui-server",
"version": "1.37.0",
"version": "1.37.2",
"description": "fast-server base midway",
"private": true,
"type": "module",
"scripts": {
"start": "cross-env NODE_ENV=production node ./bootstrap.js",
"dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app",
"dev-start": "mwtsc --watch --run @midwayjs/mock/app",
"dc": "cd ../../../ && pnpm run dev",
"dev": "cross-env NODE_ENV=local & pnpm run dev-start",
"dev-commlocal": "cross-env NODE_ENV=dev-commlocal mwtsc --watch --run @midwayjs/mock/app",
"dev-commpro": "cross-env NODE_ENV=dev-commpro mwtsc --watch --run @midwayjs/mock/app",
"dev-pg": "cross-env NODE_ENV=dev-pg mwtsc --watch --run @midwayjs/mock/app",
@@ -43,20 +45,20 @@
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-iam": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.37.0",
"@certd/basic": "^1.37.0",
"@certd/commercial-core": "^1.37.0",
"@certd/acme-client": "^1.37.2",
"@certd/basic": "^1.37.2",
"@certd/commercial-core": "^1.37.2",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.37.0",
"@certd/lib-huawei": "^1.37.0",
"@certd/lib-k8s": "^1.37.0",
"@certd/lib-server": "^1.37.0",
"@certd/midway-flyway-js": "^1.37.0",
"@certd/pipeline": "^1.37.0",
"@certd/plugin-cert": "^1.37.0",
"@certd/plugin-lib": "^1.37.0",
"@certd/plugin-plus": "^1.37.0",
"@certd/plus-core": "^1.37.0",
"@certd/jdcloud": "^1.37.2",
"@certd/lib-huawei": "^1.37.2",
"@certd/lib-k8s": "^1.37.2",
"@certd/lib-server": "^1.37.2",
"@certd/midway-flyway-js": "^1.37.2",
"@certd/pipeline": "^1.37.2",
"@certd/plugin-cert": "^1.37.2",
"@certd/plugin-lib": "^1.37.2",
"@certd/plugin-plus": "^1.37.2",
"@certd/plus-core": "^1.37.2",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
"@koa/cors": "^5.0.0",
@@ -93,7 +95,6 @@
"jsonwebtoken": "^9.0.0",
"jszip": "^3.10.1",
"koa-send": "^5.0.1",
"kubernetes-client": "^9.0.0",
"lodash-es": "^4.17.21",
"log4js": "^6.7.1",
"lru-cache": "^11.0.1",
@@ -120,7 +121,8 @@
"svg-captcha": "^1.4.0",
"tencentcloud-sdk-nodejs": "^4.1.112",
"typeorm": "^0.3.20",
"uuid": "^10.0.0"
"uuid": "^10.0.0",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@midwayjs/mock": "3.20.11",

View File

@@ -27,7 +27,7 @@ const development = {
},
keys: 'certd',
koa: {
hostname:"::",
hostname: "::",
port: 7001,
},
https: {

View File

@@ -20,9 +20,13 @@ import * as commercial from '@certd/commercial-core';
import * as upload from '@midwayjs/upload';
import { setLogger } from '@certd/acme-client';
import {HiddenMiddleware} from "./middleware/hidden.js";
process.on('uncaughtException', error => {
console.error('未捕获的异常:', error);
// 在这里可以添加日志记录、发送错误通知等操作
if(error?.message?.includes('address family not supported')){
logger.error("您的服务器不支持监听IPV6格式的地址::),请配置环境变量: certd_koa_hostname=0.0.0.0");
}
});
@Configuration({
@@ -107,5 +111,6 @@ export class MainConfiguration {
});
logger.info('当前环境:', this.app.getEnv()); // prod
// throw new Error("address family not supported")
}
}

View File

@@ -0,0 +1,47 @@
import { BaseController } from '@certd/lib-server';
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { NetTestService } from '../../../modules/sys/nettest/nettest-service.js';
@Provide()
@Controller('/api/sys/nettest/')
export class SysNetTestController extends BaseController {
@Inject()
netTestService: NetTestService;
@Post('/domainResolve', { summary: 'sys:settings:view' })
public async domainResolve(@Body(ALL) body: { domain: string }) {
const { domain } = body;
const result = await this.netTestService.domainResolve(domain);
return this.ok(result);
}
// ping
@Post('/ping', { summary: 'sys:settings:view' })
public async ping(@Body(ALL) body: { domain: string }) {
const { domain } = body;
const result = await this.netTestService.ping(domain);
return this.ok(result);
}
// telnet
@Post('/telnet', { summary: 'sys:settings:view' })
public async telnet(@Body(ALL) body: { domain: string, port: number }) {
const { domain, port } = body;
const result = await this.netTestService.telnet(domain, port);
return this.ok(result);
}
// telnet
@Post('/serverInfo', { summary: 'sys:settings:view' })
public async serverInfo() {
const result = await this.netTestService.serverInfo();
return this.ok(result);
}
}

View File

@@ -19,6 +19,8 @@ export class AutoZPrint {
@Config('https')
httpsConfig: HttpsServerOptions;
@Config('koa')
koaConfig: any;
@Init()
async init() {
@@ -58,6 +60,7 @@ export class AutoZPrint {
httpsServer.start({
...this.httpsConfig,
app: this.app,
hostname: this.httpsConfig.hostname || this.koaConfig.hostname,
});
}
}

View File

@@ -7,6 +7,7 @@ import {logger, safePromise} from '@certd/basic';
export type HttpsServerOptions = {
enabled: boolean;
app?: Application;
hostname?: string;
port: number;
key: string;
cert: string;
@@ -58,7 +59,7 @@ export class HttpsServer {
opts.app.callback()
);
this.server = httpServer;
const hostname = '::';
let hostname = opts.hostname || '::';
// A function that runs in the context of the http server
// and reports what type of server listens on which port
function listeningReporter() {
@@ -70,7 +71,19 @@ export class HttpsServer {
httpServer.listen(opts.port, hostname, listeningReporter);
return httpServer;
} catch (e) {
logger.error('启动https服务失败', e);
if ( e.message?.includes("address family not supported")) {
hostname = "0.0.0.0"
logger.error(`${e.message},尝试监听${hostname}`, e);
try{
httpServer.listen(opts.port, hostname, listeningReporter);
return httpServer;
}catch (e) {
logger.error('启动https服务失败', e);
}
}else{
logger.error('启动https服务失败', e);
}
}
}
}

View File

@@ -37,7 +37,7 @@ type CnameCheckCacheValue = {
* 授权
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CnameRecordService extends BaseService<CnameRecordEntity> {
@InjectEntityModel(CnameRecordEntity)
repository: Repository<CnameRecordEntity>;
@@ -71,16 +71,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
*/
async add(param: any): Promise<CnameRecordEntity> {
if (!param.domain) {
throw new ValidateException('域名不能为空');
throw new ValidateException("域名不能为空");
}
if (!param.userId) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
if (param.domain.startsWith('*.')) {
if (param.domain.startsWith("*.")) {
param.domain = param.domain.substring(2);
}
param.domain = param.domain.trim()
const info = await this.getRepository().findOne({where: {domain: param.domain, userId: param.userId}});
param.domain = param.domain.trim();
const info = await this.getRepository().findOne({ where: { domain: param.domain, userId: param.userId } });
if (info) {
return info;
}
@@ -90,28 +90,28 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//获取默认的cnameProviderId
cnameProvider = await this.cnameProviderService.getByPriority();
if (cnameProvider == null) {
throw new ValidateException('找不到CNAME服务请先前往“系统管理->CNAME服务设置”添加CNAME服务');
throw new ValidateException("找不到CNAME服务请先前往“系统管理->CNAME服务设置”添加CNAME服务");
}
} else {
cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
}
await this.cnameProviderChanged(param.userId, param, cnameProvider);
param.status = 'cname';
const {id} = await super.add(param);
param.status = "cname";
const { id } = await super.add(param);
return await this.info(id);
}
private async cnameProviderChanged(userId: number, param: any, cnameProvider: CnameProviderEntity) {
param.cnameProviderId = cnameProvider.id;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService);
const domainParser = new DomainParser(subDomainGetter);
const realDomain = await domainParser.parse(param.domain);
const prefix = param.domain.replace(realDomain, '');
const prefix = param.domain.replace(realDomain, "");
let hostRecord = `_acme-challenge.${prefix}`;
if (hostRecord.endsWith('.')) {
if (hostRecord.endsWith(".")) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
}
param.hostRecord = hostRecord;
@@ -119,33 +119,33 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
const randomKey = utils.id.simpleNanoId(6).toLowerCase();
const userIdHex = utils.hash.toHex(userId)
let userKeyHash = ""
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo)
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10)
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`)
const userIdHex = utils.hash.toHex(userId);
let userKeyHash = "";
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
userKeyHash = `${installInfo.siteId}_${userIdHex}_${randomKey}`;
userKeyHash = utils.hash.md5(userKeyHash).substring(0, 10);
logger.info(`userKeyHash:${userKeyHash},subjectId:${installInfo.siteId},randomKey:${randomKey},userIdHex:${userIdHex}`);
const cnameKey = `${userKeyHash}-${userIdHex}-${randomKey}`;
const safeDomain = param.domain.replaceAll('.', '-');
const safeDomain = param.domain.replaceAll(".", "-");
param.recordValue = `${safeDomain}.${cnameKey}.${cnameProvider.domain}`;
}
async update(param: any) {
if (!param.id) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
const old = await this.info(param.id);
if (!old) {
throw new ValidateException('数据不存在');
throw new ValidateException("数据不存在");
}
if (param.domain && old.domain !== param.domain) {
throw new ValidateException('域名不允许修改');
throw new ValidateException("域名不允许修改");
}
if (param.cnameProviderId && old.cnameProviderId !== param.cnameProviderId) {
const cnameProvider = await this.cnameProviderService.info(param.cnameProviderId);
await this.cnameProviderChanged(old.userId, param, cnameProvider);
param.status = 'cname';
param.status = "cname";
}
return await super.update(param);
}
@@ -170,7 +170,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
} else {
record.commonDnsProvider = new CommonDnsProvider({
config: record.cnameProvider,
plusService: this.plusService,
plusService: this.plusService
});
}
@@ -179,15 +179,15 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
async getByDomain(domain: string, userId: number, createOnNotFound = true) {
if (!domain) {
throw new ValidateException('domain不能为空');
throw new ValidateException("domain不能为空");
}
if (userId == null) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
let record = await this.getRepository().findOne({where: {domain, userId}});
let record = await this.getRepository().findOne({ where: { domain, userId } });
if (record == null) {
if (createOnNotFound) {
record = await this.add({domain, userId});
record = await this.add({ domain, userId });
} else {
throw new ValidateException(`找不到${domain}的CNAME记录`);
}
@@ -203,22 +203,34 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
return {
...record,
cnameProvider: {
...provider,
} as CnameProvider,
...provider
} as CnameProvider
} as CnameRecord;
}
private async fillMainDomain(record: CnameRecordEntity) {
if (!record.mainDomain) {
async fillMainDomain(record: CnameRecordEntity, update = true) {
const notMainDomain = !record.mainDomain;
const hasErrorMainDomain = record.mainDomain && !record.mainDomain.includes(".");
if (notMainDomain || hasErrorMainDomain) {
let domainPrefix = record.hostRecord.replace("_acme-challenge", "");
if (domainPrefix.startsWith(".")) {
domainPrefix = domainPrefix.substring(1);
}
record.mainDomain = record.domain.replace(domainPrefix+".", "");
await this.update({
id: record.id,
mainDomain: record.mainDomain
});
if (domainPrefix) {
const prefixStr = domainPrefix + ".";
record.mainDomain = record.domain.substring(prefixStr.length);
}else{
record.mainDomain = record.domain;
}
if (update) {
await this.update({
id: record.id,
mainDomain: record.mainDomain
});
}
}
}
@@ -231,11 +243,13 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (!bean) {
throw new ValidateException(`CnameRecord:${id} 不存在`);
}
if (bean.status === 'valid') {
if (bean.status === "valid") {
return true;
}
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService)
await this.getByDomain(bean.domain, bean.userId);
const subDomainGetter = new SubDomainsGetter(bean.userId, this.subDomainService);
const domainParser = new DomainParser(subDomainGetter);
const cacheKey = `cname.record.verify.${bean.id}`;
@@ -245,7 +259,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
value = {
validating: false,
pass: false,
startTime: new Date().getTime(),
startTime: new Date().getTime()
};
}
let ttl = 5 * 60 * 1000;
@@ -267,16 +281,16 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
//公共CNAME
return new CommonDnsProvider({
config: cnameProvider,
plusService: this.plusService,
plusService: this.plusService
});
}
const serviceGetter = this.taskServiceBuilder.create({userId:cnameProvider.userId})
const serviceGetter = this.taskServiceBuilder.create({ userId: cnameProvider.userId });
const access = await this.accessService.getById(cnameProvider.accessId, cnameProvider.userId);
const context = {access, logger, http, utils, domainParser,serviceGetter};
const context = { access, logger, http, utils, domainParser, serviceGetter };
const dnsProvider: IDnsProvider = await createDnsProvider({
dnsProviderType: cnameProvider.dnsProviderType,
context,
context
});
return dnsProvider;
};
@@ -284,15 +298,15 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
const clearVerifyRecord = async () => {
cache.delete(cacheKey);
try {
let dnsProvider = value.dnsProvider
let dnsProvider = value.dnsProvider;
if (!dnsProvider) {
dnsProvider = await buildDnsProvider();
}
await dnsProvider.removeRecord({
recordReq: value.recordReq,
recordRes: value.recordRes,
recordRes: value.recordRes
});
logger.info('删除CNAME的校验DNS记录成功');
logger.info("删除CNAME的校验DNS记录成功");
} catch (e) {
logger.error(`删除CNAME的校验DNS记录失败 ${e.message}req:${JSON.stringify(value.recordReq)}recordRes:${JSON.stringify(value.recordRes)}`, e);
}
@@ -305,8 +319,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (value.startTime + ttl < new Date().getTime()) {
logger.warn(`cname验证超时,停止检查,${bean.domain} ${testRecordValue}`);
clearInterval(value.intervalId);
await this.updateStatus(bean.id, 'timeout');
await clearVerifyRecord()
await this.updateStatus(bean.id, "timeout");
await clearVerifyRecord();
return false;
}
@@ -317,7 +331,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`检查CNAME配置 ${fullDomain} ${testRecordValue}`);
//检查是否有重复的acme配置
await this.checkRepeatAcmeChallengeRecords(fullDomain,bean.recordValue)
await this.checkRepeatAcmeChallengeRecords(fullDomain, bean.recordValue);
// const txtRecords = await dns.promises.resolveTxt(fullDomain);
// if (txtRecords.length) {
@@ -334,9 +348,9 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (success) {
clearInterval(value.intervalId);
logger.info(`检测到CNAME配置,修改状态 ${fullDomain} ${testRecordValue}`);
await this.updateStatus(bean.id, 'valid', "");
await this.updateStatus(bean.id, "valid", "");
value.pass = true;
await clearVerifyRecord()
await clearVerifyRecord();
return success;
}
};
@@ -347,88 +361,88 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
cache.set(cacheKey, value, {
ttl: ttl,
ttl: ttl
});
const domain = await domainParser.parse(bean.recordValue);
const fullRecord = bean.recordValue;
const hostRecord = fullRecord.replace(`.${domain}`, '');
const hostRecord = fullRecord.replace(`.${domain}`, "");
const req = {
domain: domain,
fullRecord: fullRecord,
hostRecord: hostRecord,
type: 'TXT',
value: testRecordValue,
type: "TXT",
value: testRecordValue
};
const dnsProvider = await buildDnsProvider();
if(dnsProvider.usePunyCode()){
if (dnsProvider.usePunyCode()) {
//是否需要中文转英文
req.domain = dnsProvider.punyCodeEncode(req.domain)
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord)
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord)
req.value = dnsProvider.punyCodeEncode(req.value)
req.domain = dnsProvider.punyCodeEncode(req.domain);
req.fullRecord = dnsProvider.punyCodeEncode(req.fullRecord);
req.hostRecord = dnsProvider.punyCodeEncode(req.hostRecord);
req.value = dnsProvider.punyCodeEncode(req.value);
}
const recordRes = await dnsProvider.createRecord(req);
value.dnsProvider = dnsProvider;
value.validating = true;
value.recordReq = req;
value.recordRes = recordRes;
await this.updateStatus(bean.id, 'validating', "");
await this.updateStatus(bean.id, "validating", "");
value.intervalId = setInterval(async () => {
try {
await checkRecordValue();
} catch (e) {
logger.error('检查cname出错', e);
logger.error("检查cname出错", e);
await this.updateError(bean.id, e.message);
}
}, 10000);
}
async updateStatus(id: number, status: CnameRecordStatusType, error?: string) {
const updated: any = {status}
const updated: any = { status };
if (error != null) {
updated.error = error
updated.error = error;
}
await this.getRepository().update(id, updated);
}
async updateError(id: number, error: string) {
await this.getRepository().update(id, {error});
await this.getRepository().update(id, { error });
}
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string,targetCnameDomain:string) {
async checkRepeatAcmeChallengeRecords(acmeRecordDomain: string, targetCnameDomain: string) {
let dnsResolver = null
try{
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain)
}catch (e) {
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败${e.message}`)
return
let dnsResolver = null;
try {
dnsResolver = await getAuthoritativeDnsResolver(acmeRecordDomain);
} catch (e) {
logger.error(`获取${acmeRecordDomain}的权威DNS服务器失败${e.message}`);
return;
}
let cnameRecords = []
try{
let cnameRecords = [];
try {
cnameRecords = await dnsResolver.resolveCname(acmeRecordDomain);
}catch (e) {
logger.error(`查询CNAME记录失败${e.message}`)
return
} catch (e) {
logger.error(`查询CNAME记录失败${e.message}`);
return;
}
targetCnameDomain = targetCnameDomain.toLowerCase()
targetCnameDomain = punycode.toASCII(targetCnameDomain)
targetCnameDomain = targetCnameDomain.toLowerCase();
targetCnameDomain = punycode.toASCII(targetCnameDomain);
if (cnameRecords.length > 0) {
for (const cnameRecord of cnameRecords) {
if(cnameRecord.toLowerCase() !== targetCnameDomain){
if (cnameRecord.toLowerCase() !== targetCnameDomain) {
//确保只有一个cname记录
throw new Error(`${acmeRecordDomain}存在多个CNAME记录请删除多余的CNAME记录${cnameRecord}`)
throw new Error(`${acmeRecordDomain}存在多个CNAME记录请删除多余的CNAME记录${cnameRecord}`);
}
}
}
// 确保权威服务器里面没有纯粹的TXT记录
let txtRecords = []
try{
let txtRecords = [];
try {
const txtRecordRes = await dnsResolver.resolveTxt(acmeRecordDomain);
if (txtRecordRes && txtRecordRes.length > 0) {
@@ -436,13 +450,13 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
logger.info(`TXT records: ${JSON.stringify(txtRecords)}`);
txtRecords = txtRecords.concat(...txtRecordRes);
}
}catch (e) {
logger.error(`查询Txt记录失败${e.message}`)
} catch (e) {
logger.error(`查询Txt记录失败${e.message}`);
}
if (txtRecords.length === 0) {
//如果权威服务器中查不到txt无需继续检查
return
return;
}
if (cnameRecords.length > 0) {
// 从cname记录中获取txt记录
@@ -451,7 +465,7 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
if (res.length > 0) {
for (const txtRecord of txtRecords) {
if (!res.includes(txtRecord)) {
throw new Error(`${acmeRecordDomain}存在多个TXT记录请删除多余的TXT记录:${txtRecord}`)
throw new Error(`${acmeRecordDomain}存在多个TXT记录请删除多余的TXT记录:${txtRecord}`);
}
}
}
@@ -459,10 +473,10 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
async resetStatus (id: number) {
async resetStatus(id: number) {
if (!id) {
throw new ValidateException('id不能为空');
throw new ValidateException("id不能为空");
}
await this.getRepository().update(id, {status: 'cname',mainDomain: ""});
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
}
}

View File

@@ -28,6 +28,7 @@ export class UserSiteMonitorSetting extends BaseSettings {
cron?:string = undefined;
retryTimes?:number = 3;
dnsServer?:string[] = undefined;
certValidDays?:number = 10;
}
export class UserEmailSetting extends BaseSettings {

View File

@@ -275,13 +275,12 @@ export class SiteInfoService extends BaseService<SiteInfoEntity> {
}
async sendExpiresNotify(site: SiteInfoEntity) {
const tipDays = 10;
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
const tipDays = setting?.certValidDays || 10;
const expires = site.certExpiresTime;
const validDays = dayjs(expires).diff(dayjs(), "day");
const url = await this.notificationService.getBindUrl("#/certd/monitor/site");
const setting = await this.userSettingsService.getSetting<UserSiteMonitorSetting>(site.userId, UserSiteMonitorSetting)
const content = `站点名称: ${site.name} \n站点域名 ${site.domain} \n证书域名 ${site.certDomains} \n颁发机构 ${site.certProvider} \n过期时间 ${dayjs(site.certExpiresTime).format("YYYY-MM-DD")} \n`;
if (validDays >= 0 && validDays < tipDays) {
// 发通知

View File

@@ -0,0 +1,231 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { http, logger, utils } from '@certd/basic';
// 使用@certd/basic包中已有的utils.sp.spawn函数替代自定义的asyncExec
// 该函数已经内置了Windows系统编码问题的解决方案
export type NetTestResult = {
success: boolean; //是否成功
message: string; //结果
testLog: string; //测试日志
error?: string; //执行错误信息
}
@Provide('nettestService')
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class NetTestService {
/**
* 执行Telnet测试
* @param domain 域名
* @param port 端口
* @returns 测试结果
*/
async telnet(domain: string, port: number): Promise<NetTestResult> {
try {
let command = '';
if (this.isWindows()) {
// Windows系统使用PowerShell执行测试避免输入重定向问题
// 使用PowerShell的Test-NetConnection命令进行端口测试
command = `powershell -Command "& { $result = Test-NetConnection -ComputerName ${domain} -Port ${port} -InformationLevel Quiet; if ($result) { Write-Host '端口连接成功' } else { Write-Host '端口连接失败' } }"`;
} else {
// Linux系统使用nc命令进行端口测试
command = `nc -zv -w 5 ${domain} ${port} 2>&1`;
}
// 使用utils.sp.spawn执行命令它会自动处理Windows编码问题
const output = await utils.sp.spawn({
cmd: command,
logger: undefined // 可以根据需要传入logger
});
// 判断测试是否成功
const success = this.isWindows()
? output.includes('端口连接成功')
: output.includes('Connected to');
// 处理结果
return {
success,
message: success ? '端口连接测试成功' : '端口连接测试失败',
testLog: output,
};
} catch (error) {
return {
success: false,
message: 'Telnet测试执行失败',
testLog: error.stdout || error.stderr || error?.message || String(error),
error: error.stderr || error?.message || String(error),
};
}
}
/**
* 执行Ping测试
* @param domain 域名
* @returns 测试结果
*/
async ping(domain: string): Promise<NetTestResult> {
try {
let command = '';
if (this.isWindows()) {
// Windows系统ping命令发送4个包
command = `ping -n 4 ${domain}`;
} else {
// Linux系统ping命令发送4个包
command = `ping -c 4 ${domain}`;
}
// 使用utils.sp.spawn执行命令
const output = await utils.sp.spawn({
cmd: command,
logger: undefined
});
// 判断测试是否成功
const success = this.isWindows()
? output.includes('TTL=')
: output.includes('time=');
return {
success,
message: success ? 'Ping测试成功' : 'Ping测试失败',
testLog: output,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: 'Ping测试执行失败',
testLog: error.stderr|| error.stdout || errorMessage,
error: errorMessage
};
}
}
private isWindows() {
return process.platform === 'win32';
}
/**
* 执行域名解析测试
* @param domain 域名
* @returns 解析结果
*/
async domainResolve(domain: string): Promise<NetTestResult> {
try {
let command = '';
if (this.isWindows()) {
// Windows系统使用nslookup命令
command = `nslookup ${domain}`;
} else {
// Linux系统优先使用dig命令如果没有则回退到nslookup
command = `which dig > /dev/null && dig ${domain} || nslookup ${domain}`;
}
// 使用utils.sp.spawn执行命令
const output = await utils.sp.spawn({
cmd: command,
logger: undefined
});
// 判断测试是否成功
const success = output.includes('Address:') || output.includes('IN A') || output.includes('IN AAAA') ||
(this.isWindows() && output.includes('Name:'));
return {
success,
message: success ? '域名解析测试成功' : '域名解析测试失败',
testLog: output,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: '域名解析测试执行失败',
testLog: error.stdoout || error.stderr || errorMessage,
error: errorMessage
};
}
}
async getLocalIP(): Promise<string[]> {
try {
const output = await utils.sp.spawn({
cmd: 'ip a | grep \'inet \' | grep -v \'127.0.0.1\' | awk \'{print $2}\' | cut -d/ -f1',
logger: undefined
});
// 去除 inet 前缀
let ips = output.trim().replace(/inet /g, '');
return ips.split('\n').filter(ip => ip.length > 0);
} catch (error) {
return [error instanceof Error ? error.message : String(error)];
}
}
async getPublicIP(): Promise<string[]> {
try {
const res = await http.request({
url:"https://ipinfo.io/ip",
method:"GET",
})
return[res]
} catch (error) {
return [error instanceof Error ? error.message : String(error)]
}
}
async getDNSservers(): Promise<string[]> {
let dnsServers: string[] = [];
try {
const output = await utils.sp.spawn({
cmd: 'cat /etc/resolv.conf | grep nameserver | awk \'{print $2}\'',
logger: undefined
});
dnsServers = output.trim().split('\n');
} catch (error) {
dnsServers = [error instanceof Error ? error.message : String(error)];
}
try{
/**
* /app # cat /etc/resolv.conf | grep "ExtServers"
# ExtServers: [223.5.5.5 223.6.6.6]
*/
const extDnsServers = await utils.sp.spawn({
cmd: 'cat /etc/resolv.conf | grep "ExtServers"',
logger: undefined
});
const line = extDnsServers.trim()
if (line.includes('ExtServers') && line.includes('[')) {
const extDns = line.substring(line.indexOf('[') + 1, line.indexOf(']')).split(' ');
const dnsList = extDns.map(item=>`Ext:${item}`)
dnsServers = dnsServers.concat(dnsList);
}
} catch (error) {
logger.error('获取DNS ExtServers 服务器失败', error);
// dnsServers.push(error instanceof Error ? error.message : String(error));
}
return dnsServers;
}
/**
* 获取服务器信息包括本地IP、外网IP和DNS服务器
* @returns 服务器信息
*/
async serverInfo(): Promise<any> {
const res = {
localIP: [],
publicIP: [],
dnsServers: [],
}
res.localIP = await this.getLocalIP();
res.publicIP = await this.getPublicIP();
res.dnsServers = await this.getDNSservers();
return res
}
}

View File

@@ -37,3 +37,4 @@ export * from './plugin-dokploy/index.js'
export * from './plugin-godaddy/index.js'
export * from './plugin-captcha/index.js'
export * from './plugin-xinnet/index.js'
export * from './plugin-xinnetconnet/index.js'

View File

@@ -0,0 +1,105 @@
import { IAccessService } from '@certd/pipeline';
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { AliesaAccess, AliyunAccess, AliyunClientV2 } from '@certd/plugin-lib';
@IsDnsProvider({
name: 'aliesa',
title: '阿里ESA',
desc: '阿里ESA DNS解析',
accessType: 'aliesa',
icon: 'svg:icon-aliyun',
order: 0,
})
export class AliesaDnsProvider extends AbstractDnsProvider {
client: AliyunClientV2
async onInstance() {
const access: AliesaAccess = this.ctx.access as AliesaAccess
const accessGetter = await this.ctx.serviceGetter.get("accessService") as IAccessService
const aliAccess = await accessGetter.getById(access.accessId) as AliyunAccess
const endpoint = `esa.${access.region}.aliyuncs.com`
this.client = aliAccess.getClient(endpoint)
}
async getSiteItem(domain: string) {
const ret = await this.client.doRequest({
// 接口名称
action: "ListSites",
// 接口版本
version: "2024-09-10",
// 接口协议
protocol: "HTTPS",
// 接口 HTTP 方法
method: "GET",
authType: "AK",
style: "RPC",
data: {
query: {
SiteName: domain,
// ["SiteSearchType"] = "exact";
SiteSearchType: "exact",
AccessType: "NS"
}
}
})
const list = ret.Sites
if (list?.length === 0) {
throw new Error(`阿里云ESA中不存在此域名站点:${domain}请确认域名已添加到ESA中且为NS接入方式`);
}
return list[0]
}
async createRecord(options: CreateRecordOptions): Promise<any> {
const { fullRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, domain);
const siteItem = await this.getSiteItem(domain)
const siteId = siteItem.SiteId
const res = await this.client.doRequest({
action: "CreateRecord",
version: "2024-09-10",
method: "POST",
data: {
query: {
SiteId: siteId,
RecordName: fullRecord,
Type: type,
// queries["Ttl"] = 1231311;
Ttl: 100,
Data: JSON.stringify({ Value: value }),
}
}
})
this.logger.info('添加域名解析成功:', fullRecord, value, res.RecordId);
return {
RecordId: res.RecordId,
SiteId: siteId,
}
}
async removeRecord(options: RemoveRecordOptions<any>): Promise<any> {
const record = options.recordRes;
await this.client.doRequest({
action: "DeleteRecord",
version: "2024-09-10",
data: {
query: {
RecordId: record.RecordId,
}
}
})
this.logger.info('删除域名解析成功:', record.RecordId);
}
}
new AliesaDnsProvider();

View File

@@ -1 +1,2 @@
import './aliyun-dns-provider.js';
import './aliesa-dns-provider.js';

View File

@@ -121,7 +121,7 @@ export class DeployCertToAliyunOSS extends AbstractTaskPlugin {
name: 'a-select',
options: [
{ value: 'cn-hangzhou', label: '中国大陆' },
{ value: 'southeast-1', label: '新加坡' },
{ value: 'ap-southeast-1', label: '新加坡' },
{ value: 'eu-central-1', label: '德国(法兰克福)' },
],
},

View File

@@ -6,7 +6,6 @@ import {
SshAccess,
SshClient
} from "@certd/plugin-lib";
import path from "node:path";
@IsTaskPlugin({
//命名规范,插件类型+功能就是目录plugin-demo中的demo大写字母开头驼峰命名
@@ -57,7 +56,7 @@ export class FnOSDeployToNAS extends AbstractTaskPlugin {
@TaskInput(
createRemoteSelectInputDefine({
title: "证书Id",
helper: "要更新的证书id",
helper: "面板证书请选择fnOS其他FTP、webdav等证书请选择已使用可多选如果证书域名都匹配的话",
action: FnOSDeployToNAS.prototype.onGetCertList.name
})
)
@@ -87,7 +86,9 @@ export class FnOSDeployToNAS extends AbstractTaskPlugin {
this.logger.info(`----------- 找到证书,开始部署:${item.sum},${item.domain}`)
const certPath = item.certificate;
const keyPath = item.privateKey;
const certDir = path.dirname(keyPath)
const certDir = keyPath.substring(0, keyPath.lastIndexOf("/"));
const fullchainPath = certDir+ "/fullchain.crt"
const caPath = certDir+ "/issuer_certificate.crt"
const cmd = `
sudo tee ${certPath} > /dev/null <<'EOF'
${this.cert.crt}
@@ -95,6 +96,12 @@ EOF
sudo tee ${keyPath} > /dev/null <<'EOF'
${this.cert.key}
EOF
sudo tee ${fullchainPath} > /dev/null <<'EOF'
${this.cert.crt}
EOF
sudo tee ${caPath} > /dev/null <<'EOF'
${this.cert.ic}
EOF
sudo chmod 0755 "${certDir}/" -R
@@ -157,7 +164,7 @@ echo "服务重启完成!"
}
if (!list || list.length === 0) {
throw new Error("没有找到证书,请先在证书管理也没上传一次证书");
throw new Error("没有找到证书,请先在证书管理页面上传一次证书");
}
return list
}

View File

@@ -0,0 +1,157 @@
import { IsAccess, AccessInput, BaseAccess, Pager, PageSearch } from "@certd/pipeline";
import crypto from "crypto";
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: "xinnetagent",
title: "新网授权(代理方式)",
icon: "lsicon:badge-new-filled",
desc: ""
})
export class XinnetAgentAccess extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: "代理账号",
component: {
placeholder: "代理账号agent0001"
},
required: true,
encrypt: false
})
agentCode = "";
@AccessInput({
title: "API密钥",
component: {
name: "a-input-password",
vModel: "value",
placeholder: "API密钥"
},
required: true,
encrypt: true
})
appSecret = "";
@AccessInput({
title: "测试",
component: {
name: "api-test",
action: "TestRequest"
},
helper: "点击测试接口是否正常"
})
testRequest = true;
async onTestRequest() {
// const client = new XinnetClient({
// access: this,
// logger: this.ctx.logger,
// http: this.ctx.http
// });
await this.getDomainList({ pageNo: 1, pageSize: 1 });
return "ok";
}
async getDomainList(req:PageSearch) {
const pager = new Pager(req);
const conf = {
url: "/api/domain/list",
data: {
pageNo: String(pager.pageNo),
pageSize: String(pager.pageSize)
}
}
return await this.doRequest(conf);
}
/**
* 生成 UTC 0 时区的时间戳
*/
generateTimestamp() {
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "").replaceAll("-", "");
return timestamp;
}
/**
* 字节转16进制字符串
*/
bytesToHex(bytes:any) {
return bytes.toString('hex');
}
/**
* 生成签名
*/
generateSignature(timestamp, urlPath, requestBody) {
const algorithm = 'HMAC-SHA256';
const requestMethod = 'POST';
// 构建待签名字符串
const stringToSign = `${algorithm}\n${timestamp}\n${requestMethod}\n${urlPath}\n${requestBody}`;
// 使用 HMAC-SHA256 计算签名
const hmac = crypto.createHmac('sha256', this.appSecret);
hmac.update(stringToSign);
const signatureBytes = hmac.digest();
// 转换为16进制字符串
return this.bytesToHex(signatureBytes);
}
/**
* 生成 authorization header
*/
generateAuthorization(timestamp, urlPath, requestBody) {
const signature = this.generateSignature(timestamp, urlPath, requestBody);
return `HMAC-SHA256 Access=${this.agentCode}, Signature=${signature}`;
}
/**
* 查询域名分页列表
*/
async doRequest(req:any) {
const baseURL = 'https://apiv2.xinnet.com';
const urlPath = req.url;
const requestURL = baseURL + urlPath; // 实际请求URL去掉最后的斜杠
// 请求体
const requestBody = JSON.stringify(req.data);
// 生成时间戳和授权头
const timestamp = this.generateTimestamp();
const authorization = this.generateAuthorization(timestamp, urlPath+"/", requestBody);
// 请求配置
const config = {
method: 'POST',
url: requestURL,
headers: {
'Content-Type': 'application/json',
'timestamp': timestamp,
'authorization': authorization
},
data: requestBody,
};
const res = await this.ctx.http.request(config);
if (res.code !="0"){
throw new Error(`API Error: ${res.code} ${res.requestId} - ${JSON.stringify(res.msg)}`);
}
return res.data;
}
}
new XinnetAgentAccess();

View File

@@ -0,0 +1,90 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { XinnetAgentAccess } from "./access-agent.js";
export type XinnetAgentRecord = {
recordId: number;
domainName: string;
};
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({
name: "xinnetagent",
title: "新网(代理方式)",
desc: "新网域名解析(代理方式)",
icon: "lsicon:badge-new-filled",
// 这里是对应的 cloudflare的access类型名称
accessType: "xinnetagent",
order: 7
})
export class XinnetAgentProvider extends AbstractDnsProvider<XinnetAgentRecord> {
access!: XinnetAgentAccess;
async onInstance() {
//一些初始化的操作
// 也可以通过ctx成员变量传递context
this.access = this.ctx.access as XinnetAgentAccess;
}
/**
* 创建dns解析记录用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<XinnetAgentRecord> {
/**
* fullRecord: '_acme-challenge.test.example.com',
* value: 一串uuid
* type: 'TXT',
* domain: 'example.com'
*/
const { fullRecord, value, type, domain } = options;
this.logger.info("添加域名解析:", fullRecord, value, type, domain);
/**
* /api/dns/create
* domainName 是 string 域名名称 test-xinnet-0516-ceshi.cn
recordName 是 string 记录名 test1.test-xinnet-0516-ceshi.cn如果是@和空字符只需要传域名即可
type 是 string 解析记录的类型 可选择类型如下: NS A CNAME MX TXT URL SRV AAAA A
value 是 string 解析内容 192.168.1.50
line 是 string 线路 只能传"默认"
*/
const res = await this.access.doRequest({
url:"/api/dns/create",
data:{
domainName: domain,
recordName: fullRecord,
type: type,
value: value,
line: "默认"
}
});
return {
recordId:res,
domainName: domain
};
}
/**
* 删除dns解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<XinnetAgentRecord>): Promise<void> {
const {domainName,recordId} = options.recordRes;
await this.access.doRequest({
url:"/api/dns/delete",
data:{
recordId: recordId,
domainName: domainName
}
});
}
}
//实例化这个provider将其自动注册到系统中
new XinnetAgentProvider();

View File

@@ -1,2 +1,5 @@
export * from './dns-provider.js';
export * from './access.js';
export * from './access-agent.js';
export * from './dns-provider-agent.js';

View File

@@ -0,0 +1,147 @@
import { IsAccess, AccessInput, BaseAccess } from '@certd/pipeline';
/**
* 这个注解将注册一个授权配置
* 在certd的后台管理系统中用户可以选择添加此类型的授权
*/
@IsAccess({
name: 'xinnetconnect',
title: '新网互联授权',
icon: 'lsicon:badge-new-filled',
desc: '仅支持代理账号ip需要加入白名单',
})
export class XinnetConnectAccess extends BaseAccess {
/**
* 授权属性配置
*/
@AccessInput({
title: '用户名',
component: {
placeholder: '代理用户名agent001',
help: '新网互联的代理用户名',
},
required: true,
encrypt: false,
})
username = '';
@AccessInput({
title: '密码',
component: {
name: "a-input-password",
vModel: "value",
placeholder: '密码',
},
required: true,
encrypt: true,
})
password = '';
async addDnsRecord(req: {domain:string,hostRecord:string, value:string, type:string}): Promise<any> {
const { domain,hostRecord, value, type } = req;
const bodyXml =`
<add>
<domainname>${domain}</domainname>
<resolvetype>${type}</resolvetype>
<resolvehost>${hostRecord}</resolvehost>
<resolvevalue>${value}</resolvevalue>
<mxlevel>10</mxlevel>
</add>`
const res = await this.doRequest({
url: "/addDnsRecordService",
bodyXml: bodyXml,
service: "addDnsRecord",
})
return res
}
async delDnsRecord(req: {domain:string,hostRecord:string, type:string,value:string}): Promise<any> {
const { domain,hostRecord, type,value } = req;
const bodyXml =`
<del>
<domainname>${domain}</domainname>
<resolvetype>${type}</resolvetype>
<resolvehost>${hostRecord}</resolvehost>
<resolveoldvalue>${value}</resolveoldvalue>
<mxlevel>10</mxlevel>
</del>`
const res = await this.doRequest({
url: "/delDnsRecordService",
bodyXml: bodyXml,
service: "delDnsRecord",
})
return res
}
buildUserXml(){
return `
<user>
<name>${this.username}</name>
<password>${this.password}</password>
</user>
`
}
async doRequest(req: {bodyXml:string,service:string,url:string}) {
const xml2js = await import('xml2js');
const soapRequest = `
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ws="http://ws/">
<soapenv:Header/>
<soapenv:Body>
<ws:${req.service}>
${this.buildUserXml()}
${req.bodyXml}
</ws:${req.service}>
</soapenv:Body>
</soapenv:Envelope>
`;
const response = await this.ctx.http.request({
url: req.url,
baseURL: "https://api.bizcn.com/rrpservices",
data: soapRequest,
headers: {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '' // 根据WSDLsoapAction为空
},
method: "POST",
returnOriginRes: true,
})
// 解析SOAP响应
const parser = new xml2js.Parser({ explicitArray: false });
const result = await parser.parseStringPromise(response.data);
// 提取返回结果
const soapBody = result['soap:Envelope']['soap:Body'];
const addDnsRecordResponse = soapBody["ns1:addDnsRecordResponse"];
console.log(addDnsRecordResponse)
const resultData = addDnsRecordResponse.response.result;
const res = {
code: resultData.$.code,
msg: resultData.msg
}
console.log('操作结果:', res);
if (res.code != "200") {
throw new Error(res.msg + " code:" + res.code);
}
return resultData;
}
}
new XinnetConnectAccess();

View File

@@ -0,0 +1,68 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { XinnetConnectAccess } from "./access.js";
export type XinnetConnectRecord = {
domain: string;
hostRecord: string;
type: string;
value: string;
};
// 这里通过IsDnsProvider注册一个dnsProvider
@IsDnsProvider({
name: 'xinnetconnect',
title: '新网互联',
desc: '新网互联',
icon: 'lsicon:badge-new-filled',
// 这里是对应的 cloudflare的access类型名称
accessType: 'xinnetconnect',
order:999,
})
export class XinnetConnectDnsProvider extends AbstractDnsProvider<XinnetConnectRecord> {
access!: XinnetConnectAccess;
async onInstance() {
//一些初始化的操作
// 也可以通过ctx成员变量传递context
this.access = this.ctx.access as XinnetConnectAccess;
}
/**
* 创建dns解析记录用于验证域名所有权
*/
async createRecord(options: CreateRecordOptions): Promise<XinnetConnectRecord> {
const { fullRecord,hostRecord, value, type, domain } = options;
this.logger.info('添加域名解析:', fullRecord, value, type, domain);
const recordReq = {
domain: domain,
type: 'TXT',
hostRecord: hostRecord,
value: value,
}
await this.access.addDnsRecord(recordReq)
return recordReq;
}
/**
* 删除dns解析记录,清理申请痕迹
* @param options
*/
async removeRecord(options: RemoveRecordOptions<XinnetConnectRecord>): Promise<void> {
const { fullRecord, value } = options.recordReq;
const record = options.recordRes;
this.logger.info('删除域名解析:', fullRecord, value);
if (!record) {
this.logger.info('record为空不执行删除');
return;
}
await this.access.delDnsRecord(record)
this.logger.info(`删除域名解析成功:fullRecord=${fullRecord}`);
}
}
//实例化这个provider将其自动注册到系统中
new XinnetConnectDnsProvider();

View File

@@ -0,0 +1,2 @@
export * from './dns-provider.js';
export * from './access.js';

633
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,30 @@
#
set -e
echo "即将删除packages下除ui之外的其他目录按y确认如果您没有修改过源码按y即可"
read -p "y/n: " confirm
if [ $confirm != "y" ]; then
echo "取消操作"
exit 1
# 设置SUDO命令
if [[ "$(uname -s)" == "Linux" ]]; then
SUDO_CMD="sudo"
SUDO_CMD_E="sudo -E"
else
SUDO_CMD=""
SUDO_CMD_E=""
fi
find ./packages -mindepth 1 -maxdepth 1 -type d ! -name 'ui' -exec rm -rf {} +
echo "删除成功"
# echo "即将删除packages下除ui之外的其他目录按y确认如果您没有修改过源码按y即可"
# read -p "y/n: " confirm
# if [ $confirm != "y" ]; then
# echo "取消操作"
# exit 1
# fi
# find ./packages -mindepth 1 -maxdepth 1 -type d ! -name 'ui' -exec rm -rf {} +
# echo "删除成功"
cat > pnpm-workspace.yaml << EOF
packages:
- 'packages/ui/**'
EOF
# 检查输入是否正确 循环输入
while true; do
@@ -25,30 +40,30 @@ done
echo "安装pnpm, 前提是已经安装了nodejs"
sudo npm install -g pnpm --registry https://registry.npmmirror.com
$SUDO_CMD npm install -g pnpm --registry https://registry.npmmirror.com
echo "安装依赖"
sudo pnpm install --registry https://registry.npmmirror.com
$SUDO_CMD pnpm install --registry https://registry.npmmirror.com
echo "开始构建"
echo "构建certd-client"
export NODE_OPTIONS=--max-old-space-size=32768
cd packages/ui/certd-client
sudo -E pnpm run build
$SUDO_CMD_E pnpm run build
cp -r dist/* ../certd-server/public
echo "构建certd-server"
cd ../certd-server
sudo -E pnpm run build
$SUDO_CMD_E pnpm run build
echo "构建完成"
echo "启动服务"
# 前台运行
if [ $confirmNohup != "y" ]; then
echo "当前运行模式为前台运行ctrl+c或者关闭ssh将会停止运行"
sudo pnpm run start
$SUDO_CMD pnpm run start
else
echo "当前运行模式为后台运行可以通过tail -f ./certd.log 命令查看日志"
nohup sudo pnpm run start > certd.log &
nohup $SUDO_CMD pnpm run start > certd.log &
fi

View File

@@ -1 +1 @@
09:41
20:37

View File

@@ -1 +1 @@
5
4

View File

@@ -1 +1 @@
01:50
777