fix: harden DynDNS recovery and publish v1.5.1
This commit is contained in:
1 parent
3383b91e49
commit
81def77e3c
37 files changed
+2415
-250
No files matched your search
+51
-23
@@ -14,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -144,22 +145,22 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("GET /", s.authenticated(s.dashboard))
|
||||
mux.Handle("GET /domains", s.authenticated(s.domains))
|
||||
mux.Handle("GET /domains/new", s.adminOnly(s.domainNew))
|
||||
mux.Handle("POST /domains", s.adminOnly(s.domainCreate))
|
||||
mux.Handle("POST /domains", s.adminOnly(s.dnsChange(s.domainCreate)))
|
||||
mux.Handle("GET /domains/{id}", s.authenticated(s.domainDetail))
|
||||
mux.Handle("POST /domains/{id}/import", s.adminOnly(s.domainImport))
|
||||
mux.Handle("POST /domains/{id}/ttl", s.adminOnly(s.domainTTLSave))
|
||||
mux.Handle("POST /domains/{id}/remove", s.adminOnly(s.domainRemove))
|
||||
mux.Handle("POST /domains/{id}/delete", s.adminOnly(s.domainDelete))
|
||||
mux.Handle("POST /domains/{id}/token", s.adminOnly(s.domainToken))
|
||||
mux.Handle("POST /targets", s.authenticated(s.targetCreate))
|
||||
mux.Handle("POST /targets/{id}/save", s.authenticated(s.targetSave))
|
||||
mux.Handle("POST /targets/{id}/delete", s.adminOnly(s.targetDelete))
|
||||
mux.Handle("POST /targets/{id}/archive", s.adminOnly(s.targetArchive))
|
||||
mux.Handle("POST /targets/{id}/restore", s.adminOnly(s.targetRestore))
|
||||
mux.Handle("POST /cnames", s.adminOnly(s.cnameCreate))
|
||||
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("POST /domains/{id}/import", s.adminOnly(s.dnsChange(s.domainImport)))
|
||||
mux.Handle("POST /domains/{id}/ttl", s.adminOnly(s.dnsChange(s.domainTTLSave)))
|
||||
mux.Handle("POST /domains/{id}/remove", s.adminOnly(s.dnsChange(s.domainRemove)))
|
||||
mux.Handle("POST /domains/{id}/delete", s.adminOnly(s.dnsChange(s.domainDelete)))
|
||||
mux.Handle("POST /domains/{id}/token", s.adminOnly(s.dnsChange(s.domainToken)))
|
||||
mux.Handle("POST /targets", s.authenticated(s.dnsChange(s.targetCreate)))
|
||||
mux.Handle("POST /targets/{id}/save", s.authenticated(s.dnsChange(s.targetSave)))
|
||||
mux.Handle("POST /targets/{id}/delete", s.adminOnly(s.dnsChange(s.targetDelete)))
|
||||
mux.Handle("POST /targets/{id}/archive", s.adminOnly(s.dnsChange(s.targetArchive)))
|
||||
mux.Handle("POST /targets/{id}/restore", s.adminOnly(s.dnsChange(s.targetRestore)))
|
||||
mux.Handle("POST /cnames", s.adminOnly(s.dnsChange(s.cnameCreate)))
|
||||
mux.Handle("POST /cnames/{id}/save", s.adminOnly(s.dnsChange(s.cnameSave)))
|
||||
mux.Handle("POST /cnames/{id}/remove", s.adminOnly(s.dnsChange(s.cnameRemove)))
|
||||
mux.Handle("POST /cnames/{id}/delete", s.adminOnly(s.dnsChange(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))
|
||||
@@ -173,13 +174,14 @@ func (s *Server) Handler() http.Handler {
|
||||
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 /settings", s.adminOnly(s.dnsChange(s.settingsSave)))
|
||||
mux.Handle("POST /run", s.adminOnly(s.runNow))
|
||||
mux.Handle("GET /system", s.adminOnly(s.softwareUpdatePage))
|
||||
mux.Handle("POST /system/update-credential", s.adminOnly(s.softwareUpdateCredentialSave))
|
||||
mux.Handle("POST /system/check", s.adminOnly(s.softwareUpdateCheck))
|
||||
mux.Handle("POST /system/update", s.adminOnly(s.softwareUpdateStart))
|
||||
mux.Handle("POST /system/restart", s.adminOnly(s.containerRestart))
|
||||
mux.Handle("GET /errors/diagnostics", s.adminOnly(s.downloadDiagnostics))
|
||||
mux.Handle("GET /errors", s.adminOnly(s.errorsPage))
|
||||
mux.Handle("POST /errors/clear", s.adminOnly(s.errorsClear))
|
||||
mux.Handle("GET /account", s.authenticated(s.accountPage))
|
||||
@@ -818,7 +820,7 @@ func (s *Server) targetCreate(w http.ResponseWriter, r *http.Request, session *m
|
||||
if !target.Active && hasStaticIP(target) {
|
||||
updated, status, syncErr := s.scheduler.SyncStaticTarget(r.Context(), target)
|
||||
if syncErr != nil {
|
||||
_ = s.store.UpdateTargetResult(r.Context(), target.ID, "Fehler", safeMessage(syncErr), false)
|
||||
_ = s.store.UpdateTargetResult(r.Context(), target.ID, "Fehler", safeMessage(syncErr), updated)
|
||||
s.redirectNotice(w, r, domainURL(zoneID), "Ziel angelegt, aber die feste IP konnte nicht bei Hetzner gesetzt werden: "+safeMessage(syncErr), "error")
|
||||
return
|
||||
}
|
||||
@@ -932,7 +934,7 @@ func (s *Server) targetSave(w http.ResponseWriter, r *http.Request, session *mod
|
||||
if !target.Active && hasStaticIP(target) {
|
||||
updated, status, syncErr := s.scheduler.SyncStaticTarget(r.Context(), target)
|
||||
if syncErr != nil {
|
||||
_ = s.store.UpdateTargetResult(r.Context(), target.ID, "Fehler", safeMessage(syncErr), false)
|
||||
_ = s.store.UpdateTargetResult(r.Context(), target.ID, "Fehler", safeMessage(syncErr), updated)
|
||||
s.redirectNotice(w, r, domainURL(target.ZoneID), "Feste IP konnte nicht bei Hetzner gesetzt werden: "+safeMessage(syncErr), "error")
|
||||
return
|
||||
}
|
||||
@@ -1261,11 +1263,13 @@ func (s *Server) settingsSave(w http.ResponseWriter, r *http.Request, session *m
|
||||
s.redirectNotice(w, r, "/settings", "Für aktiviertes IPv6 ist mindestens ein HTTPS-Endpoint erforderlich.", "error")
|
||||
return
|
||||
}
|
||||
if err := s.scheduler.ValidateEndpoints(r.Context(), ipv4); err != nil {
|
||||
s.redirectNotice(w, r, "/settings", "IPv4-Endpoint ungültig: "+safeMessage(err), "error")
|
||||
return
|
||||
if !slices.Equal(ipv4, current.IPv4Endpoints) {
|
||||
if err := s.scheduler.ValidateEndpoints(r.Context(), ipv4); err != nil {
|
||||
s.redirectNotice(w, r, "/settings", "IPv4-Endpoint ungültig: "+safeMessage(err), "error")
|
||||
return
|
||||
}
|
||||
}
|
||||
if ipv6Enabled {
|
||||
if ipv6Enabled && !slices.Equal(ipv6, current.IPv6Endpoints) {
|
||||
if err := s.scheduler.ValidateEndpoints(r.Context(), ipv6); err != nil {
|
||||
s.redirectNotice(w, r, "/settings", "IPv6-Endpoint ungültig: "+safeMessage(err), "error")
|
||||
return
|
||||
@@ -1364,7 +1368,7 @@ func (s *Server) softwareUpdateCheck(w http.ResponseWriter, r *http.Request, ses
|
||||
cancel()
|
||||
if err != nil {
|
||||
if errors.Is(err, appupdate.ErrCredentialMissing) {
|
||||
s.redirectNotice(w, r, "/system#update-access", "Richte zuerst den Update-Zugang ein.", "info")
|
||||
s.redirectNotice(w, r, "/system#update-access", appupdate.ErrCredentialMissing.Error(), "info")
|
||||
return
|
||||
}
|
||||
s.redirectNotice(w, r, "/system", "Versionsprüfung nicht möglich: "+safeMessage(err)+". Der laufende Dienst bleibt unverändert.", "error")
|
||||
@@ -1847,6 +1851,30 @@ func (s *Server) userPasswordReset(w http.ResponseWriter, r *http.Request, sessi
|
||||
s.redirectNotice(w, r, "/users/"+strconv.FormatInt(userID, 10), "Passwort zurückgesetzt; der Benutzer muss sich neu anmelden.")
|
||||
}
|
||||
|
||||
// The lock spans reading the old target, remote changes and saving the new
|
||||
// configuration. Locking individual HTTP calls still permits stale worker writes.
|
||||
func (s *Server) dnsChange(next func(http.ResponseWriter, *http.Request, *model.Session)) func(http.ResponseWriter, *http.Request, *model.Session) {
|
||||
return func(w http.ResponseWriter, r *http.Request, session *model.Session) {
|
||||
if err := s.parseForm(w, r); err != nil {
|
||||
return
|
||||
}
|
||||
if !s.validSessionCSRF(r, session) {
|
||||
http.Error(w, "Ungültiges CSRF-Token", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
waitCtx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
unlock, err := s.scheduler.LockChanges(waitCtx)
|
||||
if err != nil {
|
||||
w.Header().Set("Retry-After", "5")
|
||||
http.Error(w, "Eine DNS-Aktualisierung läuft noch. Bitte die Änderung in Kürze erneut senden.", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
defer unlock()
|
||||
next(w, r, session)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) authenticated(next func(http.ResponseWriter, *http.Request, *model.Session)) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := s.session(r.Context(), r)
|
||||
|
||||
Reference in new issue
Block a user