package domain import ( "crypto/rand" "fmt" "regexp" "strings" "time" ) var domainPattern = regexp.MustCompile(`^(?i)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$`) type CertificateType string const ( CertificateSingle CertificateType = "single" CertificateWildcard CertificateType = "wildcard" CertificateSAN CertificateType = "san" ) const ( ACMEEnvironmentStaging = "staging" ACMEEnvironmentProduction = "production" ) type Domain struct { ResourceScope ID string `json:"id"` Name string `json:"name"` CertificateType CertificateType `json:"certificate_type"` SANs []string `json:"sans"` AutoRenew bool `json:"auto_renew"` ACMEEnvironment string `json:"acme_environment"` DNSProvider string `json:"dns_provider,omitempty"` Status string `json:"status"` ExpiresAt *time.Time `json:"expires_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func NewDomain(name string, certificateType CertificateType, sans []string) (Domain, error) { return NewDomainWithOptions(name, certificateType, sans, true, ACMEEnvironmentStaging, "") } func NewDomainWithOptions(name string, certificateType CertificateType, sans []string, autoRenew bool, acmeEnvironment string, dnsProvider string) (Domain, error) { name = strings.ToLower(strings.TrimSpace(name)) if !IsValidName(name) { return Domain{}, fmt.Errorf("域名格式无效:%s", name) } if certificateType != CertificateSingle && certificateType != CertificateWildcard && certificateType != CertificateSAN { return Domain{}, fmt.Errorf("证书类型无效:%s", certificateType) } acmeEnvironment = strings.ToLower(strings.TrimSpace(acmeEnvironment)) if !IsValidACMEEnvironment(acmeEnvironment) { return Domain{}, fmt.Errorf("ACME 环境无效:%s", acmeEnvironment) } dnsProvider = strings.ToLower(strings.TrimSpace(dnsProvider)) cleanSANs := normalizeSANs(sans) if certificateType == CertificateSAN { cleanSANs = normalizeSANs(append([]string{name}, cleanSANs...)) } if certificateType == CertificateSAN && len(cleanSANs) < 2 { return Domain{}, fmt.Errorf("SAN 证书至少需要两个域名") } for _, san := range cleanSANs { if !IsValidName(san) { _, err := NewDomain(san, CertificateSingle, nil) return Domain{}, err } } if certificateType != CertificateSAN { cleanSANs = []string{name} } if certificateType == CertificateWildcard && !strings.HasPrefix(name, "*.") { return Domain{}, fmt.Errorf("泛域名必须以 *. 开头") } if certificateType != CertificateWildcard && strings.HasPrefix(name, "*.") { return Domain{}, fmt.Errorf("只有泛域名证书类型允许使用 *. 域名") } now := time.Now().UTC() return Domain{ ResourceScope: ResourceScope{ProjectID: DefaultProjectID}, ID: NewID(), Name: name, CertificateType: certificateType, SANs: cleanSANs, AutoRenew: autoRenew, ACMEEnvironment: acmeEnvironment, DNSProvider: dnsProvider, Status: "pending", CreatedAt: now, UpdatedAt: now, }, nil } func IsValidACMEEnvironment(value string) bool { return value == ACMEEnvironmentStaging || value == ACMEEnvironmentProduction } func IsValidName(name string) bool { name = strings.TrimSpace(strings.ToLower(name)) if strings.HasPrefix(name, "*.") { name = strings.TrimPrefix(name, "*.") } return domainPattern.MatchString(name) } func normalizeSANs(sans []string) []string { seen := make(map[string]struct{}, len(sans)) result := make([]string, 0, len(sans)) for _, item := range sans { item = strings.ToLower(strings.TrimSpace(item)) if item == "" { continue } if _, ok := seen[item]; ok { continue } seen[item] = struct{}{} result = append(result, item) } return result } func NewID() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { return fmt.Sprintf("%d", time.Now().UnixNano()) } b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) }