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) { func NewValidator() (*Validator, error) {
v := validator.New() v := validator.New()
if err := v.RegisterValidation("dns_fqdn", func(fl validator.FieldLevel) bool { if err := v.RegisterValidation("dns_name", func(fl validator.FieldLevel) bool {
return IsFQDN(fl.Field().String()) return IsDNSName(fl.Field().String())
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
@@ -41,13 +41,13 @@ func (v *Validator) ValidateRRSet(name, recordType string, ttl uint64, contents
recordType = strings.ToUpper(strings.TrimSpace(recordType)) recordType = strings.ToUpper(strings.TrimSpace(recordType))
if ttl == 0 || ttl > 1<<32-1 { 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 { if err := v.validate.Var(name, "required,dns_name"); err != nil {
return pdns.RRSet{}, fmt.Errorf("record name must be a fully qualified domain name") return pdns.RRSet{}, fmt.Errorf("o nome do registro precisa ser um domínio totalmente qualificado")
} }
if !supported(recordType) { 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)) 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}) records = append(records, pdns.Record{Content: content})
} }
if len(records) == 0 { 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{ return pdns.RRSet{
@@ -81,7 +81,17 @@ func EnsureTrailingDot(value string) string {
return value + "." return value + "."
} }
// IsFQDN reports whether value is an absolute hostname-style DNS name.
func IsFQDN(value string) bool { 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) value = strings.TrimSpace(value)
if value == "." || value == "" || !strings.HasSuffix(value, ".") || len(value) > 253 { if value == "." || value == "" || !strings.HasSuffix(value, ".") || len(value) > 253 {
return false return false
@@ -99,7 +109,7 @@ func IsFQDN(value string) bool {
return false return false
} }
for _, r := range label { 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 continue
} }
return false return false
@@ -113,18 +123,18 @@ func validateContent(recordType, content string) error {
case "A": case "A":
addr, err := netip.ParseAddr(content) addr, err := netip.ParseAddr(content)
if err != nil || !addr.Is4() { 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": case "AAAA":
addr, err := netip.ParseAddr(content) addr, err := netip.ParseAddr(content)
if err != nil || !addr.Is6() { 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": case "CAA":
return validateCAA(content) return validateCAA(content)
case "CNAME", "NS": case "CNAME", "NS":
if !IsFQDN(content) { 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": case "MX":
return validateMX(content) return validateMX(content)
@@ -134,7 +144,7 @@ func validateContent(recordType, content string) error {
return validateSRV(content) return validateSRV(content)
case "TXT": case "TXT":
if !txtValuePattern.MatchString(content) { 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 return nil
@@ -143,13 +153,13 @@ func validateContent(recordType, content string) error {
func validateMX(content string) error { func validateMX(content string) error {
fields := strings.Fields(content) fields := strings.Fields(content)
if len(fields) != 2 { 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 { 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]) { 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 return nil
} }
@@ -157,16 +167,16 @@ func validateMX(content string) error {
func validateSRV(content string) error { func validateSRV(content string) error {
fields := strings.Fields(content) fields := strings.Fields(content)
if len(fields) != 4 { 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 { 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]) target := strings.TrimSpace(fields[3])
if target != "." && !IsFQDN(target) { 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 return nil
} }
@@ -174,17 +184,17 @@ func validateSRV(content string) error {
func validateSOA(content string) error { func validateSOA(content string) error {
fields := strings.Fields(content) fields := strings.Fields(content)
if len(fields) != 7 { 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]) { 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]) { 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 { 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 return nil
@@ -193,18 +203,18 @@ func validateSOA(content string) error {
func validateCAA(content string) error { func validateCAA(content string) error {
fields := strings.Fields(content) fields := strings.Fields(content)
if len(fields) < 3 { 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) flags, err := parseUint(fields[0], 8)
if err != nil || flags > 255 { 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]) { 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:], " ") value := strings.Join(fields[2:], " ")
if !txtValuePattern.MatchString(value) { if !txtValuePattern.MatchString(value) {
return fmt.Errorf("CAA value must be quoted") return fmt.Errorf("o valor CAA deve estar entre aspas")
} }
return nil 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"}, {"SOA", "ns1.example.org. hostmaster.example.org. 2026010101 3600 600 604800 300"},
{"SRV", "10 20 443 service.example.org."}, {"SRV", "10 20 443 service.example.org."},
{"TXT", `"v=spf1 -all"`}, {"TXT", `"v=spf1 -all"`},
{"TXT", `"v=DMARC1; p=reject; rua=mailto:postmaster@example.org; ruf=mailto:postmaster@example.org"`},
} }
for _, tc := range cases { 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) { func TestValidateRRSetRequiresTTL(t *testing.T) {
v := newTestValidator(t) v := newTestValidator(t)

View File

@@ -159,14 +159,14 @@ func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) { func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" { if r.URL.Path != "/" {
http.NotFound(w, r) http.Error(w, "pagina nao encontrada", http.StatusNotFound)
return return
} }
server, serverErr := s.client.GetServer(r.Context()) server, serverErr := s.client.GetServer(r.Context())
zones, zonesErr := s.client.ListZones(r.Context()) zones, zonesErr := s.client.ListZones(r.Context())
data := pageData{ data := pageData{
Title: "Dashboard", Title: "Painel",
Server: server, Server: server,
Zones: zones, Zones: zones,
Error: firstNonEmpty(errorText(serverErr), errorText(zonesErr)), Error: firstNonEmpty(errorText(serverErr), errorText(zonesErr)),
@@ -185,7 +185,7 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
} }
data := pageData{ data := pageData{
Title: "Login", Title: "Entrar",
Error: r.URL.Query().Get("error"), Error: r.URL.Query().Get("error"),
Next: safeRedirectPath(r.URL.Query().Get("next")), Next: safeRedirectPath(r.URL.Query().Get("next")),
} }
@@ -198,7 +198,7 @@ func (s *Server) loginPost(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Redirect(w, r, "/login?error="+url.QueryEscape("invalid form data"), http.StatusSeeOther) http.Redirect(w, r, "/login?error="+url.QueryEscape("dados do formulario invalidos"), http.StatusSeeOther)
return return
} }
@@ -207,18 +207,18 @@ func (s *Server) loginPost(w http.ResponseWriter, r *http.Request) {
allowed, err := s.auth.Authenticate(r.Context(), username, password) allowed, err := s.auth.Authenticate(r.Context(), username, password)
if err != nil { if err != nil {
s.logger.Printf("authentication failed for %q: %v", username, err) s.logger.Printf("authentication failed for %q: %v", username, err)
http.Redirect(w, r, "/login?error="+url.QueryEscape("authentication backend failed"), http.StatusSeeOther) http.Redirect(w, r, "/login?error="+url.QueryEscape("falha no servico de autenticacao"), http.StatusSeeOther)
return return
} }
if !allowed { if !allowed {
http.Redirect(w, r, "/login?error="+url.QueryEscape("invalid username or password"), http.StatusSeeOther) http.Redirect(w, r, "/login?error="+url.QueryEscape("usuario ou senha invalidos"), http.StatusSeeOther)
return return
} }
token, err := s.createSession(username) token, err := s.createSession(username)
if err != nil { if err != nil {
s.logger.Printf("create session: %v", err) s.logger.Printf("create session: %v", err)
http.Error(w, "session creation failed", http.StatusInternalServerError) http.Error(w, "falha ao criar sessao", http.StatusInternalServerError)
return return
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
@@ -255,7 +255,7 @@ func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
func (s *Server) listZones(w http.ResponseWriter, r *http.Request) { func (s *Server) listZones(w http.ResponseWriter, r *http.Request) {
zones, err := s.client.ListZones(r.Context()) zones, err := s.client.ListZones(r.Context())
data := pageData{ data := pageData{
Title: "Zones", Title: "Zonas",
Zones: zones, Zones: zones,
Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)), Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)),
} }
@@ -264,30 +264,30 @@ func (s *Server) listZones(w http.ResponseWriter, r *http.Request) {
func (s *Server) createZone(w http.ResponseWriter, r *http.Request) { func (s *Server) createZone(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.redirectZonesError(w, r, "invalid form data") s.redirectZonesError(w, r, "dados do formulario invalidos")
return return
} }
zoneName := dnsrecord.EnsureTrailingDot(r.FormValue("name")) zoneName := dnsrecord.EnsureTrailingDot(r.FormValue("name"))
if !dnsrecord.IsFQDN(zoneName) { if !dnsrecord.IsFQDN(zoneName) {
s.redirectZonesError(w, r, "zone name must be a fully qualified domain name") s.redirectZonesError(w, r, "o nome da zona deve ser um nome de dominio totalmente qualificado")
return return
} }
kind := strings.TrimSpace(r.FormValue("kind")) kind := strings.TrimSpace(r.FormValue("kind"))
if !validZoneKind(kind) { if !validZoneKind(kind) {
s.redirectZonesError(w, r, "zone kind must be Native, Master, or Slave") s.redirectZonesError(w, r, "o tipo da zona deve ser Native, Master ou Slave")
return return
} }
nameservers, err := parseFQDNLines(r.FormValue("nameservers"), "nameserver") nameservers, err := parseFQDNLines(r.FormValue("nameservers"), "servidor de nomes")
if err != nil { if err != nil {
s.redirectZonesError(w, r, err.Error()) s.redirectZonesError(w, r, err.Error())
return return
} }
masters := parseLines(r.FormValue("masters")) masters := parseLines(r.FormValue("masters"))
if kind == "Slave" && len(masters) == 0 { if kind == "Slave" && len(masters) == 0 {
s.redirectZonesError(w, r, "slave zones require at least one master address") s.redirectZonesError(w, r, "zonas Slave exigem ao menos um endereco de servidor mestre")
return return
} }
@@ -317,7 +317,7 @@ func (s *Server) showZone(w http.ResponseWriter, r *http.Request) {
zoneID := r.PathValue("zoneID") zoneID := r.PathValue("zoneID")
zone, err := s.client.GetZone(r.Context(), zoneID) zone, err := s.client.GetZone(r.Context(), zoneID)
data := pageData{ data := pageData{
Title: "Zone " + zoneID, Title: "Zona " + zoneID,
ZoneID: zoneID, ZoneID: zoneID,
Zone: zone, Zone: zone,
Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)), Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)),
@@ -329,7 +329,7 @@ func (s *Server) newRRSet(w http.ResponseWriter, r *http.Request) {
zoneID := r.PathValue("zoneID") zoneID := r.PathValue("zoneID")
zone, err := s.client.GetZone(r.Context(), zoneID) zone, err := s.client.GetZone(r.Context(), zoneID)
data := pageData{ data := pageData{
Title: "Add record", Title: "Adicionar registro",
ZoneID: zoneID, ZoneID: zoneID,
Zone: zone, Zone: zone,
Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)), Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)),
@@ -337,8 +337,8 @@ func (s *Server) newRRSet(w http.ResponseWriter, r *http.Request) {
RecordForm: recordForm{ RecordForm: recordForm{
Type: "A", Type: "A",
TTL: 300, TTL: 300,
Title: "Add record", Title: "Adicionar registro",
SubmitLabel: "Create record", SubmitLabel: "Criar registro",
}, },
} }
s.render(w, r, "record_form.html", data) s.render(w, r, "record_form.html", data)
@@ -356,7 +356,7 @@ func (s *Server) editRRSet(w http.ResponseWriter, r *http.Request) {
rrset, ok := findRRSet(zone, name, recordType) rrset, ok := findRRSet(zone, name, recordType)
if !ok { if !ok {
s.redirectZoneError(w, r, zoneID, "record not found") s.redirectZoneError(w, r, zoneID, "registro nao encontrado")
return return
} }
@@ -366,11 +366,11 @@ func (s *Server) editRRSet(w http.ResponseWriter, r *http.Request) {
TTL: rrset.TTL, TTL: rrset.TTL,
Records: recordValues(rrset), Records: recordValues(rrset),
IsEdit: true, IsEdit: true,
Title: "Edit record", Title: "Editar registro",
SubmitLabel: "Save record", SubmitLabel: "Salvar registro",
} }
data := pageData{ data := pageData{
Title: "Edit record", Title: "Editar registro",
ZoneID: zoneID, ZoneID: zoneID,
Zone: zone, Zone: zone,
Error: r.URL.Query().Get("error"), Error: r.URL.Query().Get("error"),
@@ -383,13 +383,13 @@ func (s *Server) editRRSet(w http.ResponseWriter, r *http.Request) {
func (s *Server) saveRRSet(w http.ResponseWriter, r *http.Request) { func (s *Server) saveRRSet(w http.ResponseWriter, r *http.Request) {
zoneID := r.PathValue("zoneID") zoneID := r.PathValue("zoneID")
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.redirectZoneError(w, r, zoneID, "invalid form data") s.redirectZoneError(w, r, zoneID, "dados do formulario invalidos")
return return
} }
ttl, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("ttl")), 10, 32) ttl, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("ttl")), 10, 32)
if err != nil { if err != nil {
s.redirectZoneError(w, r, zoneID, "ttl must be a positive integer") s.redirectZoneError(w, r, zoneID, "ttl deve ser um numero inteiro positivo")
return return
} }
@@ -412,18 +412,18 @@ func (s *Server) saveEditedRRSet(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name") name := r.URL.Query().Get("name")
recordType := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("type"))) recordType := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("type")))
if name == "" || recordType == "" { if name == "" || recordType == "" {
s.redirectZoneError(w, r, zoneID, "record identity is required") s.redirectZoneError(w, r, zoneID, "a identidade do registro e obrigatoria")
return return
} }
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.redirectZoneError(w, r, zoneID, "invalid form data") s.redirectZoneError(w, r, zoneID, "dados do formulario invalidos")
return return
} }
ttl, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("ttl")), 10, 32) ttl, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("ttl")), 10, 32)
if err != nil { if err != nil {
s.redirectZoneError(w, r, zoneID, "ttl must be a positive integer") s.redirectZoneError(w, r, zoneID, "ttl deve ser um numero inteiro positivo")
return return
} }
@@ -443,18 +443,18 @@ func (s *Server) saveEditedRRSet(w http.ResponseWriter, r *http.Request) {
func (s *Server) deleteRRSet(w http.ResponseWriter, r *http.Request) { func (s *Server) deleteRRSet(w http.ResponseWriter, r *http.Request) {
zoneID := r.PathValue("zoneID") zoneID := r.PathValue("zoneID")
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
s.redirectZoneError(w, r, zoneID, "invalid form data") s.redirectZoneError(w, r, zoneID, "dados do formulario invalidos")
return return
} }
name := dnsrecord.EnsureTrailingDot(r.FormValue("name")) name := dnsrecord.EnsureTrailingDot(r.FormValue("name"))
recordType := strings.ToUpper(strings.TrimSpace(r.FormValue("type"))) recordType := strings.ToUpper(strings.TrimSpace(r.FormValue("type")))
if name == "." || recordType == "" { if name == "." || recordType == "" {
s.redirectZoneError(w, r, zoneID, "record name and type are required") s.redirectZoneError(w, r, zoneID, "nome e tipo do registro sao obrigatorios")
return return
} }
if isSOA(recordType) { if isSOA(recordType) {
s.redirectZoneError(w, r, zoneID, "SOA records are required for zones and cannot be deleted") s.redirectZoneError(w, r, zoneID, "registros SOA sao obrigatorios para zonas e nao podem ser excluidos")
return return
} }
@@ -475,7 +475,7 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, dat
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl, ok := s.templates[name] tmpl, ok := s.templates[name]
if !ok { if !ok {
http.Error(w, "template not found", http.StatusInternalServerError) http.Error(w, "template nao encontrado", http.StatusInternalServerError)
return return
} }
if err := tmpl.ExecuteTemplate(w, name, data); err != nil { if err := tmpl.ExecuteTemplate(w, name, data); err != nil {
@@ -507,7 +507,7 @@ func (s *Server) withSessionAuth(next http.Handler) http.Handler {
return return
} }
if isUnsafeMethod(r.Method) && !validCSRFToken(r, sess.CSRFToken) { if isUnsafeMethod(r.Method) && !validCSRFToken(r, sess.CSRFToken) {
http.Error(w, "invalid CSRF token", http.StatusForbidden) http.Error(w, "token CSRF invalido", http.StatusForbidden)
return return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@@ -664,7 +664,7 @@ func parseFQDNLines(raw, label string) ([]string, error) {
for i, value := range values { for i, value := range values {
values[i] = dnsrecord.EnsureTrailingDot(value) values[i] = dnsrecord.EnsureTrailingDot(value)
if !dnsrecord.IsFQDN(values[i]) { if !dnsrecord.IsFQDN(values[i]) {
return nil, fmt.Errorf("%s %q must be a fully qualified domain name", label, value) return nil, fmt.Errorf("%s %q deve ser um nome de dominio totalmente qualificado", label, value)
} }
} }
return values, nil return values, nil

View File

@@ -1,6 +1,6 @@
{{ define "layout" }} {{ define "layout" }}
<!doctype html> <!doctype html>
<html lang="en" data-bs-theme="light"> <html lang="pt-BR" data-bs-theme="light">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -15,16 +15,16 @@
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3"> <h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<a href="/">AehooDNS</a> <a href="/">AehooDNS</a>
</h1> </h1>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Alternar navegacao">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbar-menu"> <div class="collapse navbar-collapse" id="navbar-menu">
<div class="navbar-nav"> <div class="navbar-nav">
<a class="nav-link" href="/"> <a class="nav-link" href="/">
<span class="nav-link-title">Dashboard</span> <span class="nav-link-title">Painel</span>
</a> </a>
<a class="nav-link" href="/zones"> <a class="nav-link" href="/zones">
<span class="nav-link-title">Zones</span> <span class="nav-link-title">Zonas</span>
</a> </a>
</div> </div>
{{ if .AuthEnabled }} {{ if .AuthEnabled }}
@@ -34,7 +34,7 @@
<form method="post" action="/logout" class="nav-item"> <form method="post" action="/logout" class="nav-item">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<button class="nav-link btn btn-link px-0" type="submit"> <button class="nav-link btn btn-link px-0" type="submit">
<span class="nav-link-title">Logout</span> <span class="nav-link-title">Sair</span>
</button> </button>
</form> </form>
</div> </div>

View File

@@ -3,7 +3,7 @@
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<div class="page-pretitle">PowerDNS</div> <div class="page-pretitle">PowerDNS</div>
<h2 class="page-title">Dashboard</h2> <h2 class="page-title">Painel</h2>
</div> </div>
</div> </div>
</div> </div>
@@ -12,7 +12,7 @@
<div class="col-sm-6 col-lg-3"> <div class="col-sm-6 col-lg-3">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="subheader">Server ID</div> <div class="subheader">ID do servidor</div>
<div class="h2 mb-0 text-truncate">{{ .Server.ID }}</div> <div class="h2 mb-0 text-truncate">{{ .Server.ID }}</div>
</div> </div>
</div> </div>
@@ -20,15 +20,17 @@
<div class="col-sm-6 col-lg-3"> <div class="col-sm-6 col-lg-3">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="subheader">Daemon</div> <div class="subheader">Serviço</div>
<div class="h2 mb-0 text-truncate">{{ .Server.DaemonType }}</div> <div class="h2 mb-0 text-truncate">
{{ .Server.DaemonType }}
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6 col-lg-3"> <div class="col-sm-6 col-lg-3">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="subheader">Version</div> <div class="subheader">Versão</div>
<div class="h2 mb-0 text-truncate">{{ .Server.Version }}</div> <div class="h2 mb-0 text-truncate">{{ .Server.Version }}</div>
</div> </div>
</div> </div>
@@ -36,7 +38,7 @@
<div class="col-sm-6 col-lg-3"> <div class="col-sm-6 col-lg-3">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="subheader">Zones</div> <div class="subheader">Zonas</div>
<div class="h2 mb-0">{{ len .Zones }}</div> <div class="h2 mb-0">{{ len .Zones }}</div>
</div> </div>
</div> </div>
@@ -45,25 +47,30 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Recent zones</h3> <h3 class="card-title">Zonas recentes</h3>
</div> </div>
{{ if .Zones }} {{ if .Zones }}
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{{ range .Zones }} {{ range .Zones }}
<a class="list-group-item list-group-item-action" href="/zones/{{ .ID }}"> <a
class="list-group-item list-group-item-action"
href="/zones/{{ .ID }}"
>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col text-truncate"> <div class="col text-truncate">
<strong>{{ .Name }}</strong> <strong>{{ .Name }}</strong>
<div class="text-secondary text-truncate">{{ .DisplayKind }} · serial {{ .Serial }}</div> <div class="text-secondary text-truncate">
{{ .DisplayKind }} · serial {{ .Serial }}
</div>
</div> </div>
</div> </div>
</a> </a>
{{ end }} {{ end }}
</div> </div>
{{ else }} {{ else }}
<div class="card-body text-secondary">No zones returned by PowerDNS.</div> <div class="card-body text-secondary">
Nenhuma zona retornada pelo PowerDNS.
</div>
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ end }} {{ template "layout" . }}
{{ template "layout" . }}

View File

@@ -3,23 +3,35 @@
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<div class="card card-md"> <div class="card card-md">
<div class="card-body"> <div class="card-body">
<h2 class="h2 text-center mb-2">Login</h2> <h2 class="h2 text-center mb-2">Entrar</h2>
<form method="post" action="/login"> <form method="post" action="/login">
<input type="hidden" name="next" value="{{ .Next }}"> <input type="hidden" name="next" value="{{ .Next }}" />
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Username</label> <label class="form-label">Usuário</label>
<input class="form-control" name="username" autocomplete="username" required autofocus> <input
class="form-control"
name="username"
autocomplete="username"
required
autofocus
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Password</label> <label class="form-label">Senha</label>
<input class="form-control" name="password" type="password" autocomplete="current-password" required> <input
class="form-control"
name="password"
type="password"
autocomplete="current-password"
required
/>
</div> </div>
<button class="btn btn-primary w-100" type="submit">Sign in</button> <button class="btn btn-primary w-100" type="submit">
Entrar
</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{ end }} {{ end }} {{ template "layout" . }}
{{ template "layout" . }}

View File

@@ -6,7 +6,9 @@
<h2 class="page-title">{{ .RecordForm.Title }}</h2> <h2 class="page-title">{{ .RecordForm.Title }}</h2>
</div> </div>
<div class="col-auto ms-auto"> <div class="col-auto ms-auto">
<a class="btn btn-outline-secondary" href="/zones/{{ .ZoneID }}">Back to records</a> <a class="btn btn-outline-secondary" href="/zones/{{ .ZoneID }}"
>Voltar para registros</a
>
</div> </div>
</div> </div>
</div> </div>
@@ -18,47 +20,93 @@
{{ if .RecordForm.IsEdit }} {{ if .RecordForm.IsEdit }}
<div class="row mb-4"> <div class="row mb-4">
<div class="col-sm-8"> <div class="col-sm-8">
<div class="subheader">Name</div> <div class="subheader">Nome</div>
<div class="h3 mb-0">{{ .RecordForm.Name }}</div> <div class="h3 mb-0">{{ .RecordForm.Name }}</div>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<div class="subheader">Type</div> <div class="subheader">Tipo</div>
<div class="h3 mb-0">{{ .RecordForm.Type }}</div> <div class="h3 mb-0">{{ .RecordForm.Type }}</div>
</div> </div>
</div> </div>
{{ end }} {{ end }}
<form method="post" action="{{ if .RecordForm.IsEdit }}/zones/{{ .ZoneID }}/rrsets/edit?name={{ urlQuery .RecordForm.Name }}&type={{ urlQuery .RecordForm.Type }}{{ else }}/zones/{{ .ZoneID }}/rrsets{{ end }}"> <form
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> method="post"
action="{{ if .RecordForm.IsEdit }}/zones/{{ .ZoneID }}/rrsets/edit?name={{ urlQuery .RecordForm.Name }}&type={{ urlQuery .RecordForm.Type }}{{ else }}/zones/{{ .ZoneID }}/rrsets{{ end }}"
>
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}"
/>
{{ if not .RecordForm.IsEdit }} {{ if not .RecordForm.IsEdit }}
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Name</label> <label class="form-label">Nome</label>
<input class="form-control" name="name" value="{{ .RecordForm.Name }}" placeholder="www.{{ .Zone.Name }}" required> <input
class="form-control"
name="name"
value="{{ .RecordForm.Name }}"
placeholder="www.{{ .Zone.Name }}"
required
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Type</label> <label class="form-label">Tipo</label>
<select class="form-select" name="type" required> <select class="form-select" name="type" required>
{{ range .RecordTypes }} {{ range .RecordTypes }}
<option {{ if eq . $.RecordForm.Type }}selected{{ end }}>{{ . }}</option> <option
{{
if
eq
.
$.RecordForm.Type
}}selected{{
end
}}
>
{{ . }}
</option>
{{ end }} {{ end }}
</select> </select>
</div> </div>
{{ end }} {{ end }}
<div class="mb-3"> <div class="mb-3">
<label class="form-label">TTL</label> <label class="form-label">TTL</label>
<input class="form-control" name="ttl" type="number" min="1" value="{{ .RecordForm.TTL }}" required> <input
class="form-control"
name="ttl"
type="number"
min="1"
value="{{ .RecordForm.TTL }}"
required
/>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Records</label> <label class="form-label">Registros</label>
<textarea class="form-control" name="records" rows="8" placeholder="One record value per line" required>{{ .RecordForm.Records }}</textarea> <textarea
<div class="form-hint">Strict formats are enforced. TXT and CAA string values must be quoted. Adding A, AAAA, CAA, MX, NS, SRV, or TXT records appends to an existing RRset with the same name and type. SOA records can be edited here but cannot be deleted.</div> class="form-control"
name="records"
rows="8"
placeholder="Um valor de registro por linha"
required
>
{{ .RecordForm.Records }}</textarea
>
<div class="form-hint">
Formato estrito exigido: Valores de texto TXT e CAA
devem estar entre aspas. Adicionar registros A,
AAAA, CAA, MX, NS, SRV ou TXT acrescenta ao conjunto
de registros existente com o mesmo nome e tipo.
Registros SOA podem ser editados aqui, mas não podem
ser excluidos.
</div> </div>
<button class="btn btn-primary" type="submit">{{ .RecordForm.SubmitLabel }}</button> </div>
<button class="btn btn-primary" type="submit">
{{ .RecordForm.SubmitLabel }}
</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{ end }} {{ end }} {{ template "layout" . }}
{{ template "layout" . }}

View File

@@ -2,14 +2,14 @@
<div class="page-header d-print-none"> <div class="page-header d-print-none">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<div class="page-pretitle">Zone</div> <div class="page-pretitle">Zona</div>
<h2 class="page-title">{{ .Zone.Name }}</h2> <h2 class="page-title">{{ .Zone.Name }}</h2>
<div class="text-secondary">{{ len .Zone.RRSets }} RRsets</div> <div class="text-secondary">{{ len .Zone.RRSets }} conjuntos de registros</div>
</div> </div>
<div class="col-auto ms-auto"> <div class="col-auto ms-auto">
<div class="btn-list"> <div class="btn-list">
<a class="btn btn-outline-secondary" href="/zones">Back to zones</a> <a class="btn btn-outline-secondary" href="/zones">Voltar para zonas</a>
<a class="btn btn-primary" href="/zones/{{ .ZoneID }}/rrsets/new">Add record</a> <a class="btn btn-primary" href="/zones/{{ .ZoneID }}/rrsets/new">Adicionar registro</a>
</div> </div>
</div> </div>
</div> </div>
@@ -17,17 +17,17 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Records</h3> <h3 class="card-title">Registros</h3>
</div> </div>
{{ if .Zone.RRSets }} {{ if .Zone.RRSets }}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-vcenter card-table"> <table class="table table-vcenter card-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Nome</th>
<th>Type</th> <th>Tipo</th>
<th>TTL</th> <th>TTL</th>
<th>Records</th> <th>Registros</th>
<th class="w-1"></th> <th class="w-1"></th>
</tr> </tr>
</thead> </thead>
@@ -46,13 +46,13 @@
</td> </td>
<td> <td>
<div class="btn-list flex-nowrap"> <div class="btn-list flex-nowrap">
<a class="btn btn-outline-primary btn-sm" href="/zones/{{ $.ZoneID }}/rrsets/edit?name={{ urlQuery .Name }}&type={{ urlQuery .Type }}">Edit</a> <a class="btn btn-outline-primary btn-sm" href="/zones/{{ $.ZoneID }}/rrsets/edit?name={{ urlQuery .Name }}&type={{ urlQuery .Type }}">Editar</a>
{{ if not (isSOA .Type) }} {{ if not (isSOA .Type) }}
<form method="post" action="/zones/{{ $.ZoneID }}/rrsets/delete"> <form method="post" action="/zones/{{ $.ZoneID }}/rrsets/delete">
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}"> <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
<input type="hidden" name="name" value="{{ .Name }}"> <input type="hidden" name="name" value="{{ .Name }}">
<input type="hidden" name="type" value="{{ .Type }}"> <input type="hidden" name="type" value="{{ .Type }}">
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button> <button class="btn btn-outline-danger btn-sm" type="submit">Excluir</button>
</form> </form>
{{ end }} {{ end }}
</div> </div>
@@ -63,7 +63,7 @@
</table> </table>
</div> </div>
{{ else }} {{ else }}
<div class="card-body text-secondary">No records returned for this zone.</div> <div class="card-body text-secondary">Nenhum registro retornado para esta zona.</div>
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ end }}

View File

@@ -2,9 +2,9 @@
<div class="page-header d-print-none"> <div class="page-header d-print-none">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<div class="page-pretitle">Authoritative DNS</div> <div class="page-pretitle">DNS autoritativo</div>
<h2 class="page-title">Zones</h2> <h2 class="page-title">Zonas</h2>
<div class="text-secondary">{{ len .Zones }} zones loaded from PowerDNS.</div> <div class="text-secondary">{{ len .Zones }} zonas carregadas do PowerDNS.</div>
</div> </div>
</div> </div>
</div> </div>
@@ -13,15 +13,15 @@
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Existing zones</h3> <h3 class="card-title">Zonas existentes</h3>
</div> </div>
{{ if .Zones }} {{ if .Zones }}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-vcenter card-table"> <table class="table table-vcenter card-table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Nome</th>
<th>Kind</th> <th>Tipo</th>
<th>Serial</th> <th>Serial</th>
<th class="w-1"></th> <th class="w-1"></th>
</tr> </tr>
@@ -35,7 +35,7 @@
<td> <td>
<form method="post" action="/zones/{{ .ID }}/delete"> <form method="post" action="/zones/{{ .ID }}/delete">
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}"> <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button> <button class="btn btn-outline-danger btn-sm" type="submit">Excluir</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -44,7 +44,7 @@
</table> </table>
</div> </div>
{{ else }} {{ else }}
<div class="card-body text-secondary">No zones returned by PowerDNS.</div> <div class="card-body text-secondary">Nenhuma zona retornada pelo PowerDNS.</div>
{{ end }} {{ end }}
</div> </div>
</div> </div>
@@ -52,17 +52,17 @@
<div class="col-lg-4"> <div class="col-lg-4">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Add zone</h3> <h3 class="card-title">Adicionar zona</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="post" action="/zones"> <form method="post" action="/zones">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Zone name</label> <label class="form-label">Nome da zona</label>
<input class="form-control" name="name" placeholder="example.org." required> <input class="form-control" name="name" placeholder="example.org." required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Kind</label> <label class="form-label">Tipo</label>
<select class="form-select" name="kind" required> <select class="form-select" name="kind" required>
<option>Native</option> <option>Native</option>
<option>Master</option> <option>Master</option>
@@ -70,14 +70,14 @@
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Nameservers</label> <label class="form-label">Servidores de nomes</label>
<textarea class="form-control" name="nameservers" rows="4" placeholder="ns1.example.org.&#10;ns2.example.org."></textarea> <textarea class="form-control" name="nameservers" rows="4" placeholder="ns1.example.org.&#10;ns2.example.org."></textarea>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Masters</label> <label class="form-label">Servidores mestres</label>
<textarea class="form-control" name="masters" rows="3" placeholder="Required for Slave zones"></textarea> <textarea class="form-control" name="masters" rows="3" placeholder="Obrigatorio para zonas Slave"></textarea>
</div> </div>
<button class="btn btn-primary" type="submit">Create zone</button> <button class="btn btn-primary" type="submit">Criar zona</button>
</form> </form>
</div> </div>
</div> </div>