From cd9dac776551db29ee8e3968e8dadcf5a0cd67fa Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 26 May 2025 13:59:00 +0800 Subject: [PATCH] feat: new acme dns-01 provider: duckdns --- internal/applicant/providers.go | 15 +++++ internal/domain/access.go | 4 ++ internal/domain/provider.go | 2 + .../lego-providers/duckdns/duckdns.go | 32 ++++++++++ ui/public/imgs/providers/duckdns.png | Bin 0 -> 6821 bytes ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormDuckDNSConfig.tsx | 57 ++++++++++++++++++ ui/src/domain/access.ts | 6 ++ ui/src/domain/provider.ts | 4 ++ ui/src/i18n/locales/en/nls.access.json | 3 + ui/src/i18n/locales/en/nls.provider.json | 1 + ui/src/i18n/locales/zh/nls.access.json | 3 + ui/src/i18n/locales/zh/nls.provider.json | 1 + 13 files changed, 131 insertions(+) create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/duckdns/duckdns.go create mode 100644 ui/public/imgs/providers/duckdns.png create mode 100644 ui/src/components/access/AccessFormDuckDNSConfig.tsx diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index e35aee3e..98561daf 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -19,6 +19,7 @@ import ( pDeSEC "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/desec" pDigitalOcean "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean" pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla" + pDuckDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/duckdns" pDynv6 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6" pGcore "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gcore" pGname "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname" @@ -279,6 +280,20 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } + case domain.ACMEDns01ProviderTypeDuckDNS: + { + access := domain.AccessConfigForDuckDNS{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pDuckDNS.NewChallengeProvider(&pDuckDNS.ChallengeProviderConfig{ + Token: access.Token, + DnsPropagationTimeout: options.DnsPropagationTimeout, + }) + return applicant, err + } + case domain.ACMEDns01ProviderTypeDynv6: { access := domain.AccessConfigForDynv6{} diff --git a/internal/domain/access.go b/internal/domain/access.go index c3530a4f..dac85e31 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -131,6 +131,10 @@ type AccessConfigForDogeCloud struct { SecretKey string `json:"secretKey"` } +type AccessConfigForDuckDNS struct { + Token string `json:"token"` +} + type AccessConfigForDynv6 struct { HttpToken string `json:"httpToken"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 52a71e64..14a107b6 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -35,6 +35,7 @@ const ( AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot") AccessProviderTypeDNSLA = AccessProviderType("dnsla") AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") + AccessProviderTypeDuckDNS = AccessProviderType("duckdns") AccessProviderTypeDynv6 = AccessProviderType("dynv6") AccessProviderTypeEdgio = AccessProviderType("edgio") AccessProviderTypeEmail = AccessProviderType("email") @@ -130,6 +131,7 @@ const ( ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC) ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean) ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA) + ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS) ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6) ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore) ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname) diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/duckdns/duckdns.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/duckdns/duckdns.go new file mode 100644 index 00000000..6cc823d0 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/duckdns/duckdns.go @@ -0,0 +1,32 @@ +package namedotcom + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/providers/dns/duckdns" +) + +type ChallengeProviderConfig struct { + Token string `json:"token"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := duckdns.NewDefaultConfig() + providerConfig.Token = config.Token + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + + provider, err := duckdns.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/duckdns.png b/ui/public/imgs/providers/duckdns.png new file mode 100644 index 0000000000000000000000000000000000000000..c80f4a9ed84ecf4545da1088eb305892504ef8e3 GIT binary patch literal 6821 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 className, return ; case ACCESS_PROVIDERS.DOGECLOUD: return ; + case ACCESS_PROVIDERS.DUCKDNS: + return ; case ACCESS_PROVIDERS.DYNV6: return ; case ACCESS_PROVIDERS.EDGIO: diff --git a/ui/src/components/access/AccessFormDuckDNSConfig.tsx b/ui/src/components/access/AccessFormDuckDNSConfig.tsx new file mode 100644 index 00000000..969f78f8 --- /dev/null +++ b/ui/src/components/access/AccessFormDuckDNSConfig.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForDuckDNS } from "@/domain/access"; + +type AccessFormDuckDNSConfigFieldValues = Nullish; + +export type AccessFormDuckDNSConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormDuckDNSConfigFieldValues; + onValuesChange?: (values: AccessFormDuckDNSConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormDuckDNSConfigFieldValues => { + return { + token: "", + }; +}; + +const AccessFormDuckDNSConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormDuckDNSConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + token: z.string().nonempty(t("access.form.duckdns_token.placeholder")).trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessFormDuckDNSConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 32a1b128..9e2d4f70 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -24,9 +24,11 @@ export interface AccessModel extends BaseModel { | AccessConfigForClouDNS | AccessConfigForCMCCCloud | AccessConfigForDeSEC + | AccessConfigForDigitalOcean | AccessConfigForDingTalkBot | AccessConfigForDNSLA | AccessConfigForDogeCloud + | AccessConfigForDuckDNS | AccessConfigForDynv6 | AccessConfigForEdgio | AccessConfigForEmail @@ -189,6 +191,10 @@ export type AccessConfigForDogeCloud = { secretKey: string; }; +export type AccessConfigForDuckDNS = { + token: string; +}; + export type AccessConfigForDynv6 = { httpToken: string; }; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 6d1ad303..6576aa94 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -27,6 +27,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ DINGTALKBOT: "dingtalkbot", DNSLA: "dnsla", DOGECLOUD: "dogecloud", + DUCKDNS: "duckdns", DYNV6: "dynv6", EDGIO: "edgio", EMAIL: "email", @@ -142,6 +143,7 @@ export const accessProvidersMap: Maphttps://console.dogecloud.com/", + "access.form.duckdns_token.label": "DuckDNS token", + "access.form.duckdns_token.placeholder": "Please enter DuckDNS token", + "access.form.duckdns_token.tooltip": "For more information, see https://www.duckdns.org/spec.jsp", "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/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index c623bb27..80cb65cf 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -63,6 +63,7 @@ "provider.dnsla": "DNS.LA", "provider.dogecloud": "Doge Cloud", "provider.dogecloud.cdn": "Doge Cloud - CDN (Content Delivery Network)", + "provider.duckdns": "Duck DNS", "provider.dynv6": "dynv6", "provider.edgio": "Edgio", "provider.edgio.applications": "Edgio - Applications", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index f3a39ec2..1f538ac5 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -167,6 +167,9 @@ "access.form.dogecloud_secret_key.label": "多吉云 SecretKey", "access.form.dogecloud_secret_key.placeholder": "请输入多吉云 SecretKey", "access.form.dogecloud_secret_key.tooltip": "这是什么?请参阅 https://console.dogecloud.com/", + "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.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", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index a68a1a09..8de1ccf5 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -63,6 +63,7 @@ "provider.dnsla": "DNS.LA", "provider.dogecloud": "多吉云", "provider.dogecloud.cdn": "多吉云 - 内容分发网络 CDN", + "provider.duckdns": "Duck DNS", "provider.dynv6": "dynv6", "provider.edgio": "Edgio", "provider.edgio.applications": "Edgio - Applications",