feat: add secure Pi-hole DNS management
Container-Image veröffentlichen / container (push) Has been cancelled

This commit is contained in:
admin committed 2026-07-13 02:49:41 +02:00
1 parent 1a6b4d0939
commit 1c32ca0671
18 files changed
+2159 -34

No files matched your search

+43 -4
View File
@@ -57,7 +57,10 @@ type viewData struct {
ArchivedTargets []model.Target
CNAMERecords []model.CNAMERecord
Settings model.Settings
Token string
SecretTitle string
SecretScope string
SecretValue string
SecretBackURL string
Form map[string]string
Users []model.User
ManagedUser model.User
@@ -68,7 +71,9 @@ type viewData struct {
ErrorTargets []model.Target
ErrorZones []model.Zone
ErrorRuns []model.UpdateRun
ErrorPiHoles []model.PiHoleIntegration
ErrorCount int
PiHoleView *piHoleView
}
type flashMessage struct {
@@ -124,6 +129,18 @@ func (s *Server) Handler() http.Handler {
mux.Handle("POST /cnames/{id}/save", s.adminOnly(s.cnameSave))
mux.Handle("POST /cnames/{id}/remove", s.adminOnly(s.cnameRemove))
mux.Handle("POST /cnames/{id}/delete", s.adminOnly(s.cnameDelete))
mux.Handle("GET /pihole", s.adminOnly(s.piHolePage))
mux.Handle("POST /pihole/connect", s.adminOnly(s.piHoleConnect))
mux.Handle("POST /pihole/{id}/save", s.adminOnly(s.piHoleSave))
mux.Handle("POST /pihole/{id}/password", s.adminOnly(s.piHolePassword))
mux.Handle("POST /pihole/{id}/refresh", s.adminOnly(s.piHoleRefresh))
mux.Handle("POST /pihole/{id}/remove", s.adminOnly(s.piHoleRemove))
mux.Handle("POST /pihole/{id}/delete", s.adminOnly(s.piHoleDelete))
mux.Handle("POST /pihole/{id}/records", s.adminOnly(s.piHoleRecordCreate))
mux.Handle("POST /pihole/{id}/records/update", s.adminOnly(s.piHoleRecordUpdate))
mux.Handle("POST /pihole/{id}/records/delete", s.adminOnly(s.piHoleRecordDelete))
mux.Handle("POST /pihole/{id}/records/archive", s.adminOnly(s.piHoleRecordArchive))
mux.Handle("POST /pihole/{id}/records/restore", s.adminOnly(s.piHoleRecordRestore))
mux.Handle("GET /settings", s.adminOnly(s.settingsPage))
mux.Handle("POST /settings", s.adminOnly(s.settingsSave))
mux.Handle("POST /run", s.adminOnly(s.runNow))
@@ -640,7 +657,18 @@ func (s *Server) domainToken(w http.ResponseWriter, r *http.Request, session *mo
}
_ = s.store.SetSessionReauth(r.Context(), session.ID, time.Now().UTC().Add(5*time.Minute))
_ = s.store.AddAudit(r.Context(), "credential.revealed", "API-Token angezeigt: "+zone.Name)
s.render(w, "token", viewData{Title: "API-Token anzeigen", Page: "domains", CSRF: session.CSRFToken, Zone: zone, Token: token, User: userFromSession(session), IsAdmin: true, Now: time.Now()})
s.render(w, "token", viewData{
Title: "API-Token anzeigen",
Page: "domains",
CSRF: session.CSRFToken,
SecretTitle: "Hetzner API-Token",
SecretScope: "Zone: " + zone.Name,
SecretValue: token,
SecretBackURL: domainURL(zone.ID),
User: userFromSession(session),
IsAdmin: true,
Now: time.Now(),
})
}
func (s *Server) targetCreate(w http.ResponseWriter, r *http.Request, session *model.Session) {
@@ -1228,7 +1256,12 @@ func (s *Server) errorsPage(w http.ResponseWriter, r *http.Request, session *mod
s.serverError(w, err)
return
}
s.render(w, "errors", viewData{Title: "Fehler", Page: "errors", CSRF: session.CSRFToken, Flash: notice(r), User: userFromSession(session), IsAdmin: true, ErrorTargets: targets, ErrorZones: zones, ErrorRuns: runs, ErrorCount: len(targets) + len(zones) + len(runs), Now: time.Now()})
piHoles, err := s.store.ListPiHoleIntegrationsWithErrors(r.Context())
if err != nil {
s.serverError(w, err)
return
}
s.render(w, "errors", viewData{Title: "Fehler", Page: "errors", CSRF: session.CSRFToken, Flash: notice(r), User: userFromSession(session), IsAdmin: true, ErrorTargets: targets, ErrorZones: zones, ErrorRuns: runs, ErrorPiHoles: piHoles, ErrorCount: len(targets) + len(zones) + len(runs) + len(piHoles), Now: time.Now()})
}
func (s *Server) errorsClear(w http.ResponseWriter, r *http.Request, session *model.Session) {
@@ -1727,16 +1760,21 @@ func (s *Server) redirectNotice(w http.ResponseWriter, r *http.Request, target,
func (s *Server) securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self'; style-src 'self'")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
if s.config.CookieSecure {
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
next.ServeHTTP(w, r)
})
}
func loadTemplates() (map[string]*template.Template, error) {
pages := []string{"setup", "login", "dashboard", "domains", "domain_new", "domain_detail", "settings", "errors", "security", "token", "users", "user_new", "user_detail"}
pages := []string{"setup", "login", "dashboard", "domains", "domain_new", "domain_detail", "pihole", "settings", "errors", "security", "token", "users", "user_new", "user_detail"}
functions := template.FuncMap{
"formatTime": func(value time.Time) string {
if value.IsZero() {
@@ -1755,6 +1793,7 @@ func loadTemplates() (map[string]*template.Template, error) {
"statusClass": statusClass,
"ttlOptions": func() []int { return []int{60, 300, 600, 1800, 3600, 86400} },
"supportedTTL": supportedTTL,
"contains": strings.Contains,
"families": func(target model.Target) string {
var result []string
if target.UpdateIPv4 {