184 lines
7.0 KiB
Go
184 lines
7.0 KiB
Go
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
|
|
}
|