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 00000000..c80f4a9e Binary files /dev/null and b/ui/public/imgs/providers/duckdns.png differ diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 4fd547e8..baea8ed8 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -33,6 +33,7 @@ import AccessFormDigitalOceanConfig from "./AccessFormDigitalOceanConfig"; import AccessFormDingTalkBotConfig from "./AccessFormDingTalkBotConfig"; import AccessFormDNSLAConfig from "./AccessFormDNSLAConfig"; import AccessFormDogeCloudConfig from "./AccessFormDogeCloudConfig"; +import AccessFormDuckDNSConfig from "./AccessFormDuckDNSConfig"; import AccessFormDynv6Config from "./AccessFormDynv6Config"; import AccessFormEdgioConfig from "./AccessFormEdgioConfig"; import AccessFormEmailConfig from "./AccessFormEmailConfig"; @@ -225,6 +226,8 @@ const AccessForm = forwardRef(({ 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",