From 494a55ff3e4d39931a6206c89cc90b60152d0495 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 3 Nov 2025 15:51:40 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: hostingde --- .../certapply/applicators/sp_hostingde.go | 29 +++++++++++ internal/domain/access.go | 4 ++ internal/domain/provider.go | 2 + .../providers/hostingde/hostingde.go | 38 ++++++++++++++ ui/public/imgs/providers/baiducloud.svg | 2 +- ui/public/imgs/providers/hostingde.svg | 1 + ui/src/components/access/AccessForm.tsx | 4 ++ .../AccessConfigFieldsProviderHostingde.tsx | 52 +++++++++++++++++++ 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, 143 insertions(+), 1 deletion(-) create mode 100644 internal/certapply/applicators/sp_hostingde.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/hostingde/hostingde.go create mode 100644 ui/public/imgs/providers/hostingde.svg create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderHostingde.tsx diff --git a/internal/certapply/applicators/sp_hostingde.go b/internal/certapply/applicators/sp_hostingde.go new file mode 100644 index 000000000..9c3716a60 --- /dev/null +++ b/internal/certapply/applicators/sp_hostingde.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/hostingde" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeHostingde, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForHostingde{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := hostingde.NewChallengeProvider(&hostingde.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 991e55ab1..59e28e6db 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -247,6 +247,10 @@ type AccessConfigForHetzner struct { ApiToken string `json:"apiToken"` } +type AccessConfigForHostingde struct { + ApiKey string `json:"apiKey"` +} + type AccessConfigForHostinger struct { ApiToken string `json:"apiToken"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index d2f2c5f10..4980be0c8 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -54,6 +54,7 @@ const ( AccessProviderTypeGoEdge = AccessProviderType("goedge") AccessProviderTypeGoogleTrustServices = AccessProviderType("googletrustservices") AccessProviderTypeHetzner = AccessProviderType("hetzner") + AccessProviderTypeHostingde = AccessProviderType("hostingde") AccessProviderTypeHostinger = AccessProviderType("hostinger") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") AccessProviderTypeIONOS = AccessProviderType("ionos") @@ -165,6 +166,7 @@ const ( ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname) ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy) ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner) + ACMEDns01ProviderTypeHostingde = ACMEDns01ProviderType(AccessProviderTypeHostingde) ACMEDns01ProviderTypeHostinger = ACMEDns01ProviderType(AccessProviderTypeHostinger) ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS] ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns") diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/hostingde/hostingde.go b/pkg/core/ssl-applicator/acme-dns01/providers/hostingde/hostingde.go new file mode 100644 index 000000000..4ff4216d3 --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/hostingde/hostingde.go @@ -0,0 +1,38 @@ +package hostingde + +import ( + "errors" + "time" + + "github.com/go-acme/lego/v4/providers/dns/hostingde" + + "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 := hostingde.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 := hostingde.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/baiducloud.svg b/ui/public/imgs/providers/baiducloud.svg index e448fd3ef..f92dd1bf7 100644 --- a/ui/public/imgs/providers/baiducloud.svg +++ b/ui/public/imgs/providers/baiducloud.svg @@ -1 +1 @@ - + diff --git a/ui/public/imgs/providers/hostingde.svg b/ui/public/imgs/providers/hostingde.svg new file mode 100644 index 000000000..2929b09d0 --- /dev/null +++ b/ui/public/imgs/providers/hostingde.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index a10a82187..0ee333907 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -53,6 +53,7 @@ import AccessConfigFieldsProviderGoDaddy from "./forms/AccessConfigFieldsProvide import AccessConfigFieldsProviderGoEdge from "./forms/AccessConfigFieldsProviderGoEdge"; import AccessConfigFieldsProviderGoogleTrustServices from "./forms/AccessConfigFieldsProviderGoogleTrustServices"; import AccessConfigFieldsProviderHetzner from "./forms/AccessConfigFieldsProviderHetzner"; +import AccessConfigFieldsProviderHostingde from "./forms/AccessConfigFieldsProviderHostingde"; import AccessConfigFieldsProviderHostinger from "./forms/AccessConfigFieldsProviderHostinger"; import AccessConfigFieldsProviderHuaweiCloud from "./forms/AccessConfigFieldsProviderHuaweiCloud"; import AccessConfigFieldsProviderIONOS from "./forms/AccessConfigFieldsProviderIONOS"; @@ -266,6 +267,9 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, on case ACCESS_PROVIDERS.HETZNER: { return ; } + case ACCESS_PROVIDERS.HOSTINGDE: { + return ; + } case ACCESS_PROVIDERS.HOSTINGER: { return ; } diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderHostingde.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderHostingde.tsx new file mode 100644 index 000000000..35d09f49d --- /dev/null +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderHostingde.tsx @@ -0,0 +1,52 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { useFormNestedFieldsContext } from "./_context"; + +const AccessConfigFormFieldsProviderHostingde = () => { + 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.hostingde_api_key.placeholder")), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderHostingde, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 50f716634..1e10f03ec 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -57,6 +57,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ GOEDGE: "goedge", GOOGLETRUSTSERVICES: "googletrustservices", HETZNER: "hetzner", + HOSTINGDE: "hostingDE", HOSTINGER: "hostinger", HUAWEICLOUD: "huaweicloud", IONOS: "ionos", @@ -182,6 +183,7 @@ export const accessProvidersMap: Maphttps://docs.hetzner.com/cloud/api/getting-started/generating-api-token", + "access.form.hostingde_api_key.label": "hosting.de API key", + "access.form.hostingde_api_key.placeholder": "Please enter hosting.de API key", + "access.form.hostingde_api_key.tooltip": "For more information, see https://www.hosting.de/api/#requests-and-authentication", "access.form.hostinger_api_token.label": "Hostinger API token", "access.form.hostinger_api_token.placeholder": "Please enter Hostinger API token", "access.form.hostinger_api_token.tooltip": "For more information, see https://developers.hostinger.com/#description/authentication", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index 7b3f7d0d9..06d0270ec 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -94,6 +94,7 @@ "provider.goedge": "GoEdge", "provider.googletrustservices": "Google Trust Services", "provider.hetzner": "Hetzner", + "provider.hostingde": "hosting.de", "provider.hostinger": "Hostinger", "provider.huaweicloud": "Huawei Cloud", "provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index d394d680f..38bef4dae 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -295,6 +295,9 @@ "access.form.hetzner_api_token.label": "Hetzner API Token", "access.form.hetzner_api_token.placeholder": "请输入 Hetzner API Token", "access.form.hetzner_api_token.tooltip": "这是什么?请参阅 https://docs.hetzner.com/cloud/api/getting-started/generating-api-token", + "access.form.hostingde_api_key.label": "hosting.de API Key", + "access.form.hostingde_api_key.placeholder": "请输入 hosting.de API Key", + "access.form.hostingde_api_key.tooltip": "这是什么?请参阅 https://www.hosting.de/api/#requests-and-authentication", "access.form.hostinger_api_token.label": "Hostinger API Token", "access.form.hostinger_api_token.placeholder": "请输入 Hostinger API Token", "access.form.hostinger_api_token.tooltip": "这是什么?请参阅 https://developers.hostinger.com/#description/authentication", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index bf23ceca9..55d5af7b6 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -94,6 +94,7 @@ "provider.goedge": "GoEdge", "provider.googletrustservices": "Google Trust Services", "provider.hetzner": "Hetzner", + "provider.hostingde": "hosting.de", "provider.hostinger": "Hostinger", "provider.huaweicloud": "华为云", "provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN",