From ee2ac23ee1fcf168d666428b7505c866520c1ffc Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 29 May 2026 23:42:56 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: rucenter --- internal/certacme/certifiers/sp_rucenter.go | 29 +++++++ internal/domain/access.go | 7 ++ internal/domain/provider.go | 2 + .../challengers/dns01/rucenter/rucenter.go | 44 +++++++++++ ui/public/imgs/providers/ricru.svg | 1 + .../forms/AccessConfigFieldsProvider.tsx | 2 + .../AccessConfigFieldsProviderRuCenter.tsx | 76 +++++++++++++++++++ ui/src/domain/provider.ts | 4 + ui/src/i18n/resources/en/nls.access.json | 34 +++++++-- ui/src/i18n/resources/en/nls.provider.json | 1 + ui/src/i18n/resources/zh/nls.access.json | 34 +++++++-- ui/src/i18n/resources/zh/nls.provider.json | 1 + 12 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 internal/certacme/certifiers/sp_rucenter.go create mode 100644 pkg/core/certifier/challengers/dns01/rucenter/rucenter.go create mode 100644 ui/public/imgs/providers/ricru.svg create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderRuCenter.tsx diff --git a/internal/certacme/certifiers/sp_rucenter.go b/internal/certacme/certifiers/sp_rucenter.go new file mode 100644 index 000000000..89d01e0bf --- /dev/null +++ b/internal/certacme/certifiers/sp_rucenter.go @@ -0,0 +1,29 @@ +package certifiers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/rucenter" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeRuCenter, func(options *ProviderFactoryOptions) (core.ACMEChallenger, error) { + credentials := domain.AccessConfigForRuCenter{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := rucenter.NewChallenger(&rucenter.ChallengerConfig{ + Username: credentials.Username, + Password: credentials.Password, + ApplicationId: credentials.ApplicationId, + ApplicationToken: credentials.ApplicationToken, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index 8e3e895ab..0c6e1f4f2 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -500,6 +500,13 @@ type AccessConfigForRFC2136 struct { TsigSecret string `json:"tsigSecret,omitempty"` } +type AccessConfigForRuCenter struct { + Username string `json:"username"` + Password string `json:"password"` + ApplicationId string `json:"applicationId"` + ApplicationToken string `json:"applicationToken"` +} + type AccessConfigForS3 struct { Endpoint string `json:"endpoint"` AccessKey string `json:"accessKey"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 7a5f63269..d66ddc25b 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -106,6 +106,7 @@ const ( AccessProviderTypeRatPanel = AccessProviderType("ratpanel") AccessProviderTypeRegru = AccessProviderType("regru") AccessProviderTypeRFC2136 = AccessProviderType("rfc2136") + AccessProviderTypeRuCenter = AccessProviderType("rucenter") AccessProviderTypeS3 = AccessProviderType("s3") AccessProviderTypeSafeLine = AccessProviderType("safeline") AccessProviderTypeSamWAF = AccessProviderType("samwaf") @@ -246,6 +247,7 @@ const ( ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun) ACMEDns01ProviderTypeRegru = ACMEDns01ProviderType(AccessProviderTypeRegru) ACMEDns01ProviderTypeRFC2136 = ACMEDns01ProviderType(AccessProviderTypeRFC2136) + ACMEDns01ProviderTypeRuCenter = ACMEDns01ProviderType(AccessProviderTypeRuCenter) ACMEDns01ProviderTypeSpaceship = ACMEDns01ProviderType(AccessProviderTypeSpaceship) ACMEDns01ProviderTypeTechnitiumDNS = ACMEDns01ProviderType(AccessProviderTypeTechnitiumDNS) ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS] diff --git a/pkg/core/certifier/challengers/dns01/rucenter/rucenter.go b/pkg/core/certifier/challengers/dns01/rucenter/rucenter.go new file mode 100644 index 000000000..442bc3391 --- /dev/null +++ b/pkg/core/certifier/challengers/dns01/rucenter/rucenter.go @@ -0,0 +1,44 @@ +package rucenter + +import ( + "fmt" + "time" + + "github.com/go-acme/lego/v5/providers/dns/nicru" + + "github.com/certimate-go/certimate/pkg/core/certifier" +) + +type ChallengerConfig struct { + Username string `json:"username"` + Password string `json:"password"` + ApplicationId string `json:"applicationId"` + ApplicationToken string `json:"applicationToken"` + DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int `json:"dnsTTL,omitempty"` +} + +func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the acme challenge provider is nil") + } + + providerConfig := nicru.NewDefaultConfig() + providerConfig.Username = config.Username + providerConfig.Password = config.Password + providerConfig.ServiceID = config.ApplicationId + providerConfig.Secret = config.ApplicationToken + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = config.DnsTTL + } + + provider, err := nicru.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/ricru.svg b/ui/public/imgs/providers/ricru.svg new file mode 100644 index 000000000..76b841c3d --- /dev/null +++ b/ui/public/imgs/providers/ricru.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index a6080f9a1..9da1b8a0c 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -90,6 +90,7 @@ import AccessConfigFieldsProviderRainYun from "./AccessConfigFieldsProviderRainY import AccessConfigFieldsProviderRatPanel from "./AccessConfigFieldsProviderRatPanel"; import AccessConfigFieldsProviderRegru from "./AccessConfigFieldsProviderRegru"; import AccessConfigFieldsProviderRFC2136 from "./AccessConfigFieldsProviderRFC2136"; +import AccessConfigFieldsProviderRuCenter from "./AccessConfigFieldsProviderRuCenter"; import AccessConfigFieldsProviderS3 from "./AccessConfigFieldsProviderS3"; import AccessConfigFieldsProviderSafeLine from "./AccessConfigFieldsProviderSafeLine"; import AccessConfigFieldsProviderSamWAF from "./AccessConfigFieldsProviderSamWAF"; @@ -210,6 +211,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 { + username: "", + password: "", + applicationId: "", + applicationToken: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + username: z.union([z.string().endsWith("/NIC-D"), z.string().endsWith("/NIC-REG")]), + password: z.string().nonempty(), + applicationId: z.string().length(32), + applicationToken: z.string().length(91), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderRuCenter, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 8d63d05b8..34b0d863d 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -106,6 +106,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ RATPANEL: "ratpanel", REGRU: "regru", RFC2136: "rfc2136", + RUCENTER: "rucenter", S3: "s3", SAFELINE: "safeline", SAMWAF: "samwaf", @@ -242,6 +243,7 @@ export const accessProvidersMap: Maphttps://ratpanel.github.io/advanced/api.html" }, "regru_username": { - "label": "Рег.ру username", - "placeholder": "Please enter Рег.ру username" + "label": "Reg.ru username", + "placeholder": "Please enter Reg.ru username" }, "regru_password": { - "label": "Рег.ру password", - "placeholder": "Please enter Рег.ру password or API password" + "label": "Reg.ru password", + "placeholder": "Please enter Reg.ru password or API password" }, "regru_mtls_certificate": { - "label": "Рег.ру mTLS certificate (Optional)", - "placeholder": "Please enter Рег.ру mTLS certificate", + "label": "Reg.ru mTLS certificate (Optional)", + "placeholder": "Please enter Reg.ru mTLS certificate", "errmsg": { "invalid": "Invalid certificate" }, "tooltip": "For more information, see https://www.reg.ru/user/account/#/settings/api/api/" }, "regru_mtls_private_key": { - "label": "Рег.ру mTLS private key (Optional)", - "placeholder": "Please enter Рег.ру mTLS private key", + "label": "Reg.ru mTLS private key (Optional)", + "placeholder": "Please enter Reg.ru mTLS private key", "errmsg": { "invalid": "Invalid private key" }, @@ -1118,6 +1118,24 @@ "label": "TSIG authentication secret (Optional)", "placeholder": "Please enter TSIG authentication secret" }, + "rucenter_username": { + "label": "Ru-Center username", + "placeholder": "Please enter Ru-Center username" + }, + "rucenter_password": { + "label": "Ru-Center password", + "placeholder": "Please enter Ru-Center password" + }, + "rucenter_application_id": { + "label": "Ru-Center application ID", + "placeholder": "Please enter Ru-Center application ID", + "tooltip": "For more information, see https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list" + }, + "rucenter_application_token": { + "label": "Ru-Center application token", + "placeholder": "Please enter Ru-Center application token", + "tooltip": "For more information, see https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list" + }, "s3_endpoint": { "label": "Endpoint", "placeholder": "Please enter endpoint", diff --git a/ui/src/i18n/resources/en/nls.provider.json b/ui/src/i18n/resources/en/nls.provider.json index a8a1341d0..c21ff6a1b 100644 --- a/ui/src/i18n/resources/en/nls.provider.json +++ b/ui/src/i18n/resources/en/nls.provider.json @@ -200,6 +200,7 @@ "ratpanel_console": "AcePanel - Console itself", "regru": "Рег.ру (Reg.ru)", "rfc2136": "Dynamic DNS Updates (RFC 2136)", + "rucenter": "Руцентр (Ru-Center)", "s3": "Object storage (S3-compatible)", "s3_upload": "Upload to S3-compatible object storage", "safeline": "SafeLine", diff --git a/ui/src/i18n/resources/zh/nls.access.json b/ui/src/i18n/resources/zh/nls.access.json index 7ad0b7258..b6b5ae90d 100644 --- a/ui/src/i18n/resources/zh/nls.access.json +++ b/ui/src/i18n/resources/zh/nls.access.json @@ -1073,24 +1073,24 @@ "tooltip": "这是什么?请参阅 https://ratpanel.github.io/advanced/api.html" }, "regru_username": { - "label": "Рег.ру 用户名", - "placeholder": "请输入 Рег.ру 用户名" + "label": "Reg.ru 用户名", + "placeholder": "请输入 Reg.ru 用户名" }, "regru_password": { - "label": "Рег.ру 密码", - "placeholder": "请输入 Рег.ру 密码或 API 密码" + "label": "Reg.ru 密码", + "placeholder": "请输入 Reg.ru 密码或 API 密码" }, "regru_mtls_certificate": { - "label": "Рег.ру SSL 双向认证证书(可选)", - "placeholder": "请输入 Рег.ру SSL 双向认证证书", + "label": "Reg.ru SSL 双向认证证书(可选)", + "placeholder": "请输入 Reg.ru SSL 双向认证证书", "errmsg": { "invalid": "无效的证书" }, "tooltip": "这是什么?请参阅 https://www.reg.ru/user/account/#/settings/api/api/" }, "regru_mtls_private_key": { - "label": "Рег.ру SSL 双向认证私钥(可选)", - "placeholder": "请输入 Рег.ру SSL 双向认证私钥", + "label": "Reg.ru SSL 双向认证私钥(可选)", + "placeholder": "请输入 Reg.ru SSL 双向认证私钥", "errmsg": { "invalid": "无效的私钥" }, @@ -1116,6 +1116,24 @@ "label": "TSIG 认证密钥 Secret(可选)", "placeholder": "请输入 TSIG 认证密钥 Secret" }, + "rucenter_username": { + "label": "Ru-Center 用户名", + "placeholder": "请输入 Ru-Center 用户名" + }, + "rucenter_password": { + "label": "Ru-Center 密码", + "placeholder": "请输入 Ru-Center 密码" + }, + "rucenter_application_id": { + "label": "Ru-Center 应用服务 ID", + "placeholder": "请输入 Ru-Center 应用服务 ID", + "tooltip": "这是什么?请参阅 https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list" + }, + "rucenter_application_token": { + "label": "Ru-Center 应用服务 Token", + "placeholder": "请输入 Ru-Center 应用服务 Token", + "tooltip": "这是什么?请参阅 https://www.nic.ru/manager/oauth.cgi?step=oauth.app_list" + }, "s3_endpoint": { "label": "终端节点", "placeholder": "请输入终端节点", diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 14ee11774..221bfd80f 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -200,6 +200,7 @@ "ratpanel_console": "耗子面板 - 面板自身", "regru": "Рег.ру(REG.ru)", "rfc2136": "Dynamic DNS Updates (RFC 2136)", + "rucenter": "Руцентр(Ru-Center)", "s3": "对象存储(S3 兼容)", "s3_upload": "上传到 S3 兼容的对象存储", "safeline": "雷池",