corrige bug com registro txt e tradução

This commit is contained in:
2026-07-12 03:41:49 -03:00
parent 1901055e25
commit e629b829a3
9 changed files with 362 additions and 222 deletions

View File

@@ -23,8 +23,8 @@ type Validator struct {
func NewValidator() (*Validator, error) {
v := validator.New()
if err := v.RegisterValidation("dns_fqdn", func(fl validator.FieldLevel) bool {
return IsFQDN(fl.Field().String())
if err := v.RegisterValidation("dns_name", func(fl validator.FieldLevel) bool {
return IsDNSName(fl.Field().String())
}); err != nil {
return nil, err
}
@@ -41,13 +41,13 @@ func (v *Validator) ValidateRRSet(name, recordType string, ttl uint64, contents
recordType = strings.ToUpper(strings.TrimSpace(recordType))
if ttl == 0 || ttl > 1<<32-1 {
return pdns.RRSet{}, fmt.Errorf("ttl must be between 1 and 4294967295")
return pdns.RRSet{}, fmt.Errorf("ttl deve estar entre 1 e 4294967295")
}
if err := v.validate.Var(name, "required,dns_fqdn"); err != nil {
return pdns.RRSet{}, fmt.Errorf("record name must be a fully qualified domain name")
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("unsupported record type %q", recordType)
return pdns.RRSet{}, fmt.Errorf("tipo de registro %q não suportado", recordType)
}
records := make([]pdns.Record, 0, len(contents))
@@ -62,7 +62,7 @@ func (v *Validator) ValidateRRSet(name, recordType string, ttl uint64, contents
records = append(records, pdns.Record{Content: content})
}
if len(records) == 0 {
return pdns.RRSet{}, fmt.Errorf("at least one record value is required")
return pdns.RRSet{}, fmt.Errorf("é obrigatório ao menos um valor de registro")
}
return pdns.RRSet{
@@ -81,7 +81,17 @@ func EnsureTrailingDot(value string) string {
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
@@ -99,7 +109,7 @@ func IsFQDN(value string) bool {
return false
}
for _, r := range label {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || (allowUnderscore && r == '_') {
continue
}
return false
@@ -113,18 +123,18 @@ func validateContent(recordType, content string) error {
case "A":
addr, err := netip.ParseAddr(content)
if err != nil || !addr.Is4() {
return fmt.Errorf("A record content must be an IPv4 address")
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("AAAA record content must be an IPv6 address")
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("%s record content must be a fully qualified domain name", recordType)
return fmt.Errorf("o conteúdo do registro %s precisa ser um domínio totalmente qualificado", recordType)
}
case "MX":
return validateMX(content)
@@ -134,7 +144,7 @@ func validateContent(recordType, content string) error {
return validateSRV(content)
case "TXT":
if !txtValuePattern.MatchString(content) {
return fmt.Errorf("TXT record content must be one or more quoted strings")
return fmt.Errorf("o conteúdo do registro TXT precisa ser uma ou mais strings entre aspas")
}
}
return nil
@@ -143,13 +153,13 @@ func validateContent(recordType, content string) error {
func validateMX(content string) error {
fields := strings.Fields(content)
if len(fields) != 2 {
return fmt.Errorf("MX record content must be: priority target")
return fmt.Errorf("o conteúdo do registro MX precisa ser: prioridade destino")
}
if _, err := parseUint(fields[0], 16); err != nil {
return fmt.Errorf("MX priority must be between 0 and 65535")
return fmt.Errorf("a prioridade MX deve estar entre 0 e 65535")
}
if !IsFQDN(fields[1]) {
return fmt.Errorf("MX target must be a fully qualified domain name")
return fmt.Errorf("o destino MX precisa ser um domínio totalmente qualificado")
}
return nil
}
@@ -157,16 +167,16 @@ func validateMX(content string) error {
func validateSRV(content string) error {
fields := strings.Fields(content)
if len(fields) != 4 {
return fmt.Errorf("SRV record content must be: priority weight port target")
return fmt.Errorf("o conteúdo do registro SRV precisa ser: prioridade peso porta destino")
}
for i, label := range []string{"priority", "weight", "port"} {
for i, label := range []string{"prioridade", "peso", "porta"} {
if _, err := parseUint(fields[i], 16); err != nil {
return fmt.Errorf("SRV %s must be between 0 and 65535", label)
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("SRV target must be a fully qualified domain name or .")
return fmt.Errorf("o destino SRV precisa ser um domínio totalmente qualificado ou .")
}
return nil
}
@@ -174,17 +184,17 @@ func validateSRV(content string) error {
func validateSOA(content string) error {
fields := strings.Fields(content)
if len(fields) != 7 {
return fmt.Errorf("SOA record content must be: primary hostmaster serial refresh retry expire minimum")
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("SOA primary nameserver must be a fully qualified domain name")
return fmt.Errorf("o servidor de nomes primario do SOA precisa ser um domínio totalmente qualificado")
}
if !IsFQDN(fields[1]) {
return fmt.Errorf("SOA hostmaster must be a fully qualified domain name")
return fmt.Errorf("o hostmaster do SOA precisa ser um domínio totalmente qualificado")
}
for i, label := range []string{"serial", "refresh", "retry", "expire", "minimum"} {
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 must be between 0 and 4294967295", label)
return fmt.Errorf("SOA %s deve estar entre 0 e 4294967295", label)
}
}
return nil
@@ -193,18 +203,18 @@ func validateSOA(content string) error {
func validateCAA(content string) error {
fields := strings.Fields(content)
if len(fields) < 3 {
return fmt.Errorf("CAA record content must be: flags tag \"value\"")
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("CAA flags must be between 0 and 255")
return fmt.Errorf("o campo flags do CAA deve estar entre 0 e 255")
}
if !caaTagPattern.MatchString(fields[1]) {
return fmt.Errorf("CAA tag must start with a letter and contain only letters, numbers, and hyphens")
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("CAA value must be quoted")
return fmt.Errorf("o valor CAA deve estar entre aspas")
}
return nil
}

View File

@@ -18,6 +18,7 @@ func TestValidateRRSetAcceptsSupportedRecords(t *testing.T) {
{"SOA", "ns1.example.org. hostmaster.example.org. 2026010101 3600 600 604800 300"},
{"SRV", "10 20 443 service.example.org."},
{"TXT", `"v=spf1 -all"`},
{"TXT", `"v=DMARC1; p=reject; rua=mailto:postmaster@example.org; ruf=mailto:postmaster@example.org"`},
}
for _, tc := range cases {
@@ -56,6 +57,68 @@ func TestValidateRRSetRejectsInvalidRecords(t *testing.T) {
}
}
func TestValidateRRSetAcceptsUnderscoresInOwnerNames(t *testing.T) {
v := newTestValidator(t)
cases := []struct {
name string
recordType string
content string
wantName string
}{
{
name: "_dmarc.example.org",
recordType: "TXT",
content: `"v=DMARC1; p=reject; rua=mailto:postmaster@example.org; ruf=mailto:postmaster@example.org"`,
wantName: "_dmarc.example.org.",
},
{
name: "_sip._tcp.example.org",
recordType: "SRV",
content: "10 20 5060 sip.example.org.",
wantName: "_sip._tcp.example.org.",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rrset, err := v.ValidateRRSet(tc.name, tc.recordType, 300, []string{tc.content})
if err != nil {
t.Fatalf("ValidateRRSet returned error: %v", err)
}
if rrset.Name != tc.wantName {
t.Fatalf("unexpected normalized name: %q", rrset.Name)
}
if rrset.Records[0].Content != tc.content {
t.Fatalf("unexpected content: %q", rrset.Records[0].Content)
}
})
}
}
func TestValidateRRSetRejectsUnderscoresInHostnameTargets(t *testing.T) {
v := newTestValidator(t)
cases := []struct {
recordType string
content string
}{
{"CNAME", "_target.example.org."},
{"MX", "10 _mail.example.org."},
{"NS", "_ns.example.org."},
{"SOA", "_ns.example.org. hostmaster.example.org. 2026010101 3600 600 604800 300"},
{"SRV", "10 20 5060 _sip.example.org."},
}
for _, tc := range cases {
t.Run(tc.recordType, func(t *testing.T) {
if _, err := v.ValidateRRSet("www.example.org.", tc.recordType, 300, []string{tc.content}); err == nil {
t.Fatal("expected validation error")
}
})
}
}
func TestValidateRRSetRequiresTTL(t *testing.T) {
v := newTestValidator(t)