From 2dead299ff03b08ecafc15fe904f02b031ee2745 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 5 Nov 2025 23:04:53 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: dynu --- internal/certapply/applicators/sp_dynu.go | 29 ++++++++++ internal/domain/access.go | 4 ++ internal/domain/provider.go | 2 +- .../acme-dns01/providers/dynu/dynu.go | 38 +++++++++++++ ui/public/imgs/providers/dynu.png | Bin 0 -> 6034 bytes .../forms/AccessConfigFieldsProvider.tsx | 2 + .../forms/AccessConfigFieldsProviderDynu.tsx | 52 ++++++++++++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.access.json | 3 + ui/src/i18n/locales/zh/nls.access.json | 3 + 10 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 internal/certapply/applicators/sp_dynu.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/dynu/dynu.go create mode 100644 ui/public/imgs/providers/dynu.png create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderDynu.tsx diff --git a/internal/certapply/applicators/sp_dynu.go b/internal/certapply/applicators/sp_dynu.go new file mode 100644 index 00000000..9a5a1af5 --- /dev/null +++ b/internal/certapply/applicators/sp_dynu.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/dynu" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeDynu, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForDynu{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := dynu.NewChallengeProvider(&dynu.ChallengeProviderConfig{ + ApiKey: credentials.ApiKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }); err != nil { + panic(err) + } +} diff --git a/internal/domain/access.go b/internal/domain/access.go index 43d6d952..29f5b627 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -190,6 +190,10 @@ type AccessConfigForDuckDNS struct { Token string `json:"token"` } +type AccessConfigForDynu struct { + ApiKey string `json:"apiKey"` +} + type AccessConfigForDynv6 struct { HttpToken string `json:"httpToken"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 0076e296..8bab76c0 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -44,7 +44,7 @@ const ( AccessProviderTypeDNSMadeEasy = AccessProviderType("dnsmadeeasy") AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") AccessProviderTypeDuckDNS = AccessProviderType("duckdns") - AccessProviderTypeDynu = AccessProviderType("dynu") // Dynu(预留) + AccessProviderTypeDynu = AccessProviderType("dynu") AccessProviderTypeDynv6 = AccessProviderType("dynv6") AccessProviderTypeEmail = AccessProviderType("email") AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/dynu/dynu.go b/pkg/core/ssl-applicator/acme-dns01/providers/dynu/dynu.go new file mode 100644 index 00000000..3bdfa41f --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/dynu/dynu.go @@ -0,0 +1,38 @@ +package dynu + +import ( + "errors" + "time" + + "github.com/go-acme/lego/v4/providers/dns/dynu" + + "github.com/certimate-go/certimate/pkg/core" +) + +type ChallengeProviderConfig struct { + ApiKey string `json:"apiKey"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) { + if config == nil { + return nil, errors.New("the configuration of the acme challenge provider is nil") + } + + providerConfig := dynu.NewDefaultConfig() + providerConfig.APIKey = config.ApiKey + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := dynu.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/dynu.png b/ui/public/imgs/providers/dynu.png new file mode 100644 index 0000000000000000000000000000000000000000..8b44ea1b5f6e924e396e77fecf59bc6793e5af0b GIT binary patch literal 6034 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 TcOCwf>}aY=T8g#u){*}MYLrNR literal 0 HcmV?d00001 diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index 0309e4f4..45928647 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -35,6 +35,7 @@ import AccessConfigFieldsProviderDNSLA from "./AccessConfigFieldsProviderDNSLA"; import AccessConfigFieldsProviderDNSMadeEasy from "./AccessConfigFieldsProviderDNSMadeEasy"; import AccessConfigFieldsProviderDogeCloud from "./AccessConfigFieldsProviderDogeCloud"; import AccessConfigFieldsProviderDuckDNS from "./AccessConfigFieldsProviderDuckDNS"; +import AccessConfigFieldsProviderDynu from "./AccessConfigFieldsProviderDynu"; import AccessConfigFieldsProviderDynv6 from "./AccessConfigFieldsProviderDynv6"; import AccessConfigFieldsProviderEmail from "./AccessConfigFieldsProviderEmail"; import AccessConfigFieldsProviderFlexCDN from "./AccessConfigFieldsProviderFlexCDN"; @@ -129,6 +130,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + apiKey: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + apiKey: z.string().nonempty(t("access.form.dynu_api_key.placeholder")), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderDynu, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index b068f13c..97943696 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -185,6 +185,7 @@ export const accessProvidersMap: Maphttps://www.duckdns.org/spec.jsp", + "access.form.dynu_api_key.label": "Dynu API key", + "access.form.dynu_api_key.placeholder": "Please enter Dynu API key", + "access.form.dynu_api_key.tooltip": "For more information, see https://www.dynu.com/Support/API#Authentication", "access.form.dynv6_http_token.label": "dynv6 HTTP token", "access.form.dynv6_http_token.placeholder": "Please enter dynv6 HTTP token", "access.form.dynv6_http_token.tooltip": "For more information, see https://dynv6.com/keys", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index babc6115..ffb82b0b 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -238,6 +238,9 @@ "access.form.duckdns_token.label": "DuckDNS Token", "access.form.duckdns_token.placeholder": "请输入 DuckDNS Token", "access.form.duckdns_token.tooltip": "这是什么?请参阅 https://www.duckdns.org/spec.jsp", + "access.form.dynu_api_key.label": "Dynu API Key", + "access.form.dynu_api_key.placeholder": "请输入 Dynu API Key", + "access.form.dynu_api_key.tooltip": "这是什么?请参阅 https://www.dynu.com/Support/API#Authentication", "access.form.dynv6_http_token.label": "dynv6 HTTP Token", "access.form.dynv6_http_token.placeholder": "请输入 dynv6 HTTP Token", "access.form.dynv6_http_token.tooltip": "这是什么?请参阅 https://dynv6.com/keys",