diff --git a/go.mod b/go.mod
index 123c69ae..389cdaef 100644
--- a/go.mod
+++ b/go.mod
@@ -99,6 +99,8 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/libdns/dynv6 v1.0.0 // indirect
+ github.com/libdns/libdns v0.2.3 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
diff --git a/go.sum b/go.sum
index 04555b1e..bce17d98 100644
--- a/go.sum
+++ b/go.sum
@@ -646,6 +646,11 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/libdns/dynv6 v1.0.0 h1:JpOK9TYRTHETAe+SIw3lk8SgUi3eD250GK+4fAHu4ys=
+github.com/libdns/dynv6 v1.0.0/go.mod h1:65PL/bAlyH0J+0WGlOJYnMpoIuXcg/FmW4dTBYWtYUU=
+github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
+github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8=
+github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go
index 0dbf8844..dcfc1dde 100644
--- a/internal/applicant/providers.go
+++ b/internal/applicant/providers.go
@@ -15,6 +15,7 @@ import (
pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns"
pCMCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud"
pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla"
+ pDynv6 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6"
pGcore "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gcore"
pGname "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gname"
pGoDaddy "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/godaddy"
@@ -186,6 +187,21 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
+ case domain.ApplyDNSProviderTypeDynv6:
+ {
+ access := domain.AccessConfigForDynv6{}
+ if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ applicant, err := pDynv6.NewChallengeProvider(&pDynv6.ChallengeProviderConfig{
+ HttpToken: access.HttpToken,
+ DnsPropagationTimeout: options.DnsPropagationTimeout,
+ DnsTTL: options.DnsTTL,
+ })
+ return applicant, err
+ }
+
case domain.ApplyDNSProviderTypeGcore:
{
access := domain.AccessConfigForGcore{}
diff --git a/internal/domain/access.go b/internal/domain/access.go
index 963d4083..f19a0871 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -108,6 +108,10 @@ type AccessConfigForDogeCloud struct {
SecretKey string `json:"secretKey"`
}
+type AccessConfigForDynv6 struct {
+ HttpToken string `json:"httpToken"`
+}
+
type AccessConfigForEdgio struct {
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 6e0808ce..addb3c87 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -28,7 +28,7 @@ const (
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留)
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
- AccessProviderTypeDynv6 = AccessProviderType("dynv6") // dynv6(预留)
+ AccessProviderTypeDynv6 = AccessProviderType("dynv6")
AccessProviderTypeEdgio = AccessProviderType("edgio")
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly(预留)
AccessProviderTypeGname = AccessProviderType("gname")
@@ -80,6 +80,7 @@ const (
ApplyDNSProviderTypeClouDNS = ApplyDNSProviderType("cloudns")
ApplyDNSProviderTypeCMCCCloud = ApplyDNSProviderType("cmcccloud")
ApplyDNSProviderTypeDNSLA = ApplyDNSProviderType("dnsla")
+ ApplyDNSProviderTypeDynv6 = ApplyDNSProviderType("dynv6")
ApplyDNSProviderTypeGcore = ApplyDNSProviderType("gcore")
ApplyDNSProviderTypeGname = ApplyDNSProviderType("gname")
ApplyDNSProviderTypeGoDaddy = ApplyDNSProviderType("godaddy")
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/dnsla.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/dnsla.go
new file mode 100644
index 00000000..e5a1ea3c
--- /dev/null
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/dnsla.go
@@ -0,0 +1,37 @@
+package dynv6
+
+import (
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+
+ internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal"
+)
+
+type ChallengeProviderConfig struct {
+ HttpToken string `json:"httpToken"`
+ 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 := internal.NewDefaultConfig()
+ providerConfig.HTTPToken = config.HttpToken
+ if config.DnsPropagationTimeout != 0 {
+ providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
+ }
+ if config.DnsTTL != 0 {
+ providerConfig.TTL = int(config.DnsTTL)
+ }
+
+ provider, err := internal.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal/lego.go
new file mode 100644
index 00000000..f83949a2
--- /dev/null
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6/internal/lego.go
@@ -0,0 +1,167 @@
+package lego_dynv6
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/config/env"
+ "github.com/libdns/dynv6"
+ "github.com/libdns/libdns"
+)
+
+const (
+ envNamespace = "DYNV6_"
+
+ EnvHTTPToken = envNamespace + "HTTP_TOKEN"
+
+ EnvTTL = envNamespace + "TTL"
+ EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+ EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
+)
+
+var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
+
+type Config struct {
+ HTTPToken string
+
+ PropagationTimeout time.Duration
+ PollingInterval time.Duration
+ TTL int
+}
+
+type DNSProvider struct {
+ client *dynv6.Provider
+ config *Config
+}
+
+func NewDefaultConfig() *Config {
+ return &Config{
+ TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
+ }
+}
+
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvHTTPToken)
+ if err != nil {
+ return nil, fmt.Errorf("dynv6: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.HTTPToken = values[EnvHTTPToken]
+
+ return NewDNSProviderConfig(config)
+}
+
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("dynv6: the configuration of the DNS provider is nil")
+ }
+
+ client := &dynv6.Provider{Token: config.HTTPToken}
+
+ return &DNSProvider{
+ client: client,
+ config: config,
+ }, nil
+}
+
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("dynv6: %w", err)
+ }
+
+ subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+ if err != nil {
+ return fmt.Errorf("dynv6: %w", err)
+ }
+
+ if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil {
+ return fmt.Errorf("dynv6: %w", err)
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("dynv6: %w", err)
+ }
+
+ subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+ if err != nil {
+ return fmt.Errorf("dynv6: %w", err)
+ }
+
+ if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil {
+ return fmt.Errorf("dynv6: %w", err)
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
+}
+
+func (d *DNSProvider) getDNSRecord(zoneName, subDomain string) (*libdns.Record, error) {
+ records, err := d.client.GetRecords(context.Background(), zoneName)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, record := range records {
+ if record.Type == "TXT" && record.Name == subDomain {
+ return &record, nil
+ }
+ }
+
+ return nil, nil
+}
+
+func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error {
+ record, err := d.getDNSRecord(zoneName, subDomain)
+ if err != nil {
+ return err
+ }
+
+ if record == nil {
+ record = &libdns.Record{
+ Type: "TXT",
+ Name: subDomain,
+ Value: value,
+ TTL: time.Duration(d.config.TTL) * time.Second,
+ }
+ _, err := d.client.AppendRecords(context.Background(), zoneName, []libdns.Record{*record})
+ return err
+ } else {
+ record.Value = value
+ _, err := d.client.SetRecords(context.Background(), zoneName, []libdns.Record{*record})
+ return err
+ }
+}
+
+func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error {
+ record, err := d.getDNSRecord(zoneName, subDomain)
+ if err != nil {
+ return err
+ }
+
+ if record == nil {
+ return nil
+ } else {
+ _, err = d.client.DeleteRecords(context.Background(), zoneName, []libdns.Record{*record})
+ return err
+ }
+}
diff --git a/ui/public/imgs/providers/dynv6.svg b/ui/public/imgs/providers/dynv6.svg
new file mode 100644
index 00000000..652e4e45
--- /dev/null
+++ b/ui/public/imgs/providers/dynv6.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx
index caf5c605..63a66875 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -25,6 +25,7 @@ import AccessFormClouDNSConfig from "./AccessFormClouDNSConfig";
import AccessFormCMCCCloudConfig from "./AccessFormCMCCCloudConfig";
import AccessFormDNSLAConfig from "./AccessFormDNSLAConfig";
import AccessFormDogeCloudConfig from "./AccessFormDogeCloudConfig";
+import AccessFormDynv6Config from "./AccessFormDynv6Config";
import AccessFormEdgioConfig from "./AccessFormEdgioConfig";
import AccessFormGcoreConfig from "./AccessFormGcoreConfig";
import AccessFormGnameConfig from "./AccessFormGnameConfig";
@@ -133,6 +134,8 @@ const AccessForm = forwardRef(({ className,
return ;
case ACCESS_PROVIDERS.DOGECLOUD:
return ;
+ case ACCESS_PROVIDERS.DYNV6:
+ return ;
case ACCESS_PROVIDERS.GCORE:
return ;
case ACCESS_PROVIDERS.GNAME:
diff --git a/ui/src/components/access/AccessFormDynv6Config.tsx b/ui/src/components/access/AccessFormDynv6Config.tsx
new file mode 100644
index 00000000..92385302
--- /dev/null
+++ b/ui/src/components/access/AccessFormDynv6Config.tsx
@@ -0,0 +1,61 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { type AccessConfigForDynv6 } from "@/domain/access";
+
+type AccessFormDynv6ConfigFieldValues = Nullish;
+
+export type AccessFormDynv6ConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: AccessFormDynv6ConfigFieldValues;
+ onValuesChange?: (values: AccessFormDynv6ConfigFieldValues) => void;
+};
+
+const initFormModel = (): AccessFormDynv6ConfigFieldValues => {
+ return {
+ httpToken: "",
+ };
+};
+
+const AccessFormDynv6Config = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormDynv6ConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ httpToken: z
+ .string()
+ .min(1, t("access.form.dynv6_http_token.placeholder"))
+ .max(256, t("common.errmsg.string_max", { max: 256 }))
+ .trim(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ );
+};
+
+export default AccessFormDynv6Config;
diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts
index 59a41cc6..8c011857 100644
--- a/ui/src/domain/access.ts
+++ b/ui/src/domain/access.ts
@@ -22,6 +22,7 @@ export interface AccessModel extends BaseModel {
| AccessConfigForCMCCCloud
| AccessConfigForDNSLA
| AccessConfigForDogeCloud
+ | AccessConfigForDynv6
| AccessConfigForEdgio
| AccessConfigForGcore
| AccessConfigForGname
@@ -132,6 +133,10 @@ export type AccessConfigForDogeCloud = {
secretKey: string;
};
+export type AccessConfigForDynv6 = {
+ httpToken: string;
+};
+
export type AccessConfigForEdgio = {
clientId: string;
clientSecret: string;
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index b27c23da..999ae22d 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -20,6 +20,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
CMCCCLOUD: "cmcccloud",
DNSLA: "dnsla",
DOGECLOUD: "dogecloud",
+ DYNV6: "dynv6",
GCORE: "gcore",
GNAME: "gname",
GODADDY: "godaddy",
@@ -97,6 +98,7 @@ export const accessProvidersMap: Maphttps://console.dogecloud.com/",
+ "access.form.dynv6_http_token.label": "dynv6 HTTP token",
+ "access.form.dynv6_http_token.placeholder": "Please enter dynv6 HTTP token",
+ "access.form.dynv6_http_token.tooltip": "For more information, see https://dynv6.com/keys",
"access.form.edgio_client_id.label": "Edgio ClientId",
"access.form.edgio_client_id.placeholder": "Please enter Edgio ClientId",
"access.form.edgio_client_id.tooltip": "For more information, see https://docs.edg.io/applications/v7/rest_api/authentication#administering-api-clients",
diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json
index 88219c27..d68d813f 100644
--- a/ui/src/i18n/locales/en/nls.provider.json
+++ b/ui/src/i18n/locales/en/nls.provider.json
@@ -47,6 +47,7 @@
"provider.dnsla": "DNS.LA",
"provider.dogecloud": "Doge Cloud",
"provider.dogecloud.cdn": "Doge Cloud - CDN (Content Delivery Network)",
+ "provider.dynv6": "dynv6",
"provider.edgio": "Edgio",
"provider.edgio.applications": "Edgio - Applications",
"provider.fastly": "Fastly",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index bb2f829a..12c10595 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -132,6 +132,9 @@
"access.form.dogecloud_secret_key.label": "多吉云 SecretKey",
"access.form.dogecloud_secret_key.placeholder": "请输入多吉云 SecretKey",
"access.form.dogecloud_secret_key.tooltip": "这是什么?请参阅 https://console.dogecloud.com/",
+ "access.form.dynv6_http_token.label": "dynv6 HTTP Token",
+ "access.form.dynv6_http_token.placeholder": "请输入 dynv6 HTTP Token",
+ "access.form.dynv6_http_token.tooltip": "这是什么?请参阅 https://dynv6.com/keys",
"access.form.edgio_client_id.label": "Edgio 客户端 ID",
"access.form.edgio_client_id.placeholder": "请输入 Edgio 客户端 ID",
"access.form.edgio_client_id.tooltip": "这是什么?请参阅 https://docs.edg.io/applications/v7/rest_api/authentication#administering-api-clients",
diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json
index d400cfbc..7b7098f9 100644
--- a/ui/src/i18n/locales/zh/nls.provider.json
+++ b/ui/src/i18n/locales/zh/nls.provider.json
@@ -47,6 +47,7 @@
"provider.dnsla": "DNS.LA",
"provider.dogecloud": "多吉云",
"provider.dogecloud.cdn": "多吉云 - 内容分发网络 CDN",
+ "provider.dynv6": "dynv6",
"provider.edgio": "Edgio",
"provider.edgio.applications": "Edgio - Applications",
"provider.fastly": "Fastly",