diff --git a/go.mod b/go.mod
index 23063186d..0140ddc87 100644
--- a/go.mod
+++ b/go.mod
@@ -35,6 +35,7 @@ require (
github.com/blinkbean/dingtalk v1.1.3
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.56
github.com/go-acme/lego/v4 v4.25.2
+ github.com/go-cmd/cmd v1.4.3
github.com/go-lark/lark v1.16.0
github.com/go-resty/resty/v2 v2.16.5
github.com/go-viper/mapstructure/v2 v2.4.0
@@ -65,6 +66,7 @@ require (
github.com/volcengine/ve-tos-golang-sdk/v2 v2.7.21
github.com/volcengine/volc-sdk-golang v1.0.219
github.com/volcengine/volcengine-go-sdk v1.1.30
+ github.com/xhit/go-str2duration/v2 v2.1.0
gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1
gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0
golang.org/x/crypto v0.41.0
@@ -97,7 +99,6 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-acme/alidns-20150109/v4 v4.5.10 // indirect
github.com/go-acme/tencentclouddnspod v1.0.1208 // indirect
- github.com/go-cmd/cmd v1.4.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
@@ -127,6 +128,7 @@ require (
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
github.com/nrdcg/desec v0.11.0 // indirect
+ github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
@@ -206,7 +208,7 @@ require (
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cast v1.9.2 // indirect
- github.com/spf13/cobra v1.9.1 // indirect
+ github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.9
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/image v0.29.0 // indirect
@@ -217,7 +219,7 @@ require (
golang.org/x/sys v0.35.0 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
- golang.org/x/time v0.12.0
+ golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index e925f0e71..74227562f 100644
--- a/go.sum
+++ b/go.sum
@@ -404,9 +404,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
-github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
-github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
@@ -691,6 +690,8 @@ github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea h1:OSgRS4kqOs/WuxuF
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea/go.mod h1:IDRRngAngb2eTEaWgpO0hukQFI/vJId46fT1KErMytA=
github.com/nrdcg/desec v0.11.0 h1:XZVHy07sg12y8FozMp+l7XvzPsdzog0AYXuQMaHBsfs=
github.com/nrdcg/desec v0.11.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs=
+github.com/nrdcg/goacmedns v0.2.0 h1:ADMbThobzEMnr6kg2ohs4KGa3LFqmgiBA22/6jUWJR0=
+github.com/nrdcg/goacmedns v0.2.0/go.mod h1:T5o6+xvSLrQpugmwHvrSNkzWht0UGAwj2ACBMhh73Cg=
github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CFk=
github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
@@ -893,6 +894,8 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
+github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
diff --git a/internal/certapply/applicators/sp_acmedns.go b/internal/certapply/applicators/sp_acmedns.go
new file mode 100644
index 000000000..e0814fc9d
--- /dev/null
+++ b/internal/certapply/applicators/sp_acmedns.go
@@ -0,0 +1,29 @@
+package applicators
+
+import (
+ "fmt"
+
+ "github.com/go-acme/lego/v4/challenge"
+
+ "github.com/certimate-go/certimate/internal/domain"
+ "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/acmedns"
+ xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
+)
+
+func init() {
+ if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeACMEDNS, func(options *ProviderFactoryOptions) (challenge.Provider, error) {
+ credentials := domain.AccessConfigForACMEDNS{}
+ if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ provider, err := acmedns.NewChallengeProvider(&acmedns.ChallengeProviderConfig{
+ ApiBase: credentials.ApiBase,
+ StorageBaseUrl: credentials.StorageBaseUrl,
+ StoragePath: credentials.StoragePath,
+ })
+ return provider, err
+ }); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/certapply/client_certifier.go b/internal/certapply/client_certifier.go
index e547aa5fb..72a601042 100644
--- a/internal/certapply/client_certifier.go
+++ b/internal/certapply/client_certifier.go
@@ -16,14 +16,16 @@ import (
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/log"
"github.com/samber/lo"
+ "github.com/xhit/go-str2duration/v2"
"github.com/certimate-go/certimate/internal/certapply/applicators"
"github.com/certimate-go/certimate/internal/domain"
)
type ObtainCertificateRequest struct {
- Domains []string
- KeyType certcrypto.KeyType
+ Domains []string
+ KeyType certcrypto.KeyType
+ LifeTime string
// 提供商相关
ChallengeType string
@@ -155,6 +157,13 @@ func (c *ACMEClient) ObtainCertificate(request *ObtainCertificateRequest) (*Obta
Profile: request.ACMEProfile,
ReplacesCertID: lo.If(request.ARIReplacesAcctUrl == c.account.ACMEAcctUrl, request.ARIReplacesCertId).Else(""),
}
+ if request.LifeTime != "" {
+ lifeTime, err := str2duration.ParseDuration(request.LifeTime)
+ if err != nil {
+ return nil, fmt.Errorf("invalid lifetime: %w", err)
+ }
+ req.NotAfter = time.Now().Add(lifeTime)
+ }
resp, err := c.client.Certificate.Obtain(req)
if err != nil {
ariErr := &acme.AlreadyReplacedError{}
diff --git a/internal/domain/access.go b/internal/domain/access.go
index de11b2afa..4debd7f8a 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -32,6 +32,12 @@ type AccessConfigForACMECA struct {
Endpoint string `json:"endpoint"`
}
+type AccessConfigForACMEDNS struct {
+ ApiBase string `json:"apiBase"`
+ StorageBaseUrl string `json:"storageBaseUrl,omitempty"`
+ StoragePath string `json:"storagePath,omitempty"`
+}
+
type AccessConfigForACMEHttpReq struct {
Endpoint string `json:"endpoint"`
Mode string `json:"mode,omitempty"`
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 4da89030e..c7f32c2c9 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -11,6 +11,7 @@ NOTICE: If you add new constant, please keep ASCII order.
const (
AccessProviderType1Panel = AccessProviderType("1panel")
AccessProviderTypeACMECA = AccessProviderType("acmeca")
+ AccessProviderTypeACMEDNS = AccessProviderType("acmedns")
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
AccessProviderTypeAliyun = AccessProviderType("aliyun")
@@ -121,6 +122,7 @@ ACME DNS-01 提供商常量值。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
+ ACMEDns01ProviderTypeACMEDNS = ACMEDns01ProviderType(AccessProviderTypeACMEDNS)
ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go
index d3b484321..1d5400793 100644
--- a/internal/domain/workflow.go
+++ b/internal/domain/workflow.go
@@ -150,6 +150,7 @@ func (c WorkflowNodeConfig) AsBizApply() WorkflowNodeConfigForBizApply {
CAProvider: xmaps.GetString(c, "caProvider"),
CAProviderAccessId: xmaps.GetString(c, "caProviderAccessId"),
CAProviderConfig: xmaps.GetKVMapAny(c, "caProviderConfig"),
+ LifeTime: xmaps.GetString(c, "lifeTime"),
ACMEProfile: xmaps.GetString(c, "acmeProfile"),
Nameservers: nameservers,
DnsPropagationWait: xmaps.GetInt32(c, "dnsPropagationWait"),
@@ -216,10 +217,11 @@ type WorkflowNodeConfigForBizApply struct {
Provider string `json:"provider"` // 质询提供商
ProviderAccessId string `json:"providerAccessId"` // 质询提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 质询提供商额外配置
- KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法
CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置)
CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID
CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置
+ KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法
+ LifeTime string `json:"lifeTime,omitempty"` // 证书有效期
ACMEProfile string `json:"acmeProfile,omitempty"` // ACME Profiles Extension
Nameservers []string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数
diff --git a/internal/workflow/engine/executor_bizapply.go b/internal/workflow/engine/executor_bizapply.go
index 68f107e20..547e8e4bd 100644
--- a/internal/workflow/engine/executor_bizapply.go
+++ b/internal/workflow/engine/executor_bizapply.go
@@ -261,6 +261,7 @@ func (ne *bizApplyNodeExecutor) executeObtain(execCtx *NodeExecutionContext, nod
DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout,
DnsTTL: nodeCfg.DnsTTL,
HttpDelayWait: nodeCfg.HttpDelayWait,
+ LifeTime: nodeCfg.LifeTime,
ACMEProfile: nodeCfg.ACMEProfile,
ARIReplacesAcctUrl: lo.If(lastCertificate == nil, "").
ElseF(func() string {
diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/acmedns/acmedns.go b/pkg/core/ssl-applicator/acme-dns01/providers/acmedns/acmedns.go
new file mode 100644
index 000000000..992766f84
--- /dev/null
+++ b/pkg/core/ssl-applicator/acme-dns01/providers/acmedns/acmedns.go
@@ -0,0 +1,35 @@
+package acmedns
+
+import (
+ "errors"
+ "net/url"
+
+ "github.com/go-acme/lego/v4/providers/dns/acmedns"
+
+ "github.com/certimate-go/certimate/pkg/core"
+)
+
+type ChallengeProviderConfig struct {
+ ApiBase string `json:"apiBase,omitempty"`
+ StorageBaseUrl string `json:"storageBaseUrl,omitempty"`
+ StoragePath string `json:"storagePath,omitempty"`
+}
+
+func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) {
+ if config == nil {
+ return nil, errors.New("the configuration of the acme challenge provider is nil")
+ }
+
+ ApiBase, _ := url.Parse(config.ApiBase)
+ providerConfig := acmedns.NewDefaultConfig()
+ providerConfig.APIBase = ApiBase.String()
+ providerConfig.StorageBaseURL = config.StorageBaseUrl
+ providerConfig.StoragePath = config.StoragePath
+
+ provider, err := acmedns.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/ui/public/imgs/providers/acmedns.svg b/ui/public/imgs/providers/acmedns.svg
new file mode 100644
index 000000000..936ca0771
--- /dev/null
+++ b/ui/public/imgs/providers/acmedns.svg
@@ -0,0 +1,2 @@
+
diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx
index a339ed3fd..742782a87 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -13,6 +13,7 @@ import { FormNestedFieldsContextProvider } from "./forms/_context";
import { useProviderFilterByUsage } from "./forms/_hooks";
import AccessConfigFieldsProvider1Panel from "./forms/AccessConfigFieldsProvider1Panel";
import AccessConfigFieldsProviderACMECA from "./forms/AccessConfigFieldsProviderACMECA";
+import AccessConfigFieldsProviderACMEDNS from "./forms/AccessConfigFieldsProviderACMEDNS";
import AccessConfigFieldsProviderACMEHttpReq from "./forms/AccessConfigFieldsProviderACMEHttpReq";
import AccessConfigFieldsProviderAliyun from "./forms/AccessConfigFieldsProviderAliyun";
import AccessConfigFieldsProviderAPISIX from "./forms/AccessConfigFieldsProviderAPISIX";
@@ -133,6 +134,9 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, ..
case ACCESS_PROVIDERS.ACMECA: {
return