primeiro commit
This commit is contained in:
648
internal/server/server.go
Normal file
648
internal/server/server.go
Normal file
@@ -0,0 +1,648 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"pdns_admin/internal/dnsrecord"
|
||||
"pdns_admin/internal/pdns"
|
||||
)
|
||||
|
||||
//go:embed templates/*.html static
|
||||
var assets embed.FS
|
||||
|
||||
const (
|
||||
sessionCookieName = "pdns_admin_session"
|
||||
sessionTTL = 12 * time.Hour
|
||||
)
|
||||
|
||||
type PDNSClient interface {
|
||||
GetServer(context.Context) (pdns.Server, error)
|
||||
ListZones(context.Context) ([]pdns.Zone, error)
|
||||
CreateZone(context.Context, pdns.Zone) (pdns.Zone, error)
|
||||
DeleteZone(context.Context, string) error
|
||||
GetZone(context.Context, string) (pdns.Zone, error)
|
||||
CreateRRSet(context.Context, string, pdns.RRSet) error
|
||||
DeleteRRSet(context.Context, string, string, string) error
|
||||
}
|
||||
|
||||
type Authenticator interface {
|
||||
Authenticate(context.Context, string, string) (bool, error)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
Authenticator Authenticator
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
addr string
|
||||
client PDNSClient
|
||||
logger *log.Logger
|
||||
templates map[string]*template.Template
|
||||
validator *dnsrecord.Validator
|
||||
auth Authenticator
|
||||
sessions map[string]session
|
||||
sessionsM sync.Mutex
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Title string
|
||||
Error string
|
||||
AuthEnabled bool
|
||||
CurrentUser string
|
||||
Next string
|
||||
Server pdns.Server
|
||||
ZoneID string
|
||||
Zones []pdns.Zone
|
||||
Zone pdns.Zone
|
||||
RecordForm recordForm
|
||||
RecordTypes []string
|
||||
}
|
||||
|
||||
type session struct {
|
||||
Username string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
type recordForm struct {
|
||||
Name string
|
||||
Type string
|
||||
TTL uint32
|
||||
Records string
|
||||
OriginalName string
|
||||
OriginalType string
|
||||
IsEdit bool
|
||||
IsSOA bool
|
||||
Title string
|
||||
SubmitLabel string
|
||||
}
|
||||
|
||||
func New(cfg Config, client PDNSClient, logger *log.Logger) (*Server, error) {
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("pdns client is required")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = log.Default()
|
||||
}
|
||||
if cfg.Addr == "" {
|
||||
cfg.Addr = ":8080"
|
||||
}
|
||||
recordValidator, err := dnsrecord.NewValidator()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create record validator: %w", err)
|
||||
}
|
||||
|
||||
templates := make(map[string]*template.Template)
|
||||
funcs := template.FuncMap{
|
||||
"isSOA": isSOA,
|
||||
"recordValues": recordValues,
|
||||
"urlQuery": url.QueryEscape,
|
||||
}
|
||||
for _, page := range []string{"dashboard.html", "login.html", "zones.html", "zone.html", "record_form.html"} {
|
||||
tmpl, err := template.New("base.html").Funcs(funcs).ParseFS(assets, "templates/base.html", "templates/"+page)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse template %s: %w", page, err)
|
||||
}
|
||||
templates[page] = tmpl
|
||||
}
|
||||
|
||||
return &Server{
|
||||
addr: cfg.Addr,
|
||||
client: client,
|
||||
logger: logger,
|
||||
templates: templates,
|
||||
validator: recordValidator,
|
||||
auth: cfg.Authenticator,
|
||||
sessions: make(map[string]session),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe() error {
|
||||
return http.ListenAndServe(s.addr, s.routes())
|
||||
}
|
||||
|
||||
func (s *Server) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /static/", http.FileServerFS(assets))
|
||||
mux.HandleFunc("GET /healthz", s.healthz)
|
||||
mux.HandleFunc("GET /login", s.login)
|
||||
mux.HandleFunc("POST /login", s.loginPost)
|
||||
mux.HandleFunc("GET /logout", s.logout)
|
||||
mux.HandleFunc("GET /", s.dashboard)
|
||||
mux.HandleFunc("GET /zones", s.listZones)
|
||||
mux.HandleFunc("POST /zones", s.createZone)
|
||||
mux.HandleFunc("GET /zones/{zoneID}", s.showZone)
|
||||
mux.HandleFunc("POST /zones/{zoneID}/delete", s.deleteZone)
|
||||
mux.HandleFunc("GET /zones/{zoneID}/rrsets/new", s.newRRSet)
|
||||
mux.HandleFunc("GET /zones/{zoneID}/rrsets/edit", s.editRRSet)
|
||||
mux.HandleFunc("POST /zones/{zoneID}/rrsets", s.saveRRSet)
|
||||
mux.HandleFunc("POST /zones/{zoneID}/rrsets/edit", s.saveEditedRRSet)
|
||||
mux.HandleFunc("POST /zones/{zoneID}/rrsets/delete", s.deleteRRSet)
|
||||
return s.withLogging(s.withSessionAuth(mux))
|
||||
}
|
||||
|
||||
func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
server, serverErr := s.client.GetServer(r.Context())
|
||||
zones, zonesErr := s.client.ListZones(r.Context())
|
||||
data := pageData{
|
||||
Title: "Dashboard",
|
||||
Server: server,
|
||||
Zones: zones,
|
||||
Error: firstNonEmpty(errorText(serverErr), errorText(zonesErr)),
|
||||
}
|
||||
s.render(w, r, "dashboard.html", data)
|
||||
}
|
||||
|
||||
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
|
||||
if s.auth == nil {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if _, ok := s.currentUser(r); ok {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
data := pageData{
|
||||
Title: "Login",
|
||||
Error: r.URL.Query().Get("error"),
|
||||
Next: safeRedirectPath(r.URL.Query().Get("next")),
|
||||
}
|
||||
s.render(w, r, "login.html", data)
|
||||
}
|
||||
|
||||
func (s *Server) loginPost(w http.ResponseWriter, r *http.Request) {
|
||||
if s.auth == nil {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Redirect(w, r, "/login?error="+url.QueryEscape("invalid form data"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(r.FormValue("username"))
|
||||
password := r.FormValue("password")
|
||||
allowed, err := s.auth.Authenticate(r.Context(), username, password)
|
||||
if err != nil {
|
||||
s.logger.Printf("authentication failed for %q: %v", username, err)
|
||||
http.Redirect(w, r, "/login?error="+url.QueryEscape("authentication backend failed"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
http.Redirect(w, r, "/login?error="+url.QueryEscape("invalid username or password"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := s.createSession(username)
|
||||
if err != nil {
|
||||
s.logger.Printf("create session: %v", err)
|
||||
http.Error(w, "session creation failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(sessionTTL),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
http.Redirect(w, r, safeRedirectPath(r.FormValue("next")), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
|
||||
if cookie, err := r.Cookie(sessionCookieName); err == nil {
|
||||
s.deleteSession(cookie.Value)
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) listZones(w http.ResponseWriter, r *http.Request) {
|
||||
zones, err := s.client.ListZones(r.Context())
|
||||
data := pageData{
|
||||
Title: "Zones",
|
||||
Zones: zones,
|
||||
Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)),
|
||||
}
|
||||
s.render(w, r, "zones.html", data)
|
||||
}
|
||||
|
||||
func (s *Server) createZone(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.redirectZonesError(w, r, "invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
zoneName := dnsrecord.EnsureTrailingDot(r.FormValue("name"))
|
||||
if !dnsrecord.IsFQDN(zoneName) {
|
||||
s.redirectZonesError(w, r, "zone name must be a fully qualified domain name")
|
||||
return
|
||||
}
|
||||
|
||||
kind := strings.TrimSpace(r.FormValue("kind"))
|
||||
if !validZoneKind(kind) {
|
||||
s.redirectZonesError(w, r, "zone kind must be Native, Master, or Slave")
|
||||
return
|
||||
}
|
||||
|
||||
nameservers, err := parseFQDNLines(r.FormValue("nameservers"), "nameserver")
|
||||
if err != nil {
|
||||
s.redirectZonesError(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
masters := parseLines(r.FormValue("masters"))
|
||||
if kind == "Slave" && len(masters) == 0 {
|
||||
s.redirectZonesError(w, r, "slave zones require at least one master address")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.client.CreateZone(r.Context(), pdns.Zone{
|
||||
Name: zoneName,
|
||||
Kind: kind,
|
||||
Nameservers: nameservers,
|
||||
Masters: masters,
|
||||
}); err != nil {
|
||||
s.redirectZonesError(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/zones/"+url.PathEscape(zoneName), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) deleteZone(w http.ResponseWriter, r *http.Request) {
|
||||
zoneID := r.PathValue("zoneID")
|
||||
if err := s.client.DeleteZone(r.Context(), zoneID); err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/zones", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) showZone(w http.ResponseWriter, r *http.Request) {
|
||||
zoneID := r.PathValue("zoneID")
|
||||
zone, err := s.client.GetZone(r.Context(), zoneID)
|
||||
data := pageData{
|
||||
Title: "Zone " + zoneID,
|
||||
ZoneID: zoneID,
|
||||
Zone: zone,
|
||||
Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)),
|
||||
}
|
||||
s.render(w, r, "zone.html", data)
|
||||
}
|
||||
|
||||
func (s *Server) newRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
zoneID := r.PathValue("zoneID")
|
||||
zone, err := s.client.GetZone(r.Context(), zoneID)
|
||||
data := pageData{
|
||||
Title: "Add record",
|
||||
ZoneID: zoneID,
|
||||
Zone: zone,
|
||||
Error: firstNonEmpty(r.URL.Query().Get("error"), errorText(err)),
|
||||
RecordTypes: dnsrecord.SupportedTypes(),
|
||||
RecordForm: recordForm{
|
||||
Type: "A",
|
||||
TTL: 300,
|
||||
Title: "Add record",
|
||||
SubmitLabel: "Create record",
|
||||
},
|
||||
}
|
||||
s.render(w, r, "record_form.html", data)
|
||||
}
|
||||
|
||||
func (s *Server) editRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
zoneID := r.PathValue("zoneID")
|
||||
name := r.URL.Query().Get("name")
|
||||
recordType := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("type")))
|
||||
zone, err := s.client.GetZone(r.Context(), zoneID)
|
||||
if err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rrset, ok := findRRSet(zone, name, recordType)
|
||||
if !ok {
|
||||
s.redirectZoneError(w, r, zoneID, "record not found")
|
||||
return
|
||||
}
|
||||
|
||||
form := recordForm{
|
||||
Name: rrset.Name,
|
||||
Type: rrset.Type,
|
||||
TTL: rrset.TTL,
|
||||
Records: recordValues(rrset),
|
||||
IsEdit: true,
|
||||
IsSOA: isSOA(rrset.Type),
|
||||
Title: "Edit record",
|
||||
SubmitLabel: "Save record",
|
||||
}
|
||||
data := pageData{
|
||||
Title: "Edit record",
|
||||
ZoneID: zoneID,
|
||||
Zone: zone,
|
||||
Error: r.URL.Query().Get("error"),
|
||||
RecordTypes: dnsrecord.SupportedTypes(),
|
||||
RecordForm: form,
|
||||
}
|
||||
s.render(w, r, "record_form.html", data)
|
||||
}
|
||||
|
||||
func (s *Server) saveRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
zoneID := r.PathValue("zoneID")
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, "invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
ttl, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("ttl")), 10, 32)
|
||||
if err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, "ttl must be a positive integer")
|
||||
return
|
||||
}
|
||||
|
||||
rrset, err := s.validator.ValidateRRSet(r.FormValue("name"), r.FormValue("type"), ttl, parseLines(r.FormValue("records")))
|
||||
if err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.client.CreateRRSet(r.Context(), zoneID, rrset); err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/zones/"+zoneID, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) saveEditedRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
zoneID := r.PathValue("zoneID")
|
||||
name := r.URL.Query().Get("name")
|
||||
recordType := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("type")))
|
||||
if name == "" || recordType == "" {
|
||||
s.redirectZoneError(w, r, zoneID, "record identity is required")
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, "invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
ttl, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("ttl")), 10, 32)
|
||||
if err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, "ttl must be a positive integer")
|
||||
return
|
||||
}
|
||||
|
||||
rrset, err := s.validator.ValidateRRSet(name, recordType, ttl, parseLines(r.FormValue("records")))
|
||||
if err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, err.Error())
|
||||
return
|
||||
}
|
||||
if err := s.client.CreateRRSet(r.Context(), zoneID, rrset); err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/zones/"+zoneID, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) deleteRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
zoneID := r.PathValue("zoneID")
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, "invalid form data")
|
||||
return
|
||||
}
|
||||
|
||||
name := ensureTrailingDot(r.FormValue("name"))
|
||||
recordType := strings.ToUpper(strings.TrimSpace(r.FormValue("type")))
|
||||
if name == "." || recordType == "" {
|
||||
s.redirectZoneError(w, r, zoneID, "record name and type are required")
|
||||
return
|
||||
}
|
||||
if isSOA(recordType) {
|
||||
s.redirectZoneError(w, r, zoneID, "SOA records are required for zones and cannot be deleted")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.client.DeleteRRSet(r.Context(), zoneID, name, recordType); err != nil {
|
||||
s.redirectZoneError(w, r, zoneID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/zones/"+zoneID, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, data pageData) {
|
||||
data.AuthEnabled = s.auth != nil
|
||||
if user, ok := s.currentUser(r); ok {
|
||||
data.CurrentUser = user
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl, ok := s.templates[name]
|
||||
if !ok {
|
||||
http.Error(w, "template not found", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := tmpl.ExecuteTemplate(w, name, data); err != nil {
|
||||
s.logger.Printf("render %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) redirectZoneError(w http.ResponseWriter, r *http.Request, zoneID, message string) {
|
||||
http.Redirect(w, r, "/zones/"+url.PathEscape(zoneID)+"?error="+url.QueryEscape(message), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) redirectZonesError(w http.ResponseWriter, r *http.Request, message string) {
|
||||
http.Redirect(w, r, "/zones?error="+url.QueryEscape(message), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) withSessionAuth(next http.Handler) http.Handler {
|
||||
if s.auth == nil {
|
||||
return next
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if isPublicPath(r.URL.Path) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if _, ok := s.currentUser(r); !ok {
|
||||
http.Redirect(w, r, "/login?next="+url.QueryEscape(r.URL.RequestURI()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) currentUser(r *http.Request) (string, bool) {
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
s.sessionsM.Lock()
|
||||
defer s.sessionsM.Unlock()
|
||||
|
||||
sess, ok := s.sessions[cookie.Value]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if time.Now().After(sess.Expires) {
|
||||
delete(s.sessions, cookie.Value)
|
||||
return "", false
|
||||
}
|
||||
return sess.Username, true
|
||||
}
|
||||
|
||||
func (s *Server) createSession(username string) (string, error) {
|
||||
tokenBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := base64.RawURLEncoding.EncodeToString(tokenBytes)
|
||||
|
||||
s.sessionsM.Lock()
|
||||
defer s.sessionsM.Unlock()
|
||||
s.sessions[token] = session{
|
||||
Username: username,
|
||||
Expires: time.Now().Add(sessionTTL),
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *Server) deleteSession(token string) {
|
||||
s.sessionsM.Lock()
|
||||
defer s.sessionsM.Unlock()
|
||||
delete(s.sessions, token)
|
||||
}
|
||||
|
||||
func isPublicPath(path string) bool {
|
||||
return path == "/login" || path == "/logout" || path == "/healthz" || strings.HasPrefix(path, "/static/")
|
||||
}
|
||||
|
||||
func safeRedirectPath(value string) string {
|
||||
if value == "" {
|
||||
return "/"
|
||||
}
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil || parsed.IsAbs() || !strings.HasPrefix(parsed.Path, "/") || strings.HasPrefix(parsed.Path, "//") {
|
||||
return "/"
|
||||
}
|
||||
return parsed.RequestURI()
|
||||
}
|
||||
|
||||
func (s *Server) withLogging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
s.logger.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond))
|
||||
})
|
||||
}
|
||||
|
||||
func parseLines(raw string) []string {
|
||||
lines := strings.Split(raw, "\n")
|
||||
values := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
value := strings.TrimSpace(line)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func ensureTrailingDot(value string) string {
|
||||
return dnsrecord.EnsureTrailingDot(value)
|
||||
}
|
||||
|
||||
func parseFQDNLines(raw, label string) ([]string, error) {
|
||||
values := parseLines(raw)
|
||||
for i, value := range values {
|
||||
values[i] = dnsrecord.EnsureTrailingDot(value)
|
||||
if !dnsrecord.IsFQDN(values[i]) {
|
||||
return nil, fmt.Errorf("%s %q must be a fully qualified domain name", label, value)
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func validZoneKind(kind string) bool {
|
||||
switch kind {
|
||||
case "Native", "Master", "Slave":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func findRRSet(zone pdns.Zone, name, recordType string) (pdns.RRSet, bool) {
|
||||
name = dnsrecord.EnsureTrailingDot(name)
|
||||
recordType = strings.ToUpper(strings.TrimSpace(recordType))
|
||||
for _, rrset := range zone.RRSets {
|
||||
if rrset.Name == name && rrset.Type == recordType {
|
||||
return rrset, true
|
||||
}
|
||||
}
|
||||
return pdns.RRSet{}, false
|
||||
}
|
||||
|
||||
func isSOA(recordType string) bool {
|
||||
return strings.EqualFold(recordType, "SOA")
|
||||
}
|
||||
|
||||
func recordValues(rrset pdns.RRSet) string {
|
||||
values := make([]string, 0, len(rrset.Records))
|
||||
for _, record := range rrset.Records {
|
||||
values = append(values, record.Content)
|
||||
}
|
||||
return strings.Join(values, "\n")
|
||||
}
|
||||
|
||||
func errorText(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user