Compare commits

...

71 Commits

Author SHA1 Message Date
Yoan.liu
88b90986b1 update to version v0.3.8 2025-04-13 20:55:33 +08:00
Yoan.liu
44a6190e17 resolve build error 2025-04-13 09:14:08 +08:00
Yoan.liu
4475ed0dea resolve build error 2025-04-13 08:54:05 +08:00
Yoan.liu
6a23da3de3 Merge pull request #596 from redzl/redzl-patch-1
bugfix: tencent cloud ecdn deploy error
2025-04-13 08:24:43 +08:00
Yoan.liu
0f1d5a7730 Merge pull request #604 from banto6/main
feat(notify): add mattermost
2025-04-13 08:24:26 +08:00
Yoan.liu
5b4c3bb668 Merge branch 'main' into main 2025-04-13 08:24:16 +08:00
Yoan.liu
ad49f9d788 Merge pull request #607 from imlonghao/feat/pushover
feat: support pushover as notification
2025-04-13 08:18:59 +08:00
Yoan.liu
397ceefa02 Merge branch 'main' into feat/pushover 2025-04-13 08:18:47 +08:00
Yoan.liu
e11b1ca4e8 Merge pull request #597 from fudiwei/feat/providers
new providers
2025-04-13 08:14:50 +08:00
Yoan.liu
8e983e7286 Merge pull request #587 from fudiwei/bugfix
bugfix
2025-04-13 08:13:06 +08:00
Fu Diwei
f970ae7529 feat: add wangsu cdnpro deployer 2025-04-12 21:43:21 +08:00
Fu Diwei
b0973b5ca8 refactor: clean code 2025-04-12 20:54:02 +08:00
banto
4784bf9dba feat: add channelId tooltip 2025-04-12 20:01:03 +08:00
imlonghao
6b8dbf5235 feat: support pushover as notification 2025-04-12 13:05:37 +08:00
banto
48f698e84b style: fix code style 2025-04-12 12:45:03 +08:00
banto
ec0cdf8b96 feat(notify): add mattermost 2025-04-11 22:55:47 +08:00
Fu Diwei
2a6cc01eed feat(ui): adjust table scroll width in Dashboard 2025-04-10 21:57:22 +08:00
Fu Diwei
acc1365101 Merge branch 'feat/providers' of https://github.com/fudiwei/certimate into feat/providers 2025-04-09 23:12:52 +08:00
Fu Diwei
c5409c78ba refactor: edgio api sdk 2025-04-09 23:12:11 +08:00
RHQYZ
b97de6c06b Merge branch 'usual2970:main' into feat/providers 2025-04-09 22:56:43 +08:00
RHQYZ
4e3f499d76 chore: github issue templates 2025-04-09 10:55:53 +08:00
Fu Diwei
3cebe51796 feat: add rainyun rcdn deployer 2025-04-08 21:53:16 +08:00
Fu Diwei
25bd17dc6e feat: add rainyun ssl center uploader 2025-04-08 21:53:05 +08:00
redzl
2525f54dc3 解决腾讯云ECDN部署报错的问题
ECDN部署的时候报错:failed to execute sdk request 'ssl.DeployCertificateInstance':[TencentCloudSDKError] Code=FailedOperation.CertificateHostResourceTypeInvalid, Message=云资源类型无效。
经排查'ssl.DeployCertificateInstance接口的ResourceType不支持ecdn类型,ecdn和cdn都需要传入cdn
2025-04-08 18:06:51 +08:00
Fu Diwei
2127bb7e69 Merge branch 'feat/providers' of https://github.com/fudiwei/certimate into feat/providers 2025-04-08 16:47:49 +08:00
Fu Diwei
ed6d74f1ba feat(ui): builtin providers tag 2025-04-08 16:44:10 +08:00
Fu Diwei
02dd11f196 chore(ui): improve i18n 2025-04-08 10:19:42 +08:00
Fu Diwei
37b9ae30e2 fix: #595 2025-04-08 09:41:16 +08:00
Fu Diwei
0463dbcc75 Merge branch 'bugfix' of https://github.com/fudiwei/certimate into bugfix 2025-04-07 15:32:12 +08:00
Fu Diwei
111ef97d9c fix: migration error 2025-04-07 15:31:20 +08:00
RHQYZ
e8e854e392 Merge branch 'usual2970:main' into bugfix 2025-04-07 12:42:22 +08:00
Yoan.liu
ff43b9ab3e Update version.ts 2025-04-07 09:26:19 +08:00
Fu Diwei
47c4ba9dd6 feat(ui): workflow runs deleting warning 2025-04-05 21:23:55 +08:00
Fu Diwei
6ff738144a fix: #585 #586 2025-04-03 20:33:58 +08:00
Fu Diwei
26028bb1eb chore(ui): improve i18n 2025-04-03 20:30:44 +08:00
Yoan.liu
eb4d5ddfd5 Merge pull request #573 from fudiwei/main
Support configuring independent CA for each workflow
2025-04-03 17:42:42 +08:00
Yoan.liu
093ee006e4 Merge pull request #578 from fudiwei/feat/providers
Support cloudflare zone api token
2025-04-03 17:42:23 +08:00
Yoan.liu
9f8aa15af8 Merge pull request #579 from catfishlty/feat/gotify
feat(notify): add gotify
2025-04-03 17:42:00 +08:00
Yoan.liu
74d66a0131 Merge pull request #583 from catfishlty/feat/pushplus
feat(notify): add pushplus
2025-04-03 17:41:46 +08:00
catfishlty
626a86dea7 fix(notify): optimize gotify code and close unreleased resources. 2025-04-03 09:47:19 +08:00
catfishlty
9ab029a296 fix(notify): optimize pushplus code and close unreleased resources. 2025-04-03 09:34:23 +08:00
Fu Diwei
8e1a81ae53 chore: improve i18n 2025-04-02 21:57:33 +08:00
Fu Diwei
d76e1a3204 refactor: clean code 2025-04-02 21:46:27 +08:00
catfishlty
b585782007 feat(notify): add pushplus 2025-04-02 15:04:41 +08:00
catfishlty
2d198bcef7 fix(notify): add missing config for gotify 2025-04-02 15:00:46 +08:00
Fu Diwei
0edcd9174f feat(ui): download workflow run logs 2025-04-02 13:40:55 +08:00
Fu Diwei
daa5b44f8e refactor(ui): clean code 2025-04-02 12:54:51 +08:00
Fu Diwei
949660bc01 feat(ui): add AccessEditDrawer component 2025-04-02 11:02:09 +08:00
Fu Diwei
899a0b75b0 feat(ui): improve access provider tags appearance 2025-04-01 21:23:51 +08:00
Fu Diwei
8cdb2afa69 refactor: clean code 2025-04-01 20:44:45 +08:00
catfishlty
00ec2ce33e feat(notify): add gotify 2025-04-01 10:53:41 +08:00
Fu Diwei
2f7fd95684 feat: cloudflare zone api token 2025-03-31 21:13:07 +08:00
Fu Diwei
55b1794004 chore: improve i18n 2025-03-31 20:03:08 +08:00
Fu Diwei
e20972d4e7 chore: improve i18n 2025-03-31 20:00:03 +08:00
Fu Diwei
749d727f50 fix: could not obtain ecc certificates from sslcom 2025-03-31 10:24:35 +08:00
Fu Diwei
9b524728c0 update README 2025-03-30 22:46:33 +08:00
Fu Diwei
f81b4b9680 feat(ui): hide notification channel entry in AcessList for now 2025-03-30 22:33:05 +08:00
Fu Diwei
d2eaea7a44 feat: add buypass ca 2025-03-30 22:15:21 +08:00
Fu Diwei
f77c2dae23 feat: add ssl.com ca 2025-03-30 22:15:21 +08:00
Fu Diwei
a72737fdd5 feat(ui): different provider range of accesses in AccessList 2025-03-30 22:15:21 +08:00
Fu Diwei
4ab6b72e6f feat(ui): different provider range of accesses in AccessForm 2025-03-30 22:15:08 +08:00
Fu Diwei
1468e74a6c fix: ari 2025-03-30 14:02:43 +08:00
Fu Diwei
09b5a21af1 feat: make the builtin providers access field non mandatory 2025-03-30 13:57:26 +08:00
Fu Diwei
6ad0d8e42f feat: support configuring independent ca in workflows 2025-03-30 13:57:26 +08:00
Fu Diwei
deb3b2f412 feat: manage ca authorizations 2025-03-30 13:57:21 +08:00
Yoan.liu
893391a3d1 Merge pull request #566 from fudiwei/main
enhance & bugfix
2025-03-29 20:01:13 +08:00
Fu Diwei
7503d52857 refactor: clean code 2025-03-27 20:39:06 +08:00
Fu Diwei
fb860981d6 fix: #568 2025-03-27 15:53:29 +08:00
Fu Diwei
f302c7fb74 feat: support replacing old certificate on deployment to baishan cdn 2025-03-27 14:14:34 +08:00
Fu Diwei
a8be2a77cf fix: #565 2025-03-27 14:14:27 +08:00
Fu Diwei
c2345e6118 style: format 2025-03-27 09:47:16 +08:00
144 changed files with 5115 additions and 846 deletions

View File

@@ -1,6 +1,6 @@
name: "❓ Questions" name: "❓ Questions"
description: "遇到了困难需要求助? / Have problem in use and need help?" description: "遇到了困难需要求助? / Have problem in use and need help?"
title: "[Feature] 简要描述你遇到的问题" title: "简要描述你遇到的问题"
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -19,6 +19,14 @@ body:
3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar. 3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar.
4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly. 4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly.
- type: input
attributes:
label: 软件版本 / Release Version
description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate.
placeholder: (e.g. v1.0.0)
validations:
required: true
- type: textarea - type: textarea
attributes: attributes:
label: 问题描述 / Description label: 问题描述 / Description

View File

@@ -1,33 +1,23 @@
FROM node:20-alpine3.19 AS front-builder FROM node:20-alpine3.19 AS webui-builder
WORKDIR /app WORKDIR /app
COPY . /app/ COPY . /app/
RUN \ RUN \
cd /app/ui && \ cd /app/ui && \
npm install && \ npm install && \
npm run build npm run build
FROM golang:1.23-alpine AS builder FROM golang:1.23-alpine AS builder
WORKDIR /app WORKDIR /app
COPY ../. /app/ COPY ../. /app/
RUN rm -rf /app/ui/dist RUN rm -rf /app/ui/dist
COPY --from=webui-builder /app/ui/dist /app/ui/dist
COPY --from=front-builder /app/ui/dist /app/ui/dist
RUN go build -o certimate RUN go build -o certimate
FROM alpine:latest FROM alpine:latest
WORKDIR /app WORKDIR /app
COPY --from=builder /app/certimate . COPY --from=builder /app/certimate .
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]
ENTRYPOINT ["./certimate", "serve", "--http", "0.0.0.0:8090"]

View File

@@ -41,7 +41,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- 支持 20+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers) - 支持 20+ 域名托管商如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)
- 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers) - 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道; - 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
- 支持 Let's Encrypt、ZeroSSL、Google Trust Services 等多种 ACME 证书颁发机构; - 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构;
- 更多特性等待探索。 - 更多特性等待探索。
## ⏱️ 快速启动 ## ⏱️ 快速启动
@@ -71,7 +71,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
相关文章: 相关文章:
- [使用 CNAME 实现 DNS-01 challenge](https://docs.certimate.me/blog/cname) - [使用 CNAME 完成 ACME DNS-01 质询](https://docs.certimate.me/blog/cname)
- [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0) - [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0)
- [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0) - [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0)
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate) - [Why Certimate?](https://docs.certimate.me/blog/why-certimate)

View File

@@ -41,7 +41,7 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate
- Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers)); - Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
- Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers)); - Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers));
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more; - Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
- Supports multiple ACME CAs including Let's Encrypt, ZeroSSL, Google Trust Services, and more; - Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust ServicesSSL.com, ZeroSSL, and more;
- More features waiting to be discovered. - More features waiting to be discovered.
## ⏱️ Fast Track ## ⏱️ Fast Track
@@ -69,7 +69,7 @@ Please visit the documentation site [docs.certimate.me](https://docs.certimate.m
Related articles: Related articles:
- [使用 CNAME 实现 DNS-01 challenge](https://docs.certimate.me/blog/cname) - [使用 CNAME 完成 ACME DNS-01 质询](https://docs.certimate.me/blog/cname)
- [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0) - [v0.3.0:第二个不向后兼容的大版本](https://docs.certimate.me/blog/v0.3.0)
- [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0) - [v0.2.0:第一个不向后兼容的大版本](https://docs.certimate.me/blog/v0.2.0)
- [Why Certimate?](https://docs.certimate.me/blog/why-certimate) - [Why Certimate?](https://docs.certimate.me/blog/why-certimate)

3
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0
github.com/Edgio/edgio-api v0.0.0-workspace
github.com/G-Core/gcorelabscdn-go v1.0.28 github.com/G-Core/gcorelabscdn-go v1.0.28
github.com/alibabacloud-go/alb-20200616/v2 v2.2.8 github.com/alibabacloud-go/alb-20200616/v2 v2.2.8
github.com/alibabacloud-go/cas-20200407/v3 v3.0.4 github.com/alibabacloud-go/cas-20200407/v3 v3.0.4
@@ -211,6 +212,8 @@ require (
modernc.org/sqlite v1.36.1 // indirect modernc.org/sqlite v1.36.1 // indirect
) )
replace github.com/Edgio/edgio-api v0.0.0-workspace => ./internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace
replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0 replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0
replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1 replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1

2
go.sum
View File

@@ -498,6 +498,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=

View File

@@ -1,38 +1,30 @@
package applicant package applicant
const ( import "github.com/usual2970/certimate/internal/domain"
sslProviderLetsEncrypt = "letsencrypt"
sslProviderLetsEncryptStaging = "letsencrypt_staging"
sslProviderZeroSSL = "zerossl"
sslProviderGoogleTrustServices = "gts"
)
const defaultSSLProvider = sslProviderLetsEncrypt
const ( const (
letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory" sslProviderLetsEncrypt = string(domain.ApplyCAProviderTypeLetsEncrypt)
letsencryptStagingUrl = "https://acme-staging-v02.api.letsencrypt.org/directory" sslProviderLetsEncryptStaging = string(domain.ApplyCAProviderTypeLetsEncryptStaging)
zerosslUrl = "https://acme.zerossl.com/v2/DV90" sslProviderBuypass = string(domain.ApplyCAProviderTypeBuypass)
gtsUrl = "https://dv.acme-v02.api.pki.goog/directory" sslProviderGoogleTrustServices = string(domain.ApplyCAProviderTypeGoogleTrustServices)
sslProviderSSLCom = string(domain.ApplyCAProviderTypeSSLCom)
sslProviderZeroSSL = string(domain.ApplyCAProviderTypeZeroSSL)
sslProviderDefault = sslProviderLetsEncrypt
) )
var sslProviderUrls = map[string]string{ var sslProviderUrls = map[string]string{
sslProviderLetsEncrypt: letsencryptUrl, sslProviderLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
sslProviderLetsEncryptStaging: letsencryptStagingUrl, sslProviderLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
sslProviderZeroSSL: zerosslUrl, sslProviderBuypass: "https://api.buypass.com/acme/directory",
sslProviderGoogleTrustServices: gtsUrl, sslProviderGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
sslProviderSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
sslProviderSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
sslProviderSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
sslProviderZeroSSL: "https://acme.zerossl.com/v2/DV90",
} }
type acmeSSLProviderConfig struct { type acmeSSLProviderConfig struct {
Config acmeSSLProviderConfigContent `json:"config"` Config map[domain.ApplyCAProviderType]map[string]any `json:"config"`
Provider string `json:"provider"` Provider string `json:"provider"`
}
type acmeSSLProviderConfigContent struct {
ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"`
GoogleTrustServices acmeSSLProviderEabConfig `json:"gts"`
}
type acmeSSLProviderEabConfig struct {
EabHmacKey string `json:"eabHmacKey"`
EabKid string `json:"eabKid"`
} }

View File

@@ -14,6 +14,7 @@ import (
"github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/certutil" "github.com/usual2970/certimate/internal/pkg/utils/certutil"
"github.com/usual2970/certimate/internal/pkg/utils/maputil"
"github.com/usual2970/certimate/internal/repository" "github.com/usual2970/certimate/internal/repository"
) )
@@ -76,16 +77,11 @@ func (u *acmeUser) getPrivateKeyPEM() string {
return u.privkey return u.privkey
} }
type acmeAccountRepository interface {
GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error)
Save(ca, email, key string, resource *registration.Resource) error
}
var registerGroup singleflight.Group var registerGroup singleflight.Group
func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) { resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", user.CA, user.Email), func() (interface{}, error) {
return registerAcmeUser(client, sslProviderConfig, user) return registerAcmeUser(client, user, userRegisterOptions)
}) })
if err != nil { if err != nil {
@@ -95,45 +91,81 @@ func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *ac
return resp.(*registration.Resource), nil return resp.(*registration.Resource), nil
} }
func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
var reg *registration.Resource var reg *registration.Resource
var err error var err error
switch sslProviderConfig.Provider { switch user.CA {
case sslProviderZeroSSL:
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: sslProviderConfig.Config.ZeroSSL.EabKid,
HmacEncoded: sslProviderConfig.Config.ZeroSSL.EabHmacKey,
})
case sslProviderGoogleTrustServices:
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid,
HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey,
})
case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging: case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
case sslProviderBuypass:
{
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
case sslProviderGoogleTrustServices:
{
access := domain.AccessConfigForGoogleTrustServices{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
case sslProviderSSLCom:
{
access := domain.AccessConfigForSSLCom{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
case sslProviderZeroSSL:
{
access := domain.AccessConfigForZeroSSL{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: access.EabKid,
HmacEncoded: access.EabHmacKey,
})
}
default: default:
err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider) err = fmt.Errorf("unsupported ca provider: %s", user.CA)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
repo := repository.NewAcmeAccountRepository() repo := repository.NewAcmeAccountRepository()
resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail()) resp, err := repo.GetByCAAndEmail(user.CA, user.Email)
if err == nil { if err == nil {
user.privkey = resp.Key user.privkey = resp.Key
return resp.Resource, nil return resp.Resource, nil
} }
if _, err := repo.Save(context.Background(), &domain.AcmeAccount{ if _, err := repo.Save(context.Background(), &domain.AcmeAccount{
CA: sslProviderConfig.Provider, CA: user.CA,
Email: user.GetEmail(), Email: user.Email,
Key: user.getPrivateKeyPEM(), Key: user.getPrivateKeyPEM(),
Resource: reg, Resource: reg,
}); err != nil { }); err != nil {
return nil, fmt.Errorf("failed to save registration: %w", err) return nil, fmt.Errorf("failed to save acme account registration: %w", err)
} }
return reg, nil return reg, nil

View File

@@ -37,18 +37,21 @@ type Applicant interface {
} }
type applicantOptions struct { type applicantOptions struct {
Domains []string Domains []string
ContactEmail string ContactEmail string
Provider domain.ApplyDNSProviderType Provider domain.ApplyDNSProviderType
ProviderAccessConfig map[string]any ProviderAccessConfig map[string]any
ProviderApplyConfig map[string]any ProviderExtendedConfig map[string]any
KeyAlgorithm string CAProvider domain.ApplyCAProviderType
Nameservers []string CAProviderAccessConfig map[string]any
DnsPropagationTimeout int32 CAProviderExtendedConfig map[string]any
DnsTTL int32 KeyAlgorithm string
DisableFollowCNAME bool Nameservers []string
ReplacedARIAcctId string DnsPropagationTimeout int32
ReplacedARICertId string DnsTTL int32
DisableFollowCNAME bool
ReplacedARIAcct string
ReplacedARICert string
} }
func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
@@ -58,22 +61,55 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
nodeConfig := node.GetConfigForApply() nodeConfig := node.GetConfigForApply()
options := &applicantOptions{ options := &applicantOptions{
Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
ContactEmail: nodeConfig.ContactEmail, ContactEmail: nodeConfig.ContactEmail,
Provider: domain.ApplyDNSProviderType(nodeConfig.Provider), Provider: domain.ApplyDNSProviderType(nodeConfig.Provider),
ProviderApplyConfig: nodeConfig.ProviderConfig, ProviderAccessConfig: make(map[string]any),
KeyAlgorithm: nodeConfig.KeyAlgorithm, ProviderExtendedConfig: nodeConfig.ProviderConfig,
Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), CAProvider: domain.ApplyCAProviderType(nodeConfig.CAProvider),
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, CAProviderAccessConfig: make(map[string]any),
DnsTTL: nodeConfig.DnsTTL, CAProviderExtendedConfig: nodeConfig.CAProviderConfig,
DisableFollowCNAME: nodeConfig.DisableFollowCNAME, KeyAlgorithm: nodeConfig.KeyAlgorithm,
Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
DnsTTL: nodeConfig.DnsTTL,
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
} }
accessRepo := repository.NewAccessRepository() accessRepo := repository.NewAccessRepository()
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { if nodeConfig.ProviderAccessId != "" {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
} else { return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
options.ProviderAccessConfig = access.Config } else {
options.ProviderAccessConfig = access.Config
}
}
if nodeConfig.CAProviderAccessId != "" {
if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err)
} else {
options.CAProviderAccessConfig = access.Config
}
}
settingsRepo := repository.NewSettingsRepository()
if string(options.CAProvider) == "" {
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
sslProviderConfig := &acmeSSLProviderConfig{
Config: make(map[domain.ApplyCAProviderType]map[string]any),
Provider: sslProviderDefault,
}
if settings != nil {
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
return nil, err
} else if sslProviderConfig.Provider == "" {
sslProviderConfig.Provider = sslProviderDefault
}
}
options.CAProvider = domain.ApplyCAProviderType(sslProviderConfig.Provider)
options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider]
} }
certRepo := repository.NewCertificateRepository() certRepo := repository.NewCertificateRepository()
@@ -88,8 +124,8 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate)) lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
if lastCertX509 != nil { if lastCertX509 != nil {
replacedARICertId, _ := certificate.MakeARICertID(lastCertX509) replacedARICertId, _ := certificate.MakeARICertID(lastCertX509)
options.ReplacedARIAcctId = lastCertificate.ACMEAccountUrl options.ReplacedARIAcct = lastCertificate.ACMEAccountUrl
options.ReplacedARICertId = replacedARICertId options.ReplacedARICert = replacedARICertId
} }
} }
} }
@@ -106,24 +142,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
} }
func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) { func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) {
settingsRepo := repository.NewSettingsRepository() user, err := newAcmeUser(string(options.CAProvider), options.ContactEmail)
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
sslProviderConfig := &acmeSSLProviderConfig{
Config: acmeSSLProviderConfigContent{},
Provider: defaultSSLProvider,
}
if settings != nil {
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
return nil, err
}
}
if sslProviderConfig.Provider == "" {
sslProviderConfig.Provider = defaultSSLProvider
}
acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -133,9 +152,16 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME)) os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME))
// Create an ACME client config // Create an ACME client config
config := lego.NewConfig(acmeUser) config := lego.NewConfig(user)
config.CADirURL = sslProviderUrls[sslProviderConfig.Provider]
config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm)) config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
config.CADirURL = sslProviderUrls[user.CA]
if user.CA == sslProviderSSLCom {
if strings.HasPrefix(options.KeyAlgorithm, "RSA") {
config.CADirURL = sslProviderUrls[sslProviderSSLCom+"RSA"]
} else if strings.HasPrefix(options.KeyAlgorithm, "EC") {
config.CADirURL = sslProviderUrls[sslProviderSSLCom+"ECC"]
}
}
// Create an ACME client // Create an ACME client
client, err := lego.NewClient(config) client, err := lego.NewClient(config)
@@ -152,12 +178,12 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...) client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...)
// New users need to register first // New users need to register first
if !acmeUser.hasRegistration() { if !user.hasRegistration() {
reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser) reg, err := registerAcmeUserWithSingleFlight(client, user, options.CAProviderAccessConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to register: %w", err) return nil, fmt.Errorf("failed to register: %w", err)
} }
acmeUser.Registration = reg user.Registration = reg
} }
// Obtain a certificate // Obtain a certificate
@@ -165,8 +191,8 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
Domains: options.Domains, Domains: options.Domains,
Bundle: true, Bundle: true,
} }
if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != acmeUser.Registration.URI { if options.ReplacedARIAcct == user.Registration.URI {
certRequest.ReplacesCertID = options.ReplacedARICertId certRequest.ReplacesCertID = options.ReplacedARICert
} }
certResource, err := client.Certificate.Obtain(certRequest) certResource, err := client.Certificate.Obtain(certRequest)
if err != nil { if err != nil {
@@ -177,7 +203,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)), CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)),
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)), IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)), PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMEAccountUrl: acmeUser.Registration.URI, ACMEAccountUrl: user.Registration.URI,
ACMECertUrl: certResource.CertURL, ACMECertUrl: certResource.CertURL,
ACMECertStableUrl: certResource.CertStableURL, ACMECertStableUrl: certResource.CertStableURL,
CSR: strings.TrimSpace(string(certResource.CSR)), CSR: strings.TrimSpace(string(certResource.CSR)),
@@ -198,6 +224,8 @@ func parseKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyTy
return certcrypto.EC256 return certcrypto.EC256
case domain.CertificateKeyAlgorithmTypeEC384: case domain.CertificateKeyAlgorithmTypeEC384:
return certcrypto.EC384 return certcrypto.EC384
case domain.CertificateKeyAlgorithmTypeEC512:
return certcrypto.KeyType("P512")
} }
return certcrypto.RSA2048 return certcrypto.RSA2048

View File

@@ -86,8 +86,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{ applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId, AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey, SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderApplyConfig, "region"), Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
HostedZoneId: maputil.GetString(options.ProviderApplyConfig, "hostedZoneId"), HostedZoneId: maputil.GetString(options.ProviderExtendedConfig, "hostedZoneId"),
DnsPropagationTimeout: options.DnsPropagationTimeout, DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL, DnsTTL: options.DnsTTL,
}) })
@@ -137,6 +137,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
applicant, err := pCloudflare.NewChallengeProvider(&pCloudflare.ChallengeProviderConfig{ applicant, err := pCloudflare.NewChallengeProvider(&pCloudflare.ChallengeProviderConfig{
DnsApiToken: access.DnsApiToken, DnsApiToken: access.DnsApiToken,
ZoneApiToken: access.ZoneApiToken,
DnsPropagationTimeout: options.DnsPropagationTimeout, DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL, DnsTTL: options.DnsTTL,
}) })
@@ -278,7 +279,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{ applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId, AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey, SecretAccessKey: access.SecretAccessKey,
Region: maputil.GetString(options.ProviderApplyConfig, "region"), Region: maputil.GetString(options.ProviderExtendedConfig, "region"),
DnsPropagationTimeout: options.DnsPropagationTimeout, DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL, DnsTTL: options.DnsTTL,
}) })
@@ -295,7 +296,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{ applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{
AccessKeyId: access.AccessKeyId, AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret, AccessKeySecret: access.AccessKeySecret,
RegionId: maputil.GetString(options.ProviderApplyConfig, "regionId"), RegionId: maputil.GetString(options.ProviderExtendedConfig, "regionId"),
DnsPropagationTimeout: options.DnsPropagationTimeout, DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL, DnsTTL: options.DnsTTL,
}) })
@@ -432,7 +433,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{ applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{
SecretId: access.SecretId, SecretId: access.SecretId,
SecretKey: access.SecretKey, SecretKey: access.SecretKey,
ZoneId: maputil.GetString(options.ProviderApplyConfig, "zoneId"), ZoneId: maputil.GetString(options.ProviderExtendedConfig, "zoneId"),
DnsPropagationTimeout: options.DnsPropagationTimeout, DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL, DnsTTL: options.DnsTTL,
}) })

View File

@@ -32,18 +32,23 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct {
} }
nodeConfig := node.GetConfigForDeploy() nodeConfig := node.GetConfigForDeploy()
options := &deployerOptions{
accessRepo := repository.NewAccessRepository() Provider: domain.DeployProviderType(nodeConfig.Provider),
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) ProviderAccessConfig: make(map[string]any),
if err != nil { ProviderDeployConfig: nodeConfig.ProviderConfig,
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
} }
deployer, err := createDeployer(&deployerOptions{ accessRepo := repository.NewAccessRepository()
Provider: domain.DeployProviderType(nodeConfig.Provider), if nodeConfig.ProviderAccessId != "" {
ProviderAccessConfig: access.Config, access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
ProviderDeployConfig: nodeConfig.ProviderConfig, if err != nil {
}) return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
} else {
options.ProviderAccessConfig = access.Config
}
}
deployer, err := createDeployer(options)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -49,6 +49,7 @@ import (
pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local" pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn" pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili" pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili"
pRainYunRCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn"
pSafeLine "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline" pSafeLine "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline"
pSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh" pSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
pTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn" pTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
@@ -73,6 +74,7 @@ import (
pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex" pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex"
pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live" pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos" pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos"
pWangsuCDNPro "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook" pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
"github.com/usual2970/certimate/internal/pkg/utils/maputil" "github.com/usual2970/certimate/internal/pkg/utils/maputil"
"github.com/usual2970/certimate/internal/pkg/utils/sliceutil" "github.com/usual2970/certimate/internal/pkg/utils/sliceutil"
@@ -370,8 +372,9 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
switch options.Provider { switch options.Provider {
case domain.DeployProviderTypeBaishanCDN: case domain.DeployProviderTypeBaishanCDN:
deployer, err := pBaishanCDN.NewDeployer(&pBaishanCDN.DeployerConfig{ deployer, err := pBaishanCDN.NewDeployer(&pBaishanCDN.DeployerConfig{
ApiToken: access.ApiToken, ApiToken: access.ApiToken,
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"),
}) })
return deployer, err return deployer, err
@@ -680,6 +683,27 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
} }
} }
case domain.DeployProviderTypeRainYunRCDN:
{
access := domain.AccessConfigForRainYun{}
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.DeployProviderTypeTencentCloudCDN:
deployer, err := pRainYunRCDN.NewDeployer(&pRainYunRCDN.DeployerConfig{
ApiKey: access.ApiKey,
InstanceId: maputil.GetInt32(options.ProviderDeployConfig, "instanceId"),
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
})
return deployer, err
default:
break
}
}
case domain.DeployProviderTypeSafeLine: case domain.DeployProviderTypeSafeLine:
{ {
access := domain.AccessConfigForSafeLine{} access := domain.AccessConfigForSafeLine{}
@@ -980,6 +1004,30 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) {
} }
} }
case domain.DeployProviderTypeWangsuCDNPro:
{
access := domain.AccessConfigForWangsu{}
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.DeployProviderTypeWangsuCDNPro:
deployer, err := pWangsuCDNPro.NewDeployer(&pWangsuCDNPro.DeployerConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Environment: maputil.GetOrDefaultString(options.ProviderDeployConfig, "environment", "production"),
Domain: maputil.GetString(options.ProviderDeployConfig, "domain"),
CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"),
WebhookId: maputil.GetString(options.ProviderDeployConfig, "webhookId"),
})
return deployer, err
default:
break
}
}
case domain.DeployProviderTypeWebhook: case domain.DeployProviderTypeWebhook:
{ {
access := domain.AccessConfigForWebhook{} access := domain.AccessConfigForWebhook{}

View File

@@ -75,7 +75,8 @@ type AccessConfigForCdnfly struct {
} }
type AccessConfigForCloudflare struct { type AccessConfigForCloudflare struct {
DnsApiToken string `json:"dnsApiToken"` DnsApiToken string `json:"dnsApiToken"`
ZoneApiToken string `json:"zoneApiToken,omitempty"`
} }
type AccessConfigForClouDNS struct { type AccessConfigForClouDNS struct {
@@ -125,6 +126,11 @@ type AccessConfigForGoDaddy struct {
ApiSecret string `json:"apiSecret"` ApiSecret string `json:"apiSecret"`
} }
type AccessConfigForGoogleTrustServices struct {
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
}
type AccessConfigForHuaweiCloud struct { type AccessConfigForHuaweiCloud struct {
AccessKeyId string `json:"accessKeyId"` AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"` SecretAccessKey string `json:"secretAccessKey"`
@@ -139,8 +145,6 @@ type AccessConfigForKubernetes struct {
KubeConfig string `json:"kubeConfig,omitempty"` KubeConfig string `json:"kubeConfig,omitempty"`
} }
type AccessConfigForLocal struct{}
type AccessConfigForNamecheap struct { type AccessConfigForNamecheap struct {
Username string `json:"username"` Username string `json:"username"`
ApiKey string `json:"apiKey"` ApiKey string `json:"apiKey"`
@@ -193,6 +197,11 @@ type AccessConfigForSSH struct {
KeyPassphrase string `json:"keyPassphrase,omitempty"` KeyPassphrase string `json:"keyPassphrase,omitempty"`
} }
type AccessConfigForSSLCom struct {
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
}
type AccessConfigForTencentCloud struct { type AccessConfigForTencentCloud struct {
SecretId string `json:"secretId"` SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"` SecretKey string `json:"secretKey"`
@@ -219,6 +228,11 @@ type AccessConfigForVolcEngine struct {
SecretAccessKey string `json:"secretAccessKey"` SecretAccessKey string `json:"secretAccessKey"`
} }
type AccessConfigForWangsu struct {
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
}
type AccessConfigForWebhook struct { type AccessConfigForWebhook struct {
Url string `json:"url"` Url string `json:"url"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
@@ -228,3 +242,8 @@ type AccessConfigForWestcn struct {
Username string `json:"username"` Username string `json:"username"`
ApiPassword string `json:"password"` ApiPassword string `json:"password"`
} }
type AccessConfigForZeroSSL struct {
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
}

View File

@@ -12,7 +12,11 @@ const (
NotifyChannelTypeBark = NotifyChannelType("bark") NotifyChannelTypeBark = NotifyChannelType("bark")
NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk") NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk")
NotifyChannelTypeEmail = NotifyChannelType("email") NotifyChannelTypeEmail = NotifyChannelType("email")
NotifyChannelTypeGotify = NotifyChannelType("gotify")
NotifyChannelTypeLark = NotifyChannelType("lark") NotifyChannelTypeLark = NotifyChannelType("lark")
NotifyChannelTypeMattermost = NotifyChannelType("mattermost")
NotifyChannelTypePushover = NotifyChannelType("pushover")
NotifyChannelTypePushPlus = NotifyChannelType("pushplus")
NotifyChannelTypeServerChan = NotifyChannelType("serverchan") NotifyChannelTypeServerChan = NotifyChannelType("serverchan")
NotifyChannelTypeTelegram = NotifyChannelType("telegram") NotifyChannelTypeTelegram = NotifyChannelType("telegram")
NotifyChannelTypeWebhook = NotifyChannelType("webhook") NotifyChannelTypeWebhook = NotifyChannelType("webhook")

View File

@@ -9,55 +9,80 @@ type AccessProviderType string
NOTICE: If you add new constant, please keep ASCII order. NOTICE: If you add new constant, please keep ASCII order.
*/ */
const ( const (
AccessProviderType1Panel = AccessProviderType("1panel") AccessProviderType1Panel = AccessProviderType("1panel")
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq") AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai预留 AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai预留
AccessProviderTypeAliyun = AccessProviderType("aliyun") AccessProviderTypeAliyun = AccessProviderType("aliyun")
AccessProviderTypeAWS = AccessProviderType("aws") AccessProviderTypeAWS = AccessProviderType("aws")
AccessProviderTypeAzure = AccessProviderType("azure") AccessProviderTypeAzure = AccessProviderType("azure")
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud") AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
AccessProviderTypeBaishan = AccessProviderType("baishan") AccessProviderTypeBaishan = AccessProviderType("baishan")
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel") AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
AccessProviderTypeBytePlus = AccessProviderType("byteplus") AccessProviderTypeBytePlus = AccessProviderType("byteplus")
AccessProviderTypeCacheFly = AccessProviderType("cachefly") AccessProviderTypeBuypass = AccessProviderType("buypass")
AccessProviderTypeCdnfly = AccessProviderType("cdnfly") AccessProviderTypeCacheFly = AccessProviderType("cachefly")
AccessProviderTypeCloudflare = AccessProviderType("cloudflare") AccessProviderTypeCdnfly = AccessProviderType("cdnfly")
AccessProviderTypeClouDNS = AccessProviderType("cloudns") AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") AccessProviderTypeClouDNS = AccessProviderType("cloudns")
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留) AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留) AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留)
AccessProviderTypeDeSEC = AccessProviderType("desec") AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留)
AccessProviderTypeDNSLA = AccessProviderType("dnsla") AccessProviderTypeDeSEC = AccessProviderType("desec")
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") AccessProviderTypeDNSLA = AccessProviderType("dnsla")
AccessProviderTypeDynv6 = AccessProviderType("dynv6") AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
AccessProviderTypeEdgio = AccessProviderType("edgio") AccessProviderTypeDynv6 = AccessProviderType("dynv6")
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly预留 AccessProviderTypeEdgio = AccessProviderType("edgio")
AccessProviderTypeGname = AccessProviderType("gname") AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly预留
AccessProviderTypeGcore = AccessProviderType("gcore") AccessProviderTypeGname = AccessProviderType("gname")
AccessProviderTypeGoDaddy = AccessProviderType("godaddy") AccessProviderTypeGcore = AccessProviderType("gcore")
AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge预留 AccessProviderTypeGoDaddy = AccessProviderType("godaddy")
AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") AccessProviderTypeGoEdge = AccessProviderType("goedge") // GoEdge预留
AccessProviderTypeJDCloud = AccessProviderType("jdcloud") AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices")
AccessProviderTypeKubernetes = AccessProviderType("k8s") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud")
AccessProviderTypeLocal = AccessProviderType("local") AccessProviderTypeJDCloud = AccessProviderType("jdcloud")
AccessProviderTypeNamecheap = AccessProviderType("namecheap") AccessProviderTypeKubernetes = AccessProviderType("k8s")
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom") AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt")
AccessProviderTypeNameSilo = AccessProviderType("namesilo") AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging")
AccessProviderTypeNS1 = AccessProviderType("ns1") AccessProviderTypeLocal = AccessProviderType("local")
AccessProviderTypePorkbun = AccessProviderType("porkbun") AccessProviderTypeNamecheap = AccessProviderType("namecheap")
AccessProviderTypePowerDNS = AccessProviderType("powerdns") AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
AccessProviderTypeQiniu = AccessProviderType("qiniu") AccessProviderTypeNameSilo = AccessProviderType("namesilo")
AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留) AccessProviderTypeNS1 = AccessProviderType("ns1")
AccessProviderTypeRainYun = AccessProviderType("rainyun") AccessProviderTypePorkbun = AccessProviderType("porkbun")
AccessProviderTypeSafeLine = AccessProviderType("safeline") AccessProviderTypePowerDNS = AccessProviderType("powerdns")
AccessProviderTypeSSH = AccessProviderType("ssh") AccessProviderTypeQiniu = AccessProviderType("qiniu")
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud") AccessProviderTypeQingCloud = AccessProviderType("qingcloud") // 青云(预留)
AccessProviderTypeUCloud = AccessProviderType("ucloud") AccessProviderTypeRainYun = AccessProviderType("rainyun")
AccessProviderTypeUpyun = AccessProviderType("upyun") AccessProviderTypeSafeLine = AccessProviderType("safeline")
AccessProviderTypeVercel = AccessProviderType("vercel") AccessProviderTypeSSH = AccessProviderType("ssh")
AccessProviderTypeVolcEngine = AccessProviderType("volcengine") AccessProviderTypeSSLCOM = AccessProviderType("sslcom")
AccessProviderTypeWebhook = AccessProviderType("webhook") AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
AccessProviderTypeWestcn = AccessProviderType("westcn") AccessProviderTypeUCloud = AccessProviderType("ucloud")
AccessProviderTypeUpyun = AccessProviderType("upyun")
AccessProviderTypeVercel = AccessProviderType("vercel")
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
AccessProviderTypeWangsu = AccessProviderType("wangsu")
AccessProviderTypeWebhook = AccessProviderType("webhook")
AccessProviderTypeWestcn = AccessProviderType("westcn")
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
)
type ApplyCAProviderType string
/*
申请证书 CA 提供商常量值。
始终等于授权提供商类型。
注意:如果追加新的常量值,请保持以 ASCII 排序。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
ApplyCAProviderTypeBuypass = ApplyCAProviderType(string(AccessProviderTypeBuypass))
ApplyCAProviderTypeGoogleTrustServices = ApplyCAProviderType(string(AccessProviderTypeGoogleTrustServices))
ApplyCAProviderTypeLetsEncrypt = ApplyCAProviderType(string(AccessProviderTypeLetsEncrypt))
ApplyCAProviderTypeLetsEncryptStaging = ApplyCAProviderType(string(AccessProviderTypeLetsEncryptStaging))
ApplyCAProviderTypeSSLCom = ApplyCAProviderType(string(AccessProviderTypeSSLCOM))
ApplyCAProviderTypeZeroSSL = ApplyCAProviderType(string(AccessProviderTypeZeroSSL))
) )
type ApplyDNSProviderType string type ApplyDNSProviderType string
@@ -111,7 +136,7 @@ const (
type DeployProviderType string type DeployProviderType string
/* /*
部署目标提供商常量值。 部署证书主机提供商常量值。
短横线前的部分始终等于授权提供商类型。 短横线前的部分始终等于授权提供商类型。
注意:如果追加新的常量值,请保持以 ASCII 排序。 注意:如果追加新的常量值,请保持以 ASCII 排序。
@@ -162,6 +187,7 @@ const (
DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn") DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn")
DeployProviderTypeQiniuKodo = DeployProviderType("qiniu-kodo") DeployProviderTypeQiniuKodo = DeployProviderType("qiniu-kodo")
DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili") DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili")
DeployProviderTypeRainYunRCDN = DeployProviderType("rainyun-rcdn")
DeployProviderTypeSafeLine = DeployProviderType("safeline") DeployProviderTypeSafeLine = DeployProviderType("safeline")
DeployProviderTypeSSH = DeployProviderType("ssh") DeployProviderTypeSSH = DeployProviderType("ssh")
DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn") DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn")
@@ -187,5 +213,6 @@ const (
DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex") DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex")
DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live") DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live")
DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos") DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos")
DeployProviderTypeWangsuCDNPro = DeployProviderType("wangsu-cdnpro")
DeployProviderTypeWebhook = DeployProviderType("webhook") DeployProviderTypeWebhook = DeployProviderType("webhook")
) )

View File

@@ -62,19 +62,22 @@ type WorkflowNode struct {
} }
type WorkflowNodeConfigForApply struct { type WorkflowNodeConfigForApply struct {
Domains string `json:"domains"` // 域名列表,以半角分号分隔 Domains string `json:"domains"` // 域名列表,以半角分号分隔
ContactEmail string `json:"contactEmail"` // 联系邮箱 ContactEmail string `json:"contactEmail"` // 联系邮箱
ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01 ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01
Provider string `json:"provider"` // DNS 提供商 Provider string `json:"provider"` // DNS 提供商
ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置
KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法 CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值将使用全局配置)
Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角分号分隔 CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值) CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置
DnsTTL int32 `json:"dnsTTL"` // DNS TTL零值取决于提供商的默认值 KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法
DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随 Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
DisableARI bool `json:"disableARI"` // 是否关闭 ARI DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播超时时间(零值取决于提供商的默认值)
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30 DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS TTL零值取决于提供商的默认值
DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随
DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30
} }
type WorkflowNodeConfigForUpload struct { type WorkflowNodeConfigForUpload struct {
@@ -84,11 +87,11 @@ type WorkflowNodeConfigForUpload struct {
} }
type WorkflowNodeConfigForDeploy struct { type WorkflowNodeConfigForDeploy struct {
Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate” Certificate string `json:"certificate"` // 前序节点输出的证书,形如“${NodeId}#certificate”
Provider string `json:"provider"` // 主机提供商 Provider string `json:"provider"` // 主机提供商
ProviderAccessId string `json:"providerAccessId"` // 主机提供商授权记录 ID ProviderAccessId string `json:"providerAccessId,omitempty"` // 主机提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig"` // 主机提供商额外配置 ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 主机提供商额外配置
SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过 SkipOnLastSucceeded bool `json:"skipOnLastSucceeded"` // 上次部署成功时是否跳过
} }
type WorkflowNodeConfigForNotify struct { type WorkflowNodeConfigForNotify struct {
@@ -97,73 +100,54 @@ type WorkflowNodeConfigForNotify struct {
Message string `json:"message"` // 通知内容 Message string `json:"message"` // 通知内容
} }
func (n *WorkflowNode) getConfigString(key string) string {
return maputil.GetString(n.Config, key)
}
func (n *WorkflowNode) getConfigBool(key string) bool {
return maputil.GetBool(n.Config, key)
}
func (n *WorkflowNode) getConfigInt32(key string) int32 {
return maputil.GetInt32(n.Config, key)
}
func (n *WorkflowNode) getConfigMap(key string) map[string]any {
if val, ok := n.Config[key]; ok {
if result, ok := val.(map[string]any); ok {
return result
}
}
return make(map[string]any)
}
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
skipBeforeExpiryDays := n.getConfigInt32("skipBeforeExpiryDays") skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays")
if skipBeforeExpiryDays == 0 { if skipBeforeExpiryDays == 0 {
skipBeforeExpiryDays = 30 skipBeforeExpiryDays = 30
} }
return WorkflowNodeConfigForApply{ return WorkflowNodeConfigForApply{
Domains: n.getConfigString("domains"), Domains: maputil.GetString(n.Config, "domains"),
ContactEmail: n.getConfigString("contactEmail"), ContactEmail: maputil.GetString(n.Config, "contactEmail"),
Provider: n.getConfigString("provider"), Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: n.getConfigString("providerAccessId"), ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: n.getConfigMap("providerConfig"), ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"),
KeyAlgorithm: n.getConfigString("keyAlgorithm"), CAProvider: maputil.GetString(n.Config, "caProvider"),
Nameservers: n.getConfigString("nameservers"), CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
DnsPropagationTimeout: n.getConfigInt32("dnsPropagationTimeout"), CAProviderConfig: maputil.GetAnyMap(n.Config, "caProviderConfig"),
DnsTTL: n.getConfigInt32("dnsTTL"), KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"),
DisableFollowCNAME: n.getConfigBool("disableFollowCNAME"), Nameservers: maputil.GetString(n.Config, "nameservers"),
DisableARI: n.getConfigBool("disableARI"), DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
DnsTTL: maputil.GetInt32(n.Config, "dnsTTL"),
DisableFollowCNAME: maputil.GetBool(n.Config, "disableFollowCNAME"),
DisableARI: maputil.GetBool(n.Config, "disableARI"),
SkipBeforeExpiryDays: skipBeforeExpiryDays, SkipBeforeExpiryDays: skipBeforeExpiryDays,
} }
} }
func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload { func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload {
return WorkflowNodeConfigForUpload{ return WorkflowNodeConfigForUpload{
Certificate: n.getConfigString("certificate"), Certificate: maputil.GetString(n.Config, "certificate"),
PrivateKey: n.getConfigString("privateKey"), PrivateKey: maputil.GetString(n.Config, "privateKey"),
Domains: n.getConfigString("domains"), Domains: maputil.GetString(n.Config, "domains"),
} }
} }
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy { func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
return WorkflowNodeConfigForDeploy{ return WorkflowNodeConfigForDeploy{
Certificate: n.getConfigString("certificate"), Certificate: maputil.GetString(n.Config, "certificate"),
Provider: n.getConfigString("provider"), Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: n.getConfigString("providerAccessId"), ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: n.getConfigMap("providerConfig"), ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"),
SkipOnLastSucceeded: n.getConfigBool("skipOnLastSucceeded"), SkipOnLastSucceeded: maputil.GetBool(n.Config, "skipOnLastSucceeded"),
} }
} }
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify { func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
return WorkflowNodeConfigForNotify{ return WorkflowNodeConfigForNotify{
Channel: n.getConfigString("channel"), Channel: maputil.GetString(n.Config, "channel"),
Subject: n.getConfigString("subject"), Subject: maputil.GetString(n.Config, "subject"),
Message: n.getConfigString("message"), Message: maputil.GetString(n.Config, "message"),
} }
} }

View File

@@ -8,7 +8,11 @@ import (
pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark" pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk" pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email" pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark" pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan" pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram" pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook" pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
@@ -45,11 +49,36 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a
ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"), ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"),
}) })
case domain.NotifyChannelTypeGotify:
return pGotify.NewNotifier(&pGotify.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
Token: maputil.GetString(channelConfig, "token"),
Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
})
case domain.NotifyChannelTypeLark: case domain.NotifyChannelTypeLark:
return pLark.NewNotifier(&pLark.NotifierConfig{ return pLark.NewNotifier(&pLark.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"), WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
}) })
case domain.NotifyChannelTypeMattermost:
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
ChannelId: maputil.GetString(channelConfig, "channelId"),
Username: maputil.GetString(channelConfig, "username"),
Password: maputil.GetString(channelConfig, "password"),
})
case domain.NotifyChannelTypePushover:
return pPushover.NewNotifier(&pPushover.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
User: maputil.GetString(channelConfig, "user"),
})
case domain.NotifyChannelTypePushPlus:
return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
})
case domain.NotifyChannelTypeServerChan: case domain.NotifyChannelTypeServerChan:
return pServerChan.NewNotifier(&pServerChan.NotifierConfig{ return pServerChan.NewNotifier(&pServerChan.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"), Url: maputil.GetString(channelConfig, "url"),

View File

@@ -9,6 +9,7 @@ import (
type ChallengeProviderConfig struct { type ChallengeProviderConfig struct {
DnsApiToken string `json:"dnsApiToken"` DnsApiToken string `json:"dnsApiToken"`
ZoneApiToken string `json:"zoneApiToken,omitempty"`
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int32 `json:"dnsTTL,omitempty"` DnsTTL int32 `json:"dnsTTL,omitempty"`
} }
@@ -20,6 +21,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider,
providerConfig := cloudflare.NewDefaultConfig() providerConfig := cloudflare.NewDefaultConfig()
providerConfig.AuthToken = config.DnsApiToken providerConfig.AuthToken = config.DnsApiToken
providerConfig.ZoneToken = config.ZoneApiToken
if config.DnsPropagationTimeout != 0 { if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
} }

View File

@@ -157,7 +157,7 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPem string, pri
InstanceId: tea.String(d.config.InstanceId), InstanceId: tea.String(d.config.InstanceId),
RegionId: tea.String(d.config.Region), RegionId: tea.String(d.config.Region),
Domain: tea.String(d.config.Domain), Domain: tea.String(d.config.Domain),
Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(upres.CertId)}, Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(upres.ExtendedData["certIdentifier"].(string))},
Redirect: &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String("iphash")}, Redirect: &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String("iphash")},
} }
modifyDomainReq = assign(modifyDomainReq, describeDomainDetailResp.Body) modifyDomainReq = assign(modifyDomainReq, describeDomainDetailResp.Body)

View File

@@ -21,6 +21,9 @@ type DeployerConfig struct {
ApiToken string `json:"apiToken"` ApiToken string `json:"apiToken"`
// 加速域名(支持泛域名)。 // 加速域名(支持泛域名)。
Domain string `json:"domain"` Domain string `json:"domain"`
// 证书 ID。
// 选填。
CertificateId string `json:"certificateId,omitempty"`
} }
type DeployerProvider struct { type DeployerProvider struct {
@@ -62,63 +65,79 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
return nil, errors.New("config `domain` is required") return nil, errors.New("config `domain` is required")
} }
// 查询域名配置 if d.config.CertificateId == "" {
// REF: https://portal.baishancloud.com/track/document/api/1/1065 // 新增证书
getDomainConfigReq := &bssdk.GetDomainConfigRequest{ // REF: https://portal.baishancloud.com/track/document/downloadPdf/1441
Domains: d.config.Domain, certificateId := ""
Config: []string{"https"}, createCertificateReq := &bssdk.CreateCertificateRequest{
} Certificate: certPem,
getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq) Key: privkeyPem,
d.logger.Debug("sdk request 'baishan.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp)) Name: fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
if err != nil { }
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.GetDomainConfig'") createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
} else if len(getDomainConfigResp.Data) == 0 { d.logger.Debug("sdk request 'baishan.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
return nil, errors.New("domain config not found") if err != nil {
} if createCertificateResp != nil {
if createCertificateResp.GetCode() == 400699 && strings.Contains(createCertificateResp.GetMessage(), "this certificate is exists") {
// 新增证书 // 证书已存在,忽略新增证书接口错误
// REF: https://portal.baishancloud.com/track/document/downloadPdf/1441 re := regexp.MustCompile(`\d+`)
certificateId := "" certificateId = re.FindString(createCertificateResp.GetMessage())
createCertificateReq := &bssdk.CreateCertificateRequest{ }
Certificate: certPem,
Key: privkeyPem,
Name: fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
}
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
d.logger.Debug("sdk request 'baishan.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
if createCertificateResp != nil {
if createCertificateResp.GetCode() == 400699 && strings.Contains(createCertificateResp.GetMessage(), "this certificate is exists") {
// 证书已存在,忽略新增证书接口错误
re := regexp.MustCompile(`\d+`)
certificateId = re.FindString(createCertificateResp.GetMessage())
} }
if certificateId == "" {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.CreateCertificate'")
}
} else {
certificateId = createCertificateResp.Data.CertId.String()
} }
if certificateId == "" { // 查询域名配置
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.CreateCertificate'") // REF: https://portal.baishancloud.com/track/document/api/1/1065
getDomainConfigReq := &bssdk.GetDomainConfigRequest{
Domains: d.config.Domain,
Config: []string{"https"},
}
getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq)
d.logger.Debug("sdk request 'baishan.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.GetDomainConfig'")
} else if len(getDomainConfigResp.Data) == 0 {
return nil, errors.New("domain config not found")
}
// 设置域名配置
// REF: https://portal.baishancloud.com/track/document/api/1/1045
setDomainConfigReq := &bssdk.SetDomainConfigRequest{
Domains: d.config.Domain,
Config: &bssdk.DomainConfig{
Https: &bssdk.DomainConfigHttps{
CertId: json.Number(certificateId),
ForceHttps: getDomainConfigResp.Data[0].Config.Https.ForceHttps,
EnableHttp2: getDomainConfigResp.Data[0].Config.Https.EnableHttp2,
EnableOcsp: getDomainConfigResp.Data[0].Config.Https.EnableOcsp,
},
},
}
setDomainConfigResp, err := d.sdkClient.SetDomainConfig(setDomainConfigReq)
d.logger.Debug("sdk request 'baishan.SetDomainConfig'", slog.Any("request", setDomainConfigReq), slog.Any("response", setDomainConfigResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.SetDomainConfig'")
} }
} else { } else {
certificateId = createCertificateResp.Data.CertId.String() // 替换证书
} // REF: https://portal.baishancloud.com/track/document/downloadPdf/1441
createCertificateReq := &bssdk.CreateCertificateRequest{
// 设置域名配置 CertificateId: &d.config.CertificateId,
// REF: https://portal.baishancloud.com/track/document/api/1/1045 Certificate: certPem,
setDomainConfigReq := &bssdk.SetDomainConfigRequest{ Key: privkeyPem,
Domains: d.config.Domain, Name: fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
Config: &bssdk.DomainConfig{ }
Https: &bssdk.DomainConfigHttps{ createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
CertId: json.Number(certificateId), d.logger.Debug("sdk request 'baishan.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
ForceHttps: getDomainConfigResp.Data[0].Config.Https.ForceHttps, if err != nil {
EnableHttp2: getDomainConfigResp.Data[0].Config.Https.EnableHttp2, return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.CreateCertificate'")
EnableOcsp: getDomainConfigResp.Data[0].Config.Https.EnableOcsp, }
},
},
}
setDomainConfigResp, err := d.sdkClient.SetDomainConfig(setDomainConfigReq)
d.logger.Debug("sdk request 'baishan.SetDomainConfig'", slog.Any("request", setDomainConfigReq), slog.Any("response", setDomainConfigResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'baishan.SetDomainConfig'")
} }
return &deployer.DeployResult{}, nil return &deployer.DeployResult{}, nil

View File

@@ -4,12 +4,12 @@ import (
"context" "context"
"log/slog" "log/slog"
edgio "github.com/Edgio/edgio-api/applications/v7"
edgiodtos "github.com/Edgio/edgio-api/applications/v7/dtos"
xerrors "github.com/pkg/errors" xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/certutil" "github.com/usual2970/certimate/internal/pkg/utils/certutil"
edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7"
edgsdkdtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
) )
type DeployerConfig struct { type DeployerConfig struct {
@@ -24,7 +24,7 @@ type DeployerConfig struct {
type DeployerProvider struct { type DeployerProvider struct {
config *DeployerConfig config *DeployerConfig
logger *slog.Logger logger *slog.Logger
sdkClient *edgsdk.EdgioClient sdkClient *edgio.EdgioClient
} }
var _ deployer.Deployer = (*DeployerProvider)(nil) var _ deployer.Deployer = (*DeployerProvider)(nil)
@@ -64,7 +64,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
// 上传 TLS 证书 // 上传 TLS 证书
// REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
uploadTlsCertReq := edgsdkdtos.UploadTlsCertRequest{ uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{
EnvironmentID: d.config.EnvironmentId, EnvironmentID: d.config.EnvironmentId,
PrimaryCert: privateCertPem, PrimaryCert: privateCertPem,
IntermediateCert: intermediateCertPem, IntermediateCert: intermediateCertPem,
@@ -79,7 +79,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
return &deployer.DeployResult{}, nil return &deployer.DeployResult{}, nil
} }
func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) { func createSdkClient(clientId, clientSecret string) (*edgio.EdgioClient, error) {
client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "") client := edgio.NewEdgioClient(clientId, clientSecret, "", "")
return client, nil return client, nil
} }

View File

@@ -100,9 +100,15 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
SSlEnabled: true, SSlEnabled: true,
SSLData: int(updateResourceCertId), SSLData: int(updateResourceCertId),
ProxySSLEnabled: getResourceResp.ProxySSLEnabled, ProxySSLEnabled: getResourceResp.ProxySSLEnabled,
ProxySSLCA: &getResourceResp.ProxySSLCA, }
ProxySSLData: &getResourceResp.ProxySSLData, if getResourceResp.ProxySSLCA != 0 {
Options: getResourceResp.Options, updateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA
}
if getResourceResp.ProxySSLData != 0 {
updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData
}
if getResourceResp.Options != nil {
updateResourceReq.Options = getResourceResp.Options
} }
updateResourceResp, err := d.sdkClient.Update(context.TODO(), d.config.ResourceId, updateResourceReq) updateResourceResp, err := d.sdkClient.Update(context.TODO(), d.config.ResourceId, updateResourceReq)
d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp)) d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp))

View File

@@ -0,0 +1,102 @@
package rainyunrcdn
import (
"context"
"errors"
"log/slog"
"strconv"
xerrors "github.com/pkg/errors"
"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/rainyun-sslcenter"
rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk"
)
type DeployerConfig struct {
// 雨云 API 密钥。
ApiKey string `json:"apiKey"`
// RCDN 实例 ID。
InstanceId int32 `json:"instanceId"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *rainyunsdk.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.ApiKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
ApiKey: config.ApiKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
sslUploader: uploader,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
d.sslUploader.WithLogger(logger)
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
// 上传证书到 SSL 证书
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to upload certificate file")
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// RCDN SSL 绑定域名
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120
certId, _ := strconv.Atoi(upres.CertId)
rcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{
CertId: int32(certId),
Domains: []string{d.config.Domain},
}
rcdnInstanceSslBindResp, err := d.sdkClient.RcdnInstanceSslBind(d.config.InstanceId, rcdnInstanceSslBindReq)
d.logger.Debug("sdk request 'rcdn.InstanceSslBind'", slog.Any("instanceId", d.config.InstanceId), slog.Any("request", rcdnInstanceSslBindReq), slog.Any("response", rcdnInstanceSslBindResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'rcdn.InstanceSslBind'")
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(apiKey string) (*rainyunsdk.Client, error) {
if apiKey == "" {
return nil, errors.New("invalid rainyun api key")
}
client := rainyunsdk.NewClient(apiKey)
return client, nil
}

View File

@@ -0,0 +1,75 @@
package rainyunrcdn_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn"
)
var (
fInputCertPath string
fInputKeyPath string
fApiKey string
fInstanceId int64
fDomain string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_RAINYUNRCDN_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
flag.Int64Var(&fInstanceId, argsPrefix+"INSTANCEID", 0, "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
}
/*
Shell command to run this test:
go test -v ./ucloud_ucdn_test.go -args \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_APIKEY="your-api-key" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_INSTANCEID="your-rcdn-instance-id" \
--CERTIMATE_DEPLOYER_RAINYUNRCDN_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("APIKEY: %v", fApiKey),
fmt.Sprintf("INSTANCEID: %v", fInstanceId),
fmt.Sprintf("DOMAIN: %v", fDomain),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
PrivateKey: fApiKey,
InstanceId: fInstanceId,
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

@@ -107,7 +107,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe
// REF: https://cloud.tencent.com/document/product/400/91667 // REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest() deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn") deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn")
deployCertificateInstanceReq.Status = common.Int64Ptr(1) deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(instanceIds) deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(instanceIds)
deployCertificateInstanceResp, err := d.sdkClients.SSL.DeployCertificateInstance(deployCertificateInstanceReq) deployCertificateInstanceResp, err := d.sdkClients.SSL.DeployCertificateInstance(deployCertificateInstanceReq)

View File

@@ -182,7 +182,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
return errors.New("config `listenerId` is required") return errors.New("config `listenerId` is required")
} }
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil { if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err return err
} }

View File

@@ -178,7 +178,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
return errors.New("config `listenerId` is required") return errors.New("config `listenerId` is required")
} }
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil { if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
return err return err
} }

View File

@@ -0,0 +1,276 @@
package wangsucdnpro
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"regexp"
"time"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/deployer"
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
wangsucdn "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/cdn"
)
type DeployerConfig struct {
// 网宿云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 网宿云 AccessKeySecret。
AccessKeySecret string `json:"accessKeySecret"`
// 网宿云环境。
Environment string `json:"environment"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
// 证书 ID。
// 选填。
CertificateId string `json:"certificateId,omitempty"`
// Webhook ID。
// 选填。
WebhookId string `json:"webhookId,omitempty"`
}
type DeployerProvider struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *wangsucdn.Client
}
var _ deployer.Deployer = (*DeployerProvider)(nil)
func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &DeployerProvider{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
if logger == nil {
d.logger = slog.Default()
} else {
d.logger = logger
}
return d
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
if d.config.Domain == "" {
return nil, errors.New("config `domain` is required")
}
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 查询已部署加速域名的详情
getHostnameDetailResp, err := d.sdkClient.GetHostnameDetail(d.config.Domain)
d.logger.Debug("sdk request 'cdn.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetHostnameDetail'")
}
// 生成网宿云证书参数
encryptedPrivateKey, err := encryptPrivateKey(privkeyPem, d.config.AccessKeySecret, time.Now().Unix())
if err != nil {
return nil, xerrors.Wrap(err, "failed to encrypt private key")
}
certificateNewVersionInfo := &wangsucdn.CertificateVersion{
PrivateKey: tea.String(encryptedPrivateKey),
Certificate: tea.String(certPem),
IdentificationInfo: &wangsucdn.CertificateVersionIdentificationInfo{
CommonName: tea.String(certX509.Subject.CommonName),
SubjectAlternativeNames: &certX509.DNSNames,
},
}
// 网宿云证书 URL 中包含证书 ID 及版本号
// 格式:
// http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33
// http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5
var wangsuCertUrl string
var wangsuCertId, wangsuCertVer string
// 如果原证书 ID 为空,则创建证书;否则更新证书。
timestamp := time.Now().Unix()
if d.config.CertificateId == "" {
// 创建证书
createCertificateReq := &wangsucdn.CreateCertificateRequest{
Timestamp: timestamp,
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
AutoRenew: tea.String("Off"),
NewVersion: certificateNewVersionInfo,
}
createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateCertificate'")
}
wangsuCertUrl = createCertificateResp.CertificateUrl
d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuCertIdMatches) > 1 {
wangsuCertId = wangsuCertIdMatches[1]
}
wangsuCertVer = "1"
} else {
// 更新证书
updateCertificateReq := &wangsucdn.UpdateCertificateRequest{
Timestamp: timestamp,
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
AutoRenew: tea.String("Off"),
NewVersion: certificateNewVersionInfo,
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UpdateCertificate'")
}
wangsuCertUrl = updateCertificateResp.CertificateUrl
d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl))
wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuCertIdMatches) > 1 {
wangsuCertId = wangsuCertIdMatches[1]
}
wangsuCertVerMatches := regexp.MustCompile(`/versions/(\d+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuCertVerMatches) > 1 {
wangsuCertVer = wangsuCertVerMatches[1]
}
}
// 创建部署任务
// REF: https://www.wangsu.com/document/api-doc/27034
createDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{
Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())),
Target: tea.String(d.config.Environment),
Actions: &[]wangsucdn.DeploymentTaskAction{
{
Action: tea.String("deploy_cert"),
CertificateId: tea.String(wangsuCertId),
Version: tea.String(wangsuCertVer),
},
},
}
if d.config.WebhookId != "" {
createDeploymentTaskReq.Webhook = tea.String(d.config.WebhookId)
}
createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTask(createDeploymentTaskReq)
d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateDeploymentTask'")
}
// 循环获取部署任务详细信息,等待任务状态变更
// REF: https://www.wangsu.com/document/api-doc/27038
var wangsuTaskId string
wangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl)
if len(wangsuTaskMatches) > 1 {
wangsuTaskId = wangsuTaskMatches[1]
}
for {
if ctx.Err() != nil {
return nil, ctx.Err()
}
getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId)
d.logger.Debug("sdk request 'cdn.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDeploymentTaskDetail'")
}
if getDeploymentTaskDetailResp.Status == "failed" {
return nil, errors.New("unexpected deployment task status")
} else if getDeploymentTaskDetailResp.Status == "succeeded" {
break
}
d.logger.Info("waiting for deployment task completion ...")
time.Sleep(time.Second * 15)
}
return &deployer.DeployResult{}, nil
}
func createSdkClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) {
if accessKeyId == "" {
return nil, errors.New("invalid wangsu access key id")
}
if accessKeySecret == "" {
return nil, errors.New("invalid wangsu access key secret")
}
return wangsucdn.NewClient(accessKeyId, accessKeySecret), nil
}
func encryptPrivateKey(privkeyPem string, secretKey string, timestamp int64) (string, error) {
date := time.Unix(timestamp, 0).UTC()
dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 GMT")
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(dateStr))
aesivkey := mac.Sum(nil)
aesivkeyHex := hex.EncodeToString(aesivkey)
if len(aesivkeyHex) != 64 {
return "", fmt.Errorf("invalid hmac length: %d", len(aesivkeyHex))
}
ivHex := aesivkeyHex[:32]
keyHex := aesivkeyHex[32:64]
iv, err := hex.DecodeString(ivHex)
if err != nil {
return "", fmt.Errorf("failed to decode iv: %w", err)
}
key, err := hex.DecodeString(keyHex)
if err != nil {
return "", fmt.Errorf("failed to decode key: %w", err)
}
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
plainBytes := []byte(privkeyPem)
padlen := aes.BlockSize - len(plainBytes)%aes.BlockSize
if padlen > 0 {
paddata := bytes.Repeat([]byte{byte(padlen)}, padlen)
plainBytes = append(plainBytes, paddata...)
}
encBytes := make([]byte, len(plainBytes))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(encBytes, plainBytes)
return base64.StdEncoding.EncodeToString(encBytes), nil
}

View File

@@ -0,0 +1,90 @@
package wangsucdnpro_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro"
)
var (
fInputCertPath string
fInputKeyPath string
fAccessKeyId string
fAccessKeySecret string
fEnvironment string
fDomain string
fCertificateId string
fWebhookId string
)
func init() {
argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCDNPRO_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
flag.StringVar(&fEnvironment, argsPrefix+"ENVIRONMENT", "production", "")
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
flag.StringVar(&fWebhookId, argsPrefix+"WEBHOOKID", "", "")
}
/*
Shell command to run this test:
go test -v ./wangsu_cdnpro_test.go -args \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYID="your-access-key-id" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYSECRET="your-access-key-secret" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_ENVIRONMENT="production" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_DOMAIN="example.com" \
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_CERTIFICATEID="your-certificate-id"\
--CERTIMATE_DEPLOYER_WANGSUCDNPRO_WEBHOOKID="your-webhook-id"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
fmt.Sprintf("ENVIRONMENT: %v", fEnvironment),
fmt.Sprintf("DOMAIN: %v", fDomain),
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
fmt.Sprintf("WEBHOOKID: %v", fWebhookId),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Environment: fEnvironment,
Domain: fDomain,
CertificateId: fCertificateId,
WebhookId: fWebhookId,
})
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

@@ -1,4 +1,4 @@
package notifier package notifier
import ( import (
"context" "context"

View File

@@ -0,0 +1,104 @@
package gotify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// Gotify 服务地址
// 示例https://gotify.example.com
Url string `json:"url"`
// Gotify Token
Token string `json:"token"`
// Gotify 消息优先级
Priority int64 `json:"priority"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
// 未来将移除
httpClient *http.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
if config == nil {
panic("config is nil")
}
return &NotifierProvider{
config: config,
httpClient: http.DefaultClient,
}, nil
}
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
if logger == nil {
n.logger = slog.Default()
} else {
n.logger = logger
}
return n
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// Gotify 原生实现, notify 库没有实现, 等待合并
reqBody := &struct {
Title string `json:"title"`
Message string `json:"message"`
Priority int64 `json:"priority"`
}{
Title: subject,
Message: message,
Priority: n.config.Priority,
}
// Make request
body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encode message body")
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
fmt.Sprintf("%s/message", n.config.Url),
bytes.NewReader(body),
)
if err != nil {
return nil, errors.Wrap(err, "create new request")
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", n.config.Token))
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// Send request to gotify service
resp, err := n.httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to gotify server")
}
defer resp.Body.Close()
// Read response and verify success
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read response")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gotify returned status code %d: %s", resp.StatusCode, string(result))
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,68 @@
package gotify_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fUrl string
fToken string
fPriority int64
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_GOTIFY_"
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
flag.Int64Var(&fPriority, argsPrefix+"PRIORITY", 0, "")
}
/*
Shell command to run this test:
go test -v ./gotify_test.go -args \
--CERTIMATE_NOTIFIER_GOTIFY_URL="https://example.com" \
--CERTIMATE_NOTIFIER_GOTIFY_TOKEN="your-gotify-application-token" \
--CERTIMATE_NOTIFIER_GOTIFY_PRIORITY="your-message-priority"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fUrl),
fmt.Sprintf("TOKEN: %v", fToken),
fmt.Sprintf("PRIORITY: %d", fPriority),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Url: fUrl,
Token: fToken,
Priority: fPriority,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,89 @@
package mattermost
import (
"bytes"
"context"
"encoding/json"
"github.com/nikoksr/notify/service/mattermost"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
"io"
"log/slog"
"net/http"
)
type NotifierConfig struct {
// Mattermost 服务地址。
ServerUrl string `json:"serverUrl"`
// 频道ID
ChannelId string `json:"channelId"`
// 用户名
Username string `json:"username"`
// 密码
Password string `json:"password"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
if config == nil {
panic("config is nil")
}
return &NotifierProvider{
config: config,
}, nil
}
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
if logger == nil {
n.logger = slog.Default()
} else {
n.logger = logger
}
return n
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := mattermost.New(n.config.ServerUrl)
if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil {
return nil, err
}
srv.AddReceivers(n.config.ChannelId)
// 复写消息样式
srv.PreSend(func(req *http.Request) error {
m := map[string]interface{}{
"channel_id": n.config.ChannelId,
"props": map[string]interface{}{
"attachments": []map[string]interface{}{
{
"title": subject,
"text": message,
},
},
},
}
if body, err := json.Marshal(m); err != nil {
return err
} else {
req.ContentLength = int64(len(body))
req.Body = io.NopCloser(bytes.NewReader(body))
}
return nil
})
if err = srv.Send(ctx, subject, message); err != nil {
return nil, err
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,74 @@
package mattermost_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fServerUrl string
fChannelId string
fUsername string
fPassword string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_MATTERMOST_"
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "")
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
}
/*
Shell command to run this test:
go test -v ./mattermost_test.go -args \
--CERTIMATE_NOTIFIER_MATTERMOST_SERVERURL="https://example.com/your-server-url" \
--CERTIMATE_NOTIFIER_MATTERMOST_CHANNELID="your-chanel-id" \
--CERTIMATE_NOTIFIER_MATTERMOST_USERNAME="your-username" \
--CERTIMATE_NOTIFIER_MATTERMOST_PASSWORD="your-password"
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SERVERURL: %v", fServerUrl),
fmt.Sprintf("CHANNELID: %v", fChannelId),
fmt.Sprintf("USERNAME: %v", fUsername),
fmt.Sprintf("PASSWORD: %v", fPassword),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
ServerUrl: fServerUrl,
ChannelId: fChannelId,
Username: fUsername,
Password: fPassword,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,102 @@
package pushover
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
Token string `json:"token"` // 应用 API Token
User string `json:"user"` // 用户/分组 Key
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
// 未来将移除
httpClient *http.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
if config == nil {
panic("config is nil")
}
return &NotifierProvider{
config: config,
httpClient: http.DefaultClient,
}, nil
}
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
if logger == nil {
n.logger = slog.Default()
} else {
n.logger = logger
}
return n
}
// Notify 发送通知
// 参考文档https://pushover.net/api
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// 请求体
reqBody := &struct {
Token string `json:"token"`
User string `json:"user"`
Title string `json:"title"`
Message string `json:"message"`
}{
Token: n.config.Token,
User: n.config.User,
Title: subject,
Message: message,
}
// Make request
body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encode message body")
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://api.pushover.net/1/messages.json",
bytes.NewReader(body),
)
if err != nil {
return nil, errors.Wrap(err, "create new request")
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// Send request to pushover service
resp, err := n.httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to pushover server")
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read response")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushover returned status code %d: %s", resp.StatusCode, string(result))
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,62 @@
package pushover_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var (
fToken string
fUser string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_PUSHOVER_"
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
flag.StringVar(&fUser, argsPrefix+"USER", "", "")
}
/*
Shell command to run this test:
go test -v ./pushover_test.go -args \
--CERTIMATE_NOTIFIER_PUSHOVER_TOKEN="your-pushover-token" \
--CERTIMATE_NOTIFIER_PUSHOVER_USER="your-pushover-user" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("TOKEN: %v", fToken),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Token: fToken,
User: fUser,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@@ -0,0 +1,113 @@
package pushplus
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// PushPlus Token
Token string `json:"token"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
// 未来将移除
httpClient *http.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
if config == nil {
panic("config is nil")
}
return &NotifierProvider{
config: config,
httpClient: http.DefaultClient,
}, nil
}
func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
if logger == nil {
n.logger = slog.Default()
} else {
n.logger = logger
}
return n
}
// Notify 发送通知
// 参考文档https://pushplus.plus/doc/guide/api.html
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// 请求体
reqBody := &struct {
Token string `json:"token"`
Title string `json:"title"`
Content string `json:"content"`
}{
Token: n.config.Token,
Title: subject,
Content: message,
}
// Make request
body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encode message body")
}
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
"https://www.pushplus.plus/send",
bytes.NewReader(body),
)
if err != nil {
return nil, errors.Wrap(err, "create new request")
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// Send request to pushplus service
resp, err := n.httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to pushplus server")
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read response")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushplus returned status code %d: %s", resp.StatusCode, string(result))
}
// 解析响应
var errorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
if err := json.Unmarshal(result, &errorResponse); err != nil {
return nil, errors.Wrap(err, "decode response")
}
if errorResponse.Code != 200 {
return nil, fmt.Errorf("pushplus returned error: %s", errorResponse.Msg)
}
return &notifier.NotifyResult{}, nil
}

View File

@@ -0,0 +1,56 @@
package pushplus_test
import (
"context"
"flag"
"fmt"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
)
var fToken string
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_PUSHPLUS_"
flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "")
}
/*
Shell command to run this test:
go test -v ./pushplus_test.go -args \
--CERTIMATE_NOTIFIER_PUSHPLUS_TOKEN="your-pushplus-token" \
*/
func TestNotify(t *testing.T) {
flag.Parse()
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("TOKEN: %v", fToken),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Token: fToken,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

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

View File

@@ -116,6 +116,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
return &uploader.UploadResult{ return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)), CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)),
CertName: *certDetail.Name, CertName: *certDetail.Name,
ExtendedData: map[string]any{
"instanceId": tea.StringValue(getUserCertificateDetailResp.Body.InstanceId),
"certIdentifier": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier),
},
}, nil }, nil
} }
} }
@@ -129,8 +133,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
} }
// 生成新证书名(需符合阿里云命名规则) // 生成新证书名(需符合阿里云命名规则)
var certId, certName string certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
// 上传新证书 // 上传新证书
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate
@@ -145,10 +148,25 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.UploadUserCertificate'") return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.UploadUserCertificate'")
} }
certId = fmt.Sprintf("%d", tea.Int64Value(uploadUserCertificateResp.Body.CertId)) // 获取证书详情
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-getusercertificatedetail
getUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{
CertId: uploadUserCertificateResp.Body.CertId,
CertFilter: tea.Bool(true),
}
getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetail(getUserCertificateDetailReq)
u.logger.Debug("sdk request 'cas.GetUserCertificateDetail'", slog.Any("request", getUserCertificateDetailReq), slog.Any("response", getUserCertificateDetailResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.GetUserCertificateDetail'")
}
return &uploader.UploadResult{ return &uploader.UploadResult{
CertId: certId, CertId: fmt.Sprintf("%d", tea.Int64Value(getUserCertificateDetailResp.Body.Id)),
CertName: certName, CertName: certName,
ExtendedData: map[string]any{
"instanceId": tea.StringValue(getUserCertificateDetailResp.Body.InstanceId),
"certIdentifier": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier),
},
}, nil }, nil
} }

View File

@@ -71,7 +71,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 // REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html
listCertificatesNextToken := new(string) var listCertificatesNextToken *string = nil
listCertificatesMaxItems := int32(1000) listCertificatesMaxItems := int32(1000)
for { for {
listCertificatesReq := &awsacm.ListCertificatesInput{ listCertificatesReq := &awsacm.ListCertificatesInput{

View File

@@ -0,0 +1,169 @@
package rainyunsslcenter
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/certutil"
rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk"
)
type UploaderConfig struct {
// 雨云 API 密钥。
ApiKey string `json:"ApiKey"`
}
type UploaderProvider struct {
config *UploaderConfig
logger *slog.Logger
sdkClient *rainyunsdk.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.ApiKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
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.Default()
} else {
u.logger = logger
}
return u
}
func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
return nil, err
} else if res != nil {
u.logger.Info("ssl certificate already exists")
return res, nil
}
// SSL 证书上传
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046
sslCenterCreateReq := &rainyunsdk.SslCenterCreateRequest{
Cert: certPem,
Key: privkeyPem,
}
sslCenterCreateResp, err := u.sdkClient.SslCenterCreate(sslCenterCreateReq)
u.logger.Debug("sdk request 'sslcenter.Create'", slog.Any("request", sslCenterCreateReq), slog.Any("response", sslCenterCreateResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Create'")
}
if res, err := u.getCertIfExists(ctx, certPem); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("rainyun sslcenter: no certificate found")
} else {
return res, nil
}
}
func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 遍历 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)
sslCenterListPerPage := int32(100)
for {
sslCenterListReq := &rainyunsdk.SslCenterListRequest{
Filters: &rainyunsdk.SslCenterListFilters{
Domain: &certX509.Subject.CommonName,
},
Page: &sslCenterListPage,
PerPage: &sslCenterListPerPage,
}
sslCenterListResp, err := u.sdkClient.SslCenterList(sslCenterListReq)
u.logger.Debug("sdk request 'sslcenter.List'", slog.Any("request", sslCenterListReq), slog.Any("response", sslCenterListResp))
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.List'")
}
if sslCenterListResp.Data != nil && sslCenterListResp.Data.Records != nil {
for _, sslItem := range sslCenterListResp.Data.Records {
// 先对比证书的多域名
if sslItem.Domain != strings.Join(certX509.DNSNames, ", ") {
continue
}
// 再对比证书的有效期
if sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() {
continue
}
// 最后对比证书内容
sslCenterGetResp, err := u.sdkClient.SslCenterGet(sslItem.ID)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Get'")
}
var isSameCert bool
if sslCenterGetResp.Data != nil {
if sslCenterGetResp.Data.Cert == certPem {
isSameCert = true
} else {
oldCertX509, err := certutil.ParseCertificateFromPEM(sslCenterGetResp.Data.Cert)
if err != nil {
continue
}
isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
}
}
// 如果已存在相同证书,直接返回
if isSameCert {
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", sslItem.ID),
}, nil
}
}
}
if sslCenterListResp.Data == nil || len(sslCenterListResp.Data.Records) < int(sslCenterListPerPage) {
break
} else {
sslCenterListPage++
}
}
return nil, nil
}
func createSdkClient(apiKey string) (*rainyunsdk.Client, error) {
if apiKey == "" {
return nil, errors.New("invalid rainyun api key")
}
client := rainyunsdk.NewClient(apiKey)
return client, nil
}

View File

@@ -0,0 +1,67 @@
package rainyunsslcenter_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/rainyun-sslcenter"
)
var (
fInputCertPath string
fInputKeyPath string
fApiKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_RAINYUNSSLCENTER_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./rainyun_sslcenter_test.go -args \
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_RAINYUNSSLCENTER_APIKEY="your-api-key"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
t.Run("Deploy", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("APIKEY: %v", fApiKey),
}, "\n"))
uploader, err := provider.NewUploader(&provider.UploaderConfig{
ApiKey: fApiKey,
})
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

@@ -89,10 +89,10 @@ 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)) u.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp))
if err != nil { if err != nil {
if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 { if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 {
if res, err := u.getExistCert(ctx, certPem); err != nil { if res, err := u.getCertIfExists(ctx, certPem); err != nil {
return nil, err return nil, err
} else if res == nil { } else if res == nil {
return nil, errors.New("no certificate found") return nil, errors.New("ucloud ssl: no certificate found")
} else { } else {
u.logger.Info("ssl certificate already exists") u.logger.Info("ssl certificate already exists")
return res, nil return res, nil
@@ -112,7 +112,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
}, nil }, nil
} }
func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) { func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容 // 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPem) certX509, err := certutil.ParseCertificateFromPEM(certPem)
if err != nil { if err != nil {

View File

@@ -74,6 +74,18 @@ func GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int3
} }
} }
if result, ok := value.(int64); ok {
if result != 0 {
return int32(result)
}
}
if result, ok := value.(int); ok {
if result != 0 {
return int32(result)
}
}
// 兼容字符串类型的值 // 兼容字符串类型的值
if str, ok := value.(string); ok { if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 32); err == nil { if result, err := strconv.ParseInt(str, 10, 32); err == nil {
@@ -126,6 +138,12 @@ func GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int6
} }
} }
if result, ok := value.(int); ok {
if result != 0 {
return int64(result)
}
}
// 兼容字符串类型的值 // 兼容字符串类型的值
if str, ok := value.(string); ok { if str, ok := value.(string); ok {
if result, err := strconv.ParseInt(str, 10, 64); err == nil { if result, err := strconv.ParseInt(str, 10, 64); err == nil {
@@ -180,3 +198,25 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool {
return defaultValue return defaultValue
} }
// 以 `map[string]any` 形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的 `map[string]any` 对象。
func GetAnyMap(dict map[string]any, key string) map[string]any {
if dict == nil {
return make(map[string]any)
}
if val, ok := dict[key]; ok {
if result, ok := val.(map[string]any); ok {
return result
}
}
return make(map[string]any)
}

View File

@@ -79,7 +79,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil { if err != nil {
return resp, fmt.Errorf("1panel api error: failed to send request: %w", err) return resp, fmt.Errorf("1panel api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("1panel api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("1panel api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -75,7 +75,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil { if err != nil {
return resp, fmt.Errorf("baishan api error: failed to send request: %w", err) return resp, fmt.Errorf("baishan api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("baishan api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("baishan api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -27,9 +27,10 @@ func (r *baseResponse) GetMessage() string {
} }
type CreateCertificateRequest struct { type CreateCertificateRequest struct {
Certificate string `json:"certificate"` CertificateId *string `json:"cert_id,omitempty"`
Key string `json:"key"` Certificate string `json:"certificate"`
Name string `json:"name"` Key string `json:"key"`
Name string `json:"name"`
} }
type CreateCertificateResponse struct { type CreateCertificateResponse struct {

View File

@@ -86,7 +86,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
if err != nil { if err != nil {
return resp, fmt.Errorf("baota api error: failed to send request: %w", err) return resp, fmt.Errorf("baota api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("baota api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("baota api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -59,7 +59,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil { if err != nil {
return resp, fmt.Errorf("cachefly api error: failed to send request: %w", err) return resp, fmt.Errorf("cachefly api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -3,17 +3,18 @@ package cdnflysdk
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
) )
func (c *Client) GetSite(req *GetSiteRequest) (*GetSiteResponse, error) { func (c *Client) GetSite(req *GetSiteRequest) (*GetSiteResponse, error) {
resp := &GetSiteResponse{} resp := &GetSiteResponse{}
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp) err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp)
return resp, err return resp, err
} }
func (c *Client) UpdateSite(req *UpdateSiteRequest) (*UpdateSiteResponse, error) { func (c *Client) UpdateSite(req *UpdateSiteRequest) (*UpdateSiteResponse, error) {
resp := &UpdateSiteResponse{} resp := &UpdateSiteResponse{}
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp) err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp)
return resp, err return resp, err
} }
@@ -25,6 +26,6 @@ func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertif
func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
resp := &UpdateCertificateResponse{} resp := &UpdateCertificateResponse{}
err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", req.Id), req, resp) err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", url.PathEscape(req.Id)), req, resp)
return resp, err return resp, err
} }

View File

@@ -65,7 +65,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil { if err != nil {
return resp, fmt.Errorf("cdnfly api error: failed to send request: %w", err) return resp, fmt.Errorf("cdnfly api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -60,7 +60,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil { if err != nil {
return resp, fmt.Errorf("dnsla api error: failed to send request: %w", err) return resp, fmt.Errorf("dnsla api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -6,9 +6,8 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/Edgio/edgio-api/applications/v7/dtos"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos"
) )
// AccessTokenResponse represents the response from the token endpoint. // AccessTokenResponse represents the response from the token endpoint.

View File

@@ -3,7 +3,7 @@ package edgio_api
import ( import (
"context" "context"
"github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" "github.com/Edgio/edgio-api/applications/v7/dtos"
) )
type EdgioClientInterface interface { type EdgioClientInterface interface {

View File

@@ -0,0 +1,3 @@
module github.com/Edgio/edgio-api
go 1.23.0

View File

@@ -82,7 +82,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
if err != nil { if err != nil {
return resp, fmt.Errorf("gname api error: failed to send request: %w", err) return resp, fmt.Errorf("gname api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("gname api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("gname api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

30
internal/pkg/vendors/rainyun-sdk/api.go vendored Normal file
View File

@@ -0,0 +1,30 @@
package rainyunsdk
import (
"fmt"
"net/http"
)
func (c *Client) SslCenterList(req *SslCenterListRequest) (*SslCenterListResponse, error) {
resp := &SslCenterListResponse{}
err := c.sendRequestWithResult(http.MethodGet, "/product/sslcenter", req, resp)
return resp, err
}
func (c *Client) SslCenterGet(id int32) (*SslCenterGetResponse, error) {
resp := &SslCenterGetResponse{}
err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/product/sslcenter/%d", id), nil, resp)
return resp, err
}
func (c *Client) SslCenterCreate(req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) {
resp := &SslCenterCreateResponse{}
err := c.sendRequestWithResult(http.MethodPost, "/product/sslcenter/", req, resp)
return resp, err
}
func (c *Client) RcdnInstanceSslBind(id int32, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) {
resp := &RcdnInstanceSslBindResponse{}
err := c.sendRequestWithResult(http.MethodPost, fmt.Sprintf("/product/rcdn/instance/%d/ssl_bind", id), req, resp)
return resp, err
}

View File

@@ -0,0 +1,74 @@
package rainyunsdk
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
apiKey string
client *resty.Client
}
func NewClient(apiKey string) *Client {
client := resty.New()
return &Client{
apiKey: apiKey,
client: client,
}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) {
req := c.client.R().SetHeader("x-api-key", c.apiKey)
req.Method = method
req.URL = "https://api.v2.rainyun.com" + path
if strings.EqualFold(method, http.MethodGet) {
if params != nil {
jsonb, _ := json.Marshal(params)
req = req.SetQueryParam("options", string(jsonb))
}
} else {
req = req.
SetHeader("Content-Type", "application/json").
SetBody(params)
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("rainyun api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("rainyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil
}
func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) 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("rainyun api error: failed to parse response: %w", err)
} else if errcode := result.GetCode(); errcode/100 != 2 {
return fmt.Errorf("rainyun api error: %d - %s", errcode, result.GetMessage())
}
return nil
}

View File

@@ -0,0 +1,83 @@
package rainyunsdk
type BaseResponse interface {
GetCode() int32
GetMessage() string
}
type baseResponse struct {
Code *int32 `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
}
func (r *baseResponse) GetCode() int32 {
if r.Code != nil {
return *r.Code
}
return 0
}
func (r *baseResponse) GetMessage() string {
if r.Message != nil {
return *r.Message
}
return ""
}
type SslCenterListFilters struct {
Domain *string `json:"Domain,omitempty"`
}
type SslCenterListRequest struct {
Filters *SslCenterListFilters `json:"columnFilters,omitempty"`
Sort []*string `json:"sort,omitempty"`
Page *int32 `json:"page,omitempty"`
PerPage *int32 `json:"perPage,omitempty"`
}
type SslCenterListResponse struct {
baseResponse
Data *struct {
TotalRecords int32 `json:"TotalRecords"`
Records []*struct {
ID int32 `json:"ID"`
UID int32 `json:"UID"`
Domain string `json:"Domain"`
Issuer string `json:"Issuer"`
StartDate int64 `json:"StartDate"`
ExpireDate int64 `json:"ExpDate"`
UploadTime int64 `json:"UploadTime"`
} `json:"Records"`
} `json:"data,omitempty"`
}
type SslCenterGetResponse struct {
baseResponse
Data *struct {
Cert string `json:"Cert"`
Key string `json:"Key"`
Domain string `json:"DomainName"`
Issuer string `json:"Issuer"`
StartDate int64 `json:"StartDate"`
ExpireDate int64 `json:"ExpDate"`
RemainDays int32 `json:"RemainDays"`
} `json:"data,omitempty"`
}
type SslCenterCreateRequest struct {
Cert string `json:"cert"`
Key string `json:"key"`
}
type SslCenterCreateResponse struct {
baseResponse
}
type RcdnInstanceSslBindRequest struct {
CertId int32 `json:"cert_id"`
Domains []string `json:"domains"`
}
type RcdnInstanceSslBindResponse struct {
baseResponse
}

View File

@@ -47,7 +47,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response,
if err != nil { if err != nil {
return resp, fmt.Errorf("safeline api error: failed to send request: %w", err) return resp, fmt.Errorf("safeline api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("safeline api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("safeline api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -64,7 +64,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r
if err != nil { if err != nil {
return resp, fmt.Errorf("upyun api error: failed to send request: %w", err) return resp, fmt.Errorf("upyun api error: failed to send request: %w", err)
} else if resp.IsError() { } else if resp.IsError() {
return resp, fmt.Errorf("upyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) return resp, fmt.Errorf("upyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
} }
return resp, nil return resp, nil

View File

@@ -0,0 +1,58 @@
package cdn
import (
"fmt"
"net/http"
"net/url"
"github.com/go-resty/resty/v2"
)
func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
resp := &CreateCertificateResponse{}
r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) {
r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
})
if err != nil {
return resp, err
}
resp.CertificateUrl = r.Header().Get("Location")
return resp, err
}
func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) {
resp := &UpdateCertificateResponse{}
r, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) {
r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp))
})
if err != nil {
return resp, err
}
resp.CertificateUrl = r.Header().Get("Location")
return resp, err
}
func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) {
resp := &GetHostnameDetailResponse{}
_, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/hostnames/%s", url.PathEscape(hostname)), nil, resp)
return resp, err
}
func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) {
resp := &CreateDeploymentTaskResponse{}
r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp)
if err != nil {
return resp, err
}
resp.DeploymentTaskUrl = r.Header().Get("Location")
return resp, err
}
func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) {
resp := &GetDeploymentTaskDetailResponse{}
_, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", deploymentTaskId), nil, resp)
return resp, err
}

View File

@@ -0,0 +1,20 @@
package cdn
import (
"time"
"github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
)
type Client struct {
client *openapi.Client
}
func NewClient(accessKey, secretKey string) *Client {
return &Client{client: openapi.NewClient(accessKey, secretKey)}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.WithTimeout(timeout)
return c
}

View File

@@ -0,0 +1,107 @@
package cdn
import (
"github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi"
)
type baseResponse struct {
RequestId *string `json:"-"`
Code *string `json:"code,omitempty"`
Message *string `json:"message,omitempty"`
}
var _ openapi.Result = (*baseResponse)(nil)
func (r *baseResponse) SetRequestId(requestId string) {
r.RequestId = &requestId
}
type CertificateVersion struct {
Comments *string `json:"comments,omitempty"`
PrivateKey *string `json:"privateKey,omitempty"`
Certificate *string `json:"certificate,omitempty"`
ChainCert *string `json:"chainCert,omitempty"`
IdentificationInfo *CertificateVersionIdentificationInfo `json:"identificationInfo,omitempty"`
}
type CertificateVersionIdentificationInfo struct {
Country *string `json:"country,omitempty"`
State *string `json:"state,omitempty"`
City *string `json:"city,omitempty"`
Company *string `json:"company,omitempty"`
Department *string `json:"department,omitempty"`
CommonName *string `json:"commonName,omitempty" required:"true"`
Email *string `json:"email,omitempty"`
SubjectAlternativeNames *[]string `json:"subjectAlternativeNames,omitempty" required:"true"`
}
type CreateCertificateRequest struct {
Timestamp int64 `json:"-"`
Name *string `json:"name,omitempty" required:"true"`
Description *string `json:"description,omitempty"`
AutoRenew *string `json:"autoRenew,omitempty"`
ForceRenew *bool `json:"forceRenew,omitempty"`
NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
}
type CreateCertificateResponse struct {
baseResponse
CertificateUrl string `json:"-"`
}
type UpdateCertificateRequest struct {
Timestamp int64 `json:"-"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
AutoRenew *string `json:"autoRenew,omitempty"`
ForceRenew *bool `json:"forceRenew,omitempty"`
NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"`
}
type UpdateCertificateResponse struct {
baseResponse
CertificateUrl string `json:"-"`
}
type HostnameProperty struct {
PropertyId string `json:"propertyId"`
Version int32 `json:"version"`
CertificateId *string `json:"certificateId,omitempty"`
}
type GetHostnameDetailResponse struct {
baseResponse
Hostname string `json:"hostname"`
PropertyInProduction *HostnameProperty `json:"propertyInProduction,omitempty"`
PropertyInStaging *HostnameProperty `json:"propertyInStaging,omitempty"`
}
type DeploymentTaskAction struct {
Action *string `json:"action,omitempty" required:"true"`
PropertyId *string `json:"propertyId,omitempty"`
CertificateId *string `json:"certificateId,omitempty"`
Version *string `json:"version,omitempty"`
}
type CreateDeploymentTaskRequest struct {
Name *string `json:"name,omitempty"`
Target *string `json:"target,omitempty" required:"true"`
Actions *[]DeploymentTaskAction `json:"actions,omitempty" required:"true"`
Webhook *string `json:"webhook,omitempty"`
}
type CreateDeploymentTaskResponse struct {
baseResponse
DeploymentTaskUrl string `json:"-"`
}
type GetDeploymentTaskDetailResponse struct {
baseResponse
Target string `json:"target"`
Actions []DeploymentTaskAction `json:"actions"`
Status string `json:"status"`
StatusDetails string `json:"statusDetails"`
SubmissionTime string `json:"submissionTime"`
FinishTime string `json:"finishTime"`
ApiRequestId string `json:"apiRequestId"`
}

View File

@@ -0,0 +1,187 @@
package openapi
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/go-resty/resty/v2"
)
type Client struct {
accessKey string
secretKey string
client *resty.Client
}
type Result interface {
SetRequestId(requestId string)
}
func NewClient(accessKey, secretKey string) *Client {
client := resty.New().
SetBaseURL("https://open.chinanetcenter.com").
SetHeader("Host", "open.chinanetcenter.com").
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
// Step 1: Get request method
method := req.Method
method = strings.ToUpper(method)
// Step 2: Get request path
path := "/"
if req.URL != nil {
path = req.URL.Path
}
// Step 3: Get unencoded query string
queryString := ""
if method != http.MethodPost && req.URL != nil {
queryString = req.URL.RawQuery
s, err := url.QueryUnescape(queryString)
if err != nil {
return err
}
queryString = s
}
// Step 4: Get canonical headers & signed headers
canonicalHeaders := "" +
"content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" +
"host:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Host"))) + "\n"
signedHeaders := "content-type;host"
// Step 5: Get request payload
payload := ""
if method != http.MethodGet && req.Body != nil {
reader, err := req.GetBody()
if err != nil {
return err
}
defer reader.Close()
payloadb, err := io.ReadAll(reader)
if err != nil {
return err
}
payload = string(payloadb)
}
hashedPayload := sha256.Sum256([]byte(payload))
hashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:]))
// Step 6: Get timestamp
var reqtime time.Time
timestampString := req.Header.Get("x-cnc-timestamp")
if timestampString == "" {
reqtime = time.Now().UTC()
timestampString = fmt.Sprintf("%d", reqtime.Unix())
} else {
timestamp, err := strconv.ParseInt(timestampString, 10, 64)
if err != nil {
return err
}
reqtime = time.Unix(timestamp, 0).UTC()
}
// Step 7: Get canonical request string
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex)
hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest))
hashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:]))
// Step 8: String to sign
const SignAlgorithmHeader = "CNC-HMAC-SHA256"
stringToSign := fmt.Sprintf("%s\n%s\n%s", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex)
hmac := hmac.New(sha256.New, []byte(secretKey))
hmac.Write([]byte(stringToSign))
sign := hmac.Sum(nil)
signHex := strings.ToLower(hex.EncodeToString(sign))
// Step 9: Add headers to request
req.Header.Set("x-cnc-accessKey", accessKey)
req.Header.Set("x-cnc-timestamp", timestampString)
req.Header.Set("x-cnc-auth-method", "AKSK")
req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex))
req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT"))
return nil
})
return &Client{
accessKey: accessKey,
secretKey: secretKey,
client: client,
}
}
func (c *Client) WithTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) sendRequest(method string, path string, params interface{}, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
req := c.client.R()
req.Method = method
req.URL = path
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.SetBody(params)
}
for _, fn := range configureReq {
fn(req)
}
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("wangsu api error: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("wangsu api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body())
}
return resp, nil
}
func (c *Client) SendRequestWithResult(method string, path string, params interface{}, result Result, configureReq ...func(req *resty.Request)) (*resty.Response, error) {
resp, err := c.sendRequest(method, path, params, configureReq...)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &result)
result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
}
return resp, err
}
if err := json.Unmarshal(resp.Body(), &result); err != nil {
return resp, fmt.Errorf("wangsu api error: failed to parse response: %w", err)
}
result.SetRequestId(resp.Header().Get("x-cnc-request-id"))
return resp, nil
}

View File

@@ -109,12 +109,24 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail { if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail {
return false, "the configuration item 'ContactEmail' changed" return false, "the configuration item 'ContactEmail' changed"
} }
if currentNodeConfig.Provider != lastNodeConfig.Provider {
return false, "the configuration item 'Provider' changed"
}
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
return false, "the configuration item 'ProviderAccessId' changed" return false, "the configuration item 'ProviderAccessId' changed"
} }
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) { if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) {
return false, "the configuration item 'ProviderConfig' changed" return false, "the configuration item 'ProviderConfig' changed"
} }
if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider {
return false, "the configuration item 'CAProvider' changed"
}
if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId {
return false, "the configuration item 'CAProviderAccessId' changed"
}
if !maps.Equal(currentNodeConfig.CAProviderConfig, lastNodeConfig.CAProviderConfig) {
return false, "the configuration item 'CAProviderConfig' changed"
}
if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm { if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm {
return false, "the configuration item 'KeyAlgorithm' changed" return false, "the configuration item 'KeyAlgorithm' changed"
} }

View File

@@ -7,8 +7,6 @@ import (
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations" m "github.com/pocketbase/pocketbase/migrations"
"github.com/usual2970/certimate/internal/domain"
) )
func init() { func init() {
@@ -179,20 +177,20 @@ func init() {
} }
for _, workflowRun := range workflowRuns { for _, workflowRun := range workflowRuns {
type oldWorkflowRunLogRecord struct { type dWorkflowRunLogRecord struct {
Time string `json:"time"` Time string `json:"time"`
Level string `json:"level"` Level string `json:"level"`
Content string `json:"content"` Content string `json:"content"`
Error string `json:"error"` Error string `json:"error"`
} }
type oldWorkflowRunLog struct { type dWorkflowRunLog struct {
NodeId string `json:"nodeId"` NodeId string `json:"nodeId"`
NodeName string `json:"nodeName"` NodeName string `json:"nodeName"`
Records []oldWorkflowRunLogRecord `json:"records"` Records []dWorkflowRunLogRecord `json:"records"`
Error string `json:"error"` Error string `json:"error"`
} }
logs := make([]oldWorkflowRunLog, 0) logs := make([]dWorkflowRunLog, 0)
if err := workflowRun.UnmarshalJSONField("logs", &logs); err != nil { if err := workflowRun.UnmarshalJSONField("logs", &logs); err != nil {
continue continue
} }
@@ -259,8 +257,20 @@ func init() {
return err return err
} }
type dWorkflowNode struct {
Id string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Config map[string]any `json:"config"`
Inputs []map[string]any `json:"inputs"`
Outputs []map[string]any `json:"outputs"`
Next *dWorkflowNode `json:"next,omitempty"`
Branches []dWorkflowNode `json:"branches,omitempty"`
Validated bool `json:"validated"`
}
for _, workflowRun := range workflowRuns { for _, workflowRun := range workflowRuns {
node := &domain.WorkflowNode{} node := &dWorkflowNode{}
for _, workflowOutput := range workflowOutputs { for _, workflowOutput := range workflowOutputs {
if workflowOutput.GetString("runId") != workflowRun.Get("id") { if workflowOutput.GetString("runId") != workflowRun.Get("id") {
continue continue
@@ -270,8 +280,8 @@ func init() {
continue continue
} }
if node.Type != domain.WorkflowNodeTypeApply { if node.Type != "apply" {
node = &domain.WorkflowNode{} node = &dWorkflowNode{}
continue continue
} }
} }
@@ -286,7 +296,7 @@ func init() {
} else { } else {
workflow, _ := app.FindRecordById("workflow", workflowRun.GetString("workflowId")) workflow, _ := app.FindRecordById("workflow", workflowRun.GetString("workflowId"))
if workflow != nil { if workflow != nil {
rootNode := &domain.WorkflowNode{} rootNode := &dWorkflowNode{}
if err := workflow.UnmarshalJSONField("content", rootNode); err != nil { if err := workflow.UnmarshalJSONField("content", rootNode); err != nil {
return err return err
} }
@@ -294,9 +304,9 @@ func init() {
rootNode.Next = node rootNode.Next = node
workflowRun.Set("detail", rootNode) workflowRun.Set("detail", rootNode)
} else { } else {
rootNode := &domain.WorkflowNode{ rootNode := &dWorkflowNode{
Id: core.GenerateDefaultRandomId(), Id: core.GenerateDefaultRandomId(),
Type: domain.WorkflowNodeTypeStart, Type: "start",
Name: "开始", Name: "开始",
Config: map[string]any{ Config: map[string]any{
"trigger": "manual", "trigger": "manual",

View File

@@ -0,0 +1,173 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// update collection `settings`
{
collection, err := app.FindCollectionByNameOrId("dy6ccjb60spfy6p")
if err != nil {
return err
}
records, err := app.FindRecordsByFilter(collection, "name='sslProvider'", "-created", 1, 0)
if err != nil {
return err
}
if len(records) == 1 {
record := records[0]
content := make(map[string]any)
if err := record.UnmarshalJSONField("content", &content); err != nil {
return err
}
if provider, ok := content["provider"]; ok {
if providerStr, ok := provider.(string); ok {
if providerStr == "letsencrypt_staging" {
content["provider"] = "letsencryptstaging"
}
}
}
if config, ok := content["config"]; ok {
if configMap, ok := config.(map[string]any); ok {
if _, ok := configMap["letsencrypt_staging"]; ok {
configMap["letsencryptstaging"] = configMap["letsencrypt_staging"]
delete(configMap, "letsencrypt_staging")
}
if _, ok := configMap["gts"]; ok {
configMap["googletrustservices"] = configMap["gts"]
delete(configMap, "gts")
}
}
}
record.Set("content", content)
if err := app.Save(record); err != nil {
return err
}
}
}
// update collection `access`
{
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "hwy7m03o",
"maxSelect": 1,
"name": "provider",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"1panel",
"acmehttpreq",
"akamai",
"aliyun",
"aws",
"azure",
"baiducloud",
"baishan",
"baotapanel",
"byteplus",
"buypass",
"cachefly",
"cdnfly",
"cloudflare",
"cloudns",
"cmcccloud",
"ctcccloud",
"cucccloud",
"desec",
"dnsla",
"dogecloud",
"dynv6",
"edgio",
"fastly",
"gname",
"gcore",
"godaddy",
"goedge",
"googletrustservices",
"huaweicloud",
"jdcloud",
"k8s",
"letsencrypt",
"letsencryptstaging",
"local",
"namecheap",
"namedotcom",
"namesilo",
"ns1",
"porkbun",
"powerdns",
"qiniu",
"qingcloud",
"rainyun",
"safeline",
"ssh",
"sslcom",
"tencentcloud",
"ucloud",
"upyun",
"vercel",
"volcengine",
"webhook",
"westcn",
"zerossl"
]
}`)); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
}
// update collection `acme_accounts`
{
collection, err := app.FindCollectionByNameOrId("012d7abbod1hwvr")
if err != nil {
return err
}
records, err := app.FindRecordsByFilter(collection, "ca='letsencrypt_staging' || ca='gts'", "-created", 0, 0)
if err != nil {
return err
}
for _, record := range records {
ca := record.GetString("ca")
if ca == "letsencrypt_staging" {
record.Set("ca", "letsencryptstaging")
} else if ca == "gts" {
record.Set("ca", "googletrustservices")
} else {
continue
}
if err := app.Save(record); err != nil {
return err
}
}
}
return nil
}, func(app core.App) error {
return nil
})
}

View File

@@ -0,0 +1,91 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
// update field
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
"hidden": false,
"id": "hwy7m03o",
"maxSelect": 1,
"name": "provider",
"presentable": false,
"required": false,
"system": false,
"type": "select",
"values": [
"1panel",
"acmehttpreq",
"akamai",
"aliyun",
"aws",
"azure",
"baiducloud",
"baishan",
"baotapanel",
"byteplus",
"buypass",
"cachefly",
"cdnfly",
"cloudflare",
"cloudns",
"cmcccloud",
"ctcccloud",
"cucccloud",
"desec",
"dnsla",
"dogecloud",
"dynv6",
"edgio",
"fastly",
"gname",
"gcore",
"godaddy",
"goedge",
"googletrustservices",
"huaweicloud",
"jdcloud",
"k8s",
"letsencrypt",
"letsencryptstaging",
"local",
"namecheap",
"namedotcom",
"namesilo",
"ns1",
"porkbun",
"powerdns",
"qiniu",
"qingcloud",
"rainyun",
"safeline",
"ssh",
"sslcom",
"tencentcloud",
"ucloud",
"upyun",
"vercel",
"volcengine",
"wangsu",
"webhook",
"westcn",
"zerossl"
]
}`)); err != nil {
return err
}
return app.Save(collection)
}, func(app core.App) error {
return nil
})
}

View File

@@ -1 +0,0 @@
<svg width="200" height="200" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Color-" transform="translate(-401.000000, -860.000000)"><g id="Google" transform="translate(401.000000, 860.000000)"><path d="M9.82727273,24 C9.82727273,22.4757333 10.0804318,21.0144 10.5322727,19.6437333 L2.62345455,13.6042667 C1.08206818,16.7338667 0.213636364,20.2602667 0.213636364,24 C0.213636364,27.7365333 1.081,31.2608 2.62025,34.3882667 L10.5247955,28.3370667 C10.0772273,26.9728 9.82727273,25.5168 9.82727273,24" id="Fill-1" fill="#FBBC05"></path><path d="M23.7136364,10.1333333 C27.025,10.1333333 30.0159091,11.3066667 32.3659091,13.2266667 L39.2022727,6.4 C35.0363636,2.77333333 29.6954545,0.533333333 23.7136364,0.533333333 C14.4268636,0.533333333 6.44540909,5.84426667 2.62345455,13.6042667 L10.5322727,19.6437333 C12.3545909,14.112 17.5491591,10.1333333 23.7136364,10.1333333" id="Fill-2" fill="#EB4335"></path><path d="M23.7136364,37.8666667 C17.5491591,37.8666667 12.3545909,33.888 10.5322727,28.3562667 L2.62345455,34.3946667 C6.44540909,42.1557333 14.4268636,47.4666667 23.7136364,47.4666667 C29.4455,47.4666667 34.9177955,45.4314667 39.0249545,41.6181333 L31.5177727,35.8144 C29.3995682,37.1488 26.7323182,37.8666667 23.7136364,37.8666667" id="Fill-3" fill="#34A853"></path><path d="M46.1454545,24 C46.1454545,22.6133333 45.9318182,21.12 45.6113636,19.7333333 L23.7136364,19.7333333 L23.7136364,28.8 L36.3181818,28.8 C35.6879545,31.8912 33.9724545,34.2677333 31.5177727,35.8144 L39.0249545,41.6181333 C43.3393409,37.6138667 46.1454545,31.6490667 46.1454545,24" id="Fill-4" fill="#4285F4"></path></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M925.915429 521.545143c0.512-29.622857-2.56-59.282286-9.033143-88.100572H512v160.036572h237.714286a210.468571 210.468571 0 0 1-88.137143 139.885714l-0.804572 5.302857 127.963429 99.181715 8.850286 0.841142c81.298286-75.154286 128.365714-185.782857 128.365714-317.147428" fill="#4285F4"></path><path d="M512 943.177143c116.370286 0 214.198857-38.363429 285.622857-104.484572l-136.045714-105.472a254.829714 254.829714 0 0 1-149.430857 43.117715 259.949714 259.949714 0 0 1-245.394286-179.273143l-5.12 0.512-133.083429 102.912-1.682285 4.754286a429.860571 429.860571 0 0 0 385.097143 237.897142" fill="#34A853"></path><path d="M266.605714 597.211429a264.045714 264.045714 0 0 1-14.336-85.357715c0.182857-28.964571 4.937143-57.782857 13.970286-85.394285l-0.146286-5.632-134.802285-104.594286-4.425143 2.011428a430.262857 430.262857 0 0 0 0 387.181715l139.702857-108.214857" fill="#FBBC05"></path><path d="M512 247.515429a237.933714 237.933714 0 0 1 166.656 64.256l121.673143-118.784a414.646857 414.646857 0 0 0-288.512-112.128 430.811429 430.811429 0 0 0-385.316572 237.714285l139.410286 108.251429A260.973714 260.973714 0 0 1 512 247.478857" fill="#EB4335"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 72 72" fill="none"><circle cx="36" cy="36" r="35" stroke="#A0CC40" stroke-width="2"></circle><path d="M13.4924 16.2017H20.2845L18.8546 22.9937H12.0625L13.4924 16.2017Z" fill="#A0CC40"></path><path d="M19.8362 25.943H26.6282L25.1983 32.7351H18.4062L19.8362 25.943Z" fill="#A0CC40"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M21.9812 22.9613C21.8448 22.8247 22.8688 17.4717 23.1888 16.6485C23.3515 16.2297 24.34 16.2017 38.924 16.2017C52.2002 16.2017 54.5064 16.2555 54.6262 16.5678C54.7033 16.769 54.5273 18.1065 54.2349 19.5397C53.8001 21.6709 53.5664 22.5316 53.8871 22.8758C54.1437 23.1511 54.755 23.0959 55.902 23.0959C58.0542 23.0959 58.0979 23.1081 57.9587 23.6704C57.8806 23.9865 56.3682 31.0817 54.5978 39.4377C52.8278 47.7937 51.32 54.8314 51.2472 55.0773C51.1231 55.4958 50.2388 55.5242 37.3542 55.5242C29.7857 55.5242 23.5459 55.4381 23.4879 55.3327C23.4302 55.2275 24.902 48.0172 26.7589 39.31C28.6161 30.6029 30.0866 23.3926 30.027 23.2874C29.9675 23.1819 28.1632 23.0959 26.0174 23.0959C23.8716 23.0959 22.0553 23.0354 21.9812 22.9613ZM35.6711 44.6994C36.8749 45.2482 38.2735 45.5226 39.8669 45.5226C40.9646 45.5226 42.0445 45.3898 43.1067 45.1243C44.1867 44.8587 45.2224 44.3895 46.2138 43.7168L43.7175 40.0786C43.2572 40.415 42.7527 40.6717 42.2038 40.8487C41.655 41.0081 41.1062 41.0877 40.5574 41.0877C39.6191 41.0877 38.902 40.9019 38.4063 40.5301C38.0832 40.2668 37.8693 39.8861 37.7646 39.3882H47.9399C48.0285 38.9987 48.0993 38.5915 48.1524 38.1666C48.2232 37.7417 48.2586 37.3079 48.2586 36.8653C48.2586 35.6084 47.9665 34.5019 47.3823 33.5458C46.798 32.5898 45.9571 31.8374 44.8594 31.2886C43.7618 30.7398 42.434 30.4653 40.876 30.4653C39.0702 30.4653 37.4857 30.8283 36.1225 31.5541C34.777 32.28 33.7325 33.2714 32.9889 34.5284C32.2453 35.7854 31.8736 37.2106 31.8736 38.8039C31.8736 40.1671 32.2099 41.3533 32.8827 42.3624C33.5554 43.3716 34.4849 44.1505 35.6711 44.6994ZM38.0084 36.4936H42.7816C42.7785 36.2493 42.7364 36.028 42.6553 35.8297C42.5137 35.4225 42.2658 35.1126 41.9117 34.9002C41.5576 34.67 41.1239 34.555 40.6105 34.555C39.8846 34.555 39.3092 34.7497 38.8843 35.1392C38.4965 35.4948 38.2045 35.9462 38.0084 36.4936Z" fill="#A0CC40"></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" x="0" y="0" width="200" height="200" viewBox="0 0 340 100"><g><path d="M290,61.5c-4.1,4.6-29.3,36.7-56.9,26.7c-3.6-1.3-7.2-3.3-10.9-6.2c3.8,0.2,7.5,0,10.9-0.3 c5.3-0.5,9.8-1.5,12.4-2.9c6.1-3,4.5-13.2-12.4-16.2c-0.4-0.1-0.9-0.2-1.3-0.2c-11.3-1.7-19.5,1.7-23.7,5.3 c-0.1,0-0.3-0.1-0.4-0.2c-2.6-1.1-5.2-2.2-7.8-3.2c20-0.9,23.4-14.1,4.8-26.4c-7.5-5-16.3-9.4-25.6-13.4 c-10.7-4.5-22.3-8.5-33.7-11.7c-7.9-2.2-15.7-4.1-23.1-5.7C109.9,4.6,98.7,2.8,90,2C71.1,0.3,58.1,1.5,50.8,5.8 C47.7,7.6,46,10,46,13v0.3c0,1.6,0.5,3.3,1.4,5.2c3,6.3,11.1,13.5,24.1,21.5c11.5,7.1,28.8,15.2,43.5,20.3 c0.1,0.8,0.2,1.6,0.6,2.6c0.9,2.4,3.1,5.2,6.7,8.3c0.5,0.5,1.1,1,1.8,1.5c5.7,4.5,12.7,8.7,21.1,12.2l0.2,0.1 c10.9,4.6,22.6,8.2,33.7,10.8c16.7,3.8,32.2,5.4,42.6,4.9c0.5-0.2,0.7-0.4,0.4-0.6c-0.1-0.1-0.2-0.2-0.4-0.2 c-5.7-0.4-12.9-1.2-21.7-2.3c-7.3-1-14.3-2.3-21-3.8c-12.1-2.7-23.1-6.2-33-10.5c-0.2-0.1-0.4-0.2-0.7-0.3 c-6.7-2.9-10.5-5.7-11.4-8.2c-1.2-3.4,1.9-5.2,5-6c2,0.7,4.1,1.3,6.4,1.9c10.2,2.9,22.2,5.5,33.7,7.4c11.1,1.9,21.7,3.2,29.5,3.6 c7.6,6.7,16.1,10.2,24.6,11.3c15.2,1.8,30.4-4.1,41.1-13.8c7.3-6.6,13.2-13,16.3-17L290,61.5z M145.3,30.3 c8.5,0.3,17.4,1.3,26.7,3.1c2.5,0.5,4.8,1,7,1.5c6.7,1.6,11.5,3.3,14.5,5.2c3.4,2.1,4.1,3.7,2.1,4.9c-1.5,0.9-4.6,1.5-9.4,2 c-2.4,0.2-4.8,0.4-7.2,0.4c-2.4,0.1-4.9,0-7.4,0c-4.9-0.2-9.8-0.9-14.8-1.8c-4.9-1-8.3-2-10-3c-1.3-0.8-1.6-1.4-1-1.9 c0.4-0.2,1.1-0.4,2.3-0.5c0.9-0.1,1.5-0.4,2.1-0.7c1.1-0.7,1.4-1.6,0.8-2.9c-0.7-1.3-2-2.6-4-3.8c-0.5-0.3-1.1-0.6-1.7-1 c-0.7-0.4-1.6-0.8-2.6-1.3C143.6,30.3,144.4,30.3,145.3,30.3z M145.3,51c-6-0.5-11.4-0.5-16.2,0.1c-2.7,0.3-5,0.7-6.9,1.3 c-1.3,0.4-2.5,0.9-3.4,1.4c-0.3-0.1-0.6-0.2-1-0.4c-12.8-4.5-26.4-14.1-19.6-18.1c4.8-2.8,12.8-4.4,24-4.8c2-0.1,4.1-0.1,6.3-0.1 c-1.7,5.1,2.7,10.8,12.9,17.1c1.3,0.8,2.6,1.6,3.8,2.3c0.9,0.5,1.7,1,2.6,1.4C147,51.1,146.1,51.1,145.3,51z M206.7,79.4 c-8.5-0.8-18.3-2.9-27.7-5.2c-10.6-2.7-20.7-5.7-27.7-7.9c6.4-1.5,14.8-2.2,25.6-2c0.7,0,1.4,0,2.1,0c10.8,0.4,20,1.9,27.8,4.6 C204.6,71.5,203.8,74.9,206.7,79.4z M233.1,66.9c0.4,0,0.7,0,1.1,0.1c3.6,0.4,6.7,0.9,9.8,3.1c3.5,2.4,3.5,4.4,2.1,6.5 c-1.3,2.1-7.1,2.9-13,3.2c-4.6,0.2-9.3,0.1-12.2,0C216.1,74.7,221.9,66.4,233.1,66.9z" fill="#005BAC"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,118 @@
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks";
import { Button, Drawer, Space, notification } from "antd";
import { type AccessModel } from "@/domain/access";
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
import { getErrMsg } from "@/utils/error";
import AccessForm, { type AccessFormInstance, type AccessFormProps } from "./AccessForm";
export type AccessEditDrawerProps = {
data?: AccessFormProps["initialValues"];
loading?: boolean;
open?: boolean;
range?: AccessFormProps["range"];
scene: AccessFormProps["scene"];
trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void;
};
const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditDrawerProps) => {
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const { createAccess, updateAccess } = useAccessesStore(useZustandShallowSelector(["createAccess", "updateAccess"]));
const [open, setOpen] = useControllableValue<boolean>(props, {
valuePropName: "open",
defaultValuePropName: "defaultOpen",
trigger: "onOpenChange",
});
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
const formRef = useRef<AccessFormInstance>(null);
const [formPending, setFormPending] = useState(false);
const handleOkClick = async () => {
setFormPending(true);
try {
await formRef.current!.validateFields();
} catch (err) {
setFormPending(false);
throw err;
}
try {
let values: AccessModel = formRef.current!.getFieldsValue();
if (scene === "add") {
if (data?.id) {
throw "Invalid props: `data`";
}
values = await createAccess(values);
} else if (scene === "edit") {
if (!data?.id) {
throw "Invalid props: `data`";
}
values = await updateAccess({ ...data, ...values });
} else {
throw "Invalid props: `scene`";
}
afterSubmit?.(values);
setOpen(false);
} catch (err) {
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
throw err;
} finally {
setFormPending(false);
}
};
const handleCancelClick = () => {
if (formPending) return;
setOpen(false);
};
return (
<>
{NotificationContextHolder}
{triggerEl}
<Drawer
afterOpenChange={setOpen}
closable={!formPending}
destroyOnClose
footer={
<Space className="w-full justify-end">
<Button onClick={handleCancelClick}>{t("common.button.cancel")}</Button>
<Button loading={formPending} type="primary" onClick={handleOkClick}>
{scene === "edit" ? t("common.button.save") : t("common.button.submit")}
</Button>
</Space>
}
loading={loading}
maskClosable={!formPending}
open={open}
title={t(`access.action.${scene}`)}
width={720}
onClose={() => setOpen(false)}
>
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
</Drawer>
</>
);
};
export default AccessEditDrawer;

View File

@@ -14,13 +14,14 @@ export type AccessEditModalProps = {
data?: AccessFormProps["initialValues"]; data?: AccessFormProps["initialValues"];
loading?: boolean; loading?: boolean;
open?: boolean; open?: boolean;
preset: AccessFormProps["preset"]; range?: AccessFormProps["range"];
scene: AccessFormProps["scene"];
trigger?: React.ReactNode; trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void; afterSubmit?: (record: AccessModel) => void;
}; };
const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props }: AccessEditModalProps) => { const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification(); const [notificationApi, NotificationContextHolder] = notification.useNotification();
@@ -50,13 +51,13 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
try { try {
let values: AccessModel = formRef.current!.getFieldsValue(); let values: AccessModel = formRef.current!.getFieldsValue();
if (preset === "add") { if (scene === "add") {
if (data?.id) { if (data?.id) {
throw "Invalid props: `data`"; throw "Invalid props: `data`";
} }
values = await createAccess(values); values = await createAccess(values);
} else if (preset === "edit") { } else if (scene === "edit") {
if (!data?.id) { if (!data?.id) {
throw "Invalid props: `data`"; throw "Invalid props: `data`";
} }
@@ -96,15 +97,15 @@ const AccessEditModal = ({ data, loading, trigger, preset, afterSubmit, ...props
confirmLoading={formPending} confirmLoading={formPending}
destroyOnClose destroyOnClose
loading={loading} loading={loading}
okText={preset === "edit" ? t("common.button.save") : t("common.button.submit")} okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")}
open={open} open={open}
title={t(`access.action.${preset}`)} title={t(`access.action.${scene}`)}
width={480} width={480}
onOk={handleOkClick} onOk={handleOkClick}
onCancel={handleCancelClick} onCancel={handleCancelClick}
> >
<div className="pb-2 pt-4"> <div className="pb-2 pt-4">
<AccessForm ref={formRef} initialValues={data} preset={preset === "add" ? "add" : "edit"} /> <AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
</div> </div>
</Modal> </Modal>
</> </>

View File

@@ -6,7 +6,7 @@ import { z } from "zod";
import AccessProviderSelect from "@/components/provider/AccessProviderSelect"; import AccessProviderSelect from "@/components/provider/AccessProviderSelect";
import { type AccessModel } from "@/domain/access"; import { type AccessModel } from "@/domain/access";
import { ACCESS_PROVIDERS } from "@/domain/provider"; import { ACCESS_PROVIDERS, ACCESS_USAGES } from "@/domain/provider";
import { useAntdForm, useAntdFormName } from "@/hooks"; import { useAntdForm, useAntdFormName } from "@/hooks";
import AccessForm1PanelConfig from "./AccessForm1PanelConfig"; import AccessForm1PanelConfig from "./AccessForm1PanelConfig";
@@ -31,10 +31,10 @@ import AccessFormEdgioConfig from "./AccessFormEdgioConfig";
import AccessFormGcoreConfig from "./AccessFormGcoreConfig"; import AccessFormGcoreConfig from "./AccessFormGcoreConfig";
import AccessFormGnameConfig from "./AccessFormGnameConfig"; import AccessFormGnameConfig from "./AccessFormGnameConfig";
import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig"; import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig";
import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServicesConfig";
import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig";
import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig";
import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig";
import AccessFormLocalConfig from "./AccessFormLocalConfig";
import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig"; import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig";
import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig"; import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig";
import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig"; import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig";
@@ -45,23 +45,28 @@ import AccessFormQiniuConfig from "./AccessFormQiniuConfig";
import AccessFormRainYunConfig from "./AccessFormRainYunConfig"; import AccessFormRainYunConfig from "./AccessFormRainYunConfig";
import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig"; import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig";
import AccessFormSSHConfig from "./AccessFormSSHConfig"; import AccessFormSSHConfig from "./AccessFormSSHConfig";
import AccessFormSSLComConfig from "./AccessFormSSLComConfig";
import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig"; import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig";
import AccessFormUCloudConfig from "./AccessFormUCloudConfig"; import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
import AccessFormUpyunConfig from "./AccessFormUpyunConfig"; import AccessFormUpyunConfig from "./AccessFormUpyunConfig";
import AccessFormVercelConfig from "./AccessFormVercelConfig"; import AccessFormVercelConfig from "./AccessFormVercelConfig";
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig"; import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
import AccessFormWangsuConfig from "./AccessFormWangsuConfig";
import AccessFormWebhookConfig from "./AccessFormWebhookConfig"; import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
import AccessFormWestcnConfig from "./AccessFormWestcnConfig"; import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>; type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
type AccessFormPresets = "add" | "edit"; type AccessFormRanges = "both-dns-hosting" | "ca-only" | "notify-only";
type AccessFormScenes = "add" | "edit";
export type AccessFormProps = { export type AccessFormProps = {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
disabled?: boolean; disabled?: boolean;
initialValues?: AccessFormFieldValues; initialValues?: AccessFormFieldValues;
preset: AccessFormPresets; range?: AccessFormRanges;
scene: AccessFormScenes;
onValuesChange?: (values: AccessFormFieldValues) => void; onValuesChange?: (values: AccessFormFieldValues) => void;
}; };
@@ -71,7 +76,7 @@ export type AccessFormInstance = {
validateFields: FormInstance<AccessFormFieldValues>["validateFields"]; validateFields: FormInstance<AccessFormFieldValues>["validateFields"];
}; };
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, preset, onValuesChange }, ref) => { const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, range, scene, onValuesChange }, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
const formSchema = z.object({ const formSchema = z.object({
@@ -80,7 +85,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
.min(1, t("access.form.name.placeholder")) .min(1, t("access.form.name.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 })) .max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(), .trim(),
provider: z.nativeEnum(ACCESS_PROVIDERS, { message: t("access.form.provider.placeholder") }), provider: z.nativeEnum(ACCESS_PROVIDERS, {
message:
range === "ca-only"
? t("access.form.certificate_authority.placeholder")
: range === "notify-only"
? t("access.form.notification_channel.placeholder")
: t("access.form.provider.placeholder"),
}),
config: z.any(), config: z.any(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
@@ -88,6 +100,35 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
initialValues: initialValues, initialValues: initialValues,
}); });
const providerLabel = useMemo(() => {
switch (range) {
case "ca-only":
return t("access.form.certificate_authority.label");
case "notify-only":
return t("access.form.notification_channel.label");
}
return t("access.form.provider.label");
}, [range]);
const providerPlaceholder = useMemo(() => {
switch (range) {
case "ca-only":
return t("access.form.certificate_authority.placeholder");
case "notify-only":
return t("access.form.notification_channel.placeholder");
}
return t("access.form.provider.placeholder");
}, [range]);
const providerTooltip = useMemo(() => {
switch (range) {
case "both-dns-hosting":
return <span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>;
}
return undefined;
}, [range]);
const fieldProvider = Form.useWatch("provider", formInst); const fieldProvider = Form.useWatch("provider", formInst);
const [nestedFormInst] = Form.useForm(); const [nestedFormInst] = Form.useForm();
@@ -147,6 +188,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormGnameConfig {...nestedFormProps} />; return <AccessFormGnameConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.GODADDY: case ACCESS_PROVIDERS.GODADDY:
return <AccessFormGoDaddyConfig {...nestedFormProps} />; return <AccessFormGoDaddyConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.GOOGLETRUSTSERVICES:
return <AccessFormGoogleTrustServicesConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.EDGIO: case ACCESS_PROVIDERS.EDGIO:
return <AccessFormEdgioConfig {...nestedFormProps} />; return <AccessFormEdgioConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.HUAWEICLOUD: case ACCESS_PROVIDERS.HUAWEICLOUD:
@@ -155,8 +198,6 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormJDCloudConfig {...nestedFormProps} />; return <AccessFormJDCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.KUBERNETES: case ACCESS_PROVIDERS.KUBERNETES:
return <AccessFormKubernetesConfig {...nestedFormProps} />; return <AccessFormKubernetesConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.LOCAL:
return <AccessFormLocalConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMECHEAP: case ACCESS_PROVIDERS.NAMECHEAP:
return <AccessFormNamecheapConfig {...nestedFormProps} />; return <AccessFormNamecheapConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMEDOTCOM: case ACCESS_PROVIDERS.NAMEDOTCOM:
@@ -177,6 +218,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormSafeLineConfig {...nestedFormProps} />; return <AccessFormSafeLineConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.SSH: case ACCESS_PROVIDERS.SSH:
return <AccessFormSSHConfig {...nestedFormProps} />; return <AccessFormSSHConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.SSLCOM:
return <AccessFormSSLComConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.TENCENTCLOUD: case ACCESS_PROVIDERS.TENCENTCLOUD:
return <AccessFormTencentCloudConfig {...nestedFormProps} />; return <AccessFormTencentCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.UCLOUD: case ACCESS_PROVIDERS.UCLOUD:
@@ -187,10 +230,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormVercelConfig {...nestedFormProps} />; return <AccessFormVercelConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.VOLCENGINE: case ACCESS_PROVIDERS.VOLCENGINE:
return <AccessFormVolcEngineConfig {...nestedFormProps} />; return <AccessFormVolcEngineConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WANGSU:
return <AccessFormWangsuConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WEBHOOK: case ACCESS_PROVIDERS.WEBHOOK:
return <AccessFormWebhookConfig {...nestedFormProps} />; return <AccessFormWebhookConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WESTCN: case ACCESS_PROVIDERS.WESTCN:
return <AccessFormWestcnConfig {...nestedFormProps} />; return <AccessFormWestcnConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.ZEROSSL:
return <AccessFormZeroSSLConfig {...nestedFormProps} />;
} }
}, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]); }, [disabled, initialValues?.config, fieldProvider, nestedFormInst, nestedFormName]);
@@ -235,13 +282,25 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
<Input placeholder={t("access.form.name.placeholder")} /> <Input placeholder={t("access.form.name.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}>
name="provider" <AccessProviderSelect
label={t("access.form.provider.label")} filter={(record) => {
rules={[formRule]} if (range == null) return true;
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>}
> switch (range) {
<AccessProviderSelect disabled={preset !== "add"} placeholder={t("access.form.provider.placeholder")} showSearch={!disabled} /> case "both-dns-hosting":
return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
case "ca-only":
return record.usages.includes(ACCESS_USAGES.CA);
case "notify-only":
return record.usages.includes(ACCESS_USAGES.NOTIFICATION);
}
}}
disabled={scene !== "add"}
placeholder={providerPlaceholder}
showOptionTags={range == null || (range === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
showSearch={!disabled}
/>
</Form.Item> </Form.Item>
</Form> </Form>

View File

@@ -30,6 +30,11 @@ const AccessFormCloudflareConfig = ({ form: formInst, formName, disabled, initia
.min(1, t("access.form.cloudflare_dns_api_token.placeholder")) .min(1, t("access.form.cloudflare_dns_api_token.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 })) .max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(), .trim(),
zoneApiToken: z
.string()
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim()
.nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
@@ -54,6 +59,15 @@ const AccessFormCloudflareConfig = ({ form: formInst, formName, disabled, initia
> >
<Input.Password autoComplete="new-password" placeholder={t("access.form.cloudflare_dns_api_token.placeholder")} /> <Input.Password autoComplete="new-password" placeholder={t("access.form.cloudflare_dns_api_token.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item
name="zoneApiToken"
label={t("access.form.cloudflare_zone_api_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.cloudflare_zone_api_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.cloudflare_zone_api_token.placeholder")} />
</Form.Item>
</Form> </Form>
); );
}; };

View File

@@ -0,0 +1,82 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForGoogleTrustServices } from "@/domain/access";
type AccessFormGoogleTrustServicesConfigFieldValues = Nullish<AccessConfigForGoogleTrustServices>;
export type AccessFormGoogleTrustServicesConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormGoogleTrustServicesConfigFieldValues;
onValuesChange?: (values: AccessFormGoogleTrustServicesConfigFieldValues) => void;
};
const initFormModel = (): AccessFormGoogleTrustServicesConfigFieldValues => {
return {
eabKid: "",
eabHmacKey: "",
};
};
const AccessFormGoogleTrustServicesConfig = ({
form: formInst,
formName,
disabled,
initialValues,
onValuesChange,
}: AccessFormGoogleTrustServicesConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
eabKid: z
.string()
.min(1, t("access.form.googletrustservices_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
eabHmacKey: z
.string()
.min(1, t("access.form.googletrustservices_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="eabKid"
label={t("access.form.googletrustservices_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("access.form.googletrustservices_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.googletrustservices_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.googletrustservices_eab_hmac_key.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormGoogleTrustServicesConfig;

View File

@@ -1,36 +0,0 @@
import { Form, type FormInstance } from "antd";
import { type AccessConfigForLocal } from "@/domain/access";
type AccessFormLocalConfigFieldValues = Nullish<AccessConfigForLocal>;
export type AccessFormLocalConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormLocalConfigFieldValues;
onValuesChange?: (values: AccessFormLocalConfigFieldValues) => void;
};
const initFormModel = (): AccessFormLocalConfigFieldValues => {
return {};
};
const AccessFormLocalConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormLocalConfigProps) => {
const handleFormChange = (_: unknown, values: any) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
></Form>
);
};
export default AccessFormLocalConfig;

View File

@@ -7,7 +7,7 @@ import { z } from "zod";
import { type AccessConfigForSSH } from "@/domain/access"; import { type AccessConfigForSSH } from "@/domain/access";
import { readFileContent } from "@/utils/file"; import { readFileContent } from "@/utils/file";
import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/validators"; import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
type AccessFormSSHConfigFieldValues = Nullish<AccessConfigForSSH>; type AccessFormSSHConfigFieldValues = Nullish<AccessConfigForSSH>;
@@ -34,11 +34,13 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
host: z host: z
.string({ message: t("access.form.ssh_host.placeholder") }) .string({ message: t("access.form.ssh_host.placeholder") })
.refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")), .refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
port: z port: z.preprocess(
.number({ message: t("access.form.ssh_port.placeholder") }) (v) => Number(v),
.int() z
.gte(1, t("common.errmsg.port_invalid")) .number({ message: t("access.form.ssh_port.placeholder") })
.lte(65535, t("common.errmsg.port_invalid")), .int(t("access.form.ssh_port.placeholder"))
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),
username: z username: z
.string() .string()
.min(1, "access.form.ssh_username.placeholder") .min(1, "access.form.ssh_username.placeholder")

View File

@@ -0,0 +1,76 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForSSLCom } from "@/domain/access";
type AccessFormSSLComConfigFieldValues = Nullish<AccessConfigForSSLCom>;
export type AccessFormSSLComConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormSSLComConfigFieldValues;
onValuesChange?: (values: AccessFormSSLComConfigFieldValues) => void;
};
const initFormModel = (): AccessFormSSLComConfigFieldValues => {
return {
eabKid: "",
eabHmacKey: "",
};
};
const AccessFormSSLComConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormSSLComConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
eabKid: z
.string()
.min(1, t("access.form.sslcom_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
eabHmacKey: z
.string()
.min(1, t("access.form.sslcom_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="eabKid"
label={t("access.form.sslcom_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.sslcom_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("access.form.sslcom_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.sslcom_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.sslcom_eab_hmac_key.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormSSLComConfig;

View File

@@ -0,0 +1,76 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForWangsu } from "@/domain/access";
type AccessFormWangsuConfigFieldValues = Nullish<AccessConfigForWangsu>;
export type AccessFormWangsuConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormWangsuConfigFieldValues;
onValuesChange?: (values: AccessFormWangsuConfigFieldValues) => void;
};
const initFormModel = (): AccessFormWangsuConfigFieldValues => {
return {
accessKeyId: "",
accessKeySecret: "",
};
};
const AccessFormWangsuConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormWangsuConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
accessKeyId: z
.string()
.min(1, t("access.form.wangsu_access_key_id.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
accessKeySecret: z
.string()
.min(1, t("access.form.wangsu_access_key_secret.placeholder"))
.max(64, t("common.errmsg.string_max", { max: 64 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="accessKeyId"
label={t("access.form.wangsu_access_key_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.wangsu_access_key_id.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.wangsu_access_key_id.placeholder")} />
</Form.Item>
<Form.Item
name="accessKeySecret"
label={t("access.form.wangsu_access_key_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.wangsu_access_key_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.wangsu_access_key_secret.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormWangsuConfig;

View File

@@ -0,0 +1,76 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForZeroSSL } from "@/domain/access";
type AccessFormZeroSSLConfigFieldValues = Nullish<AccessConfigForZeroSSL>;
export type AccessFormZeroSSLConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormZeroSSLConfigFieldValues;
onValuesChange?: (values: AccessFormZeroSSLConfigFieldValues) => void;
};
const initFormModel = (): AccessFormZeroSSLConfigFieldValues => {
return {
eabKid: "",
eabHmacKey: "",
};
};
const AccessFormZeroSSLConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormZeroSSLConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
eabKid: z
.string()
.min(1, t("access.form.zerossl_eab_kid.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
eabHmacKey: z
.string()
.min(1, t("access.form.zerossl_eab_hmac_key.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 }))
.trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="eabKid"
label={t("access.form.zerossl_eab_kid.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_kid.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.zerossl_eab_kid.placeholder")} />
</Form.Item>
<Form.Item
name="eabHmacKey"
label={t("access.form.zerossl_eab_hmac_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.zerossl_eab_hmac_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.zerossl_eab_hmac_key.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormZeroSSLConfig;

View File

@@ -29,7 +29,6 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica
<Drawer <Drawer
afterOpenChange={setOpen} afterOpenChange={setOpen}
closable
destroyOnClose destroyOnClose
open={open} open={open}
loading={loading} loading={loading}

View File

@@ -7,7 +7,11 @@ import { useAntdForm } from "@/hooks";
import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields";
import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields";
import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields";
import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx";
import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields";
import NotifyChannelEditFormMattermostFields from "./NotifyChannelEditFormMattermostFields.tsx";
import NotifyChannelEditFormPushoverFields from "./NotifyChannelEditFormPushoverFields";
import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields";
import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields";
import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields";
import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields"; import NotifyChannelEditFormWebhookFields from "./NotifyChannelEditFormWebhookFields";
@@ -48,8 +52,16 @@ const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyCh
return <NotifyChannelEditFormDingTalkFields />; return <NotifyChannelEditFormDingTalkFields />;
case NOTIFY_CHANNELS.EMAIL: case NOTIFY_CHANNELS.EMAIL:
return <NotifyChannelEditFormEmailFields />; return <NotifyChannelEditFormEmailFields />;
case NOTIFY_CHANNELS.GOTIFY:
return <NotifyChannelEditFormGotifyFields />;
case NOTIFY_CHANNELS.LARK: case NOTIFY_CHANNELS.LARK:
return <NotifyChannelEditFormLarkFields />; return <NotifyChannelEditFormLarkFields />;
case NOTIFY_CHANNELS.MATTERMOST:
return <NotifyChannelEditFormMattermostFields />;
case NOTIFY_CHANNELS.PUSHOVER:
return <NotifyChannelEditFormPushoverFields />;
case NOTIFY_CHANNELS.PUSHPLUS:
return <NotifyChannelEditFormPushPlusFields />;
case NOTIFY_CHANNELS.SERVERCHAN: case NOTIFY_CHANNELS.SERVERCHAN:
return <NotifyChannelEditFormServerChanFields />; return <NotifyChannelEditFormServerChanFields />;
case NOTIFY_CHANNELS.TELEGRAM: case NOTIFY_CHANNELS.TELEGRAM:

View File

@@ -3,6 +3,8 @@ import { Form, Input, InputNumber, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod"; import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod"; import { z } from "zod";
import { validPortNumber } from "@/utils/validators";
const NotifyChannelEditFormEmailFields = () => { const NotifyChannelEditFormEmailFields = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -11,11 +13,13 @@ const NotifyChannelEditFormEmailFields = () => {
.string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") }) .string({ message: t("settings.notification.channel.form.email_smtp_host.placeholder") })
.min(1, t("settings.notification.channel.form.email_smtp_host.placeholder")) .min(1, t("settings.notification.channel.form.email_smtp_host.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })), .max(256, t("common.errmsg.string_max", { max: 256 })),
smtpPort: z smtpPort: z.preprocess(
.number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") }) (v) => Number(v),
.int() z
.gte(1, t("common.errmsg.port_invalid")) .number({ message: t("settings.notification.channel.form.email_smtp_port.placeholder") })
.lte(65535, t("common.errmsg.port_invalid")), .int(t("settings.notification.channel.form.email_smtp_port.placeholder"))
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),
smtpTLS: z.boolean().nullish(), smtpTLS: z.boolean().nullish(),
username: z username: z
.string({ message: t("settings.notification.channel.form.email_username.placeholder") }) .string({ message: t("settings.notification.channel.form.email_username.placeholder") })

View File

@@ -0,0 +1,46 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormGotifyFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
url: z.string({ message: t("settings.notification.channel.form.gotify_url.placeholder") }).url({ message: t("common.errmsg.url_invalid") }),
token: z.string({ message: t("settings.notification.channel.form.gotify_token.placeholder") }),
priority: z.preprocess(val => Number(val), z.number({ message: t("settings.notification.channel.form.gotify_priority.placeholder") }).gte(0, { message: t("settings.notification.channel.form.gotify_priority.error.gte0") })),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="url"
label={t("settings.notification.channel.form.gotify_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.gotify_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.gotify_url.placeholder")} />
</Form.Item>
<Form.Item
name="token"
label={t("settings.notification.channel.form.gotify_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.gotify_token.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.gotify_token.placeholder")} />
</Form.Item>
<Form.Item
name="priority"
label={t("settings.notification.channel.form.gotify_priority.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.gotify_priority.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.gotify_priority.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormGotifyFields;

View File

@@ -0,0 +1,64 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormMattermostFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
serverUrl: z
.string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") })
.url(t("common.errmsg.url_invalid")),
channelId: z
.string({ message: t("settings.notification.channel.form.mattermost_channel_id.placeholder") })
.nonempty(t("settings.notification.channel.form.mattermost_channel_id.placeholder")),
username: z
.string({ message: t("settings.notification.channel.form.mattermost_username.placeholder") })
.nonempty(t("settings.notification.channel.form.mattermost_username.placeholder")),
password: z
.string({ message: t("settings.notification.channel.form.mattermost_password.placeholder") })
.nonempty(t("settings.notification.channel.form.mattermost_password.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="serverUrl"
label={t("settings.notification.channel.form.mattermost_server_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.mattermost_server_url.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.mattermost_server_url.placeholder")} />
</Form.Item>
<Form.Item
name="channelId"
label={t("settings.notification.channel.form.mattermost_channel_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.mattermost_channel_id.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.mattermost_channel_id.placeholder")} />
</Form.Item>
<Form.Item
name="username"
label={t("settings.notification.channel.form.mattermost_username.label")}
rules={[formRule]}
>
<Input placeholder={t("settings.notification.channel.form.mattermost_username.placeholder")} />
</Form.Item>
<Form.Item
name="password"
label={t("settings.notification.channel.form.mattermost_password.label")}
rules={[formRule]}
>
<Input.Password placeholder={t("settings.notification.channel.form.mattermost_password.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormMattermostFields;

View File

@@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormPushPlusFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
token: z.string({ message: t("settings.notification.channel.form.pushplus_token.placeholder") }),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="token"
label={t("settings.notification.channel.form.pushplus_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushplus_token.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.pushplus_token.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormPushPlusFields;

View File

@@ -0,0 +1,41 @@
import { useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
const NotifyChannelEditFormPushoverFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
token: z
.string({ message: t("settings.notification.channel.form.pushover_token.placeholder") })
.nonempty(t("settings.notification.channel.form.pushover_token.placeholder")),
user: z
.string({ message: t("settings.notification.channel.form.pushover_user.placeholder") })
.nonempty(t("settings.notification.channel.form.pushover_user.placeholder")),
});
const formRule = createSchemaFieldRule(formSchema);
return (
<>
<Form.Item
name="token"
label={t("settings.notification.channel.form.pushover_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushover_token.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.pushover_token.placeholder")} />
</Form.Item>
<Form.Item
name="user"
label={t("settings.notification.channel.form.pushover_user.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("settings.notification.channel.form.pushover_user.tooltip") }}></span>}
>
<Input placeholder={t("settings.notification.channel.form.pushover_user.placeholder")} />
</Form.Item>
</>
);
};
export default NotifyChannelEditFormPushoverFields;

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