Compare commits

...

63 Commits

Author SHA1 Message Date
RHQYZ
3fb9b00f66 bump vertion to v0.3.18 2025-06-15 22:28:44 +08:00
RHQYZ
a492fbe18c Merge pull request #799 from fudiwei/bugfix
bugfix
2025-06-15 22:23:24 +08:00
Fu Diwei
0383233315 fix: int64 overflow 2025-06-15 22:21:36 +08:00
Fu Diwei
0434f95a1e fix: tsc build error 2025-06-15 20:51:40 +08:00
Fu Diwei
769b24aa88 refactor: clean code 2025-06-15 20:51:28 +08:00
RHQYZ
94a0292fad Merge pull request #797 from usual2970/hotfix/cer_expire
fix: correct certificate expiration check logic
2025-06-15 20:41:08 +08:00
RHQYZ
410231cd1f Merge pull request #796 from bigsk05/main
fixed 1panel system ssl deploy
2025-06-15 20:41:00 +08:00
Yoan.liu
1d4e048777 refactor code 2025-06-14 21:51:10 +08:00
RHQYZ
c16fe1a807 Merge pull request #791 from usual2970/feat/panic_recover
处理panic避免影响到其它工作流
2025-06-14 21:48:23 +08:00
PBK Bin
e8ed831b28 Merge pull request #789 from PBK-B/bin/fix_remove_email_setting
feat(ui): support deleting email input box auto-complete items #174
2025-06-14 21:48:12 +08:00
RHQYZ
c7c89efbe7 Merge pull request #784 from PBK-B/bin/chore_add_gitkeep
chore: add ui/dist/.gitkeep file
2025-06-14 21:47:57 +08:00
RHQYZ
d7f3d9c512 Merge pull request #783 from fudiwei/feat/providers
new providers
2025-06-14 21:47:37 +08:00
RHQYZ
ce67cc5a39 Merge pull request #782 from fudiwei/bugfix
bugfix
2025-06-14 21:47:26 +08:00
Fu Diwei
945d0da36f update README 2025-06-14 21:41:58 +08:00
Fu Diwei
898311c6e5 feat: new deployment provider: ctcccloud elb 2025-06-14 21:41:58 +08:00
Fu Diwei
5d6bc03f21 feat: new deployment provider: ctcccloud lvdn 2025-06-14 21:41:58 +08:00
Fu Diwei
018743299b feat: new deployment provider: ctcccloud cms 2025-06-14 21:41:58 +08:00
Fu Diwei
18e7238067 feat: new deployment provider: ctcccloud ao 2025-06-14 21:41:58 +08:00
Fu Diwei
b7d1ff8960 feat: new deployment provider: ctcccloud icdn 2025-06-14 21:41:58 +08:00
Fu Diwei
0d44373de6 feat: new deployment provider: ctcccloud cdn 2025-06-14 21:41:52 +08:00
Yoan.liu
3d680e50e2 fix: correct certificate expiration check logic 2025-06-14 21:16:58 +08:00
Bigsk
9542079e20 fixed 1panel system ssl deploy 2025-06-13 23:43:24 +08:00
Yoan.liu
081e83e0bf recover from panics 2025-06-13 13:37:14 +08:00
Fu Diwei
9c8ab98efb feat: new acme dns-01 provider: statecloud smartdns 2025-06-12 23:21:03 +08:00
Fu Diwei
bf26db77cb fix: #786 2025-06-12 14:13:43 +08:00
Fu Diwei
08ea915d24 fix: #786 2025-06-12 14:03:29 +08:00
Fu Diwei
fb62f1e105 feat: ipv6 connect 2025-06-12 13:39:01 +08:00
PBK-B
261b3a8546 chore: add ui/dist/.gitkeep file 2025-06-11 23:25:14 +08:00
Fu Diwei
563adbec2a feat: allow configuring smtp sender display name 2025-06-11 23:10:57 +08:00
Fu Diwei
c6dfe11bdb feat: add preset scripts for qnap on deployment to ssh 2025-06-11 23:09:56 +08:00
Fu Diwei
e4bfa90a77 feat: new deployment provider: apisix 2025-06-11 22:17:07 +08:00
Fu Diwei
b833d09466 feat: new deployment provider: tencentcloud gaap 2025-06-11 20:53:19 +08:00
Fu Diwei
3ec0ba7052 fix: #781 2025-06-11 19:58:27 +08:00
Fu Diwei
57eb66b889 chore: remove unused files 2025-06-10 21:52:56 +08:00
RHQYZ
a048eb95a9 Merge pull request #779 from fudiwei/main
build: keep release files as before
2025-06-10 21:41:10 +08:00
Fu Diwei
23e58b914e build: keep release files as before 2025-06-10 21:38:47 +08:00
Fu Diwei
1170f635fd build: keep release files as before 2025-06-10 21:10:27 +08:00
Yoan.liu
553aceac44 retain executable permission 2025-06-10 11:08:29 +08:00
Yoan.liu
e24de70c02 split release into multiple jobs 2025-06-09 23:40:24 +08:00
Yoan.liu
bba2b25757 split release into multiple jobs 2025-06-09 23:11:50 +08:00
Yoan.liu
4132ec3617 split release into multiple jobs 2025-06-09 22:40:43 +08:00
Fu Diwei
9b3c7e16c0 fix: #769 2025-06-09 21:34:03 +08:00
Fu Diwei
0448538073 feat: bump version to v0.3.17 2025-06-09 21:16:58 +08:00
RHQYZ
80157496d5 Merge pull request #774 from lfffffy/main
fix(docs): 修复腾讯云文档链接使用错误的 com.cn 域名
2025-06-09 21:09:39 +08:00
RHQYZ
62e2ed2fb8 Merge pull request #768 from fudiwei/main
enhance & bugfix
2025-06-09 21:09:28 +08:00
Fu Diwei
a750592eb5 feat: duplicate workflow 2025-06-09 21:07:52 +08:00
Fu Diwei
5e6d729631 fix: #769 2025-06-09 20:44:48 +08:00
Fu Diwei
24fe824757 feat: allow skip notify nodes when all previous nodes were skipped 2025-06-09 20:40:06 +08:00
Fu Diwei
84a3f3346a fix: #769 2025-06-09 19:11:04 +08:00
Fu Diwei
bd26dfecb8 feat: improve workflow log 2025-06-09 10:06:41 +08:00
leun
43182de732 fix(docs): 修复腾讯云文档链接使用错误的 com.cn 域名
将文档链接中的 cloud.tencent.com.cn 统一替换为正确的 cloud.tencent.com,
以避免链接跳转失败或文档加载异常的问题。
2025-06-08 15:00:28 +08:00
Fu Diwei
d58109f4be feat: support wildcard domains on deployment to wangsu cdn 2025-06-05 20:28:50 +08:00
Fu Diwei
59935df6b1 fix: #766 2025-06-05 20:24:43 +08:00
Fu Diwei
252da5d7e1 refactor(ui): clean code 2025-06-05 10:24:39 +08:00
RHQYZ
c3e7590f53 bump version to v0.3.16 2025-06-03 23:50:48 +08:00
RHQYZ
65cd1dc850 Merge pull request #761 from fudiwei/feat/providers
new providers
2025-06-03 23:48:27 +08:00
RHQYZ
2203bb5268 Merge pull request #760 from fudiwei/bugfix
bugfix: could not add branches in workflows
2025-06-03 23:48:13 +08:00
Fu Diwei
8e5c36968a chore: improve error 2025-06-03 23:45:00 +08:00
Fu Diwei
9ad0e6fb57 feat: support ssh challenge-response 2025-06-03 23:39:48 +08:00
Fu Diwei
7d55383cf7 feat: new deployment provider: aws iam 2025-06-03 23:39:48 +08:00
Fu Diwei
6dc65eea2f feat: new acme dns-01 provider: ucloud udnr 2025-06-03 23:39:48 +08:00
Fu Diwei
7210f63884 feat: new acme dns-01 provider: constellix 2025-06-03 23:39:42 +08:00
Fu Diwei
f94db675fb fix: could not add branches in workflows 2025-06-03 22:37:03 +08:00
211 changed files with 9292 additions and 590 deletions

View File

@@ -6,7 +6,7 @@ on:
- "v[0-9]*"
jobs:
goreleaser:
prepare-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -19,34 +19,186 @@ jobs:
with:
node-version: 20.11.0
- name: Build WebUI
run: |
npm --prefix=./ui ci
npm --prefix=./ui run build
- name: Upload UI build artifacts
uses: actions/upload-artifact@v4
with:
name: ui-build
path: ./ui/dist
retention-days: 1
build-linux:
needs: prepare-ui
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
# - name: Install upx (optional)
# run: |
# sudo apt-get update
# sudo apt-get install -y upx
- name: Build WebUI
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@v5
- name: Download UI build artifacts
uses: actions/download-artifact@v4
with:
distribution: goreleaser
version: latest
args: release --clean
name: ui-build
path: ./ui/dist
- name: Build Linux binaries
env:
CGO_ENABLED: 0
GOOS: linux
run: |
mkdir -p dist/linux
for ARCH in amd64 arm64 armv7; do
if [ "$ARCH" == "armv7" ]; then
export GOARM=7
fi
go build -ldflags="-s -w -X github.com/usual2970/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/linux/certimate_${GITHUB_REF#refs/tags/}_linux_$ARCH
done
- name: Upload Linux binaries
uses: actions/upload-artifact@v4
with:
name: linux-binaries
path: dist/linux/
retention-days: 1
build-macos:
needs: prepare-ui
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Download UI build artifacts
uses: actions/download-artifact@v4
with:
name: ui-build
path: ./ui/dist
- name: Build macOS binaries
env:
CGO_ENABLED: 0
GOOS: darwin
run: |
mkdir -p dist/darwin
for ARCH in amd64 arm64; do
go build -ldflags="-s -w -X github.com/usual2970/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/darwin/certimate_${GITHUB_REF#refs/tags/}_darwin_$ARCH
done
- name: Upload macOS binaries
uses: actions/upload-artifact@v4
with:
name: macos-binaries
path: dist/darwin/
retention-days: 1
build-windows:
needs: prepare-ui
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Download UI build artifacts
uses: actions/download-artifact@v4
with:
name: ui-build
path: ./ui/dist
- name: Build Windows binaries
env:
CGO_ENABLED: 0
GOOS: windows
run: |
mkdir -p dist/windows
for ARCH in amd64 arm64; do
go build -ldflags="-s -w -X github.com/usual2970/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/windows/certimate_${GITHUB_REF#refs/tags/}_windows_$ARCH.exe
done
- name: Upload Windows binaries
uses: actions/upload-artifact@v4
with:
name: windows-binaries
path: dist/windows/
retention-days: 1
create-release:
needs: [build-linux, build-macos, build-windows]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all binaries
uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Prepare release assets
run: |
mkdir -p dist
cp -r artifacts/linux-binaries/* dist/
cp -r artifacts/macos-binaries/* dist/
cp -r artifacts/windows-binaries/* dist/
find dist -type f -not -name "*.exe" -exec chmod +x {} \;
# 为每个二进制文件创建 zip 包
cd dist
for bin in certimate_*; do
if [[ "$bin" == *".exe" ]]; then
entrypoint="certimate.exe"
else
entrypoint="certimate"
fi
tmpdir=$(mktemp -d)
cp "$bin" "${tmpdir}/${entrypoint}"
cp ../README.md ../LICENSE.md ../CHANGELOG.md "$tmpdir"
if [[ "$bin" == *".exe" ]]; then
zip -j "${bin%.exe}.zip" "$tmpdir"/*
else
zip -j -X "${bin}.zip" "$tmpdir"/*
fi
rm -rf "$tmpdir"
done
# 创建校验和文件
sha256sum *.zip > checksums.txt
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.zip
dist/checksums.txt
draft: true
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -35,6 +35,8 @@ git clone https://github.com/your_username/certimate.git
这将启动一个 Web 服务器,默认运行在 `http://localhost:8090`,并使用来自 `ui/dist` 的预构建管理页面。
> 如果你遇到报错 `ui/embed.go:10:12: pattern all:dist: no matching files found` 请先参考 [构建 Admin UI](#修改管理页面-admin-ui)
**在向主仓库提交 PR 之前,建议你:**
- 使用 [gofumpt](https://github.com/mvdan/gofumpt) 对你的代码进行格式化。

View File

@@ -36,6 +36,8 @@ Once you have made changes to the Go code in Certimate, follow these steps to ru
This will start a web server at `http://localhost:8090` using the prebuilt Admin UI located in `ui/dist`.
> if you encounter an error `ui/embed.go:10:12: pattern all:dist: no matching files found`, please refer to [build Admin UI](#making-changes-in-the-admin-ui)
**Before submitting a PR to the main repository, consider:**
- Format your source code by using [gofumpt](https://github.com/mvdan/gofumpt).

View File

@@ -39,7 +39,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法;
- 支持 PEM、PFX、JKS 等多种格式输出证书;
- 支持 30+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 90+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-hosting-providers)
- 支持 100+ 部署目标(如 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

@@ -39,7 +39,7 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate
- 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 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 90+ 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 more than 100+ 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.

2
go.mod
View File

@@ -30,6 +30,7 @@ require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
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/aws/aws-sdk-go-v2/service/iam v1.42.0
github.com/baidubce/bce-sdk-go v0.9.228
github.com/blinkbean/dingtalk v1.1.3
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.46
@@ -51,6 +52,7 @@ require (
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1155
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1166
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1150
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1169

5
go.sum
View File

@@ -235,6 +235,8 @@ github.com/aws/aws-sdk-go-v2/service/acm v1.32.0/go.mod h1:3sKYAgRbuBa2QMYGh/WEc
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.46.1 h1:6xZNYtuVwzBs8k+TmraERt0vL68Ppg9aUi+aTQmPaVM=
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.46.1/go.mod h1:FIBJ48TS+qJb+Ne4qJ+0NeIhtPTVXItXooTeNeVI4Po=
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
github.com/aws/aws-sdk-go-v2/service/iam v1.42.0 h1:G6+UzGvubaet9QOh0664E9JeT+b6Zvop3AChozRqkrA=
github.com/aws/aws-sdk-go-v2/service/iam v1.42.0/go.mod h1:mPJkGQzeCoPs82ElNILor2JzZgYENr4UaSKUT8K27+c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
@@ -834,6 +836,7 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1166/go.mod h1
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1150/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1155/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1163/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1169/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
@@ -843,6 +846,8 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173 h1:W5b
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 h1:mrJ5Fbkd7sZIJ5F6oRfh5zebPQaudPH9Y0+GUmFytYU=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128/go.mod h1:zbsYIBT+VTX4z4ocjTAdLBIWyNYj3z0BRqd0iPdnjsk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163 h1:putqrH5n1SVRqFWHOylVqYI5yLQUjRTkHqZPLT2yeVY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163/go.mod h1:aEWRXlAvovPUUoS3kVB/LVWEQ19WqzTj2lXGvR1YArY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1150 h1:RQQYfZOFYlkxKR2+xp8el3+8xs9DhxBy+ajlHtapqtQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1150/go.mod h1:zpfr6EBWy7ClASTGUgIy01Gn4R79UXf+2QGQeyR124A=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172 h1:6SUO0hTie3zxnUEMxmhnS1iRIXpAukSZV27Nrx4NwIk=

View File

@@ -16,6 +16,8 @@ import (
pCloudflare "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudflare"
pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns"
pCMCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud"
pConstellix "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/constellix"
pCTCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud"
pDeSEC "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/desec"
pDigitalOcean "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean"
pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla"
@@ -38,6 +40,7 @@ import (
pRainYun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/rainyun"
pTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud"
pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo"
pUCloudUDNR "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ucloud-udnr"
pVercel "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/vercel"
pVolcEngine "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/volcengine"
pWestcn "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn"
@@ -218,7 +221,7 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
return applicant, err
}
case domain.ACMEDns01ProviderTypeCMCCCloud:
case domain.ACMEDns01ProviderTypeCMCCCloud, domain.ACMEDns01ProviderTypeCMCCCloudDNS:
{
access := domain.AccessConfigForCMCCCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@@ -234,6 +237,38 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
return applicant, err
}
case domain.ACMEDns01ProviderTypeConstellix:
{
access := domain.AccessConfigForConstellix{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pConstellix.NewChallengeProvider(&pConstellix.ChallengeProviderConfig{
ApiKey: access.ApiKey,
SecretKey: access.SecretKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeCTCCCloud, domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS:
{
access := domain.AccessConfigForCTCCCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pCTCCCloud.NewChallengeProvider(&pCTCCCloud.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeDeSEC:
{
access := domain.AccessConfigForDeSEC{}
@@ -579,6 +614,22 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
}
}
case domain.ACMEDns01ProviderTypeUCloudUDNR:
{
access := domain.AccessConfigForUCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
applicant, err := pUCloudUDNR.NewChallengeProvider(&pUCloudUDNR.ChallengeProviderConfig{
PrivateKey: access.PrivateKey,
PublicKey: access.PublicKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return applicant, err
}
case domain.ACMEDns01ProviderTypeVercel:
{
access := domain.AccessConfigForVercel{}

View File

@@ -25,8 +25,10 @@ import (
pAliyunOSS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
pAliyunVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-vod"
pAliyunWAF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-waf"
pAPISIX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/apisix"
pAWSACM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-acm"
pAWSCloudFront "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-cloudfront"
pAWSIAM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-iam"
pAzureKeyVault "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/azure-keyvault"
pBaiduCloudAppBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-appblb"
pBaiduCloudBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-blb"
@@ -41,6 +43,12 @@ import (
pBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly"
pCdnfly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cdnfly"
pCTCCCloudAO "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-ao"
pCTCCCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cdn"
pCTCCCloudCMS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cms"
pCTCCCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-elb"
pCTCCCloudICDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-icdn"
pCTCCCloudLVDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-lvdn"
pDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
pEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications"
pFlexCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/flexcdn"
@@ -72,6 +80,7 @@ import (
pTencentCloudCSS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-css"
pTencentCloudECDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-eo"
pTencentCloudGAAP "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-gaap"
pTencentCloudSCF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-scf"
pTencentCloudSSL "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ssl"
pTencentCloudSSLDeploy "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy"
@@ -331,7 +340,24 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
}
}
case domain.DeploymentProviderTypeAWSACM, domain.DeploymentProviderTypeAWSCloudFront:
case domain.DeploymentProviderTypeAPISIX:
{
access := domain.AccessConfigForAPISIX{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
deployer, err := pAPISIX.NewDeployer(&pAPISIX.DeployerConfig{
ServerUrl: access.ServerUrl,
ApiKey: access.ApiKey,
AllowInsecureConnections: access.AllowInsecureConnections,
ResourceType: pAPISIX.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")),
CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"),
})
return deployer, err
}
case domain.DeploymentProviderTypeAWSACM, domain.DeploymentProviderTypeAWSCloudFront, domain.DeploymentProviderTypeAWSIAM:
{
access := domain.AccessConfigForAWS{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@@ -350,10 +376,20 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
case domain.DeploymentProviderTypeAWSCloudFront:
deployer, err := pAWSCloudFront.NewDeployer(&pAWSCloudFront.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderServiceConfig, "region"),
DistributionId: maputil.GetString(options.ProviderServiceConfig, "distributionId"),
CertificateSource: maputil.GetOrDefaultString(options.ProviderServiceConfig, "certificateSource", "ACM"),
})
return deployer, err
case domain.DeploymentProviderTypeAWSIAM:
deployer, err := pAWSIAM.NewDeployer(&pAWSIAM.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderServiceConfig, "region"),
DistributionId: maputil.GetString(options.ProviderServiceConfig, "distributionId"),
CertificatePath: maputil.GetOrDefaultString(options.ProviderServiceConfig, "certificatePath", "/"),
})
return deployer, err
@@ -591,6 +627,69 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
return deployer, err
}
case domain.DeploymentProviderTypeCTCCCloudAO, domain.DeploymentProviderTypeCTCCCloudCDN, domain.DeploymentProviderTypeCTCCCloudCMS, domain.DeploymentProviderTypeCTCCCloudELB, domain.DeploymentProviderTypeCTCCCloudICDN, domain.DeploymentProviderTypeCTCCCloudLVDN:
{
access := domain.AccessConfigForCTCCCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch options.Provider {
case domain.DeploymentProviderTypeCTCCCloudAO:
deployer, err := pCTCCCloudAO.NewDeployer(&pCTCCCloudAO.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
})
return deployer, err
case domain.DeploymentProviderTypeCTCCCloudCDN:
deployer, err := pCTCCCloudCDN.NewDeployer(&pCTCCCloudCDN.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
})
return deployer, err
case domain.DeploymentProviderTypeCTCCCloudCMS:
deployer, err := pCTCCCloudCMS.NewDeployer(&pCTCCCloudCMS.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
})
return deployer, err
case domain.DeploymentProviderTypeCTCCCloudELB:
deployer, err := pCTCCCloudELB.NewDeployer(&pCTCCCloudELB.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
RegionId: maputil.GetString(options.ProviderServiceConfig, "regionId"),
ResourceType: pCTCCCloudELB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")),
LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"),
ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"),
})
return deployer, err
case domain.DeploymentProviderTypeCTCCCloudICDN:
deployer, err := pCTCCCloudICDN.NewDeployer(&pCTCCCloudICDN.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
})
return deployer, err
case domain.DeploymentProviderTypeCTCCCloudLVDN:
deployer, err := pCTCCCloudLVDN.NewDeployer(&pCTCCCloudLVDN.DeployerConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
})
return deployer, err
default:
break
}
}
case domain.DeploymentProviderTypeDogeCloudCDN:
{
access := domain.AccessConfigForDogeCloud{}
@@ -986,6 +1085,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
jumpServers[i] = pSSH.JumpServerConfig{
SshHost: jumpServer.Host,
SshPort: jumpServer.Port,
SshAuthMethod: jumpServer.AuthMethod,
SshUsername: jumpServer.Username,
SshPassword: jumpServer.Password,
SshKey: jumpServer.Key,
@@ -996,6 +1096,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
deployer, err := pSSH.NewDeployer(&pSSH.DeployerConfig{
SshHost: access.Host,
SshPort: access.Port,
SshAuthMethod: access.AuthMethod,
SshUsername: access.Username,
SshPassword: access.Password,
SshKey: access.Key,
@@ -1017,7 +1118,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
return deployer, err
}
case domain.DeploymentProviderTypeTencentCloudCDN, domain.DeploymentProviderTypeTencentCloudCLB, domain.DeploymentProviderTypeTencentCloudCOS, domain.DeploymentProviderTypeTencentCloudCSS, domain.DeploymentProviderTypeTencentCloudECDN, domain.DeploymentProviderTypeTencentCloudEO, domain.DeploymentProviderTypeTencentCloudSCF, domain.DeploymentProviderTypeTencentCloudSSL, domain.DeploymentProviderTypeTencentCloudSSLDeploy, domain.DeploymentProviderTypeTencentCloudVOD, domain.DeploymentProviderTypeTencentCloudWAF:
case domain.DeploymentProviderTypeTencentCloudCDN, domain.DeploymentProviderTypeTencentCloudCLB, domain.DeploymentProviderTypeTencentCloudCOS, domain.DeploymentProviderTypeTencentCloudCSS, domain.DeploymentProviderTypeTencentCloudECDN, domain.DeploymentProviderTypeTencentCloudEO, domain.DeploymentProviderTypeTencentCloudGAAP, domain.DeploymentProviderTypeTencentCloudSCF, domain.DeploymentProviderTypeTencentCloudSSL, domain.DeploymentProviderTypeTencentCloudSSLDeploy, domain.DeploymentProviderTypeTencentCloudVOD, domain.DeploymentProviderTypeTencentCloudWAF:
{
access := domain.AccessConfigForTencentCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@@ -1080,6 +1181,16 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
})
return deployer, err
case domain.DeploymentProviderTypeTencentCloudGAAP:
deployer, err := pTencentCloudGAAP.NewDeployer(&pTencentCloudGAAP.DeployerConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
ResourceType: pTencentCloudGAAP.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")),
ProxyId: maputil.GetString(options.ProviderServiceConfig, "proxyId"),
ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"),
})
return deployer, err
case domain.DeploymentProviderTypeTencentCloudSCF:
deployer, err := pTencentCloudSCF.NewDeployer(&pTencentCloudSCF.DeployerConfig{
SecretId: access.SecretId,

View File

@@ -41,6 +41,12 @@ type AccessConfigForAliyun struct {
ResourceGroupId string `json:"resourceGroupId,omitempty"`
}
type AccessConfigForAPISIX struct {
ServerUrl string `json:"serverUrl"`
ApiKey string `json:"apiKey"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type AccessConfigForAWS struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
@@ -109,6 +115,16 @@ type AccessConfigForCMCCCloud struct {
AccessKeySecret string `json:"accessKeySecret"`
}
type AccessConfigForConstellix struct {
ApiKey string `json:"apiKey"`
SecretKey string `json:"secretKey"`
}
type AccessConfigForCTCCCloud struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type AccessConfigForDeSEC struct {
Token string `json:"token"`
}
@@ -157,6 +173,7 @@ type AccessConfigForEmail struct {
Username string `json:"username"`
Password string `json:"password"`
DefaultSenderAddress string `json:"defaultSenderAddress,omitempty"`
DefaultSenderName string `json:"defaultSenderName,omitempty"`
DefaultReceiverAddress string `json:"defaultReceiverAddress,omitempty"`
}
@@ -310,14 +327,16 @@ type AccessConfigForSlackBot struct {
type AccessConfigForSSH struct {
Host string `json:"host"`
Port int32 `json:"port"`
Username string `json:"username"`
AuthMethod string `json:"authMethod,omitempty"`
Username string `json:"username,omitempty"`
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"`
AuthMethod string `json:"authMethod,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Key string `json:"key,omitempty"`
KeyPassphrase string `json:"keyPassphrase,omitempty"`

View File

@@ -14,6 +14,7 @@ const (
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai预留
AccessProviderTypeAliyun = AccessProviderType("aliyun")
AccessProviderTypeAPISIX = AccessProviderType("apisix")
AccessProviderTypeAWS = AccessProviderType("aws")
AccessProviderTypeAzure = AccessProviderType("azure")
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
@@ -28,7 +29,8 @@ const (
AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 天翼云(预留)
AccessProviderTypeConstellix = AccessProviderType("constellix")
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud")
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留)
AccessProviderTypeDeSEC = AccessProviderType("desec")
AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean")
@@ -117,49 +119,54 @@ ACME DNS-01 提供商常量值。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa")
ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53]
ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53")
ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure]
ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns")
ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]
ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns")
ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny)
ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare)
ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud)
ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname)
ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)
ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner)
ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]
ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns")
ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS]
ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns")
ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa")
ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53]
ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53")
ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure]
ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns")
ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]
ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns")
ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny)
ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare)
ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS]
ACMEDns01ProviderTypeCMCCCloudDNS = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + "-dns")
ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix)
ACMEDns01ProviderTypeCTCCCloud = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS]
ACMEDns01ProviderTypeCTCCCloudSmartDNS = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud + "-smartdns")
ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname)
ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)
ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner)
ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]
ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns")
ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS]
ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns")
ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr")
ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
)
type DeploymentProviderType string
@@ -190,8 +197,10 @@ const (
DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss")
DeploymentProviderTypeAliyunVOD = DeploymentProviderType(AccessProviderTypeAliyun + "-vod")
DeploymentProviderTypeAliyunWAF = DeploymentProviderType(AccessProviderTypeAliyun + "-waf")
DeploymentProviderTypeAPISIX = DeploymentProviderType(AccessProviderTypeAWS + "-apisix")
DeploymentProviderTypeAWSACM = DeploymentProviderType(AccessProviderTypeAWS + "-acm")
DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront")
DeploymentProviderTypeAWSIAM = DeploymentProviderType(AccessProviderTypeAWS + "-iam")
DeploymentProviderTypeAzureKeyVault = DeploymentProviderType(AccessProviderTypeAzure + "-keyvault")
DeploymentProviderTypeBaiduCloudAppBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-appblb")
DeploymentProviderTypeBaiduCloudBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-blb")
@@ -206,6 +215,12 @@ const (
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)
DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly)
DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao")
DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn")
DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms")
DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb")
DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn")
DeploymentProviderTypeCTCCCloudLVDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ldvn")
DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn")
DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications")
DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN)
@@ -238,6 +253,7 @@ const (
DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css")
DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn")
DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo")
DeploymentProviderTypeTencentCloudGAAP = DeploymentProviderType(AccessProviderTypeTencentCloud + "-gaap")
DeploymentProviderTypeTencentCloudSCF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-scf")
DeploymentProviderTypeTencentCloudSSL = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssl")
DeploymentProviderTypeTencentCloudSSLDeploy = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssldeploy")

View File

@@ -106,12 +106,13 @@ type WorkflowNodeConfigForDeploy struct {
}
type WorkflowNodeConfigForNotify struct {
Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃
Provider string `json:"provider"` // 通知提供商
ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
Subject string `json:"subject"` // 通知主题
Message string `json:"message"` // 通知内容
Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃
Provider string `json:"provider"` // 通知提供商
ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
Subject string `json:"subject"` // 通知主题
Message string `json:"message"` // 通知内容
SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过
}
type WorkflowNodeConfigForCondition struct {
@@ -128,7 +129,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
CAProvider: maputil.GetString(n.Config, "caProvider"),
CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
CAProviderConfig: maputil.GetKVMapAny(n.Config, "caProviderConfig"),
KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"),
KeyAlgorithm: maputil.GetOrDefaultString(n.Config, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)),
Nameservers: maputil.GetString(n.Config, "nameservers"),
DnsPropagationWait: maputil.GetInt32(n.Config, "dnsPropagationWait"),
DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
@@ -169,12 +170,13 @@ func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
return WorkflowNodeConfigForNotify{
Channel: maputil.GetString(n.Config, "channel"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
Subject: maputil.GetString(n.Config, "subject"),
Message: maputil.GetString(n.Config, "message"),
Channel: maputil.GetString(n.Config, "channel"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
Subject: maputil.GetString(n.Config, "subject"),
Message: maputil.GetString(n.Config, "message"),
SkipOnAllPrevSkipped: maputil.GetBool(n.Config, "skipOnAllPrevSkipped"),
}
}

View File

@@ -71,6 +71,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
Username: access.Username,
Password: access.Password,
SenderAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "senderAddress", access.DefaultSenderAddress),
SenderName: maputil.GetOrDefaultString(options.ProviderServiceConfig, "senderName", access.DefaultSenderName),
ReceiverAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "receiverAddress", access.DefaultReceiverAddress),
})
}

View File

@@ -1,9 +1,8 @@
package lego_aliyunesa
package internal
import (
"errors"
"fmt"
"strings"
"sync"
"time"
@@ -102,13 +101,13 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return fmt.Errorf("alicloud-esa: could not find zone for domain %q: %w", domain, err)
}
siteName := strings.TrimRight(authZone, ".")
siteName := dns01.UnFqdn(authZone)
siteId, err := d.getSiteId(siteName)
if err != nil {
return fmt.Errorf("alicloud-esa: could not find site for zone %q: %w", siteName, err)
}
if err := d.addOrUpdateDNSRecord(siteId, strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil {
if err := d.addOrUpdateDNSRecord(siteId, dns01.UnFqdn(info.EffectiveFQDN), info.Value); err != nil {
return fmt.Errorf("alicloud-esa: %w", err)
}
@@ -123,13 +122,13 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("alicloud-esa: could not find zone for domain %q: %w", domain, err)
}
siteName := strings.TrimRight(authZone, ".")
siteName := dns01.UnFqdn(authZone)
siteId, err := d.getSiteId(siteName)
if err != nil {
return fmt.Errorf("alicloud-esa: could not find site for zone %q: %w", siteName, err)
}
if err := d.removeDNSRecord(siteId, strings.TrimRight(info.EffectiveFQDN, ".")); err != nil {
if err := d.removeDNSRecord(siteId, dns01.UnFqdn(info.EffectiveFQDN)); err != nil {
return fmt.Errorf("alicloud-esa: %w", err)
}

View File

@@ -1,4 +1,4 @@
package lego_baiducloud
package internal
import (
"errors"

View File

@@ -1,7 +1,6 @@
package cmcccloud
import (
"errors"
"time"
"github.com/go-acme/lego/v4/challenge"
@@ -18,7 +17,7 @@ type ChallengeProviderConfig struct {
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
return nil, errors.New("config is nil")
panic("config is nil")
}
providerConfig := internal.NewDefaultConfig()

View File

@@ -18,8 +18,9 @@ import (
const (
envNamespace = "CMCCCLOUD_"
EnvAccessKey = envNamespace + "ACCESS_KEY"
EnvSecretKey = envNamespace + "SECRET_KEY"
EnvAccessKey = envNamespace + "ACCESS_KEY"
EnvSecretKey = envNamespace + "SECRET_KEY"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
@@ -30,13 +31,14 @@ const (
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
AccessKey string
SecretKey string
ReadTimeOut int
ConnectTimeout int
AccessKey string
SecretKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int32
ReadTimeOut int
ConnectTimeout int
}
type DNSProvider struct {

View File

@@ -0,0 +1,38 @@
package cloudns
import (
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/constellix"
)
type ChallengeProviderConfig struct {
ApiKey string `json:"apiKey"`
SecretKey string `json:"secretKey"`
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 := constellix.NewDefaultConfig()
providerConfig.APIKey = config.ApiKey
providerConfig.SecretKey = config.SecretKey
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = int(config.DnsTTL)
}
provider, err := constellix.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,39 @@
package ctcccloud
import (
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal"
)
type ChallengeProviderConfig struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
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 := internal.NewDefaultConfig()
providerConfig.AccessKeyId = config.AccessKeyId
providerConfig.SecretAccessKey = config.SecretAccessKey
if config.DnsTTL != 0 {
providerConfig.TTL = int(config.DnsTTL)
}
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
provider, err := internal.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -0,0 +1,203 @@
package internal
import (
"errors"
"fmt"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
ctyundns "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/dns"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
const (
envNamespace = "CTYUNSMARTDNS_"
EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
AccessKeyId string
SecretAccessKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
type DNSProvider struct {
client *ctyundns.Client
config *Config
}
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, 600),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)
if err != nil {
return nil, fmt.Errorf("ctyun: %w", err)
}
config := NewDefaultConfig()
config.AccessKeyId = values[EnvAccessKeyID]
config.SecretAccessKey = values[EnvSecretAccessKey]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ctyun: the configuration of the DNS provider is nil")
}
client, err := ctyundns.NewClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, err
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
client: client,
config: config,
}, nil
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("ctyun: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("ctyun: %w", err)
}
if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil {
return fmt.Errorf("ctyun: %w", err)
}
return nil
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("ctyun: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("ctyun: %w", err)
}
if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil {
return fmt.Errorf("ctyun: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) findDNSRecordId(zoneName, subDomain string) (int32, error) {
// 查询解析记录列表
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11264&data=181&isNormal=1&vid=259
request := &ctyundns.QueryRecordListRequest{}
request.Domain = typeutil.ToPtr(zoneName)
request.Host = typeutil.ToPtr(subDomain)
request.Type = typeutil.ToPtr("TXT")
response, err := d.client.QueryRecordList(request)
if err != nil {
return 0, err
}
if response.ReturnObj == nil || response.ReturnObj.Records == nil || len(response.ReturnObj.Records) == 0 {
return 0, nil
}
return response.ReturnObj.Records[0].RecordId, nil
}
func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error {
recordId, err := d.findDNSRecordId(zoneName, subDomain)
if err != nil {
return err
}
if recordId == 0 {
// 新增解析记录
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11259&data=181&isNormal=1&vid=259
request := &ctyundns.AddRecordRequest{
Domain: typeutil.ToPtr(zoneName),
Host: typeutil.ToPtr(subDomain),
Type: typeutil.ToPtr("TXT"),
LineCode: typeutil.ToPtr("Default"),
Value: typeutil.ToPtr(value),
State: typeutil.ToPtr(int32(1)),
TTL: typeutil.ToPtr(int32(d.config.TTL)),
}
_, err := d.client.AddRecord(request)
return err
} else {
// 修改解析记录
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11261&data=181&isNormal=1&vid=259
request := &ctyundns.UpdateRecordRequest{
RecordId: typeutil.ToPtr(recordId),
Domain: typeutil.ToPtr(zoneName),
Host: typeutil.ToPtr(subDomain),
Type: typeutil.ToPtr("TXT"),
LineCode: typeutil.ToPtr("Default"),
Value: typeutil.ToPtr(value),
State: typeutil.ToPtr(int32(1)),
TTL: typeutil.ToPtr(int32(d.config.TTL)),
}
_, err := d.client.UpdateRecord(request)
return err
}
}
func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error {
recordId, err := d.findDNSRecordId(zoneName, subDomain)
if err != nil {
return err
}
if recordId == 0 {
return nil
} else {
// 删除解析记录
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11262&data=181&isNormal=1&vid=259
request := &ctyundns.DeleteRecordRequest{
RecordId: typeutil.ToPtr(recordId),
}
_, err = d.client.DeleteRecord(request)
return err
}
}

View File

@@ -1,4 +1,4 @@
package lego_dnsla
package internal
import (
"errors"

View File

@@ -1,4 +1,4 @@
package lego_dynv6
package internal
import (
"context"

View File

@@ -1,4 +1,4 @@
package lego_gname
package internal
import (
"errors"

View File

@@ -1,4 +1,4 @@
package lego_jdcloud
package internal
import (
"errors"

View File

@@ -1,10 +1,9 @@
package lego_tencentcloudeo
package internal
import (
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/go-acme/lego/v4/challenge"
@@ -91,7 +90,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
if err := d.addOrUpdateDNSRecord(strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil {
if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(info.EffectiveFQDN), info.Value); err != nil {
return fmt.Errorf("tencentcloud-eo: %w", err)
}
@@ -101,7 +100,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
if err := d.removeDNSRecord(strings.TrimRight(info.EffectiveFQDN, ".")); err != nil {
if err := d.removeDNSRecord(dns01.UnFqdn(info.EffectiveFQDN)); err != nil {
return fmt.Errorf("tencentcloud-eo: %w", err)
}

View File

@@ -0,0 +1,165 @@
package internal
import (
"errors"
"fmt"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/platform/config/env"
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
"github.com/usual2970/certimate/internal/pkg/sdk3rd/ucloud/udnr"
)
const (
envNamespace = "UCLOUDUDNR_"
EnvPublicKey = envNamespace + "PUBLIC_KEY"
EnvPrivateKey = envNamespace + "PRIVATE_KEY"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
PrivateKey string
PublicKey string
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int32
HTTPTimeout time.Duration
}
type DNSProvider struct {
client *udnr.UDNRClient
config *Config
}
func NewDefaultConfig() *Config {
return &Config{
TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvPrivateKey, EnvPublicKey)
if err != nil {
return nil, fmt.Errorf("ucloud-udnr: %w", err)
}
config := NewDefaultConfig()
config.PrivateKey = values[EnvPrivateKey]
config.PublicKey = values[EnvPublicKey]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("ucloud-udnr: the configuration of the DNS provider is nil")
}
cfg := ucloud.NewConfig()
credential := auth.NewCredential()
credential.PrivateKey = config.PrivateKey
credential.PublicKey = config.PublicKey
client := udnr.NewClient(&cfg, &credential)
return &DNSProvider{
client: client,
config: config,
}, nil
}
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
}
udnrDomainDNSQueryReq := d.client.NewQueryDomainDNSRequest()
udnrDomainDNSQueryReq.Dn = ucloud.String(authZone)
if udnrDomainDNSQueryResp, err := d.client.QueryDomainDNS(udnrDomainDNSQueryReq); err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
} else {
for _, record := range udnrDomainDNSQueryResp.Data {
if record.DnsType == "TXT" && record.RecordName == subDomain {
udnrDomainDNSDeleteReq := d.client.NewDeleteDomainDNSRequest()
udnrDomainDNSDeleteReq.Dn = ucloud.String(authZone)
udnrDomainDNSDeleteReq.DnsType = ucloud.String(record.DnsType)
udnrDomainDNSDeleteReq.RecordName = ucloud.String(record.RecordName)
udnrDomainDNSDeleteReq.Content = ucloud.String(record.Content)
d.client.DeleteDomainDNS(udnrDomainDNSDeleteReq)
break
}
}
}
udnrDomainDNSAddReq := d.client.NewAddDomainDNSRequest()
udnrDomainDNSAddReq.Dn = ucloud.String(authZone)
udnrDomainDNSAddReq.DnsType = ucloud.String("TXT")
udnrDomainDNSAddReq.RecordName = ucloud.String(subDomain)
udnrDomainDNSAddReq.Content = ucloud.String(info.Value)
udnrDomainDNSAddReq.TTL = ucloud.Int(int(d.config.TTL))
if _, err := d.client.AddDomainDNS(udnrDomainDNSAddReq); err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
}
return nil
}
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(domain, keyAuth)
authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("ucloud-udnr: could not find zone for domain %q: %w", domain, err)
}
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
if err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
}
udnrDomainDNSQueryReq := d.client.NewQueryDomainDNSRequest()
udnrDomainDNSQueryReq.Dn = ucloud.String(authZone)
if udnrDomainDNSQueryResp, err := d.client.QueryDomainDNS(udnrDomainDNSQueryReq); err != nil {
return fmt.Errorf("ucloud-udnr: %w", err)
} else {
for _, record := range udnrDomainDNSQueryResp.Data {
if record.DnsType == "TXT" && record.RecordName == subDomain {
udnrDomainDNSDeleteReq := d.client.NewDeleteDomainDNSRequest()
udnrDomainDNSDeleteReq.Dn = ucloud.String(authZone)
udnrDomainDNSDeleteReq.DnsType = ucloud.String(record.DnsType)
udnrDomainDNSDeleteReq.RecordName = ucloud.String(record.RecordName)
udnrDomainDNSDeleteReq.Content = ucloud.String(record.Content)
d.client.DeleteDomainDNS(udnrDomainDNSDeleteReq)
break
}
}
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}

View File

@@ -0,0 +1,40 @@
package ucloududnr
import (
"errors"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ucloud-udnr/internal"
)
type ChallengeProviderConfig struct {
PrivateKey string `json:"privateKey"`
PublicKey string `json:"publicKey"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"`
}
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
return nil, errors.New("config is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.PrivateKey = config.PrivateKey
providerConfig.PublicKey = config.PublicKey
if config.DnsTTL != 0 {
providerConfig.TTL = config.DnsTTL
}
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
provider, err := internal.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}

View File

@@ -62,10 +62,15 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 设置面板 SSL 证书
SSLEnable := "enable"
if d.config.ApiVersion == "v2" {
SSLEnable = "Enable"
}
updateSystemSSLReq := &onepanelsdk.UpdateSystemSSLRequest{
Cert: certPEM,
Key: privkeyPEM,
SSL: "enable",
SSL: SSLEnable,
SSLType: "import-paste",
}
if d.config.AutoRestart {

View File

@@ -202,6 +202,7 @@ func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudA
listListenerCertificatesReq := &aliga.ListListenerCertificatesRequest{
RegionId: tea.String("cn-hangzhou"),
AcceleratorId: tea.String(d.config.AcceleratorId),
ListenerId: tea.String(d.config.ListenerId),
NextToken: listListenerCertificatesNextToken,
MaxResults: tea.Int32(20),
}

View File

@@ -0,0 +1,125 @@
package apisix
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net/url"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
apisixsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/apisix"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// APISIX 服务地址。
ServerUrl string `json:"serverUrl"`
// APISIX Admin API Key。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
CertificateId string `json:"certificateId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *apisixsdk.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ServerUrl, 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.New(slog.DiscardHandler)
} 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 == "" {
return errors.New("config `certificateId` is required")
}
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return err
}
// 更新 SSL 证书
// REF: https://apisix.apache.org/zh/docs/apisix/admin-api/#ssl
updateSSLReq := &apisixsdk.UpdateSSLRequest{
ID: d.config.CertificateId,
Cert: typeutil.ToPtr(certPEM),
Key: typeutil.ToPtr(privkeyPEM),
SNIs: typeutil.ToPtr(certX509.DNSNames),
Type: typeutil.ToPtr("server"),
Status: typeutil.ToPtr(int32(1)),
}
updateSSLResp, err := d.sdkClient.UpdateSSL(updateSSLReq)
d.logger.Debug("sdk request 'apisix.UpdateSSL'", slog.Any("request", updateSSLReq), slog.Any("response", updateSSLResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'apisix.UpdateSSL': %w", err)
}
return nil
}
func createSdkClient(serverUrl, apiKey string, skipTlsVerify bool) (*apisixsdk.Client, error) {
if _, err := url.Parse(serverUrl); err != nil {
return nil, errors.New("invalid apisix server url")
}
if apiKey == "" {
return nil, errors.New("invalid apisix api key")
}
client := apisixsdk.NewClient(serverUrl, apiKey)
if skipTlsVerify {
client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@@ -0,0 +1,77 @@
package apisix_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/apisix"
)
var (
fInputCertPath string
fInputKeyPath string
fServerUrl string
fApiKey string
fCertificateId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_APISIX_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
}
/*
Shell command to run this test:
go test -v ./apisix_test.go -args \
--CERTIMATE_DEPLOYER_APISIX_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_APISIX_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_APISIX_SERVERURL="http://127.0.0.1:9080" \
--CERTIMATE_DEPLOYER_APISIX_APIKEY="your-api-key" \
--CERTIMATE_DEPLOYER_APISIX_CERTIFICATEID="your-cerficiate-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("SERVERURL: %v", fServerUrl),
fmt.Sprintf("APIKEY: %v", fApiKey),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
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

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

View File

@@ -14,7 +14,8 @@ 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/aws-acm"
uploaderspacm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aws-acm"
uploaderspiam "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aws-iam"
)
type DeployerConfig struct {
@@ -26,6 +27,9 @@ type DeployerConfig struct {
Region string `json:"region"`
// AWS CloudFront 分配 ID。
DistributionId string `json:"distributionId"`
// AWS CloudFront 证书来源。
// 可取值 "ACM"、"IAM"。
CertificateSource string `json:"certificateSource"`
}
type DeployerProvider struct {
@@ -47,13 +51,28 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
var uploader uploader.Uploader
if config.CertificateSource == "ACM" {
uploader, err = uploaderspacm.NewUploader(&uploaderspacm.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
}
} else if config.CertificateSource == "IAM" {
uploader, err = uploaderspiam.NewUploader(&uploaderspiam.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
CertificatePath: "/cloudfront/",
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
}
} else {
return nil, fmt.Errorf("unsupported certificate source: '%s'", config.CertificateSource)
}
return &DeployerProvider{
@@ -79,7 +98,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return nil, errors.New("config `distribuitionId` is required")
}
// 上传证书到 ACM
// 上传证书到 ACM/IAM
upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
@@ -109,7 +128,19 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
updateDistributionReq.DistributionConfig.ViewerCertificate = &types.ViewerCertificate{}
}
updateDistributionReq.DistributionConfig.ViewerCertificate.CloudFrontDefaultCertificate = aws.Bool(false)
updateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = aws.String(upres.CertId)
if d.config.CertificateSource == "ACM" {
updateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = aws.String(upres.CertId)
updateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = nil
} else if d.config.CertificateSource == "IAM" {
updateDistributionReq.DistributionConfig.ViewerCertificate.ACMCertificateArn = nil
updateDistributionReq.DistributionConfig.ViewerCertificate.IAMCertificateId = aws.String(upres.CertId)
if updateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion == "" {
updateDistributionReq.DistributionConfig.ViewerCertificate.MinimumProtocolVersion = types.MinimumProtocolVersionTLSv1
}
if updateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod == "" {
updateDistributionReq.DistributionConfig.ViewerCertificate.SSLSupportMethod = types.SSLSupportMethodSniOnly
}
}
updateDistributionResp, err := d.sdkClient.UpdateDistribution(context.TODO(), updateDistributionReq)
d.logger.Debug("sdk request 'cloudfront.UpdateDistribution'", slog.Any("request", updateDistributionReq), slog.Any("response", updateDistributionResp))
if err != nil {

View File

@@ -0,0 +1,75 @@
package awsiam
import (
"context"
"fmt"
"log/slog"
"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-iam"
)
type DeployerConfig struct {
// AWS AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// AWS SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
// IAM 证书路径。
// 选填。
CertificatePath string `json:"certificatePath,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
Region: config.Region,
CertificatePath: config.CertificatePath,
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslUploader.WithLogger(logger)
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 IAM
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))
}
return &deployer.DeployResult{}, nil
}

View File

@@ -285,6 +285,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler,
CertIds: describeAppHTTPSListenersResp.ListenerList[0].CertIds,
AdditionalCertDomains: sliceutil.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel {
if domain.Host == d.config.Domain {
return bceappblb.AdditionalCertDomainsModel{

View File

@@ -283,6 +283,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
ClientToken: generateClientToken(),
ListenerPort: uint16(cloudHttpsListenerPort),
CertIds: describeHTTPSListenersResp.ListenerList[0].CertIds,
AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel {
if domain.Host == d.config.Domain {
return bceblb.AdditionalCertDomainsModel{

View File

@@ -0,0 +1,113 @@
package ctcccloudao
import (
"context"
"errors"
"fmt"
"log/slog"
"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/ctcccloud-ao"
ctyunao "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/ao"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *ctyunao.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.SecretAccessKey)
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,
})
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.New(slog.DiscardHandler)
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书到 AccessOne
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://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13412&data=174&isNormal=1&vid=167
getDomainConfigReq := &ctyunao.GetDomainConfigRequest{
Domain: typeutil.ToPtr(d.config.Domain),
}
getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq)
d.logger.Debug("sdk request 'cdn.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDomainConfig': %w", err)
}
// 域名基础及加速配置修改
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13413&data=174&isNormal=1&vid=167
modifyDomainConfigReq := &ctyunao.ModifyDomainConfigRequest{
Domain: typeutil.ToPtr(d.config.Domain),
ProductCode: typeutil.ToPtr(getDomainConfigResp.ReturnObj.ProductCode),
Origin: getDomainConfigResp.ReturnObj.Origin,
HttpsStatus: typeutil.ToPtr("on"),
CertName: typeutil.ToPtr(upres.CertName),
}
modifyDomainConfigResp, err := d.sdkClient.ModifyDomainConfig(modifyDomainConfigReq)
d.logger.Debug("sdk request 'cdn.ModifyDomainConfig'", slog.Any("request", modifyDomainConfigReq), slog.Any("response", modifyDomainConfigResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.ModifyDomainConfig': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) {
return ctyunao.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudao_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-ao"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDAO_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_ao_test.go -args \
--CERTIMATE_DEPLOYER_CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_CTCCCLOUDAO_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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
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,111 @@
package ctcccloudcdn
import (
"context"
"errors"
"fmt"
"log/slog"
"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/ctcccloud-cdn"
ctyuncdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/cdn"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *ctyuncdn.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.SecretAccessKey)
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,
})
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.New(slog.DiscardHandler)
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书到 CDN
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://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11304&data=161&isNormal=1&vid=154
queryDomainDetailReq := &ctyuncdn.QueryDomainDetailRequest{
Domain: typeutil.ToPtr(d.config.Domain),
}
queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
d.logger.Debug("sdk request 'cdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainDetail': %w", err)
}
// 修改域名配置
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154
updateDomainReq := &ctyuncdn.UpdateDomainRequest{
Domain: typeutil.ToPtr(d.config.Domain),
HttpsStatus: typeutil.ToPtr("on"),
CertName: typeutil.ToPtr(upres.CertName),
}
updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
d.logger.Debug("sdk request 'cdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.UpdateDomain': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) {
return ctyuncdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
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,67 @@
package ctcccloudcms
import (
"context"
"fmt"
"log/slog"
"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/ctcccloud-cms"
)
type DeployerConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
})
if err != nil {
return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 CMS
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))
}
return &deployer.DeployResult{}, nil
}

View File

@@ -0,0 +1,70 @@
package ctcccloudcms_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cms"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDCMS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cms_test.go -args \
--CERTIMATE_DEPLOYER_CTCCCLOUDCMS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCMS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCMS_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCMS_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
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 ctcccloudelb
type ResourceType string
const (
// 资源类型:部署到指定负载均衡器。
RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,199 @@
package ctcccloudelb
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"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/ctcccloud-elb"
ctyunelb "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/elb"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 天翼云资源池 ID。
RegionId string `json:"regionId"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 负载均衡实例 ID。
// 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。
LoadbalancerId string `json:"loadbalancerId,omitempty"`
// 负载均衡监听器 ID。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *ctyunelb.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.SecretAccessKey)
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,
RegionId: config.RegionId,
})
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.New(slog.DiscardHandler)
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 ELB
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_LOADBALANCER:
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
return nil, err
}
case RESOURCE_TYPE_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
if d.config.LoadbalancerId == "" {
return errors.New("config `loadbalancerId` is required")
}
// 查询监听列表
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5654&data=88&isNormal=1&vid=82
listenerIds := make([]string, 0)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &ctyunelb.ListListenersRequest{
RegionID: typeutil.ToPtr(d.config.RegionId),
LoadBalancerID: typeutil.ToPtr(d.config.LoadbalancerId),
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
d.logger.Debug("sdk request 'elb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err)
}
for _, listener := range listListenersResp.ReturnObj {
if strings.EqualFold(listener.Protocol, "HTTPS") {
listenerIds = append(listenerIds, listener.ID)
}
}
break
}
// 遍历更新监听证书
if len(listenerIds) == 0 {
d.logger.Info("no elb listeners to deploy")
} else {
d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds))
var errs []error
for _, listenerId := range listenerIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, 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.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
// 更新监听器
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5652&data=88&isNormal=1&vid=82
setLoadBalancerHTTPSListenerAttributeReq := &ctyunelb.UpdateListenerRequest{
RegionID: typeutil.ToPtr(d.config.RegionId),
ListenerID: typeutil.ToPtr(cloudListenerId),
CertificateID: typeutil.ToPtr(cloudCertId),
}
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.UpdateListener(setLoadBalancerHTTPSListenerAttributeReq)
d.logger.Debug("sdk request 'elb.UpdateListener'", slog.Any("request", setLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", setLoadBalancerHTTPSListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err)
}
return nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) {
return ctyunelb.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,118 @@
package ctcccloudelb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-elb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegionId string
fLoadbalancerId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDELB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "")
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_elb_test.go -args \
--CERTIMATE_DEPLOYER_CTCCCLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDELB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_CTCCCLOUDELB_REGIONID="your-region-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDELB_LOADBALANCERID="your-elb-instance-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDELB_LISTENERID="your-elb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy_ToLoadbalancer", 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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGIONID: %v", fRegionId),
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
RegionId: fRegionId,
ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
LoadbalancerId: fLoadbalancerId,
})
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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGIONID: %v", fRegionId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
RegionId: fRegionId,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
ListenerId: fListenerId,
})
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,111 @@
package ctcccloudicdn
import (
"context"
"errors"
"fmt"
"log/slog"
"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/ctcccloud-icdn"
ctyunicdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/icdn"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *ctyunicdn.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.SecretAccessKey)
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,
})
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.New(slog.DiscardHandler)
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书到 ICDN
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://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10849&data=173&isNormal=1&vid=166
queryDomainDetailReq := &ctyunicdn.QueryDomainDetailRequest{
Domain: typeutil.ToPtr(d.config.Domain),
}
queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
d.logger.Debug("sdk request 'icdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryDomainDetail': %w", err)
}
// 修改域名配置
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10853&data=173&isNormal=1&vid=166
updateDomainReq := &ctyunicdn.UpdateDomainRequest{
Domain: typeutil.ToPtr(d.config.Domain),
HttpsStatus: typeutil.ToPtr("on"),
CertName: typeutil.ToPtr(upres.CertName),
}
updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
d.logger.Debug("sdk request 'icdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'icdn.UpdateDomain': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) {
return ctyunicdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudicdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-icdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cdn_test.go -args \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_CTCCCLOUDCDN_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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
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,113 @@
package ctcccloudlvdn
import (
"context"
"errors"
"fmt"
"log/slog"
"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/ctcccloud-lvdn"
ctyunlvdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/lvdn"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 加速域名(不支持泛域名)。
Domain string `json:"domain"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *ctyunlvdn.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.SecretAccessKey)
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,
})
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.New(slog.DiscardHandler)
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 上传证书到 CDN
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://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11473&data=183&isNormal=1&vid=261
queryDomainDetailReq := &ctyunlvdn.QueryDomainDetailRequest{
Domain: typeutil.ToPtr(d.config.Domain),
ProductCode: typeutil.ToPtr("005"),
}
queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
d.logger.Debug("sdk request 'lvdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryDomainDetail': %w", err)
}
// 修改域名配置
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154
updateDomainReq := &ctyunlvdn.UpdateDomainRequest{
Domain: typeutil.ToPtr(d.config.Domain),
ProductCode: typeutil.ToPtr("005"),
HttpsSwitch: typeutil.ToPtr(int32(1)),
CertName: typeutil.ToPtr(upres.CertName),
}
updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
d.logger.Debug("sdk request 'lvdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'lvdn.UpdateDomain': %w", err)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) {
return ctyunlvdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,75 @@
package ctcccloudlvdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-lvdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_lvdn_test.go -args \
--CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
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

@@ -8,6 +8,8 @@ import (
"net"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/sftp"
"github.com/povsister/scp"
@@ -24,7 +26,12 @@ type JumpServerConfig struct {
// SSH 端口。
// 零值时默认值 22。
SshPort int32 `json:"sshPort,omitempty"`
// SSH 认证方式。
// 可取值 "none"、"password"、"key"。
// 零值时根据有无密码或私钥字段决定。
SshAuthMethod string `json:"sshAuthMethod,omitempty"`
// SSH 登录用户名。
// 零值时默认值 "root"。
SshUsername string `json:"sshUsername,omitempty"`
// SSH 登录密码。
SshPassword string `json:"sshPassword,omitempty"`
@@ -41,7 +48,12 @@ type DeployerConfig struct {
// SSH 端口。
// 零值时默认值 22。
SshPort int32 `json:"sshPort,omitempty"`
// SSH 认证方式。
// 可取值 "none"、"password" 或 "key"。
// 零值时根据有无密码或私钥字段决定。
SshAuthMethod string `json:"sshAuthMethod,omitempty"`
// SSH 登录用户名。
// 零值时默认值 "root"。
SshUsername string `json:"sshUsername,omitempty"`
// SSH 登录密码。
SshPassword string `json:"sshPassword,omitempty"`
@@ -128,9 +140,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
var jumpConn net.Conn
// 第一个连接是主机发起,后续通过跳板机发起
if jumpClient == nil {
jumpConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", jumpServerConf.SshHost, jumpServerConf.SshPort))
jumpConn, err = net.Dial("tcp", net.JoinHostPort(jumpServerConf.SshHost, strconv.Itoa(int(jumpServerConf.SshPort))))
} else {
jumpConn, err = jumpClient.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", jumpServerConf.SshHost, jumpServerConf.SshPort))
jumpConn, err = jumpClient.DialContext(ctx, "tcp", net.JoinHostPort(jumpServerConf.SshHost, strconv.Itoa(int(jumpServerConf.SshPort))))
}
if err != nil {
return nil, fmt.Errorf("failed to connect to jump server [%d]: %w", i+1, err)
@@ -141,6 +153,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
jumpConn,
jumpServerConf.SshHost,
jumpServerConf.SshPort,
jumpServerConf.SshAuthMethod,
jumpServerConf.SshUsername,
jumpServerConf.SshPassword,
jumpServerConf.SshKey,
@@ -156,13 +169,13 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
// 通过跳板机发起 TCP 连接到目标服务器
targetConn, err = jumpClient.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", d.config.SshHost, d.config.SshPort))
targetConn, err = jumpClient.DialContext(ctx, "tcp", net.JoinHostPort(d.config.SshHost, strconv.Itoa(int(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))
targetConn, err = net.Dial("tcp", net.JoinHostPort(d.config.SshHost, strconv.Itoa(int(d.config.SshPort))))
if err != nil {
return nil, fmt.Errorf("failed to connect to target server: %w", err)
}
@@ -174,6 +187,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
targetConn,
d.config.SshHost,
d.config.SshPort,
d.config.SshAuthMethod,
d.config.SshUsername,
d.config.SshPassword,
d.config.SshKey,
@@ -262,7 +276,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
return &deployer.DeployResult{}, nil
}
func createSshClient(conn net.Conn, host string, port int32, username string, password string, key string, keyPassphrase string) (*ssh.Client, error) {
func createSshClient(conn net.Conn, host string, port int32, authMethod string, username, password, key, keyPassphrase string) (*ssh.Client, error) {
if host == "" {
host = "localhost"
}
@@ -271,28 +285,66 @@ func createSshClient(conn net.Conn, host string, port int32, username string, pa
port = 22
}
var authMethod ssh.AuthMethod
if key != "" {
var signer ssh.Signer
var err error
if keyPassphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(key))
}
if err != nil {
return nil, err
}
authMethod = ssh.PublicKeys(signer)
} else {
authMethod = ssh.Password(password)
if username == "" {
username = "root"
}
sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
const AUTH_METHOD_NONE = "none"
const AUTH_METHOD_PASSWORD = "password"
const AUTH_METHOD_KEY = "key"
if authMethod == "" {
if key != "" {
authMethod = AUTH_METHOD_KEY
} else if password != "" {
authMethod = AUTH_METHOD_PASSWORD
} else {
authMethod = AUTH_METHOD_NONE
}
}
authentications := make([]ssh.AuthMethod, 0)
switch authMethod {
case AUTH_METHOD_NONE:
{
}
case AUTH_METHOD_PASSWORD:
{
authentications = append(authentications, ssh.Password(password))
authentications = append(authentications, ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
if len(questions) == 1 {
return []string{password}, nil
}
return nil, fmt.Errorf("unexpected keyboard interactive question [%s]", strings.Join(questions, ", "))
}))
}
case AUTH_METHOD_KEY:
{
var signer ssh.Signer
var err error
if keyPassphrase != "" {
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
} else {
signer, err = ssh.ParsePrivateKey([]byte(key))
}
if err != nil {
return nil, err
}
authentications = append(authentications, ssh.PublicKeys(signer))
}
default:
return nil, fmt.Errorf("unsupported auth method '%s'", authMethod)
}
addr := net.JoinHostPort(host, strconv.Itoa(int(port)))
sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{authMethod},
Auth: authentications,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {

View File

@@ -136,7 +136,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
// REF: https://cloud.tencent.com/document/api/400/91658
for {
select {
case <-ctx.Done():

View File

@@ -153,7 +153,7 @@ func (d *DeployerProvider) deployViaSslService(ctx context.Context, cloudCertId
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
// REF: https://cloud.tencent.com/document/api/400/91658
for {
select {
case <-ctx.Done():

View File

@@ -104,7 +104,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
// REF: https://cloud.tencent.com/document/api/400/91658
for {
select {
case <-ctx.Done():

View File

@@ -119,7 +119,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
// REF: https://cloud.tencent.com/document/api/400/91658
for {
select {
case <-ctx.Done():

View File

@@ -23,7 +23,7 @@ type DeployerConfig struct {
SecretKey string `json:"secretKey"`
// 站点 ID。
ZoneId string `json:"zoneId"`
// 加速域名(支持泛域名)。
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}

View File

@@ -0,0 +1,8 @@
package tencentcloudgaap
type ResourceType string
const (
// 资源类型:部署到指定监听器。
RESOURCE_TYPE_LISTENER = ResourceType("listener")
)

View File

@@ -0,0 +1,154 @@
package tencentcloudgaap
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcgaap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529"
"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/tencentcloud-ssl"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type DeployerConfig struct {
// 腾讯云 SecretId。
SecretId string `json:"secretId"`
// 腾讯云 SecretKey。
SecretKey string `json:"secretKey"`
// 部署资源类型。
ResourceType ResourceType `json:"resourceType"`
// 通道 ID。
// 选填。
ProxyId string `json:"proxyId,omitempty"`
// 负载均衡监听 ID。
// 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
ListenerId string `json:"listenerId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *tcgaap.Client
sslUploader uploader.Uploader
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClients(config.SecretId, config.SecretKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
SecretId: config.SecretId,
SecretKey: config.SecretKey,
})
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.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sslUploader.WithLogger(logger)
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 上传证书到 SSL
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_LISTENER:
if err := d.deployToListener(ctx, upres.CertId); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
}
return &deployer.DeployResult{}, nil
}
func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
if d.config.ListenerId == "" {
return errors.New("config `listenerId` is required")
}
// 更新监听器证书
if err := d.modifyHttpsListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err
}
return nil
}
func (d *DeployerProvider) modifyHttpsListenerCertificate(ctx context.Context, cloudListenerId, cloudCertId string) error {
// 查询 HTTPS 监听器信息
// REF: https://cloud.tencent.com/document/product/608/37001
describeHTTPSListenersReq := tcgaap.NewDescribeHTTPSListenersRequest()
describeHTTPSListenersReq.ListenerId = common.StringPtr(cloudListenerId)
describeHTTPSListenersReq.Offset = common.Uint64Ptr(0)
describeHTTPSListenersReq.Limit = common.Uint64Ptr(1)
describeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(describeHTTPSListenersReq)
d.logger.Debug("sdk request 'gaap.DescribeHTTPSListeners'", slog.Any("request", describeHTTPSListenersReq), slog.Any("response", describeHTTPSListenersResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'gaap.DescribeHTTPSListeners': %w", err)
} else if len(describeHTTPSListenersResp.Response.ListenerSet) == 0 {
return errors.New("listener not found")
}
// 修改 HTTPS 监听器配置
// REF: https://cloud.tencent.com/document/product/608/36996
modifyHTTPSListenerAttributeReq := tcgaap.NewModifyHTTPSListenerAttributeRequest()
modifyHTTPSListenerAttributeReq.ProxyId = typeutil.ToPtrOrZeroNil(d.config.ProxyId)
modifyHTTPSListenerAttributeReq.ListenerId = common.StringPtr(cloudListenerId)
modifyHTTPSListenerAttributeReq.CertificateId = common.StringPtr(cloudCertId)
modifyHTTPSListenerAttributeResp, err := d.sdkClient.ModifyHTTPSListenerAttribute(modifyHTTPSListenerAttributeReq)
d.logger.Debug("sdk request 'gaap.ModifyHTTPSListenerAttribute'", slog.Any("request", modifyHTTPSListenerAttributeReq), slog.Any("response", modifyHTTPSListenerAttributeResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'gaap.ModifyHTTPSListenerAttribute': %w", err)
}
return nil
}
func createSdkClients(secretId, secretKey string) (*tcgaap.Client, error) {
credential := common.NewCredential(secretId, secretKey)
client, err := tcgaap.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -0,0 +1,86 @@
package tencentcloudgaap_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-gaap"
)
var (
fInputCertPath string
fInputKeyPath string
fSecretId string
fSecretKey string
fProxyGroupId string
fProxyId string
fListenerId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fProxyGroupId, argsPrefix+"PROXYGROUPID", "", "")
flag.StringVar(&fProxyId, argsPrefix+"PROXYID", "", "")
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
}
/*
Shell command to run this test:
go test -v ./tencentcloud_gaap_test.go -args \
--CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_SECRETID="your-secret-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_SECRETKEY="your-secret-key" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_PROXYGROUPID="your-gaap-group-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_PROXYID="your-gaap-group-id" \
--CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_LISTENERID="your-clb-listener-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
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("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("PROXYGROUPID: %v", fProxyGroupId),
fmt.Sprintf("PROXYID: %v", fProxyId),
fmt.Sprintf("LISTENERID: %v", fListenerId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
ResourceType: provider.RESOURCE_TYPE_LISTENER,
ProxyGroupId: fProxyGroupId,
ProxyId: fProxyId,
ListenerId: fListenerId,
})
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

@@ -106,7 +106,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
// REF: https://cloud.tencent.com/document/api/400/91658
for {
select {
case <-ctx.Done():

View File

@@ -6,11 +6,13 @@ import (
"fmt"
"log/slog"
"strconv"
"strings"
"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"
sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice"
)
type DeployerConfig struct {
@@ -18,7 +20,7 @@ type DeployerConfig struct {
AccessKeyId string `json:"accessKeyId"`
// 网宿云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 加速域名数组。
// 加速域名数组(支持泛域名)
Domains []string `json:"domains"`
}
@@ -80,7 +82,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
batchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{
CertificateId: certId,
DomainNames: d.config.Domains,
DomainNames: sliceutil.Map(d.config.Domains, func(domain string) string {
// "*.example.com" → ".example.com",适配网宿云 CDN 要求的泛域名格式
return strings.TrimPrefix(domain, "*")
}),
}
batchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfig(batchUpdateCertificateConfigReq)
d.logger.Debug("sdk request 'cdn.BatchUpdateCertificateConfig'", slog.Any("request", batchUpdateCertificateConfigReq), slog.Any("response", batchUpdateCertificateConfigResp))

View File

@@ -3,9 +3,10 @@ package email
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"net"
"net/smtp"
"strconv"
"github.com/domodwyer/mailyak/v3"
@@ -26,6 +27,8 @@ type NotifierConfig struct {
Password string `json:"password"`
// 发件人邮箱。
SenderAddress string `json:"senderAddress"`
// 发件人显示名称。
SenderName string `json:"senderName,omitempty"`
// 收件人邮箱。
ReceiverAddress string `json:"receiverAddress"`
}
@@ -66,12 +69,12 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
var smtpAddr string
if n.config.SmtpPort == 0 {
if n.config.SmtpTls {
smtpAddr = fmt.Sprintf("%s:465", n.config.SmtpHost)
smtpAddr = net.JoinHostPort(n.config.SmtpHost, "465")
} else {
smtpAddr = fmt.Sprintf("%s:25", n.config.SmtpHost)
smtpAddr = net.JoinHostPort(n.config.SmtpHost, "25")
}
} else {
smtpAddr = fmt.Sprintf("%s:%d", n.config.SmtpHost, n.config.SmtpPort)
smtpAddr = net.JoinHostPort(n.config.SmtpHost, strconv.Itoa(int(n.config.SmtpPort)))
}
var yak *mailyak.MailYak
@@ -86,6 +89,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
}
yak.From(n.config.SenderAddress)
yak.FromName(n.config.SenderName)
yak.To(n.config.ReceiverAddress)
yak.Subject(subject)
yak.Plain().Set(message)

View File

@@ -61,7 +61,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 遍历证书列表,避免重复上传
if res, err := u.getCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
if res, err := u.findCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
} else if res != nil {
u.logger.Info("ssl certificate already exists")
@@ -85,7 +85,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
// 遍历证书列表,获取刚刚上传证书 ID
if res, err := u.getCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
if res, err := u.findCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
} else if res == nil {
return nil, fmt.Errorf("no ssl certificate found, may be upload failed (code: %d, message: %s)", uploadWebsiteSSLResp.GetCode(), uploadWebsiteSSLResp.GetMessage())
@@ -94,7 +94,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
}
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
searchWebsiteSSLPageNumber := int32(1)
searchWebsiteSSLPageSize := int32(100)
for {

View File

@@ -74,7 +74,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
// 获取证书列表,避免重复上传
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html
var listCertificatesNextToken *string = nil
listCertificatesMaxItems := int32(1000)
var listCertificatesMaxItems int32 = 1000
for {
select {
case <-ctx.Done():
@@ -107,7 +107,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
// 最后对比证书内容
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListTagsForCertificate.html
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_GetCertificate.html
getCertificateReq := &awsacm.GetCertificateInput{
CertificateArn: certSummary.CertificateArn,
}
@@ -115,11 +115,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'acm.GetCertificate': %w", err)
} else {
oldCertPEM := aws.ToString(getCertificateResp.CertificateChain)
if oldCertPEM == "" {
oldCertPEM = aws.ToString(getCertificateResp.Certificate)
}
oldCertPEM := aws.ToString(getCertificateResp.Certificate)
oldCertX509, err := certutil.ParseCertificateFromPEM(oldCertPEM)
if err != nil {
continue
@@ -158,7 +154,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
return &uploader.UploadResult{
CertId: *importCertificateResp.CertificateArn,
CertId: aws.ToString(importCertificateResp.CertificateArn),
}, nil
}

View File

@@ -0,0 +1,185 @@
package awsiam
import (
"context"
"fmt"
"log/slog"
"time"
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"
awsiam "github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
)
type UploaderConfig struct {
// AWS AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// AWS SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
// IAM 证书路径。
// 选填。
CertificatePath string `json:"certificatePath,omitempty"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *awsiam.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
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)
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.New(slog.DiscardHandler)
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 提取服务器证书
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/IAM/latest/APIReference/API_ListServerCertificates.html
var listServerCertificatesMarker *string = nil
var listServerCertificatesMaxItems int32 = 1000
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listServerCertificatesReq := &awsiam.ListServerCertificatesInput{
Marker: listServerCertificatesMarker,
MaxItems: aws.Int32(listServerCertificatesMaxItems),
}
if u.config.CertificatePath != "" {
listServerCertificatesReq.PathPrefix = aws.String(u.config.CertificatePath)
}
listServerCertificatesResp, err := u.sdkClient.ListServerCertificates(context.TODO(), listServerCertificatesReq)
u.logger.Debug("sdk request 'iam.ListServerCertificates'", slog.Any("request", listServerCertificatesReq), slog.Any("response", listServerCertificatesResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'iam.ListServerCertificates': %w", err)
}
for _, certMeta := range listServerCertificatesResp.ServerCertificateMetadataList {
// 先对比证书路径
if u.config.CertificatePath != "" && aws.ToString(certMeta.Path) != u.config.CertificatePath {
continue
}
// 先对比证书有效期
if certMeta.Expiration == nil || !certMeta.Expiration.Equal(certX509.NotAfter) {
continue
}
// 最后对比证书内容
// REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_GetServerCertificate.html
getServerCertificateReq := &awsiam.GetServerCertificateInput{
ServerCertificateName: certMeta.ServerCertificateName,
}
getServerCertificateResp, err := u.sdkClient.GetServerCertificate(context.TODO(), getServerCertificateReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'iam.GetServerCertificate': %w", err)
} else {
oldCertPEM := aws.ToString(getServerCertificateResp.ServerCertificate.CertificateBody)
oldCertX509, err := certutil.ParseCertificateFromPEM(oldCertPEM)
if err != nil {
continue
}
if !certutil.EqualCertificate(certX509, oldCertX509) {
continue
}
}
// 如果以上信息都一致,则视为已存在相同证书,直接返回
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: aws.ToString(certMeta.ServerCertificateId),
CertName: aws.ToString(certMeta.ServerCertificateName),
}, nil
}
if listServerCertificatesResp.Marker == nil || len(listServerCertificatesResp.ServerCertificateMetadataList) < int(listServerCertificatesMaxItems) {
break
} else {
listServerCertificatesMarker = listServerCertificatesResp.Marker
}
}
// 生成新证书名(需符合 AWS IAM 命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 导入证书
// REF: https://docs.aws.amazon.com/en_us/IAM/latest/APIReference/API_UploadServerCertificate.html
uploadServerCertificateReq := &awsiam.UploadServerCertificateInput{
ServerCertificateName: aws.String(certName),
Path: aws.String(u.config.CertificatePath),
CertificateBody: aws.String(serverCertPEM),
CertificateChain: aws.String(intermediaCertPEM),
PrivateKey: aws.String(privkeyPEM),
}
if u.config.CertificatePath == "" {
uploadServerCertificateReq.Path = aws.String("/")
}
uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificate(context.TODO(), uploadServerCertificateReq)
u.logger.Debug("sdk request 'iam.UploadServerCertificate'", slog.Any("request", uploadServerCertificateReq), slog.Any("response", uploadServerCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'iam.UploadServerCertificate': %w", err)
}
return &uploader.UploadResult{
CertId: aws.ToString(uploadServerCertificateResp.ServerCertificateMetadata.ServerCertificateId),
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey, region string) (*awsiam.Client, error) {
cfg, err := awscfg.LoadDefaultConfig(context.TODO())
if err != nil {
return nil, err
}
client := awsiam.NewFromConfig(cfg, func(o *awsiam.Options) {
o.Region = region
o.Credentials = aws.NewCredentialsCache(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, ""))
})
return client, nil
}

View File

@@ -0,0 +1,171 @@
package ctcccloudao
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
ctyunao "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/ao"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type UploaderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *ctyunao.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.New(slog.DiscardHandler)
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 查询用户名下证书列表,避免重复上传
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13175&data=174&isNormal=1&vid=167
listCertPage := int32(1)
listCertPerPage := int32(1000)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertsReq := &ctyunao.ListCertsRequest{
Page: typeutil.ToPtr(listCertPage),
PerPage: typeutil.ToPtr(listCertPerPage),
UsageMode: typeutil.ToPtr(int32(0)),
}
listCertsResp, err := u.sdkClient.ListCerts(listCertsReq)
u.logger.Debug("sdk request 'ao.ListCerts'", slog.Any("request", listCertsReq), slog.Any("response", listCertsResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ao.ListCerts': %w", err)
}
if listCertsResp.ReturnObj != nil {
for _, certRecord := range listCertsResp.ReturnObj.Results {
// 对比证书通用名称
if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
continue
}
// 对比证书扩展名称
if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
continue
}
// 对比证书有效期
if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
continue
} else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
continue
}
// 查询证书详情
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13015&data=174&isNormal=1&vid=167
queryCertReq := &ctyunao.QueryCertRequest{
Id: typeutil.ToPtr(certRecord.Id),
}
queryCertResp, err := u.sdkClient.QueryCert(queryCertReq)
u.logger.Debug("sdk request 'ao.QueryCert'", slog.Any("request", queryCertReq), slog.Any("response", queryCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ao.QueryCert': %w", err)
} else if queryCertResp.ReturnObj != nil && queryCertResp.ReturnObj.Result != nil {
var isSameCert bool
if queryCertResp.ReturnObj.Result.Certs == certPEM {
isSameCert = true
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertResp.ReturnObj.Result.Certs)
if err != nil {
continue
}
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回
if isSameCert {
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", queryCertResp.ReturnObj.Result.Id),
CertName: queryCertResp.ReturnObj.Result.Name,
}, nil
}
}
}
}
if listCertsResp.ReturnObj == nil || len(listCertsResp.ReturnObj.Results) < int(listCertPerPage) {
break
} else {
listCertPage++
}
}
// 生成新证书名(需符合天翼云命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 创建证书
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13014&data=174&isNormal=1&vid=167
createCertReq := &ctyunao.CreateCertRequest{
Name: typeutil.ToPtr(certName),
Certs: typeutil.ToPtr(certPEM),
Key: typeutil.ToPtr(privkeyPEM),
}
createCertResp, err := u.sdkClient.CreateCert(createCertReq)
u.logger.Debug("sdk request 'ao.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ao.CreateCert': %w", err)
}
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) {
return ctyunao.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,72 @@
package ctcccloudao_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-ao"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDAO_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_ao_test.go -args \
--CERTIMATE_UPLOADER_CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@@ -0,0 +1,171 @@
package ctcccloudcdn
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
ctyuncdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/cdn"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type UploaderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *ctyuncdn.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.New(slog.DiscardHandler)
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10901&data=161&isNormal=1&vid=154
queryCertListPage := int32(1)
queryCertListPerPage := int32(1000)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
queryCertListReq := &ctyuncdn.QueryCertListRequest{
Page: typeutil.ToPtr(queryCertListPage),
PerPage: typeutil.ToPtr(queryCertListPerPage),
UsageMode: typeutil.ToPtr(int32(0)),
}
queryCertListResp, err := u.sdkClient.QueryCertList(queryCertListReq)
u.logger.Debug("sdk request 'cdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryCertList': %w", err)
}
if queryCertListResp.ReturnObj != nil {
for _, certRecord := range queryCertListResp.ReturnObj.Results {
// 对比证书通用名称
if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
continue
}
// 对比证书扩展名称
if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
continue
}
// 对比证书有效期
if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
continue
} else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
continue
}
// 查询证书详情
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10899&data=161&isNormal=1&vid=154
queryCertDetailReq := &ctyuncdn.QueryCertDetailRequest{
Id: typeutil.ToPtr(certRecord.Id),
}
queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq)
u.logger.Debug("sdk request 'cdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryCertDetail': %w", err)
} else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {
var isSameCert bool
if queryCertDetailResp.ReturnObj.Result.Certs == certPEM {
isSameCert = true
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertDetailResp.ReturnObj.Result.Certs)
if err != nil {
continue
}
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回
if isSameCert {
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id),
CertName: queryCertDetailResp.ReturnObj.Result.Name,
}, nil
}
}
}
}
if queryCertListResp.ReturnObj == nil || len(queryCertListResp.ReturnObj.Results) < int(queryCertListPerPage) {
break
} else {
queryCertListPage++
}
}
// 生成新证书名(需符合天翼云命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 创建证书
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10893&data=161&isNormal=1&vid=154
createCertReq := &ctyuncdn.CreateCertRequest{
Name: typeutil.ToPtr(certName),
Certs: typeutil.ToPtr(certPEM),
Key: typeutil.ToPtr(privkeyPEM),
}
createCertResp, err := u.sdkClient.CreateCert(createCertReq)
u.logger.Debug("sdk request 'cdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.CreateCert': %w", err)
}
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) {
return ctyuncdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,72 @@
package ctcccloudcdn_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-cdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cdn_test.go -args \
--CERTIMATE_UPLOADER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@@ -0,0 +1,186 @@
package ctcccloudcms
import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
ctyuncms "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/cms"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type UploaderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *ctyuncms.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.New(slog.DiscardHandler)
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 遍历证书列表,避免重复上传
if res, _ := u.findCertIfExists(ctx, certPEM); res != nil {
return res, nil
}
// 提取服务器证书
serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 生成新证书名(需符合天翼云命名规则)
certName := fmt.Sprintf("cm%d", time.Now().Unix())
// 上传证书
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17243&data=204&isNormal=1&vid=283
uploadCertificateReq := &ctyuncms.UploadCertificateRequest{
Name: typeutil.ToPtr(certName),
Certificate: typeutil.ToPtr(serverCertPEM),
CertificateChain: typeutil.ToPtr(intermediaCertPEM),
PrivateKey: typeutil.ToPtr(privkeyPEM),
EncryptionStandard: typeutil.ToPtr("INTERNATIONAL"),
}
uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq)
u.logger.Debug("sdk request 'cms.UploadCertificate'", slog.Any("request", uploadCertificateReq), slog.Any("response", uploadCertificateResp))
if err != nil {
if uploadCertificateResp != nil && uploadCertificateResp.GetError() == "CCMS_100000067" {
if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("ctyun cms: no certificate found")
} else {
u.logger.Info("ssl certificate already exists")
return res, nil
}
}
return nil, fmt.Errorf("failed to execute sdk request 'cms.UploadCertificate': %w", err)
}
// 遍历证书列表,获取刚刚上传证书 ID
if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res == nil {
return nil, fmt.Errorf("no ssl certificate found, may be upload failed")
} else {
return res, nil
}
}
func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 查询用户证书列表
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17233&data=204&isNormal=1&vid=283
getCertificateListPageNum := int32(1)
getCertificateListPageSize := int32(10)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getCertificateListReq := &ctyuncms.GetCertificateListRequest{
PageNum: typeutil.ToPtr(getCertificateListPageNum),
PageSize: typeutil.ToPtr(getCertificateListPageSize),
Keyword: typeutil.ToPtr(certX509.Subject.CommonName),
Origin: typeutil.ToPtr("UPLOAD"),
}
getCertificateListResp, err := u.sdkClient.GetCertificateList(getCertificateListReq)
u.logger.Debug("sdk request 'cms.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cms.GetCertificateList': %w", err)
}
if getCertificateListResp.ReturnObj != nil {
fingerprint := sha1.Sum(certX509.Raw)
fingerprintHex := hex.EncodeToString(fingerprint[:])
for _, certRecord := range getCertificateListResp.ReturnObj.List {
// 对比证书名称
if !strings.EqualFold(strings.Join(certX509.DNSNames, ","), certRecord.DomainName) {
continue
}
// 对比证书有效期
oldCertNotBefore, _ := time.Parse("2006-01-02T15:04:05Z", certRecord.IssueTime)
oldCertNotAfter, _ := time.Parse("2006-01-02T15:04:05Z", certRecord.ExpireTime)
if !certX509.NotBefore.Equal(oldCertNotBefore) {
continue
} else if !certX509.NotAfter.Equal(oldCertNotAfter) {
continue
}
// 对比证书指纹
if !strings.EqualFold(fingerprintHex, certRecord.Fingerprint) {
continue
}
// 如果以上信息都一致,则视为已存在相同证书,直接返回
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: string(*&certRecord.Id),
CertName: certRecord.Name,
}, nil
}
}
if getCertificateListResp.ReturnObj == nil || len(getCertificateListResp.ReturnObj.List) < int(getCertificateListPageSize) {
break
} else {
getCertificateListPageNum++
}
}
return nil, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyuncms.Client, error) {
return ctyuncms.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,72 @@
package ctcccloudcms_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-cms"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDCMS_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_cms_test.go -args \
--CERTIMATE_UPLOADER_CTCCCLOUDCMS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDCMS_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDCMS_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_CTCCCLOUDCMS_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@@ -0,0 +1,133 @@
package ctcccloudelb
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/google/uuid"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
ctyunelb "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/elb"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type UploaderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 天翼云资源池 ID。
RegionId string `json:"regionId"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *ctyunelb.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.New(slog.DiscardHandler)
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5692&data=88&isNormal=1&vid=82
listCertificatesReq := &ctyunelb.ListCertificatesRequest{
RegionID: typeutil.ToPtr(u.config.RegionId),
}
listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq)
u.logger.Debug("sdk request 'elb.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err)
} else {
for _, certRecord := range listCertificatesResp.ReturnObj {
var isSameCert bool
if certRecord.Certificate == certPEM {
isSameCert = true
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(certRecord.Certificate)
if err != nil {
continue
}
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回
if isSameCert {
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: certRecord.ID,
CertName: certRecord.Name,
}, nil
}
}
}
// 生成新证书名(需符合天翼云命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 创建证书
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5685&data=88&isNormal=1&vid=82
createCertificateReq := &ctyunelb.CreateCertificateRequest{
ClientToken: typeutil.ToPtr(generateClientToken()),
RegionID: typeutil.ToPtr(u.config.RegionId),
Name: typeutil.ToPtr(certName),
Description: typeutil.ToPtr("upload from certimate"),
Type: typeutil.ToPtr("Server"),
Certificate: typeutil.ToPtr(certPEM),
PrivateKey: typeutil.ToPtr(privkeyPEM),
}
createCertificateResp, err := u.sdkClient.CreateCertificate(createCertificateReq)
u.logger.Debug("sdk request 'elb.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elb.CreateCertificate': %w", err)
}
return &uploader.UploadResult{
CertId: createCertificateResp.ReturnObj.ID,
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) {
return ctyunelb.NewClient(accessKeyId, secretAccessKey)
}
func generateClientToken() string {
return uuid.New().String()
}

View File

@@ -0,0 +1,77 @@
package ctcccloudelb_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-elb"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegionId string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDELB_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_elb_test.go -args \
--CERTIMATE_UPLOADER_CTCCCLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDELB_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_CTCCCLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
--CERTIMATE_UPLOADER_CTCCCLOUDELB_REGIONID="your-region-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("SECRETACCESSKEY: %v", fSecretAccessKey),
fmt.Sprintf("REGIONID: %v", fRegionId),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
RegionId: fRegionId,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@@ -0,0 +1,171 @@
package ctcccloudicdn
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
ctyunicdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/icdn"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type UploaderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *ctyunicdn.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.New(slog.DiscardHandler)
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10838&data=173&isNormal=1&vid=166
queryCertListPage := int32(1)
queryCertListPerPage := int32(1000)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
queryCertListReq := &ctyunicdn.QueryCertListRequest{
Page: typeutil.ToPtr(queryCertListPage),
PerPage: typeutil.ToPtr(queryCertListPerPage),
UsageMode: typeutil.ToPtr(int32(0)),
}
queryCertListResp, err := u.sdkClient.QueryCertList(queryCertListReq)
u.logger.Debug("sdk request 'icdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryCertList': %w", err)
}
if queryCertListResp.ReturnObj != nil {
for _, certRecord := range queryCertListResp.ReturnObj.Results {
// 对比证书通用名称
if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
continue
}
// 对比证书扩展名称
if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
continue
}
// 对比证书有效期
if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
continue
} else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
continue
}
// 查询证书详情
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10837&data=173&isNormal=1&vid=166
queryCertDetailReq := &ctyunicdn.QueryCertDetailRequest{
Id: typeutil.ToPtr(certRecord.Id),
}
queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq)
u.logger.Debug("sdk request 'icdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryCertDetail': %w", err)
} else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {
var isSameCert bool
if queryCertDetailResp.ReturnObj.Result.Certs == certPEM {
isSameCert = true
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertDetailResp.ReturnObj.Result.Certs)
if err != nil {
continue
}
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回
if isSameCert {
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id),
CertName: queryCertDetailResp.ReturnObj.Result.Name,
}, nil
}
}
}
}
if queryCertListResp.ReturnObj == nil || len(queryCertListResp.ReturnObj.Results) < int(queryCertListPerPage) {
break
} else {
queryCertListPage++
}
}
// 生成新证书名(需符合天翼云命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 创建证书
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10835&data=173&isNormal=1&vid=166
createCertReq := &ctyunicdn.CreateCertRequest{
Name: typeutil.ToPtr(certName),
Certs: typeutil.ToPtr(certPEM),
Key: typeutil.ToPtr(privkeyPEM),
}
createCertResp, err := u.sdkClient.CreateCert(createCertReq)
u.logger.Debug("sdk request 'icdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'icdn.CreateCert': %w", err)
}
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) {
return ctyunicdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,72 @@
package ctcccloudicdn_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-icdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDICDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_icdn_test.go -args \
--CERTIMATE_UPLOADER_CTCCCLOUDICDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDICDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDICDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_CTCCCLOUDICDN_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@@ -0,0 +1,171 @@
package ctcccloudlvdn
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"time"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
ctyunlvdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/lvdn"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
type UploaderConfig struct {
// 天翼云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 天翼云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *ctyunlvdn.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &UploaderProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
if logger == nil {
u.logger = slog.New(slog.DiscardHandler)
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 查询证书列表,避免重复上传
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11452&data=183&isNormal=1&vid=261
queryCertListPage := int32(1)
queryCertListPerPage := int32(1000)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
queryCertListReq := &ctyunlvdn.QueryCertListRequest{
Page: typeutil.ToPtr(queryCertListPage),
PerPage: typeutil.ToPtr(queryCertListPerPage),
UsageMode: typeutil.ToPtr(int32(0)),
}
queryCertListResp, err := u.sdkClient.QueryCertList(queryCertListReq)
u.logger.Debug("sdk request 'lvdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertList': %w", err)
}
if queryCertListResp.ReturnObj != nil {
for _, certRecord := range queryCertListResp.ReturnObj.Results {
// 对比证书通用名称
if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
continue
}
// 对比证书扩展名称
if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
continue
}
// 对比证书有效期
if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
continue
} else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
continue
}
// 查询证书详情
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11449&data=183&isNormal=1&vid=261
queryCertDetailReq := &ctyunlvdn.QueryCertDetailRequest{
Id: typeutil.ToPtr(certRecord.Id),
}
queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq)
u.logger.Debug("sdk request 'lvdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertDetail': %w", err)
} else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {
var isSameCert bool
if queryCertDetailResp.ReturnObj.Result.Certs == certPEM {
isSameCert = true
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertDetailResp.ReturnObj.Result.Certs)
if err != nil {
continue
}
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
}
// 如果已存在相同证书,直接返回
if isSameCert {
u.logger.Info("ssl certificate already exists")
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id),
CertName: queryCertDetailResp.ReturnObj.Result.Name,
}, nil
}
}
}
}
if queryCertListResp.ReturnObj == nil || len(queryCertListResp.ReturnObj.Results) < int(queryCertListPerPage) {
break
} else {
queryCertListPage++
}
}
// 生成新证书名(需符合天翼云命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 创建证书
// REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11436&data=183&isNormal=1&vid=261
createCertReq := &ctyunlvdn.CreateCertRequest{
Name: typeutil.ToPtr(certName),
Certs: typeutil.ToPtr(certPEM),
Key: typeutil.ToPtr(privkeyPEM),
}
createCertResp, err := u.sdkClient.CreateCert(createCertReq)
u.logger.Debug("sdk request 'lvdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'lvdn.CreateCert': %w", err)
}
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
CertName: certName,
}, nil
}
func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) {
return ctyunlvdn.NewClient(accessKeyId, secretAccessKey)
}

View File

@@ -0,0 +1,72 @@
package ctcccloudlvdn_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-lvdn"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDLVDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ctcccloud_lvdn_test.go -args \
--CERTIMATE_UPLOADER_CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
fInputCertData, _ := os.ReadFile(fInputCertPath)
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
if err != nil {
t.Errorf("err: %+v", err)
return
}
sres, _ := json.Marshal(res)
t.Logf("ok: %s", string(sres))
})
}

View File

@@ -52,7 +52,8 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
if res, err := u.getCertIfExists(ctx, certPEM); err != nil {
// 遍历证书列表,避免重复上传
if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res != nil {
u.logger.Info("ssl certificate already exists")
@@ -71,7 +72,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
return nil, fmt.Errorf("failed to execute sdk request 'sslcenter.Create': %w", err)
}
if res, err := u.getCertIfExists(ctx, certPEM); err != nil {
// 遍历证书列表,获取刚刚上传证书 ID
if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("rainyun sslcenter: no certificate found")
@@ -80,14 +82,14 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
}
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 遍历 SSL 证书列表,避免重复上传
// 遍历 SSL 证书列表
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048
sslCenterListPage := int32(1)

View File

@@ -88,7 +88,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
u.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp))
if err != nil {
if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 {
if res, err := u.getCertIfExists(ctx, certPEM); err != nil {
if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("ucloud ssl: no certificate found")
@@ -111,14 +111,14 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}, nil
}
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 遍历获取用户证书列表,避免重复上传
// 遍历获取用户证书列表
// REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list
// REF: https://docs.ucloud.cn/api/usslcertificate-api/download_certificate
getCertificateListPage := int(1)

View File

@@ -1,4 +1,4 @@
package jdcloudssl
package wangsucertificate
import (
"context"
@@ -9,9 +9,8 @@ import (
"strings"
"time"
wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/certificate"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/certificate"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)

View File

@@ -1,4 +1,4 @@
package jdcloudssl_test
package wangsucertificate_test
import (
"context"
@@ -20,7 +20,7 @@ var (
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_JDCLOUDSSL_"
argsPrefix := "CERTIMATE_UPLOADER_WANGSUCERTIFICATE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
@@ -34,8 +34,8 @@ Shell command to run this test:
go test -v ./wangsu_certificate_test.go -args \
--CERTIMATE_UPLOADER_WANGSUCERTIFICATE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_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_UPLOADER_WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_UPLOADER_WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret"
*/
func TestDeploy(t *testing.T) {
flag.Parse()

View File

@@ -7,7 +7,13 @@ import (
func (c *Client) UpdateSystemSSL(req *UpdateSystemSSLRequest) (*UpdateSystemSSLResponse, error) {
resp := &UpdateSystemSSLResponse{}
err := c.sendRequestWithResult(http.MethodPost, "/settings/ssl/update", req, resp)
path := "/settings/ssl/update"
if c.version == "v2" {
path = "/core" + path
}
err := c.sendRequestWithResult(http.MethodPost, path, req, resp)
return resp, err
}

View File

@@ -14,7 +14,8 @@ import (
)
type Client struct {
client *resty.Client
client *resty.Client
version string
}
func NewClient(serverUrl, apiVersion, apiKey string) *Client {
@@ -36,7 +37,8 @@ func NewClient(serverUrl, apiVersion, apiKey string) *Client {
})
return &Client{
client: client,
client: client,
version: apiVersion,
}
}

View File

@@ -0,0 +1,16 @@
package apisix
import (
"fmt"
"net/http"
)
func (c *Client) UpdateSSL(req *UpdateSSLRequest) (*UpdateSSLResponse, error) {
if req.ID == "" {
return nil, fmt.Errorf("1panel api error: invalid parameter: ID")
}
resp := &UpdateSSLResponse{}
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/ssls/%s", req.ID), req, resp)
return resp, err
}

View File

@@ -0,0 +1,87 @@
package apisix
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
client *resty.Client
}
func NewClient(serverUrl, apiKey string) *Client {
client := resty.New().
SetBaseURL(strings.TrimRight(serverUrl, "/")+"/apisix/admin").
SetHeader("User-Agent", "certimate").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
req.Header.Set("X-API-KEY", apiKey)
return nil
})
return &Client{
client: client,
}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) WithTLSConfig(config *tls.Config) *Client {
c.client.SetTLSClientConfig(config)
return c
}
func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) {
req := c.client.R()
if strings.EqualFold(method, http.MethodGet) {
qs := make(map[string]string)
if params != nil {
temp := make(map[string]any)
jsonb, _ := json.Marshal(params)
json.Unmarshal(jsonb, &temp)
for k, v := range temp {
if v != nil {
qs[k] = fmt.Sprintf("%v", v)
}
}
}
req = req.SetQueryParams(qs)
} else {
req = req.SetHeader("Content-Type", "application/json").SetBody(params)
}
resp, err := req.Execute(method, path)
if err != nil {
return resp, fmt.Errorf("apisix api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("apisix api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result interface{}) error {
resp, err := c.sendRequest(method, path, params)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &result)
}
return err
}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return fmt.Errorf("apisix api error: failed to unmarshal response: %w", err)
}
return nil
}

View File

@@ -0,0 +1,12 @@
package apisix
type UpdateSSLRequest struct {
ID string `json:"-"`
Cert *string `json:"cert,omitempty"`
Key *string `json:"key,omitempty"`
SNIs *[]string `json:"snis,omitempty"`
Type *string `json:"type,omitempty"`
Status *int32 `json:"status,omitempty"`
}
type UpdateSSLResponse struct{}

View File

@@ -0,0 +1,41 @@
package ao
import (
"context"
"net/http"
)
type CreateCertRequest struct {
Name *string `json:"name,omitempty"`
Certs *string `json:"certs,omitempty"`
Key *string `json:"key,omitempty"`
}
type CreateCertResponse struct {
baseResult
ReturnObj *struct {
Id int64 `json:"id"`
} `json:"returnObj,omitempty"`
}
func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {
return c.CreateCertWithContext(context.Background(), req)
}
func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/cert/create")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &CreateCertResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,54 @@
package ao
import (
"context"
"net/http"
)
type GetDomainConfigRequest struct {
Domain *string `json:"domain,omitempty"`
ProductCode *string `json:"product_code,omitempty"`
}
type GetDomainConfigResponse struct {
baseResult
ReturnObj *struct {
Domain string `json:"domain"`
ProductCode string `json:"product_code"`
Status int32 `json:"status"`
AreaScope int32 `json:"area_scope"`
Cname string `json:"cname"`
Origin []*DomainOriginConfig `json:"origin,omitempty"`
HttpsStatus string `json:"https_status"`
HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"`
CertName string `json:"cert_name"`
} `json:"returnObj,omitempty"`
}
func (c *Client) GetDomainConfig(req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {
return c.GetDomainConfigWithContext(context.Background(), req)
}
func (c *Client) GetDomainConfigWithContext(ctx context.Context, req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/domain/config")
if err != nil {
return nil, err
} else {
if req.Domain != nil {
httpreq.SetQueryParam("domain", *req.Domain)
}
if req.ProductCode != nil {
httpreq.SetQueryParam("product_code", *req.ProductCode)
}
httpreq.SetContext(ctx)
}
result := &GetDomainConfigResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,55 @@
package ao
import (
"context"
"net/http"
"strconv"
)
type ListCertsRequest struct {
Page *int32 `json:"page,omitempty"`
PerPage *int32 `json:"per_page,omitempty"`
UsageMode *int32 `json:"usage_mode,omitempty"`
}
type ListCertsResponse struct {
baseResult
ReturnObj *struct {
Results []*CertRecord `json:"result,omitempty"`
Page int32 `json:"page,omitempty"`
PerPage int32 `json:"per_page,omitempty"`
TotalPage int32 `json:"total_page,omitempty"`
TotalRecords int32 `json:"total_records,omitempty"`
} `json:"returnObj,omitempty"`
}
func (c *Client) ListCerts(req *ListCertsRequest) (*ListCertsResponse, error) {
return c.ListCertsWithContext(context.Background(), req)
}
func (c *Client) ListCertsWithContext(ctx context.Context, req *ListCertsRequest) (*ListCertsResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/cert/list")
if err != nil {
return nil, err
} else {
if req.Page != nil {
httpreq.SetQueryParam("page", strconv.Itoa(int(*req.Page)))
}
if req.PerPage != nil {
httpreq.SetQueryParam("per_page", strconv.Itoa(int(*req.PerPage)))
}
if req.UsageMode != nil {
httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
}
httpreq.SetContext(ctx)
}
result := &ListCertsResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,40 @@
package ao
import (
"context"
"net/http"
)
type ModifyDomainConfigRequest struct {
Domain *string `json:"domain,omitempty"`
ProductCode *string `json:"product_code,omitempty"`
Origin []*DomainOriginConfig `json:"origin,omitempty"`
HttpsStatus *string `json:"https_status,omitempty"`
HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"`
CertName *string `json:"cert_name,omitempty"`
}
type ModifyDomainConfigResponse struct {
baseResult
}
func (c *Client) ModifyDomainConfig(req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) {
return c.ModifyDomainConfigWithContext(context.Background(), req)
}
func (c *Client) ModifyDomainConfigWithContext(ctx context.Context, req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/domain/modify_config")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &ModifyDomainConfigResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,51 @@
package ao
import (
"context"
"net/http"
"strconv"
)
type QueryCertRequest struct {
Id *int64 `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
UsageMode *int32 `json:"usage_mode,omitempty"`
}
type QueryCertResponse struct {
baseResult
ReturnObj *struct {
Result *CertDetail `json:"result,omitempty"`
} `json:"returnObj,omitempty"`
}
func (c *Client) QueryCert(req *QueryCertRequest) (*QueryCertResponse, error) {
return c.QueryCertWithContext(context.Background(), req)
}
func (c *Client) QueryCertWithContext(ctx context.Context, req *QueryCertRequest) (*QueryCertResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/cert/query")
if err != nil {
return nil, err
} else {
if req.Id != nil {
httpreq.SetQueryParam("id", strconv.Itoa(int(*req.Id)))
}
if req.Name != nil {
httpreq.SetQueryParam("name", *req.Name)
}
if req.UsageMode != nil {
httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
}
httpreq.SetContext(ctx)
}
result := &QueryCertResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,49 @@
package ao
import (
"fmt"
"time"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
)
const endpoint = "https://accessone-global.ctapi.ctyun.cn"
type Client struct {
client *openapi.Client
}
func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
if err != nil {
return nil, err
}
return &Client{client: client}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
return c.client.NewRequest(method, path)
}
func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
return c.client.DoRequest(request)
}
func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
response, err := c.client.DoRequestWithResult(request, result)
if err == nil {
statusCode := result.GetStatusCode()
if statusCode != "" && statusCode != "100000" {
return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
}
}
return response, err
}

View File

@@ -0,0 +1,101 @@
package ao
import (
"bytes"
"encoding/json"
"strconv"
)
type baseResultInterface interface {
GetStatusCode() string
GetMessage() string
GetError() string
GetErrorMessage() string
}
type baseResult struct {
StatusCode json.RawMessage `json:"statusCode,omitempty"`
Message *string `json:"message,omitempty"`
Error *string `json:"error,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
RequestId *string `json:"requestId,omitempty"`
}
func (r *baseResult) GetStatusCode() string {
if r.StatusCode == nil {
return ""
}
decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
token, err := decoder.Token()
if err != nil {
return ""
}
switch t := token.(type) {
case string:
return t
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case json.Number:
return t.String()
default:
return ""
}
}
func (r *baseResult) GetMessage() string {
if r.Message == nil {
return ""
}
return *r.Message
}
func (r *baseResult) GetError() string {
if r.Error == nil {
return ""
}
return *r.Error
}
func (r *baseResult) GetErrorMessage() string {
if r.ErrorMessage == nil {
return ""
}
return *r.ErrorMessage
}
var _ baseResultInterface = (*baseResult)(nil)
type CertRecord struct {
Id int64 `json:"id"`
Name string `json:"name"`
CN string `json:"cn"`
SANs []string `json:"sans"`
UsageMode int32 `json:"usage_mode"`
State int32 `json:"state"`
ExpiresTime int64 `json:"expires"`
IssueTime int64 `json:"issue"`
Issuer string `json:"issuer"`
CreatedTime int64 `json:"created"`
}
type CertDetail struct {
CertRecord
Certs string `json:"certs"`
Key string `json:"key"`
}
type DomainOriginConfig struct {
Origin string `json:"origin"`
Role string `json:"role"`
Weight int32 `json:"weight"`
}
type DomainHttpsBasicConfig struct {
HttpsForce string `json:"https_force"`
ForceStatus string `json:"force_status"`
}

View File

@@ -0,0 +1,41 @@
package cdn
import (
"context"
"net/http"
)
type CreateCertRequest struct {
Name *string `json:"name,omitempty"`
Certs *string `json:"certs,omitempty"`
Key *string `json:"key,omitempty"`
}
type CreateCertResponse struct {
baseResult
ReturnObj *struct {
Id int64 `json:"id"`
} `json:"returnObj,omitempty"`
}
func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {
return c.CreateCertWithContext(context.Background(), req)
}
func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/v1/cert/creat-cert")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &CreateCertResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,51 @@
package cdn
import (
"context"
"net/http"
"strconv"
)
type QueryCertDetailRequest struct {
Id *int64 `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
UsageMode *int32 `json:"usage_mode,omitempty"`
}
type QueryCertDetailResponse struct {
baseResult
ReturnObj *struct {
Result *CertDetail `json:"result,omitempty"`
} `json:"returnObj,omitempty"`
}
func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
return c.QueryCertDetailWithContext(context.Background(), req)
}
func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-detail")
if err != nil {
return nil, err
} else {
if req.Id != nil {
httpreq.SetQueryParam("id", strconv.Itoa(int(*req.Id)))
}
if req.Name != nil {
httpreq.SetQueryParam("name", *req.Name)
}
if req.UsageMode != nil {
httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
}
httpreq.SetContext(ctx)
}
result := &QueryCertDetailResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,55 @@
package cdn
import (
"context"
"net/http"
"strconv"
)
type QueryCertListRequest struct {
Page *int32 `json:"page,omitempty"`
PerPage *int32 `json:"per_page,omitempty"`
UsageMode *int32 `json:"usage_mode,omitempty"`
}
type QueryCertListResponse struct {
baseResult
ReturnObj *struct {
Results []*CertRecord `json:"result,omitempty"`
Page int32 `json:"page,omitempty"`
PerPage int32 `json:"per_page,omitempty"`
TotalPage int32 `json:"total_page,omitempty"`
TotalRecords int32 `json:"total_records,omitempty"`
} `json:"returnObj,omitempty"`
}
func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) {
return c.QueryCertListWithContext(context.Background(), req)
}
func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-list")
if err != nil {
return nil, err
} else {
if req.Page != nil {
httpreq.SetQueryParam("page", strconv.Itoa(int(*req.Page)))
}
if req.PerPage != nil {
httpreq.SetQueryParam("per_page", strconv.Itoa(int(*req.PerPage)))
}
if req.UsageMode != nil {
httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
}
httpreq.SetContext(ctx)
}
result := &QueryCertListResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,64 @@
package cdn
import (
"context"
"net/http"
)
type QueryDomainDetailRequest struct {
Domain *string `json:"domain,omitempty"`
ProductCode *string `json:"product_code,omitempty"`
FunctionNames *string `json:"function_names,omitempty"`
}
type QueryDomainDetailResponse struct {
baseResult
ReturnObj *struct {
Domain string `json:"domain"`
ProductCode string `json:"product_code"`
Status int32 `json:"status"`
AreaScope int32 `json:"area_scope"`
Cname string `json:"cname"`
HttpsStatus string `json:"https_status"`
HttpsBasic *struct {
HttpsForce string `json:"https_force"`
HttpForce string `json:"http_force"`
ForceStatus string `json:"force_status"`
OriginProtocol string `json:"origin_protocol"`
} `json:"https_basic,omitempty"`
CertName string `json:"cert_name"`
Ssl string `json:"ssl"`
SslStapling string `json:"ssl_stapling"`
} `json:"returnObj,omitempty"`
}
func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
return c.QueryDomainDetailWithContext(context.Background(), req)
}
func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-detail")
if err != nil {
return nil, err
} else {
if req.Domain != nil {
httpreq.SetQueryParam("domain", *req.Domain)
}
if req.ProductCode != nil {
httpreq.SetQueryParam("product_code", *req.ProductCode)
}
if req.FunctionNames != nil {
httpreq.SetQueryParam("function_names", *req.FunctionNames)
}
httpreq.SetContext(ctx)
}
result := &QueryDomainDetailResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,37 @@
package cdn
import (
"context"
"net/http"
)
type UpdateDomainRequest struct {
Domain *string `json:"domain,omitempty"`
HttpsStatus *string `json:"https_status,omitempty"`
CertName *string `json:"cert_name,omitempty"`
}
type UpdateDomainResponse struct {
baseResult
}
func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
return c.UpdateDomainWithContext(context.Background(), req)
}
func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/v1/domain/update-domain")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &UpdateDomainResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,49 @@
package cdn
import (
"fmt"
"time"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
)
const endpoint = "https://ctcdn-global.ctapi.ctyun.cn"
type Client struct {
client *openapi.Client
}
func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
if err != nil {
return nil, err
}
return &Client{client: client}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
return c.client.NewRequest(method, path)
}
func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
return c.client.DoRequest(request)
}
func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
response, err := c.client.DoRequestWithResult(request, result)
if err == nil {
statusCode := result.GetStatusCode()
if statusCode != "" && statusCode != "100000" {
return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
}
}
return response, err
}

View File

@@ -0,0 +1,90 @@
package cdn
import (
"bytes"
"encoding/json"
"strconv"
)
type baseResultInterface interface {
GetStatusCode() string
GetMessage() string
GetError() string
GetErrorMessage() string
}
type baseResult struct {
StatusCode json.RawMessage `json:"statusCode,omitempty"`
Message *string `json:"message,omitempty"`
Error *string `json:"error,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
RequestId *string `json:"requestId,omitempty"`
}
func (r *baseResult) GetStatusCode() string {
if r.StatusCode == nil {
return ""
}
decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
token, err := decoder.Token()
if err != nil {
return ""
}
switch t := token.(type) {
case string:
return t
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
case json.Number:
return t.String()
default:
return ""
}
}
func (r *baseResult) GetMessage() string {
if r.Message == nil {
return ""
}
return *r.Message
}
func (r *baseResult) GetError() string {
if r.Error == nil {
return ""
}
return *r.Error
}
func (r *baseResult) GetErrorMessage() string {
if r.ErrorMessage == nil {
return ""
}
return *r.ErrorMessage
}
var _ baseResultInterface = (*baseResult)(nil)
type CertRecord struct {
Id int64 `json:"id"`
Name string `json:"name"`
CN string `json:"cn"`
SANs []string `json:"sans"`
UsageMode int32 `json:"usage_mode"`
State int32 `json:"state"`
ExpiresTime int64 `json:"expires"`
IssueTime int64 `json:"issue"`
Issuer string `json:"issuer"`
CreatedTime int64 `json:"created"`
}
type CertDetail struct {
CertRecord
Certs string `json:"certs"`
Key string `json:"key"`
}

View File

@@ -0,0 +1,44 @@
package cms
import (
"context"
"net/http"
)
type GetCertificateListRequest struct {
Status *string `json:"status,omitempty"`
Keyword *string `json:"keyword,omitempty"`
PageNum *int32 `json:"pageNum,omitempty"`
PageSize *int32 `json:"pageSize,omitempty"`
Origin *string `json:"origin,omitempty"`
}
type GetCertificateListResponse struct {
baseResult
ReturnObj *struct {
List []*CertificateRecord `json:"list,omitempty"`
TotalSize int32 `json:"totalSize,omitempty"`
} `json:"returnObj,omitempty"`
}
func (c *Client) GetCertificateList(req *GetCertificateListRequest) (*GetCertificateListResponse, error) {
return c.GetCertificateListWithContext(context.Background(), req)
}
func (c *Client) GetCertificateListWithContext(ctx context.Context, req *GetCertificateListRequest) (*GetCertificateListResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/v1/certificate/list")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &GetCertificateListResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

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