package dnsrecord import ( "fmt" "net/netip" "regexp" "strconv" "strings" "github.com/go-playground/validator/v10" "pdns_admin/internal/pdns" ) var ( txtValuePattern = regexp.MustCompile(`^"([^"\\]|\\.)*"( "([^"\\]|\\.)*")*$`) caaTagPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9-]*$`) ) type Validator struct { validate *validator.Validate } func NewValidator() (*Validator, error) { v := validator.New() if err := v.RegisterValidation("dns_name", func(fl validator.FieldLevel) bool { return IsDNSName(fl.Field().String()) }); err != nil { return nil, err } return &Validator{validate: v}, nil } func SupportedTypes() []string { return []string{"A", "AAAA", "CAA", "CNAME", "MX", "NS", "SOA", "SRV", "TXT"} } func (v *Validator) ValidateRRSet(name, recordType string, ttl uint64, contents []string) (pdns.RRSet, error) { name = EnsureTrailingDot(name) recordType = strings.ToUpper(strings.TrimSpace(recordType)) if ttl == 0 || ttl > 1<<32-1 { return pdns.RRSet{}, fmt.Errorf("ttl deve estar entre 1 e 4294967295") } if err := v.validate.Var(name, "required,dns_name"); err != nil { return pdns.RRSet{}, fmt.Errorf("o nome do registro precisa ser um domínio totalmente qualificado") } if !supported(recordType) { return pdns.RRSet{}, fmt.Errorf("tipo de registro %q não suportado", recordType) } records := make([]pdns.Record, 0, len(contents)) for _, content := range contents { content = strings.TrimSpace(content) if content == "" { continue } if err := validateContent(recordType, content); err != nil { return pdns.RRSet{}, err } records = append(records, pdns.Record{Content: content}) } if len(records) == 0 { return pdns.RRSet{}, fmt.Errorf("é obrigatório ao menos um valor de registro") } return pdns.RRSet{ Name: name, Type: recordType, TTL: uint32(ttl), Records: records, }, nil } func EnsureTrailingDot(value string) string { value = strings.TrimSpace(value) if value == "" || strings.HasSuffix(value, ".") { return value } return value + "." } // IsFQDN reports whether value is an absolute hostname-style DNS name. func IsFQDN(value string) bool { return isDomainName(value, false) } // IsDNSName reports whether value is an absolute DNS owner name. func IsDNSName(value string) bool { return isDomainName(value, true) } func isDomainName(value string, allowUnderscore bool) bool { value = strings.TrimSpace(value) if value == "." || value == "" || !strings.HasSuffix(value, ".") || len(value) > 253 { return false } labels := strings.Split(strings.TrimSuffix(value, "."), ".") for i, label := range labels { if label == "" || len(label) > 63 { return false } if label == "*" && i == 0 { continue } if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") { return false } for _, r := range label { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || (allowUnderscore && r == '_') { continue } return false } } return true } func validateContent(recordType, content string) error { switch recordType { case "A": addr, err := netip.ParseAddr(content) if err != nil || !addr.Is4() { return fmt.Errorf("o conteúdo do registro A precisa ser um endereco IPv4") } case "AAAA": addr, err := netip.ParseAddr(content) if err != nil || !addr.Is6() { return fmt.Errorf("o conteúdo do registro AAAA precisa ser um endereco IPv6") } case "CAA": return validateCAA(content) case "CNAME", "NS": if !IsFQDN(content) { return fmt.Errorf("o conteúdo do registro %s precisa ser um domínio totalmente qualificado", recordType) } case "MX": return validateMX(content) case "SOA": return validateSOA(content) case "SRV": return validateSRV(content) case "TXT": if !txtValuePattern.MatchString(content) { return fmt.Errorf("o conteúdo do registro TXT precisa ser uma ou mais strings entre aspas") } } return nil } func validateMX(content string) error { fields := strings.Fields(content) if len(fields) != 2 { return fmt.Errorf("o conteúdo do registro MX precisa ser: prioridade destino") } if _, err := parseUint(fields[0], 16); err != nil { return fmt.Errorf("a prioridade MX deve estar entre 0 e 65535") } if !IsFQDN(fields[1]) { return fmt.Errorf("o destino MX precisa ser um domínio totalmente qualificado") } return nil } func validateSRV(content string) error { fields := strings.Fields(content) if len(fields) != 4 { return fmt.Errorf("o conteúdo do registro SRV precisa ser: prioridade peso porta destino") } for i, label := range []string{"prioridade", "peso", "porta"} { if _, err := parseUint(fields[i], 16); err != nil { return fmt.Errorf("o campo SRV %s deve estar entre 0 e 65535", label) } } target := strings.TrimSpace(fields[3]) if target != "." && !IsFQDN(target) { return fmt.Errorf("o destino SRV precisa ser um domínio totalmente qualificado ou .") } return nil } func validateSOA(content string) error { fields := strings.Fields(content) if len(fields) != 7 { return fmt.Errorf("o conteúdo do registro SOA precisa ser: primario hostmaster serial atualizacao nova-tentativa expiracao minimo") } if !IsFQDN(fields[0]) { return fmt.Errorf("o servidor de nomes primario do SOA precisa ser um domínio totalmente qualificado") } if !IsFQDN(fields[1]) { return fmt.Errorf("o hostmaster do SOA precisa ser um domínio totalmente qualificado") } for i, label := range []string{"serial", "atualizacao", "nova-tentativa", "expiracao", "minimo"} { if _, err := parseUint(fields[i+2], 32); err != nil { return fmt.Errorf("SOA %s deve estar entre 0 e 4294967295", label) } } return nil } func validateCAA(content string) error { fields := strings.Fields(content) if len(fields) < 3 { return fmt.Errorf("o conteúdo do registro CAA precisa ser: flags tag \"valor\"") } flags, err := parseUint(fields[0], 8) if err != nil || flags > 255 { return fmt.Errorf("o campo flags do CAA deve estar entre 0 e 255") } if !caaTagPattern.MatchString(fields[1]) { return fmt.Errorf("a tag CAA deve começar com uma letra e conter apenas letras, numeros e hífens") } value := strings.Join(fields[2:], " ") if !txtValuePattern.MatchString(value) { return fmt.Errorf("o valor CAA deve estar entre aspas") } return nil } func parseUint(value string, bits int) (uint64, error) { return strconv.ParseUint(value, 10, bits) } func supported(recordType string) bool { for _, candidate := range SupportedTypes() { if candidate == recordType { return true } } return false }