139 lines
4.4 KiB
Go
139 lines
4.4 KiB
Go
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"`
|
|
DNSAccountID string `json:"dns_account_id,omitempty"`
|
|
DNSBindingID string `json:"dns_binding_id,omitempty"`
|
|
InitialDNSRevision int64 `json:"-"`
|
|
ChallengeType string `json:"challenge_type"`
|
|
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,
|
|
ChallengeType: ChallengeDNS01,
|
|
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])
|
|
}
|