diff --git a/server/server.go b/server/server.go index efd6461a..d2d9d869 100644 --- a/server/server.go +++ b/server/server.go @@ -681,14 +681,42 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.handleHealth(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiReadyPath { return s.handleReady(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == apiVersionPath { + return s.ensureAdmin(s.handleVersion)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiConfigPath { return s.handleConfig(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == webAppConfigPath { return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == webAppManifestPath { return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v) - } else if h := s.adminHandler(r, false); h != nil { - return s.ensureAdmin(h)(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersGet)(w, r, v) + } else if r.Method == http.MethodPost && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersAdd)(w, r, v) + } else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersUpdate)(w, r, v) + } else if r.Method == http.MethodDelete && r.URL.Path == apiUsersPath { + return s.ensureAdmin(s.handleUsersDelete)(w, r, v) + } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiUsersAccessPath { + return s.ensureAdmin(s.handleAccessAllow)(w, r, v) + } else if r.Method == http.MethodDelete && r.URL.Path == apiUsersAccessPath { + return s.ensureAdmin(s.handleAccessReset)(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == apiUsersTokensPath { + return s.ensureAdmin(s.handleUsersTokensGet)(w, r, v) + } else if r.Method == http.MethodPost && r.URL.Path == apiUsersTokensPath { + return s.ensureAdmin(s.handleUsersTokensAdd)(w, r, v) + } else if r.Method == http.MethodDelete && r.URL.Path == apiUsersTokensPath { + return s.ensureAdmin(s.handleUsersTokensDelete)(w, r, v) + } else if r.Method == http.MethodPost && r.URL.Path == apiUsersPasswordResetPath { + return s.ensureAdmin(s.handleUsersPasswordReset)(w, r, v) + } else if r.Method == http.MethodGet && r.URL.Path == apiAdminTiersPath { + return s.ensureAdmin(s.handleAdminTiersGet)(w, r, v) + } else if r.Method == http.MethodPost && r.URL.Path == apiAdminTiersPath { + return s.ensureAdmin(s.handleAdminTiersAdd)(w, r, v) + } else if r.Method == http.MethodPut && r.URL.Path == apiAdminTiersPath { + return s.ensureAdmin(s.handleAdminTiersUpdate)(w, r, v) + } else if r.Method == http.MethodDelete && r.URL.Path == apiAdminTiersPath { + return s.ensureAdmin(s.handleAdminTiersDelete)(w, r, v) } else if r.Method == http.MethodPost && r.URL.Path == apiAccountPath { return s.ensureUserManager(s.handleAccountCreate)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiAccountPath { diff --git a/server/server_admin.go b/server/server_admin.go index fb63ea3c..164791c0 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -17,45 +17,6 @@ const ( apiTierReset = "-" // Tier value that removes a user's tier (PUT /v1/users); same marker the CLI uses ) -// adminHandler returns the handler for an admin API route, or nil if the request is not one. -// Routes that must not be reachable from the network are only returned when socket is true, i.e. -// when the request came in over the admin socket (see server_admin_socket.go). -func (s *Server) adminHandler(r *http.Request, socket bool) handleFunc { - switch { - case r.Method == http.MethodGet && r.URL.Path == apiVersionPath: - return s.handleVersion - case r.Method == http.MethodGet && r.URL.Path == apiUsersPath: - return s.handleUsersGet - case r.Method == http.MethodPost && r.URL.Path == apiUsersPath: - return s.handleUsersAdd - case r.Method == http.MethodPut && r.URL.Path == apiUsersPath: - return s.handleUsersUpdate - case r.Method == http.MethodDelete && r.URL.Path == apiUsersPath: - return s.handleUsersDelete - case (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiUsersAccessPath: - return s.handleAccessAllow - case r.Method == http.MethodDelete && r.URL.Path == apiUsersAccessPath: - return s.handleAccessReset - case r.Method == http.MethodGet && r.URL.Path == apiUsersTokensPath: - return s.handleUsersTokensGet - case r.Method == http.MethodPost && r.URL.Path == apiUsersTokensPath: - return s.handleUsersTokensAdd - case r.Method == http.MethodDelete && r.URL.Path == apiUsersTokensPath: - return s.handleUsersTokensDelete - case r.Method == http.MethodPost && r.URL.Path == apiUsersPasswordResetPath: - return s.handleUsersPasswordReset - case r.Method == http.MethodGet && r.URL.Path == apiAdminTiersPath: - return s.handleAdminTiersGet - case r.Method == http.MethodPost && r.URL.Path == apiAdminTiersPath: - return s.handleAdminTiersAdd - case r.Method == http.MethodPut && r.URL.Path == apiAdminTiersPath: - return s.handleAdminTiersUpdate - case r.Method == http.MethodDelete && r.URL.Path == apiAdminTiersPath: - return s.handleAdminTiersDelete - } - return nil -} - func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request, v *visitor) error { return s.writeJSON(w, &apiVersionResponse{ Version: s.config.BuildVersion, diff --git a/server/server_admin_socket.go b/server/server_admin_socket.go index 01b25c6c..6ec80d15 100644 --- a/server/server_admin_socket.go +++ b/server/server_admin_socket.go @@ -3,11 +3,23 @@ package server import ( "net/http" "net/netip" + "slices" "heckel.io/ntfy/v2/user" ) var ( + // adminSocketPaths are the paths served over the admin socket. All of them are admin-only + // routes in handleInternal (ensureAdmin), which the socket's synthetic admin user passes. + adminSocketPaths = []string{ + apiVersionPath, + apiUsersPath, + apiUsersAccessPath, + apiUsersTokensPath, + apiUsersPasswordResetPath, + apiAdminTiersPath, + } + // adminSocketUser is the synthetic admin every socket request runs as. Its name is one that // user.AllowedUsername rejects, so it can never collide with a real account. adminSocketUser = &user.User{ @@ -26,12 +38,11 @@ var ( func (s *Server) handleAdminSocket(w http.ResponseWriter, r *http.Request) { r = withContext(r, map[contextKey]any{contextAdminSocket: true}) v := newVisitor(s.config, s.messageCache, s.userManager, netip.AddrFrom4([4]byte{127, 0, 0, 1}), adminSocketUser) - h := s.adminHandler(r, true) - if h == nil || s.userManager == nil { - s.handleError(w, r, v, errHTTPNotFound) // No auth configured means nothing to administer, same as ensureUserManager + if !slices.Contains(adminSocketPaths, r.URL.Path) { + s.handleError(w, r, v, errHTTPNotFound) // Publishing, subscribing, the web app, etc. are not served here return } - if err := h(w, r, v); err != nil { + if err := s.handleInternal(w, r, v); err != nil { s.handleError(w, r, v, err) } }