diff --git a/README.md b/README.md index 10987477..1bcf2034 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ make local.run | [GNAME](https://www.gname.com/) | | | [GoDaddy](https://www.godaddy.com/) | | | [Name.com](https://www.name.com/) | | +| [Namecheap](https://www.namecheap.com/) | | | [NameSilo](https://www.namesilo.com/) | | | [IBM NS1 Connect](https://www.ibm.com/cn-zh/products/ns1-connect/) | | | [移动云](https://ecloud.10086.cn/) | | diff --git a/README_EN.md b/README_EN.md index bd58f8bb..2e850330 100644 --- a/README_EN.md +++ b/README_EN.md @@ -101,6 +101,7 @@ The following DNS providers are supported: | [GNAME](https://www.gname.com/) | | | [GoDaddy](https://www.godaddy.com/) | | | [Name.com](https://www.name.com/) | | +| [Namecheap](https://www.namecheap.com/) | | | [NameSilo](https://www.namesilo.com/) | | | [IBM NS1 Connect](https://www.ibm.com/products/ns1-connect/) | | | [CMCC Cloud](https://ecloud.10086.cn/) | | diff --git a/go.mod b/go.mod index 5694ec54..40efa5a4 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/cloudfront v1.44.10 github.com/baidubce/bce-sdk-go v0.9.217 github.com/byteplus-sdk/byteplus-sdk-golang v1.0.41 - github.com/go-acme/lego/v4 v4.21.0 + github.com/go-acme/lego/v4 v4.22.2 github.com/go-resty/resty/v2 v2.16.5 github.com/go-viper/mapstructure/v2 v2.2.1 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.136 @@ -215,4 +215,5 @@ require ( ) replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0 + replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1 diff --git a/go.sum b/go.sum index e85c4f8d..d15c396f 100644 --- a/go.sum +++ b/go.sum @@ -376,6 +376,8 @@ github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o= github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI= +github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0= +github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index 9989da8f..ef013490 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -19,6 +19,7 @@ import ( pGoDaddy "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/godaddy" pHuaweiCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/huaweicloud" pJDCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/jdcloud" + pNamecheap "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap" pNameDotCom "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namedotcom" pNameSilo "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/namesilo" pNS1 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ns1" @@ -249,6 +250,22 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { return applicant, err } + case domain.ApplyDNSProviderTypeNamecheap: + { + access := domain.AccessConfigForNamecheap{} + if err := maps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pNamecheap.NewChallengeProvider(&pNamecheap.ChallengeProviderConfig{ + Username: access.Username, + ApiKey: access.ApiKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ApplyDNSProviderTypeNameDotCom: { access := domain.AccessConfigForNameDotCom{} diff --git a/internal/domain/access.go b/internal/domain/access.go index fd01af3b..4b693522 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -131,6 +131,11 @@ type AccessConfigForKubernetes struct { type AccessConfigForLocal struct{} +type AccessConfigForNamecheap struct { + Username string `json:"username"` + ApiKey string `json:"apiKey"` +} + type AccessConfigForNameDotCom struct { Username string `json:"username"` ApiToken string `json:"apiToken"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 375176ce..f0949e03 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -26,6 +26,7 @@ const ( AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud") AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留) AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留) + AccessProviderTypeDNSLA = AccessProviderType("dnsla") // DNS.LA(预留) AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") AccessProviderTypeEdgio = AccessProviderType("edgio") AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留) @@ -37,6 +38,7 @@ const ( AccessProviderTypeJDCloud = AccessProviderType("jdcloud") AccessProviderTypeKubernetes = AccessProviderType("k8s") AccessProviderTypeLocal = AccessProviderType("local") + AccessProviderTypeNamecheap = AccessProviderType("namecheap") AccessProviderTypeNameDotCom = AccessProviderType("namedotcom") AccessProviderTypeNameSilo = AccessProviderType("namesilo") AccessProviderTypeNS1 = AccessProviderType("ns1") @@ -82,6 +84,7 @@ const ( ApplyDNSProviderTypeHuaweiCloudDNS = ApplyDNSProviderType("huaweicloud-dns") ApplyDNSProviderTypeJDCloud = ApplyDNSProviderType("jdcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeJDCloudDNS] ApplyDNSProviderTypeJDCloudDNS = ApplyDNSProviderType("jdcloud-dns") + ApplyDNSProviderTypeNamecheap = ApplyDNSProviderType("namecheap") ApplyDNSProviderTypeNameDotCom = ApplyDNSProviderType("namedotcom") ApplyDNSProviderTypeNameSilo = ApplyDNSProviderType("namesilo") ApplyDNSProviderTypeNS1 = ApplyDNSProviderType("ns1") diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap/namecheap.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap/namecheap.go new file mode 100644 index 00000000..9bf2f3c3 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/namecheap/namecheap.go @@ -0,0 +1,38 @@ +package namedotcom + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/providers/dns/namecheap" +) + +type ChallengeProviderConfig struct { + Username string `json:"username"` + ApiKey string `json:"apiKey"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := namecheap.NewDefaultConfig() + providerConfig.APIUser = config.Username + 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 := namecheap.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/migrations/1739462400_collections_snapshot.go b/migrations/1739462400_collections_snapshot.go index 9c079405..67453954 100644 --- a/migrations/1739462400_collections_snapshot.go +++ b/migrations/1739462400_collections_snapshot.go @@ -65,6 +65,7 @@ func init() { "cmcccloud", "ctcccloud", "cucccloud", + "dnsla", "dogecloud", "edgio", "fastly", @@ -76,6 +77,7 @@ func init() { "jdcloud", "k8s", "local", + "namecheap", "namedotcom", "namesilo", "ns1", @@ -173,6 +175,7 @@ func init() { "cmcccloud", "ctcccloud", "cucccloud", + "dnsla", "dogecloud", "edgio", "fastly", @@ -184,6 +187,7 @@ func init() { "jdcloud", "k8s", "local", + "namecheap", "namedotcom", "namesilo", "ns1", diff --git a/ui/public/imgs/providers/namecheap.svg b/ui/public/imgs/providers/namecheap.svg new file mode 100644 index 00000000..e90dbfe9 --- /dev/null +++ b/ui/public/imgs/providers/namecheap.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 90bb6ff5..681d80f1 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -31,6 +31,7 @@ import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; import AccessFormLocalConfig from "./AccessFormLocalConfig"; +import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig"; import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig"; import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig"; import AccessFormNS1Config from "./AccessFormNS1Config"; @@ -141,6 +142,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.LOCAL: return ; + case ACCESS_PROVIDERS.NAMECHEAP: + return ; case ACCESS_PROVIDERS.NAMEDOTCOM: return ; case ACCESS_PROVIDERS.NAMESILO: diff --git a/ui/src/components/access/AccessFormNamecheapConfig.tsx b/ui/src/components/access/AccessFormNamecheapConfig.tsx new file mode 100644 index 00000000..d6a79f2a --- /dev/null +++ b/ui/src/components/access/AccessFormNamecheapConfig.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForNamecheap } from "@/domain/access"; + +type AccessFormNamecheapConfigFieldValues = Nullish; + +export type AccessFormNamecheapConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormNamecheapConfigFieldValues; + onValuesChange?: (values: AccessFormNamecheapConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormNamecheapConfigFieldValues => { + return { + username: "", + apiKey: "", + }; +}; + +const AccessFormNamecheapConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormNamecheapConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + username: z + .string() + .min(1, t("access.form.namecheap_username.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + apiKey: z + .string() + .min(1, t("access.form.namecheap_api_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormNamecheapConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index da8739b7..a1e14d86 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -28,6 +28,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForJDCloud | AccessConfigForKubernetes | AccessConfigForLocal + | AccessConfigForNamecheap | AccessConfigForNameDotCom | AccessConfigForNameSilo | AccessConfigForPowerDNS @@ -151,6 +152,11 @@ export type AccessConfigForKubernetes = { export type AccessConfigForLocal = NonNullable; +export type AccessConfigForNamecheap = { + username: string; + apiKey: string; +}; + export type AccessConfigForNameDotCom = { username: string; apiToken: string; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index e99abb5c..3a806b88 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -26,6 +26,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ JDCLOUD: "jdcloud", KUBERNETES: "k8s", LOCAL: "local", + NAMECHEAP: "namecheap", NAMEDOTCOM: "namedotcom", NAMESILO: "namesilo", NS1: "ns1", @@ -92,6 +93,7 @@ export const accessProvidersMap: Maphttps://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/

Leave it blank to use the Pod's ServiceAccount.", + "access.form.namecheap_username.label": "Namecheap username", + "access.form.namecheap_username.placeholder": "Please enter Namecheap username", + "access.form.namecheap_username.tooltip": "For more information, see https://www.namecheap.com/support/api/intro/", + "access.form.namecheap_api_key.label": "Namecheap API key", + "access.form.namecheap_api_key.placeholder": "Please enter Namecheap API key", + "access.form.namecheap_api_key.tooltip": "For more information, see https://www.namecheap.com/support/api/intro/", "access.form.namedotcom_username.label": "Name.com username", "access.form.namedotcom_username.placeholder": "Please enter Name.com username", "access.form.namedotcom_username.tooltip": "For more information, see https://www.name.com/account/settings/api", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index f0b13069..e924b43a 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -38,6 +38,7 @@ "provider.cmcccloud": "China Mobile Cloud (ECloud)", "provider.ctcccloud": "China Telecom Cloud (State Cloud)", "provider.cucccloud": "China Unicom Cloud", + "provider.dnsla": "DNS.LA", "provider.dogecloud": "Doge Cloud", "provider.dogecloud.cdn": "Doge Cloud - CDN (Content Delivery Network)", "provider.edgio": "Edgio", @@ -63,6 +64,7 @@ "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", "provider.local": "Local deployment", + "provider.namecheap": "Namecheap", "provider.namedotcom": "Name.com", "provider.namesilo": "NameSilo", "provider.ns1": "NS1 (IBM NS1 Connect)", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index f4648c4d..cb9befa4 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -149,6 +149,12 @@ "access.form.k8s_kubeconfig.placeholder": "请选择 KubeConfig 文件", "access.form.k8s_kubeconfig.upload": "选择文件", "access.form.k8s_kubeconfig.tooltip": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/

为空时,将使用 Pod 的 ServiceAccount 作为凭证。", + "access.form.namecheap_username.label": "Namecheap 用户名", + "access.form.namecheap_username.placeholder": "请输入 Namecheap 用户名", + "access.form.namecheap_username.tooltip": "这是什么?请参阅 https://www.namecheap.com/support/api/intro/", + "access.form.namecheap_api_key.label": "Namecheap API Key", + "access.form.namecheap_api_key.placeholder": "请输入 Namecheap API Key", + "access.form.namecheap_api_key.tooltip": "这是什么?请参阅 https://www.namecheap.com/support/api/intro/", "access.form.namedotcom_username.label": "Name.com 用户名", "access.form.namedotcom_username.placeholder": "请输入 Name.com 用户名", "access.form.namedotcom_username.tooltip": "这是什么?请参阅 https://www.name.com/account/settings/api", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 101b0553..f8d36830 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -38,6 +38,7 @@ "provider.cmcccloud": "移动云", "provider.ctcccloud": "联通云", "provider.cucccloud": "天翼云", + "provider.dnsla": "DNS.LA", "provider.dogecloud": "多吉云", "provider.dogecloud.cdn": "多吉云 - 内容分发网络 CDN", "provider.edgio": "Edgio", @@ -63,6 +64,7 @@ "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", "provider.local": "本地部署", + "provider.namecheap": "Namecheap", "provider.namedotcom": "Name.com", "provider.namesilo": "NameSilo", "provider.ns1": "NS1(IBM NS1 Connect)",