package dnsprovider import ( "errors" "fmt" "sort" "strings" ) const ( Cloudflare = "cloudflare" Aliyun = "aliyun" DNSPod = "dnspod" CNAME = "cname" HuaweiCloud = "huaweicloud" TencentCloud = "tencentcloud" Route53 = "route53" GoogleCloud = "googlecloud" Other = "other" ) var ErrProviderUnavailable = errors.New("DNS 服务商适配器尚未开放") type CredentialField struct { Name string `json:"name"` Label string `json:"label"` Secret bool `json:"secret"` Required bool `json:"required"` Placeholder string `json:"placeholder,omitempty"` } type Descriptor struct { ID string `json:"id"` Name string `json:"name"` Available bool `json:"available"` CredentialFields []CredentialField `json:"credential_fields"` Description string `json:"description"` } var descriptors = map[string]Descriptor{ Cloudflare: { ID: Cloudflare, Name: "Cloudflare", Available: true, Description: "使用 API Token 创建并清理 DNS-01 测试记录。", CredentialFields: []CredentialField{ {Name: "api_token", Label: "DNS API Token", Secret: true, Required: true, Placeholder: "需要 Zone:Read 与 DNS:Edit 权限"}, {Name: "zone_token", Label: "Zone API Token", Secret: true, Placeholder: "可选;用于拆分 Zone 读取权限"}, }, }, Aliyun: { ID: Aliyun, Name: "阿里云 DNS", Available: true, Description: "输入目标域名后,系统立即使用当前账号创建、查询并清理临时 TXT 记录,以验证该域名的实际 DNS 操作权限;不枚举账号域名,也不尝试其他托管区。", CredentialFields: []CredentialField{ {Name: "access_key_id", Label: "AccessKey ID", Required: true}, {Name: "access_key_secret", Label: "AccessKey Secret", Secret: true, Required: true}, {Name: "security_token", Label: "Security Token", Secret: true}, }, }, DNSPod: { ID: DNSPod, Name: "DNSPod", Available: true, Description: "使用 DNSPod API 凭据创建和清理 DNS-01 测试记录。", CredentialFields: []CredentialField{ {Name: "secret_id", Label: "SecretId", Required: true}, {Name: "secret_key", Label: "SecretKey", Secret: true, Required: true}, {Name: "session_token", Label: "Session Token", Secret: true}, }, }, CNAME: { ID: CNAME, Name: "CNAME 委派", Description: "使用已配置的 CNAME 将 DNS-01 挑战转发到专用验证域,并由目标 DNS 服务商创建和清理 TXT 记录。", Available: true, CredentialFields: []CredentialField{ {Name: "delegated_domain", Label: "专用验证域", Required: true, Placeholder: "例如 acme.example.com"}, {Name: "delegated_provider", Label: "委派目标 DNS 服务商", Required: true}, {Name: "delegated_credentials_json", Label: "委派凭据 JSON", Secret: true, Required: true, Placeholder: "例如 {\"api_token\":\"...\"}"}, }, }, HuaweiCloud: { ID: HuaweiCloud, Name: "华为云", Available: true, Description: "使用 Access Key、Secret Access Key 和区域创建 DNS-01 测试记录。", CredentialFields: []CredentialField{ {Name: "access_key_id", Label: "Access Key ID", Required: true}, {Name: "secret_access_key", Label: "Secret Access Key", Secret: true, Required: true}, {Name: "region", Label: "区域", Required: true, Placeholder: "例如 cn-north-4"}, }, }, TencentCloud: { ID: TencentCloud, Name: "腾讯云", Available: true, Description: "使用腾讯云 DNSPod API 凭据创建和清理 DNS-01 测试记录。", CredentialFields: []CredentialField{ {Name: "secret_id", Label: "SecretId", Required: true}, {Name: "secret_key", Label: "SecretKey", Secret: true, Required: true}, {Name: "region", Label: "区域", Placeholder: "可选"}, {Name: "session_token", Label: "Session Token", Secret: true}, }, }, Route53: { ID: Route53, Name: "AWS Route 53", Available: true, Description: "使用 AWS 凭据和区域创建和清理 DNS-01 测试记录。", CredentialFields: []CredentialField{ {Name: "access_key_id", Label: "Access Key ID", Required: true}, {Name: "secret_access_key", Label: "Secret Access Key", Secret: true, Required: true}, {Name: "region", Label: "区域", Required: true, Placeholder: "例如 us-east-1"}, {Name: "session_token", Label: "Session Token", Secret: true}, {Name: "hosted_zone_id", Label: "Hosted Zone ID"}, }, }, GoogleCloud: { ID: GoogleCloud, Name: "Google Cloud DNS", Available: true, Description: "使用 Google Cloud 服务账号创建和清理 DNS-01 测试记录。", CredentialFields: []CredentialField{ {Name: "service_account_json", Label: "服务账号 JSON", Secret: true, Required: true}, {Name: "project", Label: "项目 ID", Required: true}, {Name: "zone_id", Label: "托管区域 ID"}, }, }, Other: { ID: Other, Name: "其他服务商", Description: "扩展占位;需要实现专用 Provider 适配器后才能在线验证。", CredentialFields: []CredentialField{ {Name: "provider_name", Label: "服务商标识", Required: true}, {Name: "config_json", Label: "Provider 配置 JSON", Secret: true, Required: true}, }, }, } func Descriptors() []Descriptor { order := []string{Cloudflare, Aliyun, DNSPod, CNAME, HuaweiCloud, TencentCloud, Route53, GoogleCloud, Other} result := make([]Descriptor, 0, len(order)) for _, id := range order { result = append(result, descriptors[id]) } return result } func GetDescriptor(providerID string) (Descriptor, error) { providerID = strings.ToLower(strings.TrimSpace(providerID)) descriptor, ok := descriptors[providerID] if !ok { return Descriptor{}, fmt.Errorf("未知 DNS 服务商:%s", providerID) } return descriptor, nil } func ValidateCredentials(providerID string, credentials map[string]string) (map[string]string, error) { descriptor, err := GetDescriptor(providerID) if err != nil { return nil, err } cleaned := make(map[string]string, len(descriptor.CredentialFields)) allowed := make(map[string]struct{}, len(descriptor.CredentialFields)) for _, field := range descriptor.CredentialFields { allowed[field.Name] = struct{}{} value := strings.TrimSpace(credentials[field.Name]) if field.Required && value == "" { return nil, fmt.Errorf("%s不能为空", field.Label) } if value != "" { cleaned[field.Name] = value } } for name := range credentials { if _, ok := allowed[name]; !ok { // 旧版本曾把用户填写的范围随凭据保存。保留读取兼容, // 但绝不将它们带入新凭据或作为授权依据。 if providerID == Aliyun && (name == "managed_zones" || name == "allowed_domains") { continue } return nil, fmt.Errorf("DNS 凭据字段无效:%s", name) } } return cleaned, nil } func CredentialNames(providerID string) []string { descriptor, err := GetDescriptor(providerID) if err != nil { return nil } result := make([]string, 0, len(descriptor.CredentialFields)) for _, field := range descriptor.CredentialFields { result = append(result, field.Name) } sort.Strings(result) return result }