feat(provider): new acme dns-01 provider: regru

This commit is contained in:
Fu Diwei
2026-05-29 23:47:06 +08:00
committed by RHQYZ
parent 795e8df4ad
commit 0be80d7303
12 changed files with 237 additions and 0 deletions
+29
View File
@@ -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(&regru.ChallengerConfig{
Username: credentials.Username,
Password: credentials.Password,
MtlsCertificate: credentials.MtlsCertificate,
MtlsPrivateKey: credentials.MtlsPrivateKey,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
return provider, err
})
}
+7
View File
@@ -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"`
+2
View File
@@ -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)
@@ -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
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-20 0 740 210" width="200" height="200"><path fill-rule="evenodd" clip-rule="evenodd" d="M485.533 39.512c14.29 0 20.146 1.513 29.814 5.06 11.33 4.158 20.263 13.032 24.449 24.287 7.432 19.99 7.531 46.628 0 66.884-4.186 11.255-13.119 20.13-24.449 24.287-9.552 3.505-14.503 5.265-31.067 5.27-12.205.003-26.537-5.27-30.95-9.578l-2.912 2.443c3.869 4.611 5.99 13.314 5.99 19.333V210h-37V41.985h33.449a16.465 16.465 0 0 1-1.897 7.636l2.793 1.017s10.946-11.126 31.78-11.126Zm11.514 33.203c-8.603-3.697-20.857-3.62-29.279 0-5.036 2.164-9.006 6.783-10.867 12.641-1.243 3.918-2.019 8.295-2.333 12.782a53.916 53.916 0 0 0-.158 4.408c-.019 6.176.815 12.349 2.491 17.629 1.861 5.858 5.831 10.479 10.867 12.643 8.602 3.697 20.856 3.62 29.279 0 5.036-2.164 9.007-6.784 10.867-12.643 1.694-5.335 2.517-11.522 2.491-17.68a53.497 53.497 0 0 0-.151-4.244c-.311-4.548-1.094-8.968-2.34-12.895-1.86-5.858-5.832-10.477-10.867-12.641Z" fill="#181818"></path><path d="M622.233 113.643a68.127 68.127 0 0 1 4.412 16.759l.281 3.029h3.808l.3-3.029a83.96 83.96 0 0 1 4.046-16.905l25.177-71.486H700l-57.151 144.205v.002c-3.399 8.222-7.926 14.186-13.449 17.72l-.057.038c-5.37 3.69-13.927 5.562-25.433 5.562h-33.589v-33.542H599.8c3.127 0 6.075-1.464 7.966-3.955l1.292-1.702-55.651-128.328h39.807l29.019 71.632Z" fill="#181818"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M169.215 44.782c18.951-6.953 46.522-7.101 65.878 0 11.33 4.158 20.264 13.032 24.449 24.287 4.79 12.882 6.533 28.526 5.152 43.443h-24.583v.037h-65.179c.41 2.577.982 5.059 1.715 7.37 1.861 5.858 5.831 10.479 10.867 12.643 8.602 3.697 20.856 3.62 29.279 0 3.278-1.408 6.105-3.858 8.192-7.013h37.642a83.21 83.21 0 0 1-3.085 10.404c-4.185 11.255-13.119 20.13-24.449 24.287-18.952 6.953-46.522 7.102-65.878 0-11.33-4.157-20.264-13.032-24.449-24.287-7.433-19.989-7.532-46.628 0-66.884 4.185-11.255 13.118-20.13 24.449-24.287Zm47.578 27.677c-8.603-3.697-20.857-3.62-29.279 0-5.036 2.164-9.006 6.784-10.867 12.643-.743 2.343-1.319 4.85-1.729 7.447h54.471c-.412-2.606-.988-5.114-1.729-7.447-1.86-5.859-5.832-10.479-10.867-12.643Z" fill="#181818"></path><path d="M378.648 128.502c3.45 0 6.899.517 9.713 1.549a11.17 11.17 0 0 1 6.643 6.593c2.193 5.897 2.193 13.821 0 19.717a11.17 11.17 0 0 1-6.643 6.593c-5.627 2.064-13.797 2.065-19.425 0a11.169 11.169 0 0 1-6.642-6.593c-2.193-5.897-2.193-13.82 0-19.717a11.171 11.171 0 0 1 6.642-6.593c2.814-1.032 6.263-1.549 9.712-1.549Z" fill="#181818"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M65.903.003C77.23.086 88.545 1.779 97.93 5.13c11.33 4.046 20.264 12.682 24.449 23.636 6.612 17.304 6.612 40.557 0 57.862-3.242 8.485-11.757 20.051-24.617 24.331-11.68 3.887-22.044 4.364-30.822 4.401-9.654.042-24.818-1.286-31.185-7l-2.208 1.853c2.048 3.298 3.161 6.122 3.454 9.975 1.257 16.537 3.009 42.355 3.01 42.387H0V0h65.903v.003ZM37 82.796h29.677v-.034c4.638-.157 9.18-.952 12.953-2.371 5.035-1.895 9.006-5.94 10.866-11.068 2.38-6.563 2.264-17.01 0-23.253-1.86-5.128-5.83-9.171-10.866-11.066-3.817-1.436-8.353-2.22-12.953-2.373v-.03H37v50.195Z" fill="#181818"></path><path d="M377.154 75.518h-52.901v86.984h-37.099v-49.298c0-17.391 0-29.957 1.338-39.77 1.369-10.035 4.055-16.39 8.984-21.248 4.929-4.859 11.376-7.506 21.556-8.856 9.958-1.32 22.703-1.32 40.344-1.32h17.778v33.508Z" fill="#181818"></path></svg>
@@ -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<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.QINIU]: AccessConfigFieldsProviderQiniu,
[ACCESS_PROVIDERS.RAINYUN]: AccessConfigFieldsProviderRainYun,
[ACCESS_PROVIDERS.RATPANEL]: AccessConfigFieldsProviderRatPanel,
[ACCESS_PROVIDERS.REGRU]: AccessConfigFieldsProviderRegru,
[ACCESS_PROVIDERS.RFC2136]: AccessConfigFieldsProviderRFC2136,
[ACCESS_PROVIDERS.S3]: AccessConfigFieldsProviderS3,
[ACCESS_PROVIDERS.SAFELINE]: AccessConfigFieldsProviderSafeLine,
@@ -0,0 +1,98 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import FileTextInput from "@/components/FileTextInput";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderRegru = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item name={[parentNamePath, "username"]} initialValue={initialValues.username} label={t("access.form.regru_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.regru_username.placeholder")} />
</Form.Item>
<Form.Item name={[parentNamePath, "password"]} initialValue={initialValues.password} label={t("access.form.regru_password.label")} rules={[formRule]}>
<Input.Password autoComplete="new-password" placeholder={t("access.form.regru_password.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "mtlsCertificate"]}
initialValue={initialValues.mtlsCertificate}
label={t("access.form.regru_mtls_certificate.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.regru_mtls_certificate.tooltip") }}></span>}
>
<FileTextInput autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.regru_mtls_certificate.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "mtlsPrivateKey"]}
initialValue={initialValues.mtlsPrivateKey}
label={t("access.form.regru_mtls_private_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.regru_mtls_private_key.tooltip") }}></span>}
>
<FileTextInput autoSize={{ minRows: 1, maxRows: 5 }} placeholder={t("access.form.regru_mtls_private_key.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
username: "",
password: "",
mtlsCertificate: "",
mtlsPrivateKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
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;
+4
View File
@@ -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: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.NS1, "provider.ns1", "/imgs/providers/ns1.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.OVHCLOUD, "provider.ovhcloud", "/imgs/providers/ovhcloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.PORKBUN, "provider.porkbun", "/imgs/providers/porkbun.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.REGRU, "provider.regru", "/imgs/providers/regru.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.SPACESHIP, "provider.spaceship", "/imgs/providers/spaceship.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.VULTR, "provider.vultr", "/imgs/providers/vultr.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CMCCCLOUD, "provider.cmcccloud", "/imgs/providers/cmcccloud.svg", [ACCESS_USAGES.DNS]],
@@ -417,6 +419,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
QINGCLOUD: `${ACCESS_PROVIDERS.QINGCLOUD}`, // 兼容旧值,等同于 `QINGCLOUD_DNS`
QINGCLOUD_DNS: `${ACCESS_PROVIDERS.QINGCLOUD}-dns`,
RAINYUN: `${ACCESS_PROVIDERS.RAINYUN}`,
REGRU: `${ACCESS_PROVIDERS.REGRU}`,
RFC2136: `${ACCESS_PROVIDERS.RFC2136}`,
SPACESHIP: `${ACCESS_PROVIDERS.SPACESHIP}`,
UCLOUD: `${ACCESS_PROVIDERS.UCLOUD}`, // 兼容旧值,等同于 `UCLOUD_UDNR`
@@ -490,6 +493,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.NS1, "provider.ns1"],
[ACME_DNS01_PROVIDERS.OVHCLOUD, "provider.ovhcloud"],
[ACME_DNS01_PROVIDERS.PORKBUN, "provider.porkbun"],
[ACME_DNS01_PROVIDERS.REGRU, "provider.regru"],
[ACME_DNS01_PROVIDERS.SPACESHIP, "provider.spaceship"],
[ACME_DNS01_PROVIDERS.VERCEL, "provider.vercel"],
[ACME_DNS01_PROVIDERS.VULTR, "provider.vultr"],
+24
View File
@@ -1074,6 +1074,30 @@
"placeholder": "Please enter RatPanel access token",
"tooltip": "For more information, see <a href=\"https://ratpanel.github.io/advanced/api.html\" target=\"_blank\">https://ratpanel.github.io/advanced/api.html</a>"
},
"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 <a href=\"https://www.reg.ru/user/account/#/settings/api/\" target=\"_blank\">https://www.reg.ru/user/account/#/settings/api/api/</a>"
},
"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 <a href=\"https://www.reg.ru/user/account/#/settings/api/\" target=\"_blank\">https://www.reg.ru/user/account/#/settings/api/api/</a>"
},
"rfc2136_host": {
"label": "DNS server host",
"placeholder": "Please enter DNS server host"
@@ -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",
+24
View File
@@ -1072,6 +1072,30 @@
"placeholder": "请输入耗子面板 AccessToken",
"tooltip": "这是什么?请参阅 <a href=\"https://ratpanel.github.io/advanced/api.html\" target=\"_blank\">https://ratpanel.github.io/advanced/api.html</a>"
},
"regru_username": {
"label": "Рег.ру 用户名",
"placeholder": "请输入 Рег.ру 用户名"
},
"regru_password": {
"label": "Рег.ру 密码",
"placeholder": "请输入 Рег.ру 密码或 API 密码"
},
"regru_mtls_certificate": {
"label": "Рег.ру SSL 双向认证证书(可选)",
"placeholder": "请输入 Рег.ру SSL 双向认证证书",
"errmsg": {
"invalid": "无效的证书"
},
"tooltip": "这是什么?请参阅 <a href=\"https://www.reg.ru/user/account/#/settings/api/\" target=\"_blank\">https://www.reg.ru/user/account/#/settings/api/api/</a>"
},
"regru_mtls_private_key": {
"label": "Рег.ру SSL 双向认证私钥(可选)",
"placeholder": "请输入 Рег.ру SSL 双向认证私钥",
"errmsg": {
"invalid": "无效的私钥"
},
"tooltip": "这是什么?请参阅 <a href=\"https://www.reg.ru/user/account/#/settings/api/\" target=\"_blank\">https://www.reg.ru/user/account/#/settings/api/api/</a>"
},
"rfc2136_host": {
"label": "DNS 服务器地址",
"placeholder": "请输入 DNS 服务器地址"
@@ -198,6 +198,7 @@
"ratpanel": "耗子面板(又名:AcePanel)",
"ratpanel_common": "耗子面板",
"ratpanel_console": "耗子面板 - 面板自身",
"regru": "Рег.ру(REG.ru)",
"rfc2136": "Dynamic DNS Updates (RFC 2136)",
"s3": "对象存储(S3 兼容)",
"s3_upload": "上传到 S3 兼容的对象存储",