diff --git a/internal/certacme/certifiers/sp_regru.go b/internal/certacme/certifiers/sp_regru.go new file mode 100644 index 000000000..95488f15a --- /dev/null +++ b/internal/certacme/certifiers/sp_regru.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/regru" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeRegru, func(options *ProviderFactoryOptions) (core.ACMEChallenger, error) { + credentials := domain.AccessConfigForRegru{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := regru.NewChallenger(®ru.ChallengerConfig{ + Username: credentials.Username, + Password: credentials.Password, + MtlsCertificate: credentials.MtlsCertificate, + MtlsPrivateKey: credentials.MtlsPrivateKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index c6491bcd4..8e3e895ab 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -485,6 +485,13 @@ type AccessConfigForRatPanel struct { AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } +type AccessConfigForRegru struct { + Username string `json:"username"` + Password string `json:"password"` + MtlsCertificate string `json:"mtlsCertificate,omitempty"` + MtlsPrivateKey string `json:"mtlsPrivateKey,omitempty"` +} + type AccessConfigForRFC2136 struct { Host string `json:"host"` Port int32 `json:"port"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 581d10fca..7a5f63269 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -104,6 +104,7 @@ const ( AccessProviderTypeQingCloud = AccessProviderType("qingcloud") AccessProviderTypeRainYun = AccessProviderType("rainyun") AccessProviderTypeRatPanel = AccessProviderType("ratpanel") + AccessProviderTypeRegru = AccessProviderType("regru") AccessProviderTypeRFC2136 = AccessProviderType("rfc2136") AccessProviderTypeS3 = AccessProviderType("s3") AccessProviderTypeSafeLine = AccessProviderType("safeline") @@ -243,6 +244,7 @@ const ( ACMEDns01ProviderTypeQingCloud = ACMEDns01ProviderType(AccessProviderTypeQingCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeQingCloudDNS] ACMEDns01ProviderTypeQingCloudDNS = ACMEDns01ProviderType(AccessProviderTypeQingCloud + "-dns") ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun) + ACMEDns01ProviderTypeRegru = ACMEDns01ProviderType(AccessProviderTypeRegru) ACMEDns01ProviderTypeRFC2136 = ACMEDns01ProviderType(AccessProviderTypeRFC2136) ACMEDns01ProviderTypeSpaceship = ACMEDns01ProviderType(AccessProviderTypeSpaceship) ACMEDns01ProviderTypeTechnitiumDNS = ACMEDns01ProviderType(AccessProviderTypeTechnitiumDNS) diff --git a/pkg/core/certifier/challengers/dns01/regru/regru.go b/pkg/core/certifier/challengers/dns01/regru/regru.go new file mode 100644 index 000000000..07ab413ef --- /dev/null +++ b/pkg/core/certifier/challengers/dns01/regru/regru.go @@ -0,0 +1,44 @@ +package regru + +import ( + "fmt" + "time" + + "github.com/go-acme/lego/v5/providers/dns/regru" + + "github.com/certimate-go/certimate/pkg/core/certifier" +) + +type ChallengerConfig struct { + Username string `json:"username"` + Password string `json:"password"` + MtlsCertificate string `json:"mtlsCertificate,omitempty"` + MtlsPrivateKey string `json:"mtlsPrivateKey,omitempty"` + 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 := regru.NewDefaultConfig() + providerConfig.Username = config.Username + providerConfig.Password = config.Password + providerConfig.TLSCert = config.MtlsCertificate + providerConfig.TLSKey = config.MtlsPrivateKey + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = config.DnsTTL + } + + provider, err := regru.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/regru.svg b/ui/public/imgs/providers/regru.svg new file mode 100644 index 000000000..38888884e --- /dev/null +++ b/ui/public/imgs/providers/regru.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index 04d4e316e..a6080f9a1 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -88,6 +88,7 @@ import AccessConfigFieldsProviderQingCloud from "./AccessConfigFieldsProviderQin import AccessConfigFieldsProviderQiniu from "./AccessConfigFieldsProviderQiniu"; import AccessConfigFieldsProviderRainYun from "./AccessConfigFieldsProviderRainYun"; import AccessConfigFieldsProviderRatPanel from "./AccessConfigFieldsProviderRatPanel"; +import AccessConfigFieldsProviderRegru from "./AccessConfigFieldsProviderRegru"; import AccessConfigFieldsProviderRFC2136 from "./AccessConfigFieldsProviderRFC2136"; import AccessConfigFieldsProviderS3 from "./AccessConfigFieldsProviderS3"; import AccessConfigFieldsProviderSafeLine from "./AccessConfigFieldsProviderSafeLine"; @@ -207,6 +208,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: "", + mtlsCertificate: "", + mtlsPrivateKey: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z + .object({ + username: z.string().nonempty(), + password: z.string().nonempty(), + mtlsCertificate: z.string().nullish(), + mtlsPrivateKey: z.string().nullish(), + }) + .superRefine((values, ctx) => { + const scMtlsCertificate = z.string().nonempty(); + const spMtlsCertificate = scMtlsCertificate.safeParse(values.mtlsCertificate); + const scMtlsPrivateKey = z.string().nonempty(); + const spMtlsPrivateKey = scMtlsPrivateKey.safeParse(values.mtlsPrivateKey); + if (!spMtlsCertificate.success && spMtlsPrivateKey.success) { + ctx.addIssue({ + code: "custom", + message: t("access.form.regru_mtls_certificate.errmsg.invalid"), + path: ["mtlsCertificate"], + }); + } else if (spMtlsCertificate.success && !spMtlsPrivateKey.success) { + ctx.addIssue({ + code: "custom", + message: t("access.form.regru_mtls_private_key.errmsg.invalid"), + path: ["mtlsPrivateKey"], + }); + } + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderRegru, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index fea37e264..8d63d05b8 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -104,6 +104,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ QINIU: "qiniu", RAINYUN: "rainyun", RATPANEL: "ratpanel", + REGRU: "regru", RFC2136: "rfc2136", S3: "s3", SAFELINE: "safeline", @@ -240,6 +241,7 @@ export const accessProvidersMap: Maphttps://ratpanel.github.io/advanced/api.html" }, + "regru_username": { + "label": "Рег.ру username", + "placeholder": "Please enter Рег.ру username" + }, + "regru_password": { + "label": "Рег.ру password", + "placeholder": "Please enter Рег.ру password or API password" + }, + "regru_mtls_certificate": { + "label": "Рег.ру mTLS certificate (Optional)", + "placeholder": "Please enter Рег.ру 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", + "errmsg": { + "invalid": "Invalid private key" + }, + "tooltip": "For more information, see https://www.reg.ru/user/account/#/settings/api/api/" + }, "rfc2136_host": { "label": "DNS server host", "placeholder": "Please enter DNS server host" diff --git a/ui/src/i18n/resources/en/nls.provider.json b/ui/src/i18n/resources/en/nls.provider.json index 81f28df36..a8a1341d0 100644 --- a/ui/src/i18n/resources/en/nls.provider.json +++ b/ui/src/i18n/resources/en/nls.provider.json @@ -198,6 +198,7 @@ "ratpanel": "AcePanel (aka RatPanel)", "ratpanel_common": "AcePanel", "ratpanel_console": "AcePanel - Console itself", + "regru": "Рег.ру (Reg.ru)", "rfc2136": "Dynamic DNS Updates (RFC 2136)", "s3": "Object storage (S3-compatible)", "s3_upload": "Upload to S3-compatible object storage", diff --git a/ui/src/i18n/resources/zh/nls.access.json b/ui/src/i18n/resources/zh/nls.access.json index c62fea365..7ad0b7258 100644 --- a/ui/src/i18n/resources/zh/nls.access.json +++ b/ui/src/i18n/resources/zh/nls.access.json @@ -1072,6 +1072,30 @@ "placeholder": "请输入耗子面板 AccessToken", "tooltip": "这是什么?请参阅 https://ratpanel.github.io/advanced/api.html" }, + "regru_username": { + "label": "Рег.ру 用户名", + "placeholder": "请输入 Рег.ру 用户名" + }, + "regru_password": { + "label": "Рег.ру 密码", + "placeholder": "请输入 Рег.ру 密码或 API 密码" + }, + "regru_mtls_certificate": { + "label": "Рег.ру SSL 双向认证证书(可选)", + "placeholder": "请输入 Рег.ру SSL 双向认证证书", + "errmsg": { + "invalid": "无效的证书" + }, + "tooltip": "这是什么?请参阅 https://www.reg.ru/user/account/#/settings/api/api/" + }, + "regru_mtls_private_key": { + "label": "Рег.ру SSL 双向认证私钥(可选)", + "placeholder": "请输入 Рег.ру SSL 双向认证私钥", + "errmsg": { + "invalid": "无效的私钥" + }, + "tooltip": "这是什么?请参阅 https://www.reg.ru/user/account/#/settings/api/api/" + }, "rfc2136_host": { "label": "DNS 服务器地址", "placeholder": "请输入 DNS 服务器地址" diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 6015de294..14ee11774 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -198,6 +198,7 @@ "ratpanel": "耗子面板(又名:AcePanel)", "ratpanel_common": "耗子面板", "ratpanel_console": "耗子面板 - 面板自身", + "regru": "Рег.ру(REG.ru)", "rfc2136": "Dynamic DNS Updates (RFC 2136)", "s3": "对象存储(S3 兼容)", "s3_upload": "上传到 S3 兼容的对象存储",