Compare commits

...

76 Commits

Author SHA1 Message Date
RHQYZ
c34346cb31 bump version to v0.3.13 2025-05-21 00:20:08 +08:00
RHQYZ
7643975ef9 Merge pull request #714 from fudiwei/bugfix
bugfix
2025-05-20 23:14:06 +08:00
Fu Diwei
799ad61dcc fix: #713 2025-05-20 23:10:44 +08:00
Fu Diwei
1c3cb1b21b fix: #710 2025-05-20 23:04:34 +08:00
Fu Diwei
9d9ca88ebe fix: tsc build error 2025-05-20 23:04:28 +08:00
Fu Diwei
d81a33f24a fix: #704 2025-05-20 21:52:33 +08:00
Fu Diwei
398337826e fix: #706 2025-05-20 21:51:38 +08:00
RHQYZ
7469310fdb Merge pull request #699 from Lensual/feat-ssh-jumpserver
feat: ssh jump server support (#49) (#95)
2025-05-20 21:42:53 +08:00
RHQYZ
fe993b54f3 Merge branch 'main' into feat-ssh-jumpserver 2025-05-20 21:42:38 +08:00
RHQYZ
1ed1c62f76 Merge pull request #697 from fudiwei/main
new providers
2025-05-20 21:41:07 +08:00
Fu Diwei
524c4fd1e8 feat: new deployment provider: baotawaf console 2025-05-20 21:27:26 +08:00
Fu Diwei
591df58992 feat: new deployment provider: baotawaf site 2025-05-20 21:27:26 +08:00
Fu Diwei
4ad08d983a refactor: clean code 2025-05-20 21:27:26 +08:00
Fu Diwei
7a663d31cb feat: new deployment provider: wangsu cdn 2025-05-20 21:27:26 +08:00
Fu Diwei
e6fc92eccb feat: new deployment provider: wangsu certificate management 2025-05-20 21:27:19 +08:00
Fu Diwei
c9e6bd0c2f feat: new deployment provider: lecdn 2025-05-19 22:51:17 +08:00
神楽坂ニャン
36dd4ef3eb update: update formSchema for jump server & fix username validate message 2025-05-19 14:27:35 +08:00
Fu Diwei
a66e1c04c9 feat(ui): allow clear input when field is optional 2025-05-19 11:39:45 +08:00
Fu Diwei
3098f6a82f feat: rename certificate props 'issuer' to 'issuerOrg' 2025-05-19 10:35:05 +08:00
神楽坂ニャン
4a8eaa9ffa feat: ssh jump server support 2025-05-17 19:47:31 +08:00
Fu Diwei
3462e454d0 feat: new deployment provider: aliyun ga 2025-05-16 23:30:03 +08:00
Fu Diwei
eabd16dd71 feat: new deployment provider: flexcdn 2025-05-16 22:18:03 +08:00
Fu Diwei
122d766cab feat: new ca provider: custom acme ca 2025-05-16 21:40:40 +08:00
Fu Diwei
980d1ee0b9 feat: new deployment providers: ratpanel console & site 2025-05-16 20:15:59 +08:00
RHQYZ
e9f248d8ec Merge branch 'usual2970:main' into main 2025-05-16 19:23:03 +08:00
RHQYZ
2906576de0 Merge pull request #696 from devhaozi/main
feat: backend support for ratpanel
2025-05-16 19:22:15 +08:00
Fu Diwei
fd875feef3 feat: support 1panel v2 2025-05-16 18:57:39 +08:00
耗子
871d3ece90 fix: test case 2025-05-16 18:51:03 +08:00
耗子
8014abc836 feat: add test cases 2025-05-16 18:43:50 +08:00
Fu Diwei
0840454143 update README 2025-05-16 15:10:44 +08:00
RHQYZ
06fd95782a Merge pull request #693 from fudiwei/main
refactor
2025-05-16 15:07:21 +08:00
Fu Diwei
ef0f0f6b43 refactor: remove nikoksr/notify deps 2025-05-16 15:04:39 +08:00
Fu Diwei
b15bf8ef98 refactor: clean code 2025-05-16 13:56:45 +08:00
Fu Diwei
dd2087b101 refactor: clean code 2025-05-16 11:55:07 +08:00
RHQYZ
12d0b66c61 Merge pull request #691 from fudiwei/main
refactor
2025-05-16 11:15:38 +08:00
Fu Diwei
f3bbb9e8b2 refactor: optimize third-party sdks 2025-05-16 11:10:43 +08:00
RHQYZ
dcd646b465 Merge branch 'usual2970:main' into main 2025-05-16 02:52:51 +08:00
耗子
bd4aa4806f feat: backend support for ratpanel 2025-05-16 02:33:46 +08:00
Fu Diwei
b122bbced9 build: config goreleaser 2025-05-16 02:26:38 +08:00
Fu Diwei
5edae845cb build: config goreleaser 2025-05-16 02:03:49 +08:00
Fu Diwei
b7af3c10e4 build: config goreleaser 2025-05-16 01:58:53 +08:00
Fu Diwei
2453048288 build: config goreleaser 2025-05-16 01:50:43 +08:00
Fu Diwei
221b6a6fc6 feat(ui): improve i18n 2025-05-16 01:34:05 +08:00
Fu Diwei
851ad70a6c bump version to v0.3.12 2025-05-15 23:52:21 +08:00
Fu Diwei
1844e9a9db update README 2025-05-15 23:41:25 +08:00
RHQYZ
04e9f4e909 Merge pull request #689 from fudiwei/feat/providers
new providers
2025-05-15 23:35:48 +08:00
Fu Diwei
e6e2587bfc feat: new deployment provider: netlify site 2025-05-15 23:25:48 +08:00
Fu Diwei
70bd2f0581 feat: new acme dns-01 provider: netlify 2025-05-15 23:25:48 +08:00
Fu Diwei
9e08cfd1d1 feat: support replacing old certificate on deployment to aws acm 2025-05-15 23:25:48 +08:00
Fu Diwei
cd93a2d72c feat: new acme dns-01 provider: netcup 2025-05-15 23:25:42 +08:00
RHQYZ
11a4d4f55c Merge pull request #684 from fudiwei/main
enhance & bugfix
2025-05-15 21:16:17 +08:00
RHQYZ
f33570d514 Merge branch 'usual2970:main' into main 2025-05-15 21:15:49 +08:00
Fu Diwei
1ee3b64a19 feat: config goedge api user role 2025-05-15 21:05:22 +08:00
Fu Diwei
a3a56f3346 feat: add preset scripts for synologydsm and fnos on deployment to ssh 2025-05-15 20:52:43 +08:00
Fu Diwei
564ed8f0d3 fix: #686 2025-05-15 17:52:24 +08:00
Fu Diwei
268ec4bd7f feat(ui): browser happy detecting 2025-05-15 02:20:47 +08:00
Fu Diwei
e55e6cc512 fix(ui): tsc build error 2025-05-15 01:57:00 +08:00
Fu Diwei
6c6bb78568 feat: deploy server certificate or intermedia certificate 2025-05-15 01:40:37 +08:00
Fu Diwei
178c62512d fix(ui): fix incorrect webhook guide 2025-05-15 01:16:29 +08:00
Fu Diwei
9eaf9fd933 feat(ui): CodeInput 2025-05-15 00:58:02 +08:00
Fu Diwei
04abf9dd76 feat(ui): TextFileInput 2025-05-14 00:42:59 +08:00
Fu Diwei
355059df3c fix(ui): disallow to create access of builtin providers 2025-05-13 22:32:33 +08:00
Fu Diwei
4a6c32877f chore: github issue templates 2025-05-13 22:00:15 +08:00
Fu Diwei
258f6b5001 fix: resolve #681 2025-05-13 21:33:59 +08:00
Fu Diwei
78d9501fce fix: fix typo 2025-05-13 21:22:01 +08:00
RHQYZ
15975bb92c Merge branch 'usual2970:main' into main 2025-05-13 21:19:13 +08:00
RHQYZ
4b226d7730 Merge branch 'usual2970:main' into main 2025-05-13 01:00:07 +08:00
Fu Diwei
2ec4adf7d5 fix(ui): tsc build error 2025-05-13 00:52:05 +08:00
Fu Diwei
709684d00f fix(ui): scale origin 2025-05-13 00:43:58 +08:00
Fu Diwei
989fd1ec6d feat(ui): max content height on AccessEditModal 2025-05-13 00:39:18 +08:00
Fu Diwei
7d57c5abc0 feat(ui): show search on AccessSelect 2025-05-13 00:33:48 +08:00
Fu Diwei
0c42bb845d feat(ui): AccessProviderPicker 2025-05-13 00:28:58 +08:00
Fu Diwei
07037e8d49 feat: support replacing old certificate on deployment to gcore cdn 2025-05-12 23:19:13 +08:00
Fu Diwei
a1fc3841df chore(deps): upgrade gomod dependencies 2025-05-12 23:00:39 +08:00
Fu Diwei
1fee156457 chore(deps): upgrade npm dependencies 2025-05-12 22:12:59 +08:00
Fu Diwei
82bdccf7e7 feat(ui): move settings page entry from topbar to sider-menu 2025-05-12 21:34:00 +08:00
261 changed files with 8807 additions and 1703 deletions

View File

@@ -10,21 +10,23 @@ body:
## Welcome!
**在提交 Issue 之前,请确认以下事项**
1. 我**确认**已尝试过使用当前最新版本,并能复现问题。
1. 我**确认**已尝试过使用当前最新版本,并能复现问题。由于开发者精力有限,非当前最新版本的问题将被直接关闭,感谢理解。
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个缺陷报告。如果有多个缺陷,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release and can reproduce the issue.
1. Yes, I'm using the latest release and can reproduce the issue. Issues that are not in the latest version will be closed directly.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
5. Please limit one report per issue.
- type: input
attributes:
label: 软件版本 / Release Version
description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate.
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
placeholder: (e.g. v1.0.0)
validations:
required: true
@@ -70,8 +72,9 @@ body:
validations:
required: false
- type: markdown
- type: checkboxes
attributes:
value: |
请保持每个 Issue 只包含一个缺陷报告。
Please limit one report per issue.
label: 贡献 / Contribution
options:
- label: 我乐意为此贡献代码! / I am interested in contributing to this issue!
required: false

View File

@@ -14,12 +14,14 @@ body:
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个功能请求。如果有多个需求,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
5. Please limit one request per issue.
- type: textarea
attributes:
@@ -38,7 +40,7 @@ body:
- type: textarea
attributes:
label: 其他 / Miscellaneous
description: 在此处添加关于该 Issue 的任何其他信息。 / Add any other context about the problem here.
description: 在此处添加关于该 Issue 的任何其他信息(新增提供商请求请补充 API 文档链接等资料)。 / Add any other context about the problem here.
validations:
required: false
@@ -46,11 +48,5 @@ body:
attributes:
label: 贡献 / Contribution
options:
- label: 我乐意为此贡献代码! / I am interested in contributing to this feature!
- label: 我乐意为此贡献代码! / I am interested in contributing to this issue!
required: false
- type: markdown
attributes:
value: |
请保持每个 Issue 只包含一个功能请求。
Please limit one request per issue.

View File

@@ -3,7 +3,7 @@ description: "遇到了困难需要求助? / Have problem in use and need help
title: "简要描述你遇到的问题"
body:
- type: markdown
attributes:
attributes:
value: |
## Welcome!
@@ -12,17 +12,19 @@ body:
2. 我**确认**已搜索过[已有的 Issues](https://github.com/usual2970/certimate/issues)(包括已关闭的),没有类似的问题。
3. 我**确认**已阅读过[文档](https://docs.certimate.me/),没有类似的问题。
4. 请**务必**按照模板规范详细描述问题,否则 Issue 将会被直接关闭。
5. 请保持每个 Issue 只包含一个问题求助。如果有多个问题,请分别提交 Issue。
**Before you submit the issue, please make sure of the following checklist**:
1. Yes, I'm using the latest release.
2. Yes, I've searched for [existing issues](https://github.com/usual2970/certimate/issues) (including closed ones) on GitHub and didn't find any similar.
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
5. Please limit one question per issue.
- type: input
attributes:
label: 软件版本 / Release Version
description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate.
description: 请提供 Certimate 的具体版本(请不要填写 `latest` 之类的无效版本号)。 / Please provide the specific version of Certimate.
placeholder: (e.g. v1.0.0)
validations:
required: true
@@ -40,9 +42,3 @@ body:
description: 在此处添加关于该问题的任何其他信息。 / Add any other context about the problem here.
validations:
required: false
- type: markdown
attributes:
value: |
请保持每个 Issue 只包含一个问题求助。
Please limit one question per issue.

View File

@@ -1,4 +1,4 @@
name: Base Build
name: Release
on:
push:
@@ -22,13 +22,22 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
go-version-file: "go.mod"
- name: Build WebUI
run: npm --prefix=./ui ci && npm --prefix=./ui run build
run: |
npm --prefix=./ui ci
npm --prefix=./ui run build
npm cache clean --force
rm -rf ./ui/node_modules
- name: Check disk usage
run: |
df -h
du -sh /opt/hostedtoolcache/go/*
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v3
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest

View File

@@ -36,7 +36,7 @@ release:
archives:
- id: archive_noncgo
builds: [build_noncgo]
format: zip
format: "zip"
files:
- CHANGELOG.md
- LICENSE.md

View File

@@ -9,7 +9,7 @@
## 前提条件
- Go 1.22+ (用于修改 Go 代码)
- Go 1.24+ (用于修改 Go 代码)
- Node 20+ (用于修改 UI)
如果还没有这样做,你可以 fork Certimate 的主仓库,并克隆到本地以便进行修改:

View File

@@ -9,7 +9,7 @@ Thank you for taking the time to improve Certimate! Below is a guide for submitt
## Prerequisites
- Go 1.22+ (for Go code changes)
- Go 1.24+ (for Go code changes)
- Node 20+ (for Admin UI changes)
If you haven't done so already, you can fork the Certimate repository and clone your fork to work locally:

View File

@@ -38,8 +38,8 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- 灵活的工作流编排方式,证书从申请到部署完全自动化;
- 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法;
- 支持 PEM、PFX、JKS 等多种格式输出证书;
- 支持 20+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)
- 支持 30+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 80+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-hosting-providers)
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
- 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构;
- 更多特性等待探索。

View File

@@ -38,8 +38,8 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate
- Flexible workflow orchestration, fully automation from certificate application to deployment;
- Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC.
- Supports various certificate formats such as PEM, PFX, JKS.
- Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
- Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers));
- Supports more than 30+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
- Supports more than 80+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-hosting-providers));
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
- Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust ServicesSSL.com, ZeroSSL, and more;
- More features waiting to be discovered.

11
go.mod
View File

@@ -20,6 +20,7 @@ require (
github.com/alibabacloud-go/esa-20240910/v2 v2.32.0
github.com/alibabacloud-go/fc-20230330/v4 v4.3.5
github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12
github.com/alibabacloud-go/ga-20191120/v3 v3.1.8
github.com/alibabacloud-go/live-20161101 v1.1.1
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3
github.com/alibabacloud-go/slb-20140515/v4 v4.0.10
@@ -30,8 +31,10 @@ require (
github.com/aws/aws-sdk-go-v2/service/acm v1.32.0
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.46.1
github.com/baidubce/bce-sdk-go v0.9.226
github.com/blinkbean/dingtalk v1.1.3
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.46
github.com/go-acme/lego/v4 v4.23.1
github.com/go-lark/lark v1.16.0
github.com/go-resty/resty/v2 v2.16.5
github.com/go-viper/mapstructure/v2 v2.2.1
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.148
@@ -39,7 +42,6 @@ require (
github.com/libdns/dynv6 v1.0.0
github.com/libdns/libdns v0.2.3
github.com/luthermonson/go-proxmox v0.2.2
github.com/nikoksr/notify v1.3.0
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
github.com/pkg/sftp v1.13.9
github.com/pocketbase/dbx v1.11.0
@@ -84,13 +86,11 @@ require (
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/diskfs/go-diskfs v1.5.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-lark/lark v1.15.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
@@ -99,7 +99,6 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
@@ -126,7 +125,6 @@ require (
github.com/qiniu/x v1.10.5 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.mongodb.org/mongo-driver v1.17.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
@@ -195,12 +193,9 @@ require (
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/image v0.27.0 // indirect

15
go.sum
View File

@@ -112,6 +112,7 @@ github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+M
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.0/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.2/go.mod h1:5JHVmnHvGzR2wNdgaW1zDLQG8kOC4Uec8ubkMogW7OQ=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5/go.mod h1:kUe8JqFmoVU7lfBauaDD5taFaW7mBI+xVsyHutYtabg=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.11/go.mod h1:wHxkgZT1ClZdcwEVP/pDgYK/9HucsnCfMipmJgCz4xY=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7 h1:ASXSBga98QrGMxbIThCD6jAti09gedLfvry6yJtsoBE=
@@ -137,6 +138,8 @@ github.com/alibabacloud-go/fc-20230330/v4 v4.3.5 h1:nDNjVzGwkQPbQnAuxAmxvS9x8QGL
github.com/alibabacloud-go/fc-20230330/v4 v4.3.5/go.mod h1:vEJimQ6E/e+m2z0/oXdeQWlFw/Pi/Ar6NKcMrSvcILE=
github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12 h1:A3D8Mp6qf8DfR6Dt5MpS8aDVaWfS4N85T5CvGUvgrjM=
github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12/go.mod h1:F5c0E5UB3k8v6neTtw3FBcJ1YCNFzVoL1JPRHTe33u4=
github.com/alibabacloud-go/ga-20191120/v3 v3.1.8 h1:5GF0PXijDhxRQ3gTg9Ee/CVPtglkxuVdz4yIQgYLPgw=
github.com/alibabacloud-go/ga-20191120/v3 v3.1.8/go.mod h1:RVpR9VL4YECKoZCQijTYfPk8k52O61v6hSRekjxF0kw=
github.com/alibabacloud-go/live-20161101 v1.1.1 h1:rUGfA8RHmCMtQ5M3yMSyRde+yRXWqVecmiXBU3XrGJ8=
github.com/alibabacloud-go/live-20161101 v1.1.1/go.mod h1:g84w6qeAodT0/IHdc0tEed2a8PyhQhYl7TAj3jGl4A4=
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3 h1:LtyUVlgBEKyzWgQJurzXM6MXCt84sQr9cE5OKqYymko=
@@ -360,8 +363,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-lark/lark v1.15.1 h1:fo6PQKBJht/71N9Zn3/xjknOYx0TmdVuP+VP8NrUCsI=
github.com/go-lark/lark v1.15.1/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM=
github.com/go-lark/lark v1.16.0 h1:U6BwkLM9wrZedSM7cIiMofganr8PCvJN+M75w2lf2Gg=
github.com/go-lark/lark v1.16.0/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
@@ -400,8 +403,6 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
@@ -569,8 +570,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -679,8 +678,6 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nikoksr/notify v1.3.0 h1:UxzfxzAYGQD9a5JYLBTVx0lFMxeHCke3rPCkfWdPgLs=
github.com/nikoksr/notify v1.3.0/go.mod h1:Xor2hMmkvrCfkCKvXGbcrESez4brac2zQjhd6U2BbeM=
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI=
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8=
github.com/nrdcg/desec v0.10.0 h1:qrEDiqnsvNU9QE7lXIXi/tIHAfyaFXKxF2/8/52O8uM=
@@ -830,8 +827,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1155 h1:ildxJtjnqiKZxWDVKHT/ncIknGDijtg60MuFELON8bY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1155/go.mod h1:iLASpooTdyXtx642E5Ws7cfWENsp4/uZ/78TFoln7OI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1161 h1:yGFg9/6j3NP10r9PfSWHfekuq4SwPyqblWnfISfKANo=

View File

@@ -3,25 +3,26 @@ package applicant
import "github.com/usual2970/certimate/internal/domain"
const (
sslProviderLetsEncrypt = string(domain.CAProviderTypeLetsEncrypt)
sslProviderLetsEncryptStaging = string(domain.CAProviderTypeLetsEncryptStaging)
sslProviderBuypass = string(domain.CAProviderTypeBuypass)
sslProviderGoogleTrustServices = string(domain.CAProviderTypeGoogleTrustServices)
sslProviderSSLCom = string(domain.CAProviderTypeSSLCom)
sslProviderZeroSSL = string(domain.CAProviderTypeZeroSSL)
caLetsEncrypt = string(domain.CAProviderTypeLetsEncrypt)
caLetsEncryptStaging = string(domain.CAProviderTypeLetsEncryptStaging)
caBuypass = string(domain.CAProviderTypeBuypass)
caGoogleTrustServices = string(domain.CAProviderTypeGoogleTrustServices)
caSSLCom = string(domain.CAProviderTypeSSLCom)
caZeroSSL = string(domain.CAProviderTypeZeroSSL)
caCustom = string(domain.CAProviderTypeACMECA)
sslProviderDefault = sslProviderLetsEncrypt
caDefault = caLetsEncrypt
)
var sslProviderUrls = map[string]string{
sslProviderLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
sslProviderLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
sslProviderBuypass: "https://api.buypass.com/acme/directory",
sslProviderGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
sslProviderSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
sslProviderSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
sslProviderSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
sslProviderZeroSSL: "https://acme.zerossl.com/v2/DV90",
var caDirUrls = map[string]string{
caLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
caLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
caBuypass: "https://api.buypass.com/acme/directory",
caGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
caSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
caSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
caSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
caZeroSSL: "https://acme.zerossl.com/v2/DV90",
}
type acmeSSLProviderConfig struct {

View File

@@ -7,6 +7,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"fmt"
"strings"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
@@ -19,22 +20,31 @@ import (
)
type acmeUser struct {
CA string
Email string
// 证书颁发机构标识。
// 通常等同于 [CAProviderType] 的值。
// 对于自定义 ACME CA值为 "custom#{access_id}"。
CA string
// 邮箱。
Email string
// 注册信息。
Registration *registration.Resource
// CSR 私钥。
privkey string
}
func newAcmeUser(ca, email string) (*acmeUser, error) {
func newAcmeUser(ca, caAccessId, email string) (*acmeUser, error) {
repo := repository.NewAcmeAccountRepository()
applyUser := &acmeUser{
CA: ca,
Email: email,
}
if ca == caCustom {
applyUser.CA = fmt.Sprintf("%s#%s", ca, caAccessId)
}
acmeAccount, err := repo.GetByCAAndEmail(ca, email)
acmeAccount, err := repo.GetByCAAndEmail(applyUser.CA, applyUser.Email)
if err != nil {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
@@ -73,6 +83,10 @@ func (u *acmeUser) hasRegistration() bool {
return u.Registration != nil
}
func (u *acmeUser) getCAProvider() string {
return strings.Split(u.CA, "#")[0]
}
func (u *acmeUser) getPrivateKeyPEM() string {
return u.privkey
}
@@ -94,16 +108,16 @@ func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userR
func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
var reg *registration.Resource
var err error
switch user.CA {
case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
switch user.getCAProvider() {
case caLetsEncrypt, caLetsEncryptStaging:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
case sslProviderBuypass:
case caBuypass:
{
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
case sslProviderGoogleTrustServices:
case caGoogleTrustServices:
{
access := domain.AccessConfigForGoogleTrustServices{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
@@ -117,7 +131,7 @@ func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions m
})
}
case sslProviderSSLCom:
case caSSLCom:
{
access := domain.AccessConfigForSSLCom{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
@@ -131,7 +145,7 @@ func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions m
})
}
case sslProviderZeroSSL:
case caZeroSSL:
{
access := domain.AccessConfigForZeroSSL{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
@@ -145,6 +159,26 @@ func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions m
})
}
case caCustom:
{
access := domain.AccessConfigForACMECA{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
if access.EabKid == "" && access.EabHmacKey == "" {
reg, err = client.Registration.Register(registration.RegisterOptions{
TermsOfServiceAgreed: true,
})
} else {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
}
default:
err = fmt.Errorf("unsupported ca provider '%s'", user.CA)
}

View File

@@ -20,12 +20,13 @@ import (
"golang.org/x/time/rate"
"github.com/usual2970/certimate/internal/domain"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice"
"github.com/usual2970/certimate/internal/repository"
)
type ApplyResult struct {
CertificateFullChain string
FullChainCertificate string
IssuerCertificate string
PrivateKey string
ACMEAccountUrl string
@@ -53,20 +54,20 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err
nodeConfig := config.Node.GetConfigForApply()
options := &applicantProviderOptions{
Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
ContactEmail: nodeConfig.ContactEmail,
Provider: domain.ACMEDns01ProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderExtendedConfig: nodeConfig.ProviderConfig,
CAProvider: domain.CAProviderType(nodeConfig.CAProvider),
CAProviderAccessConfig: make(map[string]any),
CAProviderExtendedConfig: nodeConfig.CAProviderConfig,
KeyAlgorithm: nodeConfig.KeyAlgorithm,
Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
DnsPropagationWait: nodeConfig.DnsPropagationWait,
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
DnsTTL: nodeConfig.DnsTTL,
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
ContactEmail: nodeConfig.ContactEmail,
Provider: domain.ACMEDns01ProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderServiceConfig: nodeConfig.ProviderConfig,
CAProvider: domain.CAProviderType(nodeConfig.CAProvider),
CAProviderAccessConfig: make(map[string]any),
CAProviderServiceConfig: nodeConfig.CAProviderConfig,
KeyAlgorithm: nodeConfig.KeyAlgorithm,
Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
DnsPropagationWait: nodeConfig.DnsPropagationWait,
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
DnsTTL: nodeConfig.DnsTTL,
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
}
accessRepo := repository.NewAccessRepository()
@@ -81,6 +82,7 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err
if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err)
} else {
options.CAProviderAccessId = access.Id
options.CAProviderAccessConfig = access.Config
}
}
@@ -91,13 +93,13 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err
sslProviderConfig := &acmeSSLProviderConfig{
Config: make(map[domain.CAProviderType]map[string]any),
Provider: sslProviderDefault,
Provider: caDefault,
}
if settings != nil {
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
return nil, err
} else if sslProviderConfig.Provider == "" {
sslProviderConfig.Provider = sslProviderDefault
sslProviderConfig.Provider = caDefault
}
}
@@ -163,7 +165,7 @@ func getLimiter(key string) *rate.Limiter {
}
func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOptions) (*ApplyResult, error) {
user, err := newAcmeUser(string(options.CAProvider), options.ContactEmail)
user, err := newAcmeUser(string(options.CAProvider), options.CAProviderAccessId, options.ContactEmail)
if err != nil {
return nil, err
}
@@ -175,13 +177,26 @@ func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOpt
// Create an ACME client config
config := lego.NewConfig(user)
config.Certificate.KeyType = parseLegoKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
config.CADirURL = sslProviderUrls[user.CA]
if user.CA == sslProviderSSLCom {
switch user.getCAProvider() {
case caSSLCom:
if strings.HasPrefix(options.KeyAlgorithm, "RSA") {
config.CADirURL = sslProviderUrls[sslProviderSSLCom+"RSA"]
config.CADirURL = caDirUrls[caSSLCom+"RSA"]
} else if strings.HasPrefix(options.KeyAlgorithm, "EC") {
config.CADirURL = sslProviderUrls[sslProviderSSLCom+"ECC"]
config.CADirURL = caDirUrls[caSSLCom+"ECC"]
} else {
config.CADirURL = caDirUrls[caSSLCom]
}
case caCustom:
caDirURL := maputil.GetString(options.CAProviderAccessConfig, "endpoint")
if caDirURL != "" {
config.CADirURL = caDirURL
} else {
return nil, fmt.Errorf("invalid ca provider endpoint")
}
default:
config.CADirURL = caDirUrls[user.CA]
}
// Create an ACME client
@@ -229,7 +244,7 @@ func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOpt
}
return &ApplyResult{
CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)),
FullChainCertificate: strings.TrimSpace(string(certResource.Certificate)),
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMEAccountUrl: user.Registration.URI,

View File

@@ -27,6 +27,8 @@ import (
pNamecheap "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap"
pNameDotCom "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namedotcom"
pNameSilo "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namesilo"
pNetcup "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netcup"
pNetlify "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/netlify"
pNS1 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ns1"
pPorkbun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/porkbun"
pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns"
@@ -40,22 +42,23 @@ import (
)
type applicantProviderOptions struct {
Domains []string
ContactEmail string
Provider domain.ACMEDns01ProviderType
ProviderAccessConfig map[string]any
ProviderExtendedConfig map[string]any
CAProvider domain.CAProviderType
CAProviderAccessConfig map[string]any
CAProviderExtendedConfig map[string]any
KeyAlgorithm string
Nameservers []string
DnsPropagationWait int32
DnsPropagationTimeout int32
DnsTTL int32
DisableFollowCNAME bool
ReplacedARIAcct string
ReplacedARICert string
Domains []string
ContactEmail string
Provider domain.ACMEDns01ProviderType
ProviderAccessConfig map[string]any
ProviderServiceConfig map[string]any
CAProvider domain.CAProviderType
CAProviderAccessId string
CAProviderAccessConfig map[string]any
CAProviderServiceConfig map[string]any
KeyAlgorithm string
Nameservers []string
DnsPropagationWait int32
DnsPropagationTimeout int32
DnsTTL int32
DisableFollowCNAME bool
ReplacedARIAcct string
ReplacedARICert string
}
func createApplicantProvider(options *applicantProviderOptions) (challenge.Provider, error) {
@@ -102,7 +105,7 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
applicant, err := pAliyunESA.NewChallengeProvider(&pAliyunESA.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
Region: maputil.GetString(options.ProviderServiceConfig, "region"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
@@ -123,8 +126,8 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
HostedZoneId: maputil.GetString(options.ProviderExtendedConfig, "hostedZoneId"),
Region: maputil.GetString(options.ProviderServiceConfig, "region"),
HostedZoneId: maputil.GetString(options.ProviderServiceConfig, "hostedZoneId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
@@ -331,7 +334,7 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
Region: maputil.GetString(options.ProviderServiceConfig, "region"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
@@ -348,7 +351,7 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
RegionId: maputil.GetString(options.ProviderExtendedConfig, "regionId"),
RegionId: maputil.GetString(options.ProviderServiceConfig, "regionId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
@@ -402,6 +405,38 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
return applicant, err
}
case domain.ACMEDns01ProviderTypeNetcup:
{
access := domain.AccessConfigForNetcup{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pNetcup.NewChallengeProvider(&pNetcup.ChallengeProviderConfig{
CustomerNumber: access.CustomerNumber,
ApiKey: access.ApiKey,
ApiPassword: access.ApiPassword,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeNetlify:
{
access := domain.AccessConfigForNetlify{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pNetlify.NewChallengeProvider(&pNetlify.ChallengeProviderConfig{
ApiToken: access.ApiToken,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeNS1:
{
access := domain.AccessConfigForNS1{}
@@ -486,7 +521,7 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
ZoneId: maputil.GetString(options.ProviderExtendedConfig, "zoneId"),
ZoneId: maputil.GetString(options.ProviderServiceConfig, "zoneId"),
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})

View File

@@ -31,9 +31,9 @@ func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error
nodeConfig := config.Node.GetConfigForDeploy()
options := &deployerProviderOptions{
Provider: domain.DeploymentProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderExtendedConfig: nodeConfig.ProviderConfig,
Provider: domain.DeploymentProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderServiceConfig: nodeConfig.ProviderConfig,
}
accessRepo := repository.NewAccessRepository()

View File

File diff suppressed because it is too large Load Diff

View File

@@ -17,10 +17,17 @@ type Access struct {
type AccessConfigFor1Panel struct {
ApiUrl string `json:"apiUrl"`
ApiVersion string `json:"apiVersion"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForACMECA struct {
Endpoint string `json:"endpoint"`
EabKid string `json:"eabKid,omitempty"`
EabHmacKey string `json:"eabHmacKey,omitempty"`
}
type AccessConfigForACMEHttpReq struct {
Endpoint string `json:"endpoint"`
Mode string `json:"mode,omitempty"`
@@ -60,6 +67,12 @@ type AccessConfigForBaotaPanel struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForBaotaWAF struct {
ApiUrl string `json:"apiUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForBytePlus struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
@@ -133,6 +146,14 @@ type AccessConfigForEmail struct {
DefaultReceiverAddress string `json:"defaultReceiverAddress,omitempty"`
}
type AccessConfigForFlexCDN struct {
ApiUrl string `json:"apiUrl"`
ApiRole string `json:"apiRole"`
AccessKeyId string `json:"accessKeyId"`
AccessKey string `json:"accessKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForGcore struct {
ApiToken string `json:"apiToken"`
}
@@ -149,6 +170,7 @@ type AccessConfigForGoDaddy struct {
type AccessConfigForGoEdge struct {
ApiUrl string `json:"apiUrl"`
ApiRole string `json:"apiRole"`
AccessKeyId string `json:"accessKeyId"`
AccessKey string `json:"accessKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
@@ -177,6 +199,15 @@ type AccessConfigForLarkBot struct {
WebhookUrl string `json:"webhookUrl"`
}
type AccessConfigForLeCDN struct {
ApiUrl string `json:"apiUrl"`
ApiVersion string `json:"apiVersion"`
ApiRole string `json:"apiRole"`
Username string `json:"username"`
Password string `json:"password"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForMattermost struct {
ServerUrl string `json:"serverUrl"`
Username string `json:"username"`
@@ -198,6 +229,16 @@ type AccessConfigForNameSilo struct {
ApiKey string `json:"apiKey"`
}
type AccessConfigForNetcup struct {
CustomerNumber string `json:"customerNumber"`
ApiKey string `json:"apiKey"`
ApiPassword string `json:"apiPassword"`
}
type AccessConfigForNetlify struct {
ApiToken string `json:"apiToken"`
}
type AccessConfigForNS1 struct {
ApiKey string `json:"apiKey"`
}
@@ -229,6 +270,13 @@ type AccessConfigForRainYun struct {
ApiKey string `json:"apiKey"`
}
type AccessConfigForRatPanel struct {
ApiUrl string `json:"apiUrl"`
AccessTokenId int32 `json:"accessTokenId"`
AccessToken string `json:"accessToken"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForSafeLine struct {
ApiUrl string `json:"apiUrl"`
ApiToken string `json:"apiToken"`
@@ -242,6 +290,14 @@ type AccessConfigForSSH struct {
Password string `json:"password,omitempty"`
Key string `json:"key,omitempty"`
KeyPassphrase string `json:"keyPassphrase,omitempty"`
JumpServers []struct {
Host string `json:"host"`
Port int32 `json:"port"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
Key string `json:"key,omitempty"`
KeyPassphrase string `json:"keyPassphrase,omitempty"`
} `json:"jumpServers,omitempty"`
}
type AccessConfigForSSLCom struct {
@@ -249,7 +305,7 @@ type AccessConfigForSSLCom struct {
EabHmacKey string `json:"eabHmacKey"`
}
type AccessConfigForTelegram struct {
type AccessConfigForTelegramBot struct {
BotToken string `json:"botToken"`
DefaultChatId int64 `json:"defaultChatId,omitempty"`
}

View File

@@ -20,7 +20,7 @@ type Certificate struct {
SerialNumber string `json:"serialNumber" db:"serialNumber"`
Certificate string `json:"certificate" db:"certificate"`
PrivateKey string `json:"privateKey" db:"privateKey"`
Issuer string `json:"issuer" db:"issuer"`
IssuerOrg string `json:"issuerOrg" db:"issuerOrg"`
IssuerCertificate string `json:"issuerCertificate" db:"issuerCertificate"`
KeyAlgorithm CertificateKeyAlgorithmType `json:"keyAlgorithm" db:"keyAlgorithm"`
EffectAt time.Time `json:"effectAt" db:"effectAt"`
@@ -38,7 +38,7 @@ type Certificate struct {
func (c *Certificate) PopulateFromX509(certX509 *x509.Certificate) *Certificate {
c.SubjectAltNames = strings.Join(certX509.DNSNames, ";")
c.SerialNumber = strings.ToUpper(certX509.SerialNumber.Text(16))
c.Issuer = strings.Join(certX509.Issuer.Organization, ";")
c.IssuerOrg = strings.Join(certX509.Issuer.Organization, ";")
c.EffectAt = certX509.NotBefore
c.ExpireAt = certX509.NotAfter

View File

@@ -10,6 +10,7 @@ type AccessProviderType string
*/
const (
AccessProviderType1Panel = AccessProviderType("1panel")
AccessProviderTypeACMECA = AccessProviderType("acmeca")
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai预留
AccessProviderTypeAliyun = AccessProviderType("aliyun")
@@ -18,6 +19,7 @@ const (
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
AccessProviderTypeBaishan = AccessProviderType("baishan")
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
AccessProviderTypeBaotaWAF = AccessProviderType("baotawaf")
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
AccessProviderTypeBunny = AccessProviderType("bunny")
AccessProviderTypeBuypass = AccessProviderType("buypass")
@@ -36,6 +38,7 @@ const (
AccessProviderTypeEdgio = AccessProviderType("edgio")
AccessProviderTypeEmail = AccessProviderType("email")
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly预留
AccessProviderTypeFlexCDN = AccessProviderType("flexcdn")
AccessProviderTypeGname = AccessProviderType("gname")
AccessProviderTypeGcore = AccessProviderType("gcore")
AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
@@ -47,11 +50,14 @@ const (
AccessProviderTypeLarkBot = AccessProviderType("larkbot")
AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt")
AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging")
AccessProviderTypeLeCDN = AccessProviderType("lecdn")
AccessProviderTypeLocal = AccessProviderType("local")
AccessProviderTypeMattermost = AccessProviderType("mattermost")
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
AccessProviderTypeNetcup = AccessProviderType("netcup")
AccessProviderTypeNetlify = AccessProviderType("netlify")
AccessProviderTypeNS1 = AccessProviderType("ns1")
AccessProviderTypePorkbun = AccessProviderType("porkbun")
AccessProviderTypePowerDNS = AccessProviderType("powerdns")
@@ -59,10 +65,11 @@ const (
AccessProviderTypeQiniu = AccessProviderType("qiniu")
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
AccessProviderTypeRainYun = AccessProviderType("rainyun")
AccessProviderTypeRatPanel = AccessProviderType("ratpanel")
AccessProviderTypeSafeLine = AccessProviderType("safeline")
AccessProviderTypeSSH = AccessProviderType("ssh")
AccessProviderTypeSSLCOM = AccessProviderType("sslcom")
AccessProviderTypeTelegram = AccessProviderType("telegram")
AccessProviderTypeTelegramBot = AccessProviderType("telegrambot")
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
AccessProviderTypeUCloud = AccessProviderType("ucloud")
AccessProviderTypeUpyun = AccessProviderType("upyun")
@@ -85,6 +92,7 @@ type CAProviderType string
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
CAProviderTypeACMECA = CAProviderType(AccessProviderTypeACMECA)
CAProviderTypeBuypass = CAProviderType(AccessProviderTypeBuypass)
CAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices)
CAProviderTypeLetsEncrypt = CAProviderType(AccessProviderTypeLetsEncrypt)
@@ -130,6 +138,8 @@ const (
ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
@@ -165,6 +175,7 @@ const (
DeploymentProviderTypeAliyunDDoS = DeploymentProviderType(AccessProviderTypeAliyun + "-ddos")
DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa")
DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc")
DeploymentProviderTypeAliyunGA = DeploymentProviderType(AccessProviderTypeAliyun + "-ga")
DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live")
DeploymentProviderTypeAliyunNLB = DeploymentProviderType(AccessProviderTypeAliyun + "-nlb")
DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss")
@@ -180,12 +191,15 @@ const (
DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn")
DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console")
DeploymentProviderTypeBaotaPanelSite = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-site")
DeploymentProviderTypeBaotaWAFConsole = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-console")
DeploymentProviderTypeBaotaWAFSite = DeploymentProviderType(AccessProviderTypeBaotaWAF + "-site")
DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn")
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)
DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly)
DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn")
DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications")
DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN)
DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn")
DeploymentProviderTypeGoEdge = DeploymentProviderType(AccessProviderTypeGoEdge)
DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn")
@@ -197,12 +211,16 @@ const (
DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live")
DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod")
DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret")
DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN)
DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal)
DeploymentProviderTypeNetlifySite = DeploymentProviderType(AccessProviderTypeNetlify + "-site")
DeploymentProviderTypeProxmoxVE = DeploymentProviderType(AccessProviderTypeProxmoxVE)
DeploymentProviderTypeQiniuCDN = DeploymentProviderType(AccessProviderTypeQiniu + "-cdn")
DeploymentProviderTypeQiniuKodo = DeploymentProviderType(AccessProviderTypeQiniu + "-kodo")
DeploymentProviderTypeQiniuPili = DeploymentProviderType(AccessProviderTypeQiniu + "-pili")
DeploymentProviderTypeRainYunRCDN = DeploymentProviderType(AccessProviderTypeRainYun + "-rcdn")
DeploymentProviderTypeRatPanelConsole = DeploymentProviderType(AccessProviderTypeRatPanel + "-console")
DeploymentProviderTypeRatPanelSite = DeploymentProviderType(AccessProviderTypeRatPanel + "-site")
DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine)
DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH)
DeploymentProviderTypeTencentCloudCDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cdn")
@@ -228,7 +246,9 @@ const (
DeploymentProviderTypeVolcEngineImageX = DeploymentProviderType(AccessProviderTypeVolcEngine + "-imagex")
DeploymentProviderTypeVolcEngineLive = DeploymentProviderType(AccessProviderTypeVolcEngine + "-live")
DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos")
DeploymentProviderTypeWangsuCDN = DeploymentProviderType(AccessProviderTypeWangsu + "-cdn")
DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro")
DeploymentProviderTypeWangsuCertificate = DeploymentProviderType(AccessProviderTypeWangsu + "-certificate")
DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook)
)
@@ -246,7 +266,7 @@ const (
NotificationProviderTypeEmail = NotificationProviderType(AccessProviderTypeEmail)
NotificationProviderTypeLarkBot = NotificationProviderType(AccessProviderTypeLarkBot)
NotificationProviderTypeMattermost = NotificationProviderType(AccessProviderTypeMattermost)
NotificationProviderTypeTelegram = NotificationProviderType(AccessProviderTypeTelegram)
NotificationProviderTypeTelegramBot = NotificationProviderType(AccessProviderTypeTelegramBot)
NotificationProviderTypeWebhook = NotificationProviderType(AccessProviderTypeWebhook)
NotificationProviderTypeWeComBot = NotificationProviderType(AccessProviderTypeWeComBot)
)

View File

@@ -105,11 +105,6 @@ type WorkflowNodeConfigForNotify struct {
}
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays")
if skipBeforeExpiryDays == 0 {
skipBeforeExpiryDays = 30
}
return WorkflowNodeConfigForApply{
Domains: maputil.GetString(n.Config, "domains"),
ContactEmail: maputil.GetString(n.Config, "contactEmail"),
@@ -126,7 +121,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
DnsTTL: maputil.GetInt32(n.Config, "dnsTTL"),
DisableFollowCNAME: maputil.GetBool(n.Config, "disableFollowCNAME"),
DisableARI: maputil.GetBool(n.Config, "disableARI"),
SkipBeforeExpiryDays: skipBeforeExpiryDays,
SkipBeforeExpiryDays: maputil.GetOrDefaultInt32(n.Config, "skipBeforeExpiryDays", 30),
}
}

View File

@@ -31,9 +31,9 @@ func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error
nodeConfig := config.Node.GetConfigForNotify()
options := &notifierProviderOptions{
Provider: domain.NotificationProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderExtendedConfig: nodeConfig.ProviderConfig,
Provider: domain.NotificationProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderServiceConfig: nodeConfig.ProviderConfig,
}
accessRepo := repository.NewAccessRepository()

View File

@@ -6,21 +6,21 @@ import (
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
pDingTalkBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalkbot"
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
pLarkBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/larkbot"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
pTelegramBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegrambot"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
pWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
pWeComBot "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecombot"
httputil "github.com/usual2970/certimate/internal/pkg/utils/http"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
type notifierProviderOptions struct {
Provider domain.NotificationProviderType
ProviderAccessConfig map[string]any
ProviderExtendedConfig map[string]any
Provider domain.NotificationProviderType
ProviderAccessConfig map[string]any
ProviderServiceConfig map[string]any
}
func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier, error) {
@@ -36,7 +36,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pDingTalk.NewNotifier(&pDingTalk.NotifierConfig{
return pDingTalkBot.NewNotifier(&pDingTalkBot.NotifierConfig{
WebhookUrl: access.WebhookUrl,
Secret: access.Secret,
})
@@ -55,8 +55,8 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
SmtpTls: access.SmtpTls,
Username: access.Username,
Password: access.Password,
SenderAddress: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "senderAddress", access.DefaultSenderAddress),
ReceiverAddress: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "receiverAddress", access.DefaultReceiverAddress),
SenderAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "senderAddress", access.DefaultSenderAddress),
ReceiverAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "receiverAddress", access.DefaultReceiverAddress),
})
}
@@ -67,7 +67,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pLark.NewNotifier(&pLark.NotifierConfig{
return pLarkBot.NewNotifier(&pLarkBot.NotifierConfig{
WebhookUrl: access.WebhookUrl,
})
}
@@ -83,20 +83,20 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
ServerUrl: access.ServerUrl,
Username: access.Username,
Password: access.Password,
ChannelId: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "channelId", access.DefaultChannelId),
ChannelId: maputil.GetOrDefaultString(options.ProviderServiceConfig, "channelId", access.DefaultChannelId),
})
}
case domain.NotificationProviderTypeTelegram:
case domain.NotificationProviderTypeTelegramBot:
{
access := domain.AccessConfigForTelegram{}
access := domain.AccessConfigForTelegramBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pTelegram.NewNotifier(&pTelegram.NotifierConfig{
return pTelegramBot.NewNotifier(&pTelegramBot.NotifierConfig{
BotToken: access.BotToken,
ChatId: maputil.GetOrDefaultInt64(options.ProviderExtendedConfig, "chatId", access.DefaultChatId),
ChatId: maputil.GetOrDefaultInt64(options.ProviderServiceConfig, "chatId", access.DefaultChatId),
})
}
@@ -117,7 +117,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)
}
}
if extendedHeadersString := maputil.GetString(options.ProviderExtendedConfig, "headers"); extendedHeadersString != "" {
if extendedHeadersString := maputil.GetString(options.ProviderServiceConfig, "headers"); extendedHeadersString != "" {
h, err := httputil.ParseHeaders(extendedHeadersString)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook headers: %w", err)
@@ -129,7 +129,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
WebhookUrl: access.Url,
WebhookData: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", access.DefaultDataForNotification),
WebhookData: maputil.GetOrDefaultString(options.ProviderServiceConfig, "webhookData", access.DefaultDataForNotification),
Method: access.Method,
Headers: mergedHeaders,
AllowInsecureConnections: access.AllowInsecureConnections,
@@ -143,7 +143,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pWeCom.NewNotifier(&pWeCom.NotifierConfig{
return pWeComBot.NewNotifier(&pWeComBot.NotifierConfig{
WebhookUrl: access.WebhookUrl,
})
}

View File

@@ -6,17 +6,17 @@ import (
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalkbot"
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/larkbot"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegrambot"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
pWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
pWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecombot"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
@@ -52,9 +52,9 @@ func createNotifierProviderUseGlobalSettings(channel domain.NotifyChannelType, c
case domain.NotifyChannelTypeGotify:
return pGotify.NewNotifier(&pGotify.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
Token: maputil.GetString(channelConfig, "token"),
Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
ServerUrl: maputil.GetString(channelConfig, "url"),
Token: maputil.GetString(channelConfig, "token"),
Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
})
case domain.NotifyChannelTypeLark:
@@ -83,7 +83,7 @@ func createNotifierProviderUseGlobalSettings(channel domain.NotifyChannelType, c
case domain.NotifyChannelTypeServerChan:
return pServerChan.NewNotifier(&pServerChan.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
ServerUrl: maputil.GetString(channelConfig, "url"),
})
case domain.NotifyChannelTypeTelegram:

View File

@@ -24,6 +24,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider,
providerConfig := internal.NewDefaultConfig()
providerConfig.SecretID = config.AccessKeyId
providerConfig.SecretKey = config.AccessKeySecret
providerConfig.RegionID = config.Region
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}

View File

@@ -0,0 +1,40 @@
package netcup
import (
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/netcup"
)
type ChallengeProviderConfig struct {
CustomerNumber string `json:"customerNumber"`
ApiKey string `json:"apiKey"`
ApiPassword string `json:"apiPassword"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
panic("config is nil")
}
providerConfig := netcup.NewDefaultConfig()
providerConfig.Customer = config.CustomerNumber
providerConfig.Key = config.ApiKey
providerConfig.Password = config.ApiPassword
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = int(config.DnsTTL)
}
provider, err := netcup.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,36 @@
package netcup
import (
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/netlify"
)
type ChallengeProviderConfig struct {
ApiToken string `json:"apiToken"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
panic("config is nil")
}
providerConfig := netlify.NewDefaultConfig()
providerConfig.Token = config.ApiToken
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = int(config.DnsTTL)
}
provider, err := netlify.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -9,12 +9,15 @@ import (
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
opsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
onepanelsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
)
type DeployerConfig struct {
// 1Panel 地址。
ApiUrl string `json:"apiUrl"`
// 1Panel 版本。
// 可取值 "v1"、"v2"。
ApiVersion string `json:"apiVersion"`
// 1Panel 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
@@ -26,7 +29,7 @@ type DeployerConfig struct {
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *opsdk.Client
sdkClient *onepanelsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
@@ -36,7 +39,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiKey, config.AllowInsecureConnections)
client, err := createSdkClient(config.ApiUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
@@ -59,7 +62,7 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 设置面板 SSL 证书
updateSystemSSLReq := &opsdk.UpdateSystemSSLRequest{
updateSystemSSLReq := &onepanelsdk.UpdateSystemSSLRequest{
Cert: certPEM,
Key: privkeyPEM,
SSL: "enable",
@@ -79,16 +82,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiUrl, apiKey string, skipTlsVerify bool) (*opsdk.Client, error) {
func createSdkClient(apiUrl, apiVersion, apiKey string, skipTlsVerify bool) (*onepanelsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid 1panel api url")
}
if apiVersion == "" {
return nil, errors.New("invalid 1panel api version")
}
if apiKey == "" {
return nil, errors.New("invalid 1panel api key")
}
client := opsdk.NewClient(apiUrl, apiKey)
client := onepanelsdk.NewClient(apiUrl, apiVersion, apiKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}

View File

@@ -15,6 +15,7 @@ var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fApiVersion string
fApiKey string
)
@@ -24,6 +25,7 @@ func init() {
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v1", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
}
@@ -34,6 +36,7 @@ Shell command to run this test:
--CERTIMATE_DEPLOYER_1PANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_1PANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_1PANELCONSOLE_APIURL="http://127.0.0.1:20410" \
--CERTIMATE_DEPLOYER_1PANELCONSOLE_APIVERSION="v1" \
--CERTIMATE_DEPLOYER_1PANELCONSOLE_APIKEY="your-api-key"
*/
func TestDeploy(t *testing.T) {
@@ -45,11 +48,13 @@ func TestDeploy(t *testing.T) {
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("APIVERSION: %v", fApiVersion),
fmt.Sprintf("APIKEY: %v", fApiKey),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiVersion: fApiVersion,
ApiKey: fApiKey,
AllowInsecureConnections: true,
AutoRestart: true,

View File

@@ -12,12 +12,15 @@ import (
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/1panel-ssl"
opsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
onepanelsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
)
type DeployerConfig struct {
// 1Panel 地址。
ApiUrl string `json:"apiUrl"`
// 1Panel 版本。
// 可取值 "v1"、"v2"。
ApiVersion string `json:"apiVersion"`
// 1Panel 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
@@ -35,7 +38,7 @@ type DeployerConfig struct {
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *opsdk.Client
sdkClient *onepanelsdk.Client
sslUploader uploader.Uploader
}
@@ -46,14 +49,15 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiKey, config.AllowInsecureConnections)
client, err := createSdkClient(config.ApiUrl, config.ApiVersion, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
ApiUrl: config.ApiUrl,
ApiKey: config.ApiKey,
ApiUrl: config.ApiUrl,
ApiVersion: config.ApiVersion,
ApiKey: config.ApiKey,
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
@@ -103,7 +107,7 @@ func (d *DeployerProvider) deployToWebsite(ctx context.Context, certPEM string,
}
// 获取网站 HTTPS 配置
getHttpsConfReq := &opsdk.GetHttpsConfRequest{
getHttpsConfReq := &onepanelsdk.GetHttpsConfRequest{
WebsiteID: d.config.WebsiteId,
}
getHttpsConfResp, err := d.sdkClient.GetHttpsConf(getHttpsConfReq)
@@ -122,7 +126,7 @@ func (d *DeployerProvider) deployToWebsite(ctx context.Context, certPEM string,
// 修改网站 HTTPS 配置
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
updateHttpsConfReq := &opsdk.UpdateHttpsConfRequest{
updateHttpsConfReq := &onepanelsdk.UpdateHttpsConfRequest{
WebsiteID: d.config.WebsiteId,
Type: "existed",
WebsiteSSLID: certId,
@@ -147,7 +151,7 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri
}
// 获取证书详情
getWebsiteSSLReq := &opsdk.GetWebsiteSSLRequest{
getWebsiteSSLReq := &onepanelsdk.GetWebsiteSSLRequest{
SSLID: d.config.CertificateId,
}
getWebsiteSSLResp, err := d.sdkClient.GetWebsiteSSL(getWebsiteSSLReq)
@@ -157,7 +161,7 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri
}
// 更新证书
uploadWebsiteSSLReq := &opsdk.UploadWebsiteSSLRequest{
uploadWebsiteSSLReq := &onepanelsdk.UploadWebsiteSSLRequest{
Type: "paste",
SSLID: d.config.CertificateId,
Description: getWebsiteSSLResp.Data.Description,
@@ -173,16 +177,20 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri
return nil
}
func createSdkClient(apiUrl, apiKey string, skipTlsVerify bool) (*opsdk.Client, error) {
func createSdkClient(apiUrl, apiVersion, apiKey string, skipTlsVerify bool) (*onepanelsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid 1panel api url")
}
if apiVersion == "" {
return nil, errors.New("invalid 1panel api version")
}
if apiKey == "" {
return nil, errors.New("invalid 1panel api key")
}
client := opsdk.NewClient(apiUrl, apiKey)
client := onepanelsdk.NewClient(apiUrl, apiVersion, apiKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}

View File

@@ -15,6 +15,7 @@ var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fApiVersion string
fApiKey string
fWebsiteId int64
)
@@ -25,6 +26,7 @@ func init() {
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v1", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.Int64Var(&fWebsiteId, argsPrefix+"WEBSITEID", 0, "")
}
@@ -36,6 +38,7 @@ Shell command to run this test:
--CERTIMATE_DEPLOYER_1PANELSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_1PANELSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_1PANELSITE_APIURL="http://127.0.0.1:20410" \
--CERTIMATE_DEPLOYER_1PANELSITE_APIVERSION="v1" \
--CERTIMATE_DEPLOYER_1PANELSITE_APIKEY="your-api-key" \
--CERTIMATE_DEPLOYER_1PANELSITE_WEBSITEID="your-website-id"
*/
@@ -48,12 +51,14 @@ func TestDeploy(t *testing.T) {
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("APIVERSION: %v", fApiVersion),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("WEBSITEID: %v", fWebsiteId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiVersion: fApiVersion,
ApiKey: fApiKey,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_WEBSITE,

View File

@@ -157,7 +157,7 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, *listener.ListenerId)
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
@@ -192,7 +192,7 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, *listener.ListenerId)
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
@@ -211,8 +211,13 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
d.logger.Info("found https/quic listeners to deploy", slog.Any("listenerIds", listenerIds))
for _, listenerId := range listenerIds {
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@@ -96,6 +96,7 @@ func TestDeploy(t *testing.T) {
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("REGION: %v", fRegion),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"log/slog"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
alislb "github.com/alibabacloud-go/slb-20140515/v4/client"
@@ -172,7 +171,6 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {
errs = append(errs, err)
@@ -310,22 +308,10 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien
}
func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) {
casRegion := region
if casRegion != "" {
// 阿里云 CAS 服务接入点是独立于 CLB 服务的
// 国内版固定接入点:华东一杭州
// 国际版固定接入点:亚太东南一新加坡
if !strings.HasPrefix(casRegion, "cn-") {
casRegion = "ap-southeast-1"
} else {
casRegion = "cn-hangzhou"
}
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
Region: casRegion,
Region: region,
})
return uploader, err
}

View File

@@ -18,7 +18,7 @@ var (
fAccessKeySecret string
fRegion string
fLoadbalancerId string
fListenerPort int
fListenerPort int64
fDomain string
)
@@ -31,7 +31,7 @@ func init() {
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.IntVar(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "")
flag.Int64Var(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}

View File

@@ -22,6 +22,7 @@ type DeployerConfig struct {
// 阿里云地域。
Region string `json:"region"`
// 服务版本。
// 可取值 "2.0"、"3.0"。
ServiceVersion string `json:"serviceVersion"`
// 自定义域名(支持泛域名)。
Domain string `json:"domain"`

View File

@@ -0,0 +1,322 @@
package aliyunga
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliga "github.com/alibabacloud-go/ga-20191120/v3/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice"
)
type DeployerConfig struct {
// 阿里云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 阿里云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 全球加速实例 ID。
AcceleratorId string `json:"acceleratorId"`
// 全球加速监听 ID。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
// SNI 域名(不支持泛域名)。
// 部署资源类型为 [RESOURCE_TYPE_ACCELERATOR]、[RESOURCE_TYPE_LISTENER] 时选填。
Domain string `json:"domain,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *aliga.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
d.sslUploader.WithLogger(logger)
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 CAS
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_ACCELERATOR:
if err := d.deployToAccelerator(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *DeployerProvider) deployToAccelerator(ctx context.Context, cloudCertId string) error {
if d.config.AcceleratorId == "" {
return errors.New("config `acceleratorId` is required")
}
// 查询 HTTPS 监听列表
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlisteners
listenerIds := make([]string, 0)
listListenersPageNumber := int32(1)
listListenersPageSize := int32(50)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &aliga.ListListenersRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(d.config.AcceleratorId),
PageNumber: tea.Int32(listListenersPageNumber),
PageSize: tea.Int32(listListenersPageSize),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
d.logger.Debug("sdk request 'ga.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.ListListeners': %w", err)
}
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
if strings.EqualFold(tea.StringValue(listener.Protocol), "https") {
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
}
if len(listListenersResp.Body.Listeners) < int(listListenersPageSize) {
break
} else {
listListenersPageNumber++
}
}
// 遍历更新监听证书
if len(listenerIds) == 0 {
d.logger.Info("no ga listeners to deploy")
} else {
var errs []error
d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds))
for _, listenerId := range listenerIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
}
func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.AcceleratorId == "" {
return errors.New("config `acceleratorId` is required")
}
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.AcceleratorId, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudAcceleratorId string, cloudListenerId string, cloudCertId string) error {
// 查询监听绑定的证书列表
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-listlistenercertificates
var listenerDefaultCertificate *aliga.ListListenerCertificatesResponseBodyCertificates
var listenerAdditionalCertificates []*aliga.ListListenerCertificatesResponseBodyCertificates = make([]*aliga.ListListenerCertificatesResponseBodyCertificates, 0)
var listListenerCertificatesNextToken *string
for {
listListenerCertificatesReq := &aliga.ListListenerCertificatesRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(d.config.AcceleratorId),
NextToken: listListenerCertificatesNextToken,
MaxResults: tea.Int32(20),
}
listListenerCertificatesResp, err := d.sdkClient.ListListenerCertificates(listListenerCertificatesReq)
d.logger.Debug("sdk request 'ga.ListListenerCertificates'", slog.Any("request", listListenerCertificatesReq), slog.Any("response", listListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.ListListenerCertificates': %w", err)
}
if listListenerCertificatesResp.Body.Certificates != nil {
for _, certificate := range listListenerCertificatesResp.Body.Certificates {
if tea.BoolValue(certificate.IsDefault) {
listenerDefaultCertificate = certificate
} else {
listenerAdditionalCertificates = append(listenerAdditionalCertificates, certificate)
}
}
}
if listListenerCertificatesResp.Body.NextToken == nil {
break
} else {
listListenerCertificatesNextToken = listListenerCertificatesResp.Body.NextToken
}
}
if d.config.Domain == "" {
// 未指定 SNI只需部署到监听器
if listenerDefaultCertificate != nil && tea.StringValue(listenerDefaultCertificate.CertificateId) == cloudCertId {
d.logger.Info("no need to update ga listener default certificate")
return nil
}
// 修改监听的属性
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updatelistener
updateListenerReq := &aliga.UpdateListenerRequest{
RegionId: tea.String("cn-hangzhou"),
ListenerId: tea.String(cloudListenerId),
Certificates: []*aliga.UpdateListenerRequestCertificates{{
Id: tea.String(cloudCertId),
}},
}
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
d.logger.Debug("sdk request 'ga.UpdateListener'", slog.Any("request", updateListenerReq), slog.Any("response", updateListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.UpdateListener': %w", err)
}
} else {
// 指定 SNI需部署到扩展域名
if sliceutil.Some(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool {
return tea.StringValue(item.CertificateId) == cloudCertId
}) {
d.logger.Info("no need to update ga listener additional certificate")
return nil
}
if sliceutil.Some(listenerAdditionalCertificates, func(item *aliga.ListListenerCertificatesResponseBodyCertificates) bool {
return tea.StringValue(item.Domain) == d.config.Domain
}) {
// 为监听替换扩展证书
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-updateadditionalcertificatewithlistener
updateAdditionalCertificateWithListenerReq := &aliga.UpdateAdditionalCertificateWithListenerRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(cloudAcceleratorId),
ListenerId: tea.String(cloudListenerId),
CertificateId: tea.String(cloudCertId),
Domain: tea.String(d.config.Domain),
}
updateAdditionalCertificateWithListenerResp, err := d.sdkClient.UpdateAdditionalCertificateWithListener(updateAdditionalCertificateWithListenerReq)
d.logger.Debug("sdk request 'ga.UpdateAdditionalCertificateWithListener'", slog.Any("request", updateAdditionalCertificateWithListenerReq), slog.Any("response", updateAdditionalCertificateWithListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.UpdateAdditionalCertificateWithListener': %w", err)
}
} else {
// 为监听绑定扩展证书
// REF: https://help.aliyun.com/zh/ga/developer-reference/api-ga-2019-11-20-associateadditionalcertificateswithlistener
associateAdditionalCertificatesWithListenerReq := &aliga.AssociateAdditionalCertificatesWithListenerRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(cloudAcceleratorId),
ListenerId: tea.String(cloudListenerId),
Certificates: []*aliga.AssociateAdditionalCertificatesWithListenerRequestCertificates{{
Id: tea.String(cloudCertId),
Domain: tea.String(d.config.Domain),
}},
}
associateAdditionalCertificatesWithListenerResp, err := d.sdkClient.AssociateAdditionalCertificatesWithListener(associateAdditionalCertificatesWithListenerReq)
d.logger.Debug("sdk request 'ga.AssociateAdditionalCertificatesWithListener'", slog.Any("request", associateAdditionalCertificatesWithListenerReq), slog.Any("response", associateAdditionalCertificatesWithListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ga.AssociateAdditionalCertificatesWithListener': %w", err)
}
}
}
return nil
}
func createSdkClient(accessKeyId, accessKeySecret string) (*aliga.Client, error) {
// 接入点一览 https://api.aliyun.com/product/Ga
config := &aliopen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("ga.cn-hangzhou.aliyuncs.com"),
}
client, err := aliga.NewClient(config)
if err != nil {
return nil, err
}
return client, nil
}
func createSslUploader(accessKeyId, accessKeySecret string) (uploader.Uploader, error) {
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: accessKeyId,
AccessKeySecret: accessKeySecret,
Region: "cn-hangzhou",
})
return uploader, err
}

View File

@@ -0,0 +1,118 @@
package aliyunga_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-ga"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fAcceleratorId string
fListenerId string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNGA_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fAcceleratorId, argsPrefix+"ACCELERATORID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./aliyun_ga_test.go -args \
--CERTIMATE_DEPLOYER_ALIYUNGA_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_ALIYUNGA_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_ALIYUNGA_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_ALIYUNGA_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_ALIYUNGA_ACCELERATORID="your-ga-accelerator-id" \
--CERTIMATE_DEPLOYER_ALIYUNGA_LISTENERID="your-ga-listener-id" \
--CERTIMATE_DEPLOYER_ALIYUNGA_DOMAIN="your-ga-sni-domain"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToAccelerator", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("ACCELERATORID: %v", fAcceleratorId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
ResourceType: provider.RESOURCE_TYPE_ACCELERATOR,
AcceleratorId: fAcceleratorId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
t.Run("Deploy_ToListener", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("LISTENERID: %v", fListenerId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
ListenerId: fListenerId,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunga
type ResourceType string
const (
// 资源类型:部署到指定全球加速器。
RESOURCE_TYPE_ACCELERATOR = ResourceType("accelerator")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -145,7 +145,7 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
if listListenersResp.Body.Listeners != nil {
for _, listener := range listListenersResp.Body.Listeners {
listenerIds = append(listenerIds, *listener.ListenerId)
listenerIds = append(listenerIds, tea.StringValue(listener.ListenerId))
}
}
@@ -167,7 +167,6 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)

View File

@@ -5,9 +5,15 @@ import (
"fmt"
"log/slog"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
awsacm "github.com/aws/aws-sdk-go-v2/service/acm"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aws-acm"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type DeployerConfig struct {
@@ -17,11 +23,15 @@ type DeployerConfig struct {
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
// ACM 证书 ARN。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateArn string `json:"certificateArn,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *awsacm.Client
sslUploader uploader.Uploader
}
@@ -32,6 +42,11 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
@@ -44,6 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
@@ -59,13 +75,48 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 ACM
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
if d.config.CertificateArn == "" {
// 上传证书到 ACM
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
// 提取服务器证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 导入证书
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html
importCertificateReq := &awsacm.ImportCertificateInput{
CertificateArn: aws.String(d.config.CertificateArn),
Certificate: ([]byte)(serverCertPEM),
CertificateChain: ([]byte)(intermediaCertPEM),
PrivateKey: ([]byte)(privkeyPEM),
}
importCertificateResp, err := d.sdkClient.ImportCertificate(context.TODO(), importCertificateReq)
d.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err)
}
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*awsacm.Client, error) {
cfg, err := awscfg.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
client := awsacm.NewFromConfig(cfg, func(o *awsacm.Options) {
o.Region = region
o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, ""))
})
return client, nil
}

View File

@@ -32,7 +32,7 @@ type DeployerConfig struct {
// Key Vault 名称。
KeyVaultName string `json:"keyvaultName"`
// Key Vault 证书名称。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateName string `json:"certificateName,omitempty"`
}

View File

@@ -20,7 +20,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
}

View File

@@ -20,11 +20,11 @@ type DeployerConfig struct {
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 站类型。
// 站类型。
SiteType string `json:"siteType"`
// 站名称(单个)。
// 站名称(单个)。
SiteName string `json:"siteName,omitempty"`
// 站名称(多个)。
// 站名称(多个)。
SiteNames []string `json:"siteNames,omitempty"`
}

View File

@@ -0,0 +1,88 @@
package baotapanelconsole
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
btsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btwaf"
)
type DeployerConfig struct {
// 堡塔云 WAF 地址。
ApiUrl string `json:"apiUrl"`
// 堡塔云 WAF 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *btsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 设置面板 SSL
configSetSSLReq := &btsdk.ConfigSetSSLRequest{
CertContent: certPEM,
KeyContent: privkeyPEM,
}
configSetSSLResp, err := d.sdkClient.ConfigSetSSL(configSetSSLReq)
d.logger.Debug("sdk request 'bt.ConfigSetSSL'", slog.Any("request", configSetSSLReq), slog.Any("response", configSetSSLResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.ConfigSetSSL': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid baota api url")
}
if apiKey == "" {
return nil, errors.New("invalid baota api key")
}
client := btsdk.NewClient(apiUrl, apiKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,73 @@
package baotapanelconsole_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotawaf-console"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fApiKey string
fSiteName string
fSitePort int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BAOTAWAFCONSOLE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./baotawaf_console_test.go -args \
--CERTIMATE_DEPLOYER_BAOTAWAFCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFCONSOLE_APIURL="http://127.0.0.1:8888" \
--CERTIMATE_DEPLOYER_BAOTAWAFCONSOLE_APIKEY="your-api-key"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,151 @@
package baotapanelwaf
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
btsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/btwaf"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 堡塔云 WAF 地址。
ApiUrl string `json:"apiUrl"`
// 堡塔云 WAF 接口密钥。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 网站名称。
SiteName string `json:"siteName"`
// 网站 SSL 端口。
// 零值时默认为 443。
SitePort int32 `json:"sitePort,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *btsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.SiteName == "" {
return nil, errors.New("config `siteName` is required")
}
if d.config.SitePort == 0 {
d.config.SitePort = 443
}
// 遍历获取网站列表,获取网站 ID
// REF: https://support.huaweicloud.com/api-waf/ListHost.html
siteId := ""
getSitListPage := int32(1)
getSitListPageSize := int32(100)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getSiteListReq := &btsdk.GetSiteListRequest{
SiteName: typeutil.ToPtr(d.config.SiteName),
Page: typeutil.ToPtr(getSitListPage),
PageSize: typeutil.ToPtr(getSitListPageSize),
}
getSiteListResp, err := d.sdkClient.GetSiteList(getSiteListReq)
d.logger.Debug("sdk request 'bt.GetSiteList'", slog.Any("request", getSiteListReq), slog.Any("response", getSiteListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.GetSiteList': %w", err)
}
if getSiteListResp.Result != nil && getSiteListResp.Result.List != nil {
for _, siteItem := range getSiteListResp.Result.List {
if siteItem.SiteName == d.config.SiteName {
siteId = siteItem.SiteId
break
}
}
}
if getSiteListResp.Result == nil || len(getSiteListResp.Result.List) < int(getSitListPageSize) {
break
} else {
getSitListPage++
}
}
if siteId == "" {
return nil, errors.New("site not found")
}
// 修改站点配置
modifySiteReq := &btsdk.ModifySiteRequest{
SiteId: siteId,
Type: typeutil.ToPtr("openCert"),
Server: &btsdk.SiteServerInfo{
ListenSSLPort: typeutil.ToPtr(d.config.SitePort),
SSL: &btsdk.SiteServerSSLInfo{
IsSSL: typeutil.ToPtr(int32(1)),
FullChain: typeutil.ToPtr(certPEM),
PrivateKey: typeutil.ToPtr(privkeyPEM),
},
},
}
modifySiteResp, err := d.sdkClient.ModifySite(modifySiteReq)
d.logger.Debug("sdk request 'bt.ModifySite'", slog.Any("request", modifySiteReq), slog.Any("response", modifySiteResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'bt.ModifySite': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiUrl, apiKey string, skipTlsVerify bool) (*btsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid baota api url")
}
if apiKey == "" {
return nil, errors.New("invalid baota api key")
}
client := btsdk.NewClient(apiUrl, apiKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,81 @@
package baotapanelwaf_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotawaf-site"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fApiKey string
fSiteName string
fSitePort int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_BAOTAWAFSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "")
flag.Int64Var(&fSitePort, argsPrefix+"SITEPORT", 0, "")
}
/*
Shell command to run this test:
go test -v ./baotawaf_site_test.go -args \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIURL="http://127.0.0.1:8888" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIKEY="your-api-key" \
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name"\
--CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITEPORT=443
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("SITENAME: %v", fSiteName),
fmt.Sprintf("SITEPORT: %v", fSitePort),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
SiteName: fSiteName,
SitePort: int32(fSitePort),
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -51,6 +51,7 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书
// REF: https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post
createCertificateReq := &cfsdk.CreateCertificateRequest{
Certificate: certPEM,
CertificateKey: privkeyPEM,

View File

@@ -56,18 +56,18 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 提取 Edgio 所需的服务证书和中间证书内容
privateCertPEM, intermediateCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
// 提取服务证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 上传 TLS 证书
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{
EnvironmentID: d.config.EnvironmentId,
PrimaryCert: privateCertPEM,
IntermediateCert: intermediateCertPEM,
PrimaryCert: serverCertPEM,
IntermediateCert: intermediaCertPEM,
PrivateKey: privkeyPEM,
}
uploadTlsCertResp, err := d.sdkClient.UploadTlsCert(uploadTlsCertReq)

View File

@@ -0,0 +1,8 @@
package flexcdn
type ResourceType string
const (
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@@ -0,0 +1,145 @@
package flexcdn
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"net/url"
"time"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
flexcdnsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/flexcdn"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type DeployerConfig struct {
// FlexCDN URL。
ApiUrl string `json:"apiUrl"`
// FlexCDN 用户角色。
// 可取值 "user"、"admin"。
ApiRole string `json:"apiRole"`
// FlexCDN AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// FlexCDN AccessKey。
AccessKey string `json:"accessKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId int64 `json:"certificateId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *flexcdnsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.CertificateId == 0 {
return errors.New("config `certificateId` is required")
}
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return err
}
// 修改证书
// REF: https://flexcdn.cloud/dev/api/service/SSLCertService?role=user#updateSSLCert
updateSSLCertReq := &flexcdnsdk.UpdateSSLCertRequest{
SSLCertId: d.config.CertificateId,
IsOn: true,
Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
Description: "upload from certimate",
ServerName: certX509.Subject.CommonName,
IsCA: false,
CertData: base64.StdEncoding.EncodeToString([]byte(certPEM)),
KeyData: base64.StdEncoding.EncodeToString([]byte(privkeyPEM)),
TimeBeginAt: certX509.NotBefore.Unix(),
TimeEndAt: certX509.NotAfter.Unix(),
DNSNames: certX509.DNSNames,
CommonNames: []string{certX509.Subject.CommonName},
}
updateSSLCertResp, err := d.sdkClient.UpdateSSLCert(updateSSLCertReq)
d.logger.Debug("sdk request 'flexcdn.UpdateSSLCert'", slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'flexcdn.UpdateSSLCert': %w", err)
}
return nil
}
func createSdkClient(apiUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*flexcdnsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid flexcdn api url")
}
if apiRole != "user" && apiRole != "admin" {
return nil, errors.New("invalid flexcdn api role")
}
if accessKeyId == "" {
return nil, errors.New("invalid flexcdn access key id")
}
if accessKey == "" {
return nil, errors.New("invalid flexcdn access key")
}
client := flexcdnsdk.NewClient(apiUrl, apiRole, accessKeyId, accessKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,83 @@
package flexcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/flexcdn"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fAccessKeyId string
fAccessKey string
fCertificateId int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_FLEXCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./flexcdn_test.go -args \
--CERTIMATE_DEPLOYER_FLEXCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_FLEXCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_FLEXCDN_APIURL="http://127.0.0.1:7788" \
--CERTIMATE_DEPLOYER_FLEXCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_FLEXCDN_ACCESSKEY="your-access-key" \
--CERTIMATE_DEPLOYER_FLEXCDN_CERTIFICATEID="your-cerficiate-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToCertificate", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiRole: "user",
AccessKeyId: fAccessKeyId,
AccessKey: fAccessKey,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_CERTIFICATE,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -24,7 +24,7 @@ type DeployerConfig struct {
// CDN 资源 ID。
ResourceId int64 `json:"resourceId"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId int64 `json:"certificateId,omitempty"`
}
@@ -112,7 +112,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
ValidateRootCA: false,
}
changeCertificateResp, err := d.sdkClients.SSLCerts.Update(context.TODO(), getCertificateDetailResp.ID, changeCertificateReq)
d.logger.Debug("sdk request 'sslcerts.Create'", slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp))
d.logger.Debug("sdk request 'sslcerts.Update'", slog.Any("sslId", getCertificateDetailResp.ID), slog.Any("request", changeCertificateReq), slog.Any("response", changeCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcerts.Update': %w", err)
}

View File

@@ -18,9 +18,12 @@ import (
type DeployerConfig struct {
// GoEdge URL。
ApiUrl string `json:"apiUrl"`
// GoEdge 用户 AccessKeyId
// GoEdge 用户角色
// 可取值 "user"、"admin"。
ApiRole string `json:"apiRole"`
// GoEdge AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// GoEdge 用户 AccessKey。
// GoEdge AccessKey。
AccessKey string `json:"accessKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
@@ -44,7 +47,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)
client, err := createSdkClient(config.ApiUrl, config.ApiRole, config.AccessKeyId, config.AccessKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
@@ -116,11 +119,15 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri
return nil
}
func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) {
func createSdkClient(apiUrl, apiRole, accessKeyId, accessKey string, skipTlsVerify bool) (*goedgesdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid goedge api url")
}
if apiRole != "user" && apiRole != "admin" {
return nil, errors.New("invalid goedge api role")
}
if accessKeyId == "" {
return nil, errors.New("invalid goedge access key id")
}
@@ -129,7 +136,7 @@ func createSdkClient(apiUrl, accessKeyId, accessKey string, skipTlsVerify bool)
return nil, errors.New("invalid goedge access key")
}
client := goedgesdk.NewClient(apiUrl, "user", accessKeyId, accessKey)
client := goedgesdk.NewClient(apiUrl, apiRole, accessKeyId, accessKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}

View File

@@ -17,7 +17,7 @@ var (
fApiUrl string
fAccessKeyId string
fAccessKey string
fCertificateId int
fCertificateId int64
)
func init() {
@@ -28,7 +28,7 @@ func init() {
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "")
flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "")
}
/*
@@ -45,7 +45,7 @@ Shell command to run this test:
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Run("Deploy_ToCertificate", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
@@ -58,11 +58,12 @@ func TestDeploy(t *testing.T) {
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiRole: "user",
AccessKeyId: fAccessKeyId,
AccessKey: fAccessKey,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_CERTIFICATE,
CertificateId: int64(fCertificateId),
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)

View File

@@ -210,7 +210,6 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, certPEM str
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.modifyListenerCertificate(ctx, listenerId, upres.CertId); err != nil {
errs = append(errs, err)

View File

@@ -0,0 +1,8 @@
package lecdn
type ResourceType string
const (
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@@ -0,0 +1,176 @@
package lecdn
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"time"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
leclientsdkv3 "github.com/usual2970/certimate/internal/pkg/sdk3rd/lecdn/v3/client"
lemastersdkv3 "github.com/usual2970/certimate/internal/pkg/sdk3rd/lecdn/v3/master"
)
type DeployerConfig struct {
// LeCDN URL。
ApiUrl string `json:"apiUrl"`
// LeCDN 版本。
// 可取值 "v3"。
ApiVersion string `json:"apiVersion"`
// LeCDN 用户角色。
// 可取值 "client"、"master"。
ApiRole string `json:"apiRole"`
// LeCDN 用户名。
Username string `json:"accessKeyId"`
// LeCDN 用户密码。
Password string `json:"accessKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId int64 `json:"certificateId,omitempty"`
// 客户 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时选填。
ClientId int64 `json:"clientId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient interface{}
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
const (
apiVersionV3 = "v3"
apiRoleClient = "client"
apiRoleMaster = "master"
)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiVersion, config.ApiRole, config.Username, config.Password, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 根据部署资源类型决定部署方式
switch d.config.ResourceType {
case RESOURCE_TYPE_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
if d.config.CertificateId == 0 {
return errors.New("config `certificateId` is required")
}
// 修改证书
// REF: https://wdk0pwf8ul.feishu.cn/wiki/YE1XwCRIHiLYeKkPupgcXrlgnDd
switch sdkClient := d.sdkClient.(type) {
case *leclientsdkv3.Client:
updateSSLCertReq := &leclientsdkv3.UpdateCertificateRequest{
Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
Description: "upload from certimate",
Type: "upload",
SSLPEM: certPEM,
SSLKey: privkeyPEM,
AutoRenewal: false,
}
updateSSLCertResp, err := sdkClient.UpdateCertificate(d.config.CertificateId, updateSSLCertReq)
d.logger.Debug("sdk request 'lecdn.UpdateCertificate'", slog.Int64("certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'lecdn.UpdateCertificate': %w", err)
}
case *lemastersdkv3.Client:
updateSSLCertReq := &lemastersdkv3.UpdateCertificateRequest{
ClientId: d.config.ClientId,
Name: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
Description: "upload from certimate",
Type: "upload",
SSLPEM: certPEM,
SSLKey: privkeyPEM,
AutoRenewal: false,
}
updateSSLCertResp, err := sdkClient.UpdateCertificate(d.config.CertificateId, updateSSLCertReq)
d.logger.Debug("sdk request 'lecdn.UpdateCertificate'", slog.Int64("certId", d.config.CertificateId), slog.Any("request", updateSSLCertReq), slog.Any("response", updateSSLCertResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'lecdn.UpdateCertificate': %w", err)
}
default:
panic("sdk client is not implemented")
}
return nil
}
func createSdkClient(apiUrl, apiVersion, apiRole, username, password string, skipTlsVerify bool) (interface{}, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid lecdn api url")
}
if username == "" {
return nil, errors.New("invalid lecdn username")
}
if password == "" {
return nil, errors.New("invalid lecdn password")
}
if apiVersion == apiVersionV3 && apiRole == apiRoleClient {
// v3 版客户端
client := leclientsdkv3.NewClient(apiUrl, username, password)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
} else if apiVersion == apiVersionV3 && apiRole == apiRoleMaster {
// v3 版主控端
client := lemastersdkv3.NewClient(apiUrl, username, password)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}
return nil, fmt.Errorf("invalid lecdn api version or user role")
}

View File

@@ -0,0 +1,87 @@
package lecdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/lecdn"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fApiVersion string
fUsername string
fPassword string
fCertificateId int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_LECDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiVersion, argsPrefix+"APIVERSION", "v3", "")
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./lecdn_test.go -args \
--CERTIMATE_DEPLOYER_LECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_LECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_LECDN_APIURL="http://127.0.0.1:5090" \
--CERTIMATE_DEPLOYER_LECDN_USERNAME="your-username" \
--CERTIMATE_DEPLOYER_LECDN_PASSWORD="your-password" \
--CERTIMATE_DEPLOYER_LECDN_CERTIFICATEID="your-cerficiate-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToCertificate", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("APIVERSION: %v", fApiVersion),
fmt.Sprintf("USERNAME: %v", fUsername),
fmt.Sprintf("PASSWORD: %v", fPassword),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
ApiVersion: fApiVersion,
ApiRole: "user",
Username: fUsername,
Password: fPassword,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_CERTIFICATE,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -25,6 +25,12 @@ type DeployerConfig struct {
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出服务器证书文件路径。
// 选填。
OutputServerCertPath string `json:"outputServerCertPath,omitempty"`
// 输出中间证书文件路径。
// 选填。
OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
@@ -69,6 +75,12 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 执行前置命令
if d.config.PreCommand != "" {
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
@@ -86,6 +98,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
d.logger.Info("ssl certificate file saved", slog.String("path", d.config.OutputCertPath))
if d.config.OutputServerCertPath != "" {
if err := fileutil.WriteString(d.config.OutputServerCertPath, serverCertPEM); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
}
d.logger.Info("ssl server certificate file saved", slog.String("path", d.config.OutputServerCertPath))
}
if d.config.OutputIntermediaCertPath != "" {
if err := fileutil.WriteString(d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
}
d.logger.Info("ssl intermedia certificate file saved", slog.String("path", d.config.OutputIntermediaCertPath))
}
if err := fileutil.WriteString(d.config.OutputKeyPath, privkeyPEM); err != nil {
return nil, fmt.Errorf("failed to save private key file: %w", err)
}

View File

@@ -0,0 +1,89 @@
package netlifysite
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
netlifysdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/netlify"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type DeployerConfig struct {
// netlify API Token。
ApiToken string `json:"apiToken"`
// netlify 网站 ID。
SiteId string `json:"siteId"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *netlifysdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiToken)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.SiteId == "" {
return nil, errors.New("config `siteId` is required")
}
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 上传网站证书
// REF: https://open-api.netlify.com/#tag/sniCertificate/operation/provisionSiteTLSCertificate
provisionSiteTLSCertificateReq := &netlifysdk.ProvisionSiteTLSCertificateParams{
Certificate: serverCertPEM,
CACertificates: intermediaCertPEM,
Key: privkeyPEM,
}
provisionSiteTLSCertificateResp, err := d.sdkClient.ProvisionSiteTLSCertificate(d.config.SiteId, provisionSiteTLSCertificateReq)
d.logger.Debug("sdk request 'netlify.provisionSiteTLSCertificate'", slog.String("siteId", d.config.SiteId), slog.Any("request", provisionSiteTLSCertificateReq), slog.Any("response", provisionSiteTLSCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'netlify.provisionSiteTLSCertificate': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiToken string) (*netlifysdk.Client, error) {
if apiToken == "" {
return nil, errors.New("invalid netlify api token")
}
client := netlifysdk.NewClient(apiToken)
return client, nil
}

View File

@@ -0,0 +1,70 @@
package netlifysite_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/netlify-site"
)
var (
fInputCertPath string
fInputKeyPath string
fApiToken string
fSiteId int64
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_NETLIFYSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.Int64Var(&fSiteId, argsPrefix+"SITEID", 0, "")
}
/*
Shell command to run this test:
go test -v ./netlify_site_test.go -args \
--CERTIMATE_DEPLOYER_NETLIFYSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_NETLIFYSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_NETLIFYSITE_APITOKEN="your-api-token" \
--CERTIMATE_DEPLOYER_NETLIFYSITE_SITEID="your-site-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APITOKEN: %v", fApiToken),
fmt.Sprintf("SITEID: %v", fSiteId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiToken: fApiToken,
SiteId: fSiteId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,94 @@
package ratpanelconsole
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
rpsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/ratpanel"
)
type DeployerConfig struct {
// 耗子面板地址。
ApiUrl string `json:"apiUrl"`
// 耗子面板访问令牌 ID。
AccessTokenId int32 `json:"accessTokenId"`
// 耗子面板访问令牌。
AccessToken string `json:"accessToken"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *rpsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.AccessTokenId, config.AccessToken, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 设置面板 SSL 证书
settingCertReq := &rpsdk.SettingCertRequest{
Certificate: certPEM,
PrivateKey: privkeyPEM,
}
settingCertResp, err := d.sdkClient.SettingCert(settingCertReq)
d.logger.Debug("sdk request 'ratpanel.SettingCert'", slog.Any("request", settingCertReq), slog.Any("response", settingCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ratpanel.SettingCert': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiUrl string, accessTokenId int32, accessToken string, skipTlsVerify bool) (*rpsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid ratpanel api url")
}
if accessTokenId == 0 {
return nil, errors.New("invalid ratpanel access token id")
}
if accessToken == "" {
return nil, errors.New("invalid ratpanel access token")
}
client := rpsdk.NewClient(apiUrl, accessTokenId, accessToken)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,76 @@
package ratpanelconsole_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ratpanel-console"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fAccessTokenId int64
fAccessToken string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_RATPANELCONSOLE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.Int64Var(&fAccessTokenId, argsPrefix+"ACCESSTOKENID", 0, "")
flag.StringVar(&fAccessToken, argsPrefix+"ACCESSTOKEN", "", "")
}
/*
Shell command to run this test:
go test -v ./ratpanel_console_test.go -args \
--CERTIMATE_DEPLOYER_RATPANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_RATPANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_RATPANELCONSOLE_APIURL="http://127.0.0.1:8888" \
--CERTIMATE_DEPLOYER_RATPANELCONSOLE_ACCESSTOKENID="your-access-token-id" \
--CERTIMATE_DEPLOYER_RATPANELCONSOLE_ACCESSTOKEN="your-access-token"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("ACCESSTOKENID: %v", fAccessTokenId),
fmt.Sprintf("ACCESSTOKEN: %v", fAccessToken),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
AccessTokenId: int32(fAccessTokenId),
AccessToken: fAccessToken,
AllowInsecureConnections: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,101 @@
package ratpanelsite
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
rpsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/ratpanel"
)
type DeployerConfig struct {
// 耗子面板地址。
ApiUrl string `json:"apiUrl"`
// 耗子面板访问令牌 ID。
AccessTokenId int32 `json:"accessTokenId"`
// 耗子面板访问令牌。
AccessToken string `json:"accessToken"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 网站名称。
SiteName string `json:"siteName"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *rpsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.AccessTokenId, config.AccessToken, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.SiteName == "" {
return nil, errors.New("config `siteName` is required")
}
// 设置站点 SSL 证书
websiteCertReq := &rpsdk.WebsiteCertRequest{
SiteName: d.config.SiteName,
Certificate: certPEM,
PrivateKey: privkeyPEM,
}
websiteCertResp, err := d.sdkClient.WebsiteCert(websiteCertReq)
d.logger.Debug("sdk request 'ratpanel.WebsiteCert'", slog.Any("request", websiteCertReq), slog.Any("response", websiteCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ratpanel.WebsiteCert': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiUrl string, accessTokenId int32, accessToken string, skipTlsVerify bool) (*rpsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid ratpanel api url")
}
if accessTokenId == 0 {
return nil, errors.New("invalid ratpanel access token id")
}
if accessToken == "" {
return nil, errors.New("invalid ratpanel access token")
}
client := rpsdk.NewClient(apiUrl, accessTokenId, accessToken)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,81 @@
package ratpanelsite_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ratpanel-site"
)
var (
fInputCertPath string
fInputKeyPath string
fApiUrl string
fAccessTokenId int64
fAccessToken string
fSiteName string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_RATPANELSITE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.Int64Var(&fAccessTokenId, argsPrefix+"ACCESSTOKENID", 0, "")
flag.StringVar(&fAccessToken, argsPrefix+"ACCESSTOKEN", "", "")
flag.StringVar(&fSiteName, argsPrefix+"SITENAME", "", "")
}
/*
Shell command to run this test:
go test -v ./ratpanel_site_test.go -args \
--CERTIMATE_DEPLOYER_RATPANELSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_RATPANELSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_RATPANELSITE_APIURL="http://127.0.0.1:8888" \
--CERTIMATE_DEPLOYER_RATPANELSITE_ACCESSTOKENID="your-access-token-id" \
--CERTIMATE_DEPLOYER_RATPANELSITE_ACCESSTOKEN="your-access-token" \
--CERTIMATE_DEPLOYER_RATPANELSITE_SITENAME="your-site-name"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIURL: %v", fApiUrl),
fmt.Sprintf("ACCESSTOKENID: %v", fAccessTokenId),
fmt.Sprintf("ACCESSTOKEN: %v", fAccessToken),
fmt.Sprintf("SITENAME: %v", fSiteName),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ApiUrl: fApiUrl,
AccessTokenId: int32(fAccessTokenId),
AccessToken: fAccessToken,
AllowInsecureConnections: true,
SiteName: fSiteName,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -16,7 +16,7 @@ var (
fInputKeyPath string
fApiUrl string
fApiToken string
fCertificateId int
fCertificateId int64
)
func init() {
@@ -26,7 +26,7 @@ func init() {
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiUrl, argsPrefix+"APIURL", "", "")
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.IntVar(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "")
flag.Int64Var(&fCertificateId, argsPrefix+"CERTIFICATEID", 0, "")
}
/*

View File

@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"log/slog"
"net"
"os"
"path/filepath"
@@ -16,6 +17,23 @@ import (
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type JumpServerConfig struct {
// SSH 主机。
// 零值时默认为 "localhost"。
SshHost string `json:"sshHost,omitempty"`
// SSH 端口。
// 零值时默认为 22。
SshPort int32 `json:"sshPort,omitempty"`
// SSH 登录用户名。
SshUsername string `json:"sshUsername,omitempty"`
// SSH 登录密码。
SshPassword string `json:"sshPassword,omitempty"`
// SSH 登录私钥。
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
}
type DeployerConfig struct {
// SSH 主机。
// 零值时默认为 "localhost"。
@@ -31,6 +49,8 @@ type DeployerConfig struct {
SshKey string `json:"sshKey,omitempty"`
// SSH 登录私钥口令。
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
// 跳板机配置数组。
JumpServers []JumpServerConfig `json:"jumpServers,omitempty"`
// 是否回退使用 SCP。
UseSCP bool `json:"useSCP,omitempty"`
// 前置命令。
@@ -41,6 +61,12 @@ type DeployerConfig struct {
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
// 输出证书文件路径。
OutputCertPath string `json:"outputCertPath,omitempty"`
// 输出服务器证书文件路径。
// 选填。
OutputServerCertPath string `json:"outputServerCertPath,omitempty"`
// 输出中间证书文件路径。
// 选填。
OutputIntermediaCertPath string `json:"outputIntermediaCertPath,omitempty"`
// 输出私钥文件路径。
OutputKeyPath string `json:"outputKeyPath,omitempty"`
// PFX 导出密码。
@@ -85,8 +111,67 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 连接
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
var targetConn net.Conn
// 连接到跳板机
if len(d.config.JumpServers) > 0 {
var jumpClient *ssh.Client
for i, jumpServerConf := range d.config.JumpServers {
d.logger.Info(fmt.Sprintf("connecting to jump server [%d]", i+1), slog.String("host", jumpServerConf.SshHost))
var jumpConn net.Conn
// 第一个连接是主机发起,后续通过跳板机发起
if jumpClient == nil {
jumpConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", jumpServerConf.SshHost, jumpServerConf.SshPort))
} else {
jumpConn, err = jumpClient.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", jumpServerConf.SshHost, jumpServerConf.SshPort))
}
if err != nil {
return nil, fmt.Errorf("failed to connect to jump server [%d]: %w", i+1, err)
}
defer jumpConn.Close()
newClient, err := createSshClient(
jumpConn,
jumpServerConf.SshHost,
jumpServerConf.SshPort,
jumpServerConf.SshUsername,
jumpServerConf.SshPassword,
jumpServerConf.SshKey,
jumpServerConf.SshKeyPassphrase,
)
if err != nil {
return nil, fmt.Errorf("failed to create jump server ssh client[%d]: %w", i+1, err)
}
defer newClient.Close()
jumpClient = newClient
d.logger.Info(fmt.Sprintf("jump server connected [%d]", i+1), slog.String("host", jumpServerConf.SshHost))
}
// 通过跳板机发起 TCP 连接到目标服务器
targetConn, err = jumpClient.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", d.config.SshHost, d.config.SshPort))
if err != nil {
return nil, fmt.Errorf("failed to connect to target server: %w", err)
}
} else {
// 直接发起 TCP 连接到目标服务器
targetConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", d.config.SshHost, d.config.SshPort))
if err != nil {
return nil, fmt.Errorf("failed to connect to target server: %w", err)
}
}
defer targetConn.Close()
// 通过已有的连接创建目标服务器 SSH 客户端
client, err := createSshClient(
targetConn,
d.config.SshHost,
d.config.SshPort,
d.config.SshUsername,
@@ -118,6 +203,20 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
d.logger.Info("ssl certificate file uploaded", slog.String("path", d.config.OutputCertPath))
if d.config.OutputServerCertPath != "" {
if err := writeFileString(client, d.config.UseSCP, d.config.OutputServerCertPath, serverCertPEM); err != nil {
return nil, fmt.Errorf("failed to save server certificate file: %w", err)
}
d.logger.Info("ssl server certificate file uploaded", slog.String("path", d.config.OutputServerCertPath))
}
if d.config.OutputIntermediaCertPath != "" {
if err := writeFileString(client, d.config.UseSCP, d.config.OutputIntermediaCertPath, intermediaCertPEM); err != nil {
return nil, fmt.Errorf("failed to save intermedia certificate file: %w", err)
}
d.logger.Info("ssl intermedia certificate file uploaded", slog.String("path", d.config.OutputIntermediaCertPath))
}
if err := writeFileString(client, d.config.UseSCP, d.config.OutputKeyPath, privkeyPEM); err != nil {
return nil, fmt.Errorf("failed to upload private key file: %w", err)
}
@@ -163,7 +262,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return &deployer.DeployResult{}, nil
}
func createSshClient(host string, port int32, username string, password string, key string, keyPassphrase string) (*ssh.Client, error) {
func createSshClient(conn net.Conn, host string, port int32, username string, password string, key string, keyPassphrase string) (*ssh.Client, error) {
if host == "" {
host = "localhost"
}
@@ -191,11 +290,16 @@ func createSshClient(host string, port int32, username string, password string,
authMethod = ssh.Password(password)
}
return ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
return nil, err
}
return ssh.NewClient(sshConn, chans, reqs), nil
}
func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) {

View File

@@ -15,7 +15,7 @@ var (
fInputCertPath string
fInputKeyPath string
fSshHost string
fSshPort int
fSshPort int64
fSshUsername string
fSshPassword string
fOutputCertPath string
@@ -28,7 +28,7 @@ func init() {
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSshHost, argsPrefix+"SSHHOST", "", "")
flag.IntVar(&fSshPort, argsPrefix+"SSHPORT", 0, "")
flag.Int64Var(&fSshPort, argsPrefix+"SSHPORT", 0, "")
flag.StringVar(&fSshUsername, argsPrefix+"SSHUSERNAME", "", "")
flag.StringVar(&fSshPassword, argsPrefix+"SSHPASSWORD", "", "")
flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "")

View File

@@ -2,9 +2,11 @@ package tencentcloudcdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
tccdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
@@ -132,6 +134,49 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DeployCertificateInstance': %w", err)
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()
describeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf("%d", *deployCertificateInstanceResp.Response.DeployRecordId))
describeHostDeployRecordDetailResp, err := d.sdkClients.SSL.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq)
d.logger.Debug("sdk request 'ssl.DescribeHostDeployRecordDetail'", slog.Any("request", describeHostDeployRecordDetailReq), slog.Any("response", describeHostDeployRecordDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w", err)
}
var runningCount, succeededCount, failedCount, totalCount int64
if describeHostDeployRecordDetailResp.Response.TotalCount == nil {
return nil, errors.New("unexpected deployment job status")
} else {
if describeHostDeployRecordDetailResp.Response.RunningTotalCount != nil {
runningCount = *describeHostDeployRecordDetailResp.Response.RunningTotalCount
}
if describeHostDeployRecordDetailResp.Response.SuccessTotalCount != nil {
succeededCount = *describeHostDeployRecordDetailResp.Response.SuccessTotalCount
}
if describeHostDeployRecordDetailResp.Response.FailedTotalCount != nil {
failedCount = *describeHostDeployRecordDetailResp.Response.FailedTotalCount
}
if describeHostDeployRecordDetailResp.Response.TotalCount != nil {
totalCount = *describeHostDeployRecordDetailResp.Response.TotalCount
}
if succeededCount+failedCount == totalCount {
break
}
}
d.logger.Info(fmt.Sprintf("waiting for deployment job completion (running: %d, succeeded: %d, failed: %d, total: %d) ...", runningCount, succeededCount, failedCount, totalCount))
time.Sleep(time.Second * 5)
}
}
return &deployer.DeployResult{}, nil

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"time"
tcclb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
@@ -151,6 +152,49 @@ func (d *DeployerProvider) deployViaSslService(ctx context.Context, cloudCertId
return fmt.Errorf("failed to execute sdk request 'ssl.DeployCertificateInstance': %w", err)
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()
describeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf("%d", *deployCertificateInstanceResp.Response.DeployRecordId))
describeHostDeployRecordDetailResp, err := d.sdkClients.SSL.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq)
d.logger.Debug("sdk request 'ssl.DescribeHostDeployRecordDetail'", slog.Any("request", describeHostDeployRecordDetailReq), slog.Any("response", describeHostDeployRecordDetailResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w", err)
}
var runningCount, succeededCount, failedCount, totalCount int64
if describeHostDeployRecordDetailResp.Response.TotalCount == nil {
return errors.New("unexpected deployment job status")
} else {
if describeHostDeployRecordDetailResp.Response.RunningTotalCount != nil {
runningCount = *describeHostDeployRecordDetailResp.Response.RunningTotalCount
}
if describeHostDeployRecordDetailResp.Response.SuccessTotalCount != nil {
succeededCount = *describeHostDeployRecordDetailResp.Response.SuccessTotalCount
}
if describeHostDeployRecordDetailResp.Response.FailedTotalCount != nil {
failedCount = *describeHostDeployRecordDetailResp.Response.FailedTotalCount
}
if describeHostDeployRecordDetailResp.Response.TotalCount != nil {
totalCount = *describeHostDeployRecordDetailResp.Response.TotalCount
}
if succeededCount+failedCount == totalCount {
break
}
}
d.logger.Info(fmt.Sprintf("waiting for deployment job completion (running: %d, succeeded: %d, failed: %d, total: %d) ...", runningCount, succeededCount, failedCount, totalCount))
time.Sleep(time.Second * 5)
}
return nil
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log/slog"
"time"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
@@ -102,6 +103,49 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DeployCertificateInstance': %w", err)
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()
describeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf("%d", *deployCertificateInstanceResp.Response.DeployRecordId))
describeHostDeployRecordDetailResp, err := d.sdkClient.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq)
d.logger.Debug("sdk request 'ssl.DescribeHostDeployRecordDetail'", slog.Any("request", describeHostDeployRecordDetailReq), slog.Any("response", describeHostDeployRecordDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w", err)
}
var runningCount, succeededCount, failedCount, totalCount int64
if describeHostDeployRecordDetailResp.Response.TotalCount == nil {
return nil, errors.New("unexpected deployment job status")
} else {
if describeHostDeployRecordDetailResp.Response.RunningTotalCount != nil {
runningCount = *describeHostDeployRecordDetailResp.Response.RunningTotalCount
}
if describeHostDeployRecordDetailResp.Response.SuccessTotalCount != nil {
succeededCount = *describeHostDeployRecordDetailResp.Response.SuccessTotalCount
}
if describeHostDeployRecordDetailResp.Response.FailedTotalCount != nil {
failedCount = *describeHostDeployRecordDetailResp.Response.FailedTotalCount
}
if describeHostDeployRecordDetailResp.Response.TotalCount != nil {
totalCount = *describeHostDeployRecordDetailResp.Response.TotalCount
}
if succeededCount+failedCount == totalCount {
break
}
}
d.logger.Info(fmt.Sprintf("waiting for deployment job completion (running: %d, succeeded: %d, failed: %d, total: %d) ...", runningCount, succeededCount, failedCount, totalCount))
time.Sleep(time.Second * 5)
}
return &deployer.DeployResult{}, nil
}

View File

@@ -2,9 +2,11 @@ package tencentcloudecdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"time"
tccdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
@@ -103,7 +105,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
} else {
d.logger.Info("found ecdn instances to deploy", slog.Any("instanceIds", instanceIds))
// 证书部署到 ECDN 实例
// 证书部署到 CDN 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
@@ -115,6 +117,49 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DeployCertificateInstance': %w", err)
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()
describeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf("%d", *deployCertificateInstanceResp.Response.DeployRecordId))
describeHostDeployRecordDetailResp, err := d.sdkClients.SSL.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq)
d.logger.Debug("sdk request 'ssl.DescribeHostDeployRecordDetail'", slog.Any("request", describeHostDeployRecordDetailReq), slog.Any("response", describeHostDeployRecordDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w", err)
}
var runningCount, succeededCount, failedCount, totalCount int64
if describeHostDeployRecordDetailResp.Response.TotalCount == nil {
return nil, errors.New("unexpected deployment job status")
} else {
if describeHostDeployRecordDetailResp.Response.RunningTotalCount != nil {
runningCount = *describeHostDeployRecordDetailResp.Response.RunningTotalCount
}
if describeHostDeployRecordDetailResp.Response.SuccessTotalCount != nil {
succeededCount = *describeHostDeployRecordDetailResp.Response.SuccessTotalCount
}
if describeHostDeployRecordDetailResp.Response.FailedTotalCount != nil {
failedCount = *describeHostDeployRecordDetailResp.Response.FailedTotalCount
}
if describeHostDeployRecordDetailResp.Response.TotalCount != nil {
totalCount = *describeHostDeployRecordDetailResp.Response.TotalCount
}
if succeededCount+failedCount == totalCount {
break
}
}
d.logger.Info(fmt.Sprintf("waiting for deployment job completion (running: %d, succeeded: %d, failed: %d, total: %d) ...", runningCount, succeededCount, failedCount, totalCount))
time.Sleep(time.Second * 5)
}
}
return &deployer.DeployResult{}, nil

View File

@@ -116,30 +116,35 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()
describeHostDeployRecordDetailReq.DeployRecordId = common.StringPtr(fmt.Sprintf("%d", *deployCertificateInstanceResp.Response.DeployRecordId))
describeHostDeployRecordDetailReq.Limit = common.Uint64Ptr(100)
describeHostDeployRecordDetailResp, err := d.sdkClient.DescribeHostDeployRecordDetail(describeHostDeployRecordDetailReq)
d.logger.Debug("sdk request 'ssl.DescribeHostDeployRecordDetail'", slog.Any("request", describeHostDeployRecordDetailReq), slog.Any("response", describeHostDeployRecordDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeHostDeployRecordDetail': %w", err)
}
var runningCount, succeededCount, failedCount, totalCount int64
if describeHostDeployRecordDetailResp.Response.TotalCount == nil {
return nil, errors.New("unexpected deployment job status")
} else {
acc := int64(0)
if describeHostDeployRecordDetailResp.Response.RunningTotalCount != nil {
runningCount = *describeHostDeployRecordDetailResp.Response.RunningTotalCount
}
if describeHostDeployRecordDetailResp.Response.SuccessTotalCount != nil {
acc += *describeHostDeployRecordDetailResp.Response.SuccessTotalCount
succeededCount = *describeHostDeployRecordDetailResp.Response.SuccessTotalCount
}
if describeHostDeployRecordDetailResp.Response.FailedTotalCount != nil {
acc += *describeHostDeployRecordDetailResp.Response.FailedTotalCount
failedCount = *describeHostDeployRecordDetailResp.Response.FailedTotalCount
}
if describeHostDeployRecordDetailResp.Response.TotalCount != nil {
totalCount = *describeHostDeployRecordDetailResp.Response.TotalCount
}
if acc == *describeHostDeployRecordDetailResp.Response.TotalCount {
if succeededCount+failedCount == totalCount {
break
}
}
d.logger.Info("waiting for deployment job completion ...")
d.logger.Info(fmt.Sprintf("waiting for deployment job completion (running: %d, succeeded: %d, failed: %d, total: %d) ...", runningCount, succeededCount, failedCount, totalCount))
time.Sleep(time.Second * 5)
}

View File

@@ -0,0 +1,104 @@
package wangsucdn
import (
"context"
"errors"
"fmt"
"log/slog"
"strconv"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate"
wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdn"
)
type DeployerConfig struct {
// 网宿云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 网宿云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 加速域名数组。
Domains []string `json:"domains"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *wangsusdk.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到证书管理
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 批量修改域名证书配置
// REF: https://www.wangsu.com/document/api-doc/37447
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
batchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{
CertificateId: certId,
DomainNames: d.config.Domains,
}
batchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfig(batchUpdateCertificateConfigReq)
d.logger.Debug("sdk request 'cdn.BatchUpdateCertificateConfig'", slog.Any("request", batchUpdateCertificateConfigReq), slog.Any("response", batchUpdateCertificateConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.BatchUpdateCertificateConfig': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) {
if accessKeyId == "" {
return nil, errors.New("invalid wangsu access key id")
}
if accessKeySecret == "" {
return nil, errors.New("invalid wangsu access key secret")
}
return wangsusdk.NewClient(accessKeyId, accessKeySecret), nil
}

View File

@@ -0,0 +1,75 @@
package wangsucdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./wangsu_cdn_test.go -args \
--CERTIMATE_DEPLOYER_WANGSUCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_WANGSUCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_WANGSUCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_WANGSUCDN_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_WANGSUCDN_DOMAIN="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Domains: []string{fDomain},
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -17,7 +17,7 @@ import (
"time"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
wangsucdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdn"
wangsucdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdnpro"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
@@ -34,7 +34,7 @@ type DeployerConfig struct {
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
// Webhook ID。
// 选填。
@@ -88,9 +88,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
// 查询已部署加速域名的详情
getHostnameDetailResp, err := d.sdkClient.GetHostnameDetail(d.config.Domain)
d.logger.Debug("sdk request 'cdn.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp))
d.logger.Debug("sdk request 'cdnpro.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetHostnameDetail': %w", err)
return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.GetHostnameDetail': %w", err)
}
// 生成网宿云证书参数
@@ -126,9 +126,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
NewVersion: certificateNewVersionInfo,
}
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
d.logger.Debug("sdk request 'cdnpro.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.CreateCertificate': %w", err)
return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.CreateCertificate': %w", err)
}
wangsuCertUrl = createCertificateResp.CertificateUrl
@@ -149,9 +149,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
NewVersion: certificateNewVersionInfo,
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp))
d.logger.Debug("sdk request 'cdnpro.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.UpdateCertificate': %w", err)
return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.UpdateCertificate': %w", err)
}
wangsuCertUrl = updateCertificateResp.CertificateUrl
@@ -186,9 +186,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
createDeploymentTaskReq.Webhook = typeutil.ToPtr(d.config.WebhookId)
}
createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTask(createDeploymentTaskReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp))
d.logger.Debug("sdk request 'cdnpro.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.CreateDeploymentTask': %w", err)
return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.CreateDeploymentTask': %w", err)
}
// 循环获取部署任务详细信息,等待任务状态变更
@@ -206,9 +206,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId)
d.logger.Info("sdk request 'cdn.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp))
d.logger.Info("sdk request 'cdnpro.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDeploymentTaskDetail': %w", err)
return nil, fmt.Errorf("failed to execute sdk request 'cdnpro.GetDeploymentTaskDetail': %w", err)
}
if getDeploymentTaskDetailResp.Status == "failed" {

View File

@@ -0,0 +1,109 @@
package wangsucertificate
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate"
wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/certificate"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 网宿云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 网宿云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 证书 ID。
// 选填。零值时表示新建证书;否则表示更新证书。
CertificateId string `json:"certificateId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *wangsusdk.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
AccessKeySecret: config.AccessKeySecret,
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.CertificateId == "" {
// 上传证书到证书管理
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
} else {
// 修改证书
// REF: https://www.wangsu.com/document/api-doc/25568?productCode=certificatemanagement
updateCertificateReq := &wangsusdk.UpdateCertificateRequest{
Name: typeutil.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
Certificate: typeutil.ToPtr(certPEM),
PrivateKey: typeutil.ToPtr(privkeyPEM),
Comment: typeutil.ToPtr("upload from certimate"),
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq)
d.logger.Debug("sdk request 'certificatemanagement.UpdateCertificate'", slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'certificatemanagement.CreateCertificate': %w", err)
}
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) {
if accessKeyId == "" {
return nil, errors.New("invalid wangsu access key id")
}
if accessKeySecret == "" {
return nil, errors.New("invalid wangsu access key secret")
}
return wangsusdk.NewClient(accessKeyId, accessKeySecret), nil
}

View File

@@ -0,0 +1,75 @@
package wangsucertificate_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-certificate"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fCertificateId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
}
/*
Shell command to run this test:
go test -v ./wangsu_certificate_test.go -args \
--CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_CERTIFICATEID="your-certificate-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
CertificateId: fCertificateId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -75,6 +75,12 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return nil, fmt.Errorf("failed to parse x509: %w", err)
}
// 提取服务器证书和中间证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 处理 Webhook URL
webhookUrl, err := url.Parse(d.config.WebhookUrl)
if err != nil {
@@ -134,6 +140,8 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM)
replaceJsonValueRecursively(webhookData, "${SERVER_CERTIFICATE}", serverCertPEM)
replaceJsonValueRecursively(webhookData, "${INTERMEDIA_CERTIFICATE}", intermediaCertPEM)
replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM)
if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {
@@ -151,9 +159,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
// 生成请求
// 其中 GET 请求需转换为查询参数
req := d.httpClient.R().
SetContext(ctx).
SetHeaderMultiValues(webhookHeaders)
req := d.httpClient.R().SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
@@ -170,7 +176,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
// 发送请求
resp, err := req.SetDebug(true).Send()
resp, err := req.Send()
if err != nil {
return nil, fmt.Errorf("failed to send webhook request: %w", err)
} else if resp.IsError() {

View File

@@ -2,10 +2,10 @@ package bark
import (
"context"
"fmt"
"log/slog"
"github.com/nikoksr/notify"
"github.com/nikoksr/notify/service/bark"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -19,8 +19,9 @@ type NotifierConfig struct {
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
config *NotifierConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -30,9 +31,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
@@ -46,16 +50,25 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
var srv notify.Notifier
if n.config.ServerUrl == "" {
srv = bark.New(n.config.DeviceKey)
} else {
srv = bark.NewWithServers(n.config.DeviceKey, n.config.ServerUrl)
const defaultServerURL = "https://api.day.app/"
serverUrl := defaultServerURL
if n.config.ServerUrl != "" {
serverUrl = n.config.ServerUrl
}
err = srv.Send(ctx, subject, message)
// REF: https://bark.day.app/#/tutorial
req := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"title": subject,
"body": message,
"device_key": n.config.DeviceKey,
})
resp, err := req.Post(serverUrl)
if err != nil {
return nil, err
return nil, fmt.Errorf("bark api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("bark api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &notifier.NotifyResult{}, nil

View File

@@ -1,4 +1,4 @@
package dingtalk
package dingtalkbot
import (
"context"
@@ -6,7 +6,7 @@ import (
"log/slog"
"net/url"
"github.com/nikoksr/notify/service/dingding"
"github.com/blinkbean/dingtalk"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -48,17 +48,18 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
webhookUrl, err := url.Parse(n.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("invalid webhook url: %w", err)
return nil, fmt.Errorf("dingtalk api error: invalid webhook url: %w", err)
}
srv := dingding.New(&dingding.Config{
Token: webhookUrl.Query().Get("access_token"),
Secret: n.config.Secret,
})
var bot *dingtalk.DingTalk
if n.config.Secret == "" {
bot = dingtalk.InitDingTalk([]string{webhookUrl.Query().Get("access_token")}, "")
} else {
bot = dingtalk.InitDingTalkWithSecret(webhookUrl.Query().Get("access_token"), n.config.Secret)
}
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
if err := bot.SendTextMessage(subject + "\n" + message); err != nil {
return nil, fmt.Errorf("dingtalk api error: %w", err)
}
return &notifier.NotifyResult{}, nil

View File

@@ -1,4 +1,4 @@
package dingtalk_test
package dingtalkbot_test
import (
"context"
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalkbot"
)
const (
@@ -16,22 +16,23 @@ const (
)
var (
fAccessToken string
fSecret string
fWebhookUrl string
fSecret string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_DINGTALK_"
argsPrefix := "CERTIMATE_NOTIFIER_DINGTALKBOT_"
flag.StringVar(&fAccessToken, argsPrefix+"ACCESSTOKEN", "", "")
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
flag.StringVar(&fSecret, argsPrefix+"SECRET", "", "")
}
/*
Shell command to run this test:
go test -v ./dingtalk_test.go -args \
--CERTIMATE_NOTIFIER_DINGTALK_URL="https://example.com/your-webhook-url"
go test -v ./dingtalkbot_test.go -args \
--CERTIMATE_NOTIFIER_DINGTALKBOT_WEBHOOKURL="https://example.com/your-webhook-url" \
--CERTIMATE_NOTIFIER_DINGTALKBOT_SECRET="your-secret"
*/
func TestNotify(t *testing.T) {
flag.Parse()
@@ -39,13 +40,13 @@ func TestNotify(t *testing.T) {
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("ACCESSTOKEN: %v", fAccessToken),
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
fmt.Sprintf("SECRET: %v", fSecret),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
AccessToken: fAccessToken,
Secret: fSecret,
WebhookUrl: fWebhookUrl,
Secret: fSecret,
})
if err != nil {
t.Errorf("err: %+v", err)

View File

@@ -17,7 +17,7 @@ const (
var (
fSmtpHost string
fSmtpPort int
fSmtpPort int64
fSmtpTLS bool
fUsername string
fPassword string
@@ -29,7 +29,7 @@ func init() {
argsPrefix := "CERTIMATE_NOTIFIER_EMAIL_"
flag.StringVar(&fSmtpHost, argsPrefix+"SMTPHOST", "", "")
flag.IntVar(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "")
flag.Int64Var(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "")
flag.BoolVar(&fSmtpTLS, argsPrefix+"SMTPTLS", false, "")
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")

View File

@@ -1,20 +1,19 @@
package gotify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// Gotify 服务地址。
Url string `json:"url"`
ServerUrl string `json:"serverUrl"`
// Gotify Token。
Token string `json:"token"`
// Gotify 消息优先级。
@@ -24,7 +23,7 @@ type NotifierConfig struct {
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
httpClient *http.Client
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -34,10 +33,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: http.DefaultClient,
httpClient: client,
}, nil
}
@@ -51,45 +52,22 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
reqBody := &struct {
Title string `json:"title"`
Message string `json:"message"`
Priority int64 `json:"priority"`
}{
Title: subject,
Message: message,
Priority: n.config.Priority,
}
serverUrl := strings.TrimRight(n.config.ServerUrl, "/")
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("gotify api error: failed to encode message body: %w", err)
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("%s/message", n.config.Url),
bytes.NewReader(body),
)
if err != nil {
return nil, fmt.Errorf("gotify api error: failed to create new request: %w", err)
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", n.config.Token))
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err := n.httpClient.Do(req)
// REF: https://gotify.net/api-docs#/message/createMessage
req := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+n.config.Token).
SetBody(map[string]any{
"title": subject,
"message": message,
"priority": n.config.Priority,
})
resp, err := req.Post(fmt.Sprintf("%s/message", serverUrl))
if err != nil {
return nil, fmt.Errorf("gotify api error: failed to send request: %w", err)
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("gotify api error: failed to read response: %w", err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gotify api error: unexpected status code: %d, resp: %s", resp.StatusCode, string(result))
} else if resp.IsError() {
return nil, fmt.Errorf("gotify api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &notifier.NotifyResult{}, nil

View File

@@ -48,9 +48,9 @@ func TestNotify(t *testing.T) {
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Url: fUrl,
Token: fToken,
Priority: fPriority,
ServerUrl: fUrl,
Token: fToken,
Priority: fPriority,
})
if err != nil {
t.Errorf("err: %+v", err)

View File

@@ -1,10 +1,11 @@
package lark
package larkbot
import (
"context"
"fmt"
"log/slog"
"github.com/nikoksr/notify/service/lark"
"github.com/go-lark/lark"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -42,11 +43,17 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := lark.NewWebhookService(n.config.WebhookUrl)
err = srv.Send(ctx, subject, message)
bot := lark.NewNotificationBot(n.config.WebhookUrl)
content := lark.NewPostBuilder().
Title(subject).
TextTag(message, 1, false).
Render()
msg := lark.NewMsgBuffer(lark.MsgPost).Post(content)
resp, err := bot.PostNotificationV2(msg.Build())
if err != nil {
return nil, err
return nil, fmt.Errorf("lark api error: %w", err)
} else if resp.Code != 0 {
return nil, fmt.Errorf("lark api error: code='%d', message='%s'", resp.Code, resp.Msg)
}
return &notifier.NotifyResult{}, nil

View File

@@ -1,4 +1,4 @@
package serverchan_test
package larkbot_test
import (
"context"
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/larkbot"
)
const (
@@ -18,7 +18,7 @@ const (
var fWebhookUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_WECOM_"
argsPrefix := "CERTIMATE_NOTIFIER_LARKBOT_"
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
}
@@ -26,8 +26,8 @@ func init() {
/*
Shell command to run this test:
go test -v ./wecom_test.go -args \
--CERTIMATE_NOTIFIER_WECOM_WEBHOOKURL="https://example.com/your-webhook-url" \
go test -v ./larkbot_test.go -args \
--CERTIMATE_NOTIFIER_LARKBOT_WEBHOOKURL="https://example.com/your-webhook-url"
*/
func TestNotify(t *testing.T) {
flag.Parse()

View File

@@ -1,15 +1,13 @@
package mattermost
import (
"bytes"
"context"
"encoding/json"
"io"
"fmt"
"log/slog"
"net/http"
"strings"
"github.com/nikoksr/notify/service/mattermost"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -25,8 +23,9 @@ type NotifierConfig struct {
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
config *NotifierConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -36,9 +35,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
@@ -52,17 +54,29 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := mattermost.New(strings.TrimRight(n.config.ServerUrl, "/"))
serverUrl := strings.TrimRight(n.config.ServerUrl, "/")
if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil {
return nil, err
// REF: https://developers.mattermost.com/api-documentation/#/operations/Login
loginReq := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"login_id": n.config.Username,
"password": n.config.Password,
})
loginResp, err := loginReq.Post(fmt.Sprintf("%s/api/v4/users/login", serverUrl))
if err != nil {
return nil, fmt.Errorf("mattermost api error: failed to send request: %w", err)
} else if loginResp.IsError() {
return nil, fmt.Errorf("mattermost api error: unexpected status code: %d, resp: %s", loginResp.StatusCode(), loginResp.String())
} else if loginResp.Header().Get("Token") == "" {
return nil, fmt.Errorf("mattermost api error: received empty login token")
}
srv.AddReceivers(n.config.ChannelId)
// 复写消息样式
srv.PreSend(func(req *http.Request) error {
m := map[string]interface{}{
// REF: https://developers.mattermost.com/api-documentation/#/operations/CreatePost
postReq := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+loginResp.Header().Get("Token")).
SetBody(map[string]any{
"channel_id": n.config.ChannelId,
"props": map[string]interface{}{
"attachments": []map[string]interface{}{
@@ -72,20 +86,12 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
},
},
},
}
if body, err := json.Marshal(m); err != nil {
return err
} else {
req.ContentLength = int64(len(body))
req.Body = io.NopCloser(bytes.NewReader(body))
}
return nil
})
if err = srv.Send(ctx, subject, message); err != nil {
return nil, err
})
postResp, err := postReq.Post(fmt.Sprintf("%s/api/v4/posts", serverUrl))
if err != nil {
return nil, fmt.Errorf("mattermost api error: failed to send request: %w", err)
} else if postResp.IsError() {
return nil, fmt.Errorf("mattermost api error: unexpected status code: %d, resp: %s", postResp.StatusCode(), postResp.String())
}
return &notifier.NotifyResult{}, nil

View File

@@ -1,13 +1,11 @@
package pushover
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -22,7 +20,7 @@ type NotifierConfig struct {
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
httpClient *http.Client
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -32,10 +30,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: http.DefaultClient,
httpClient: client,
}, nil
}
@@ -50,46 +50,19 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// REF: https://pushover.net/api
reqBody := &struct {
Token string `json:"token"`
User string `json:"user"`
Title string `json:"title"`
Message string `json:"message"`
}{
Token: n.config.Token,
User: n.config.User,
Title: subject,
Message: message,
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("pushover api error: failed to encode message body: %w", err)
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://api.pushover.net/1/messages.json",
bytes.NewReader(body),
)
if err != nil {
return nil, fmt.Errorf("pushover api error: failed to create new request: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err := n.httpClient.Do(req)
req := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"title": subject,
"message": message,
"token": n.config.Token,
"user": n.config.User,
})
resp, err := req.Post("https://api.pushover.net/1/messages.json")
if err != nil {
return nil, fmt.Errorf("pushover api error: failed to send request: %w", err)
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("pushover api error: failed to read response: %w", err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushover api error: unexpected status code: %d, resp: %s", resp.StatusCode, string(result))
} else if resp.IsError() {
return nil, fmt.Errorf("pushover api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &notifier.NotifyResult{}, nil

View File

@@ -1,13 +1,12 @@
package pushplus
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -20,7 +19,7 @@ type NotifierConfig struct {
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
httpClient *http.Client
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -30,10 +29,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: http.DefaultClient,
httpClient: client,
}, nil
}
@@ -47,55 +48,29 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// REF: https://pushplus.plus/doc/guide/api.html
reqBody := &struct {
Token string `json:"token"`
Title string `json:"title"`
Content string `json:"content"`
}{
Token: n.config.Token,
Title: subject,
Content: message,
}
body, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("pushplus api error: failed to encode message body: %w", err)
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://www.pushplus.plus/send",
bytes.NewReader(body),
)
if err != nil {
return nil, fmt.Errorf("pushplus api error: failed to create new request: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
resp, err := n.httpClient.Do(req)
// REF: https://pushplus.plus/doc/guide/api.html#%E4%B8%80%E3%80%81%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF%E6%8E%A5%E5%8F%A3
req := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"title": subject,
"content": message,
"token": n.config.Token,
})
resp, err := req.Post("https://www.pushplus.plus/send")
if err != nil {
return nil, fmt.Errorf("pushplus api error: failed to send request: %w", err)
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("pushplus api error: failed to read response: %w", err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushplus api error: unexpected status code: %d, resp: %s", resp.StatusCode, string(result))
} else if resp.IsError() {
return nil, fmt.Errorf("pushplus api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
var errorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Code int `json:"code"`
Message string `json:"msg"`
}
if err := json.Unmarshal(result, &errorResponse); err != nil {
return nil, fmt.Errorf("pushplus api error: failed to decode response: %w", err)
if err := json.Unmarshal(resp.Body(), &errorResponse); err != nil {
return nil, fmt.Errorf("pushplus api error: failed to unmarshal response: %w", err)
} else if errorResponse.Code != 200 {
return nil, fmt.Errorf("pushplus api error: unexpected response code: %d, msg: %s", errorResponse.Code, errorResponse.Msg)
return nil, fmt.Errorf("pushplus api error: code='%d', message='%s'", errorResponse.Code, errorResponse.Message)
}
return &notifier.NotifyResult{}, nil

View File

@@ -2,22 +2,23 @@ package serverchan
import (
"context"
"fmt"
"log/slog"
"net/http"
notifyHttp "github.com/nikoksr/notify/service/http"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// ServerChan 服务地址。
Url string `json:"url"`
ServerUrl string `json:"serverUrl"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
config *NotifierConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -27,9 +28,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
@@ -43,24 +47,18 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := notifyHttp.New()
srv.AddReceivers(&notifyHttp.Webhook{
URL: n.config.Url,
Header: http.Header{},
ContentType: "application/json",
Method: http.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]string{
"text": subject,
"desp": message,
}
},
})
err = srv.Send(ctx, subject, message)
// REF: https://sct.ftqq.com/
req := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"text": subject,
"desp": message,
})
resp, err := req.Post(n.config.ServerUrl)
if err != nil {
return nil, err
return nil, fmt.Errorf("serverchan api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("serverchan api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &notifier.NotifyResult{}, nil

View File

@@ -39,7 +39,7 @@ func TestNotify(t *testing.T) {
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Url: fUrl,
ServerUrl: fUrl,
})
if err != nil {
t.Errorf("err: %+v", err)

View File

@@ -1,10 +1,11 @@
package telegram
package telegrambot
import (
"context"
"fmt"
"log/slog"
"github.com/nikoksr/notify/service/telegram"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -17,8 +18,9 @@ type NotifierConfig struct {
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
config *NotifierConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -28,9 +30,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
logger: slog.Default(),
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
@@ -44,16 +49,18 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv, err := telegram.New(n.config.BotToken)
// REF: https://core.telegram.org/bots/api#sendmessage
req := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"chat_id": n.config.ChatId,
"text": subject + "\n" + message,
})
resp, err := req.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", n.config.BotToken))
if err != nil {
return nil, err
}
srv.AddReceivers(n.config.ChatId)
err = srv.Send(ctx, subject, message)
if err != nil {
return nil, err
return nil, fmt.Errorf("telegram api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("telegram api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &notifier.NotifyResult{}, nil

View File

@@ -1,4 +1,4 @@
package telegram_test
package telegrambot_test
import (
"context"
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegrambot"
)
const (
@@ -21,7 +21,7 @@ var (
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_TELEGRAM_"
argsPrefix := "CERTIMATE_NOTIFIER_TELEGRAMBOT_"
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
flag.Int64Var(&fChartId, argsPrefix+"CHATID", 0, "")
@@ -30,9 +30,9 @@ func init() {
/*
Shell command to run this test:
go test -v ./telegram_test.go -args \
--CERTIMATE_NOTIFIER_TELEGRAM_APITOKEN="your-api-token" \
--CERTIMATE_NOTIFIER_TELEGRAM_CHATID=123456
go test -v ./telegrambot_test.go -args \
--CERTIMATE_NOTIFIER_TELEGRAMBOT_APITOKEN="your-api-token" \
--CERTIMATE_NOTIFIER_TELEGRAMBOT_CHATID=123456
*/
func TestNotify(t *testing.T) {
flag.Parse()

View File

@@ -139,9 +139,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
// 生成请求
// 其中 GET 请求需转换为查询参数
req := n.httpClient.R().
SetContext(ctx).
SetHeaderMultiValues(webhookHeaders)
req := n.httpClient.R().SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
@@ -160,12 +158,12 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
// 发送请求
resp, err := req.Send()
if err != nil {
return nil, fmt.Errorf("failed to send webhook request: %w", err)
return nil, fmt.Errorf("webhook error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("unexpected webhook response status code: %d", resp.StatusCode())
return nil, fmt.Errorf("webhook error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
n.logger.Debug("webhook responded", slog.Any("response", resp.String()))
n.logger.Debug("webhook responded", slog.String("response", resp.String()))
return &notifier.NotifyResult{}, nil
}

View File

@@ -1,11 +1,11 @@
package serverchan
package wecombot
import (
"context"
"fmt"
"log/slog"
"net/http"
notifyHttp "github.com/nikoksr/notify/service/http"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
@@ -16,8 +16,9 @@ type NotifierConfig struct {
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
config *NotifierConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@@ -27,8 +28,12 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New()
return &NotifierProvider{
config: config,
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
@@ -42,26 +47,20 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := notifyHttp.New()
srv.AddReceivers(&notifyHttp.Webhook{
URL: n.config.WebhookUrl,
Header: http.Header{},
ContentType: "application/json",
Method: http.MethodPost,
BuildPayload: func(subject, message string) (payload any) {
return map[string]any{
"msgtype": "text",
"text": map[string]string{
"content": subject + "\n\n" + message,
},
}
},
})
err = srv.Send(ctx, subject, message)
// REF: https://developer.work.weixin.qq.com/document/path/91770
req := n.httpClient.R().
SetHeader("Content-Type", "application/json").
SetBody(map[string]any{
"msgtype": "text",
"text": map[string]string{
"content": subject + "\n\n" + message,
},
})
resp, err := req.Post(n.config.WebhookUrl)
if err != nil {
return nil, err
return nil, fmt.Errorf("wecom api error: failed to send request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("wecom api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return &notifier.NotifyResult{}, nil

View File

@@ -1,4 +1,4 @@
package lark_test
package wecombot_test
import (
"context"
@@ -7,7 +7,7 @@ import (
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecombot"
)
const (
@@ -18,7 +18,7 @@ const (
var fWebhookUrl string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_LARK_"
argsPrefix := "CERTIMATE_NOTIFIER_WECOMBOT_"
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
}
@@ -26,8 +26,8 @@ func init() {
/*
Shell command to run this test:
go test -v ./lark_test.go -args \
--CERTIMATE_NOTIFIER_LARK_WEBHOOKURL="https://example.com/your-webhook-url"
go test -v ./wecombot_test.go -args \
--CERTIMATE_NOTIFIER_WECOMBOT_WEBHOOKURL="https://example.com/your-webhook-url" \
*/
func TestNotify(t *testing.T) {
flag.Parse()

View File

@@ -10,12 +10,14 @@ import (
"time"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
opsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
onepanelsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/1panel"
)
type UploaderConfig struct {
// 1Panel 地址。
ApiUrl string `json:"apiUrl"`
// 1Panel 版本。
ApiVersion string `json:"apiVersion"`
// 1Panel 接口密钥。
ApiKey string `json:"apiKey"`
}
@@ -23,7 +25,7 @@ type UploaderConfig struct {
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *opsdk.Client
sdkClient *onepanelsdk.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
@@ -33,7 +35,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
panic("config is nil")
}
client, err := createSdkClient(config.ApiUrl, config.ApiKey)
client, err := createSdkClient(config.ApiUrl, config.ApiVersion, config.ApiKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
@@ -67,7 +69,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传证书
uploadWebsiteSSLReq := &opsdk.UploadWebsiteSSLRequest{
uploadWebsiteSSLReq := &onepanelsdk.UploadWebsiteSSLRequest{
Type: "paste",
Description: certName,
Certificate: certPEM,
@@ -99,7 +101,7 @@ func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string,
default:
}
searchWebsiteSSLReq := &opsdk.SearchWebsiteSSLRequest{
searchWebsiteSSLReq := &onepanelsdk.SearchWebsiteSSLRequest{
Page: searchWebsiteSSLPageNumber,
PageSize: searchWebsiteSSLPageSize,
}
@@ -130,15 +132,19 @@ func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string,
return nil, nil
}
func createSdkClient(apiUrl, apiKey string) (*opsdk.Client, error) {
func createSdkClient(apiUrl, apiVersion, apiKey string) (*onepanelsdk.Client, error) {
if _, err := url.Parse(apiUrl); err != nil {
return nil, errors.New("invalid 1panel api url")
}
if apiVersion == "" {
return nil, errors.New("invalid 1panel api version")
}
if apiKey == "" {
return nil, errors.New("invalid 1panel api key")
}
client := opsdk.NewClient(apiUrl, apiKey)
client := onepanelsdk.NewClient(apiUrl, apiVersion, apiKey)
return client, nil
}

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