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",