fixa aumento no serial do soa
This commit is contained in:
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@@ -23,7 +24,8 @@ import (
|
||||
var assets embed.FS
|
||||
|
||||
const (
|
||||
sessionCookieName = "pdns_admin_session"
|
||||
csrfFieldName = "csrf_token"
|
||||
sessionCookieName = "__Host-pdns_admin_session"
|
||||
sessionTTL = 12 * time.Hour
|
||||
)
|
||||
|
||||
@@ -62,6 +64,7 @@ type pageData struct {
|
||||
Error string
|
||||
AuthEnabled bool
|
||||
CurrentUser string
|
||||
CSRFToken string
|
||||
Next string
|
||||
Server pdns.Server
|
||||
ZoneID string
|
||||
@@ -72,21 +75,19 @@ type pageData struct {
|
||||
}
|
||||
|
||||
type session struct {
|
||||
Username string
|
||||
Expires time.Time
|
||||
Username string
|
||||
CSRFToken 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
|
||||
Name string
|
||||
Type string
|
||||
TTL uint32
|
||||
Records string
|
||||
IsEdit bool
|
||||
Title string
|
||||
SubmitLabel string
|
||||
}
|
||||
|
||||
func New(cfg Config, client PDNSClient, logger *log.Logger) (*Server, error) {
|
||||
@@ -106,9 +107,8 @@ func New(cfg Config, client PDNSClient, logger *log.Logger) (*Server, error) {
|
||||
|
||||
templates := make(map[string]*template.Template)
|
||||
funcs := template.FuncMap{
|
||||
"isSOA": isSOA,
|
||||
"recordValues": recordValues,
|
||||
"urlQuery": url.QueryEscape,
|
||||
"isSOA": isSOA,
|
||||
"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)
|
||||
@@ -139,7 +139,7 @@ func (s *Server) routes() http.Handler {
|
||||
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("POST /logout", s.logout)
|
||||
mux.HandleFunc("GET /", s.dashboard)
|
||||
mux.HandleFunc("GET /zones", s.listZones)
|
||||
mux.HandleFunc("POST /zones", s.createZone)
|
||||
@@ -150,7 +150,7 @@ func (s *Server) routes() http.Handler {
|
||||
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))
|
||||
return s.withLogging(s.withSecurityHeaders(s.withSessionAuth(mux)))
|
||||
}
|
||||
|
||||
func (s *Server) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -226,8 +226,10 @@ func (s *Server) loginPost(w http.ResponseWriter, r *http.Request) {
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(sessionTTL),
|
||||
MaxAge: int(sessionTTL.Seconds()),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
http.Redirect(w, r, safeRedirectPath(r.FormValue("next")), http.StatusSeeOther)
|
||||
@@ -244,7 +246,8 @@ func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
|
||||
Expires: time.Unix(0, 0),
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
@@ -363,7 +366,6 @@ func (s *Server) editRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
TTL: rrset.TTL,
|
||||
Records: recordValues(rrset),
|
||||
IsEdit: true,
|
||||
IsSOA: isSOA(rrset.Type),
|
||||
Title: "Edit record",
|
||||
SubmitLabel: "Save record",
|
||||
}
|
||||
@@ -445,7 +447,7 @@ func (s *Server) deleteRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := ensureTrailingDot(r.FormValue("name"))
|
||||
name := dnsrecord.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")
|
||||
@@ -466,8 +468,9 @@ func (s *Server) deleteRRSet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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
|
||||
if sess, ok := s.currentSession(r); ok {
|
||||
data.CurrentUser = sess.Username
|
||||
data.CSRFToken = sess.CSRFToken
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl, ok := s.templates[name]
|
||||
@@ -498,46 +501,64 @@ func (s *Server) withSessionAuth(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if _, ok := s.currentUser(r); !ok {
|
||||
sess, ok := s.currentSession(r)
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/login?next="+url.QueryEscape(r.URL.RequestURI()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if isUnsafeMethod(r.Method) && !validCSRFToken(r, sess.CSRFToken) {
|
||||
http.Error(w, "invalid CSRF token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) currentUser(r *http.Request) (string, bool) {
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
sess, ok := s.currentSession(r)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return sess.Username, true
|
||||
}
|
||||
|
||||
func (s *Server) currentSession(r *http.Request) (session, bool) {
|
||||
cookie, err := r.Cookie(sessionCookieName)
|
||||
if err != nil || cookie.Value == "" || len(cookie.Value) > 128 {
|
||||
return session{}, false
|
||||
}
|
||||
|
||||
s.sessionsM.Lock()
|
||||
defer s.sessionsM.Unlock()
|
||||
|
||||
sess, ok := s.sessions[cookie.Value]
|
||||
if !ok {
|
||||
return "", false
|
||||
return session{}, false
|
||||
}
|
||||
if time.Now().After(sess.Expires) {
|
||||
delete(s.sessions, cookie.Value)
|
||||
return "", false
|
||||
return session{}, false
|
||||
}
|
||||
return sess.Username, true
|
||||
return sess, true
|
||||
}
|
||||
|
||||
func (s *Server) createSession(username string) (string, error) {
|
||||
tokenBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
token, err := randomToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
csrfToken, err := randomToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := base64.RawURLEncoding.EncodeToString(tokenBytes)
|
||||
|
||||
s.sessionsM.Lock()
|
||||
defer s.sessionsM.Unlock()
|
||||
s.pruneExpiredSessionsLocked(time.Now())
|
||||
s.sessions[token] = session{
|
||||
Username: username,
|
||||
Expires: time.Now().Add(sessionTTL),
|
||||
Username: username,
|
||||
CSRFToken: csrfToken,
|
||||
Expires: time.Now().Add(sessionTTL),
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
@@ -548,8 +569,16 @@ func (s *Server) deleteSession(token string) {
|
||||
delete(s.sessions, token)
|
||||
}
|
||||
|
||||
func (s *Server) pruneExpiredSessionsLocked(now time.Time) {
|
||||
for token, sess := range s.sessions {
|
||||
if now.After(sess.Expires) {
|
||||
delete(s.sessions, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isPublicPath(path string) bool {
|
||||
return path == "/login" || path == "/logout" || path == "/healthz" || strings.HasPrefix(path, "/static/")
|
||||
return path == "/login" || path == "/healthz" || strings.HasPrefix(path, "/static/")
|
||||
}
|
||||
|
||||
func safeRedirectPath(value string) string {
|
||||
@@ -563,6 +592,52 @@ func safeRedirectPath(value string) string {
|
||||
return parsed.RequestURI()
|
||||
}
|
||||
|
||||
func isUnsafeMethod(method string) bool {
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func validCSRFToken(r *http.Request, expected string) bool {
|
||||
if expected == "" {
|
||||
return false
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return false
|
||||
}
|
||||
token := r.FormValue(csrfFieldName)
|
||||
if token == "" {
|
||||
token = r.Header.Get("X-CSRF-Token")
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
|
||||
}
|
||||
|
||||
func randomToken() (string, error) {
|
||||
tokenBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(tokenBytes), nil
|
||||
}
|
||||
|
||||
func (s *Server) withSecurityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self' data:; base-uri 'self'; form-action 'self'; frame-ancestors 'none'")
|
||||
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
||||
w.Header().Set("Referrer-Policy", "same-origin")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
if !strings.HasPrefix(r.URL.Path, "/static/") {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) withLogging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
@@ -584,10 +659,6 @@ func parseLines(raw string) []string {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user