@@ -14,7 +14,6 @@ import (
1414 "encoding/json"
1515 "errors"
1616 "fmt"
17- "io"
1817 "net"
1918 "net/http"
2019 "os"
@@ -36,8 +35,9 @@ import (
3635 cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
3736 "github.com/ClusterCockpit/cc-lib/v2/nats"
3837 "github.com/ClusterCockpit/cc-lib/v2/runtime"
39- "github.com/gorilla/handlers"
40- "github.com/gorilla/mux"
38+ "github.com/go-chi/chi/v5"
39+ "github.com/go-chi/chi/v5/middleware"
40+ "github.com/go-chi/cors"
4141 httpSwagger "github.com/swaggo/http-swagger"
4242)
4343
@@ -50,7 +50,7 @@ const (
5050
5151// Server encapsulates the HTTP server state and dependencies
5252type Server struct {
53- router * mux .Router
53+ router chi .Router
5454 server * http.Server
5555 restAPIHandle * api.RestAPI
5656 natsAPIHandle * api.NatsAPI
@@ -70,7 +70,7 @@ func NewServer(version, commit, buildDate string) (*Server, error) {
7070 buildInfo = web.Build {Version : version , Hash : commit , Buildtime : buildDate }
7171
7272 s := & Server {
73- router : mux .NewRouter (),
73+ router : chi .NewRouter (),
7474 }
7575
7676 if err := s .init (); err != nil {
@@ -106,6 +106,27 @@ func (s *Server) init() error {
106106
107107 authHandle := auth .GetAuthInstance ()
108108
109+ // Middleware must be defined before routes in chi
110+ s .router .Use (func (next http.Handler ) http.Handler {
111+ return http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
112+ start := time .Now ()
113+ ww := middleware .NewWrapResponseWriter (rw , r .ProtoMajor )
114+ next .ServeHTTP (ww , r )
115+ cclog .Debugf ("%s %s (%d, %.02fkb, %dms)" ,
116+ r .Method , r .URL .RequestURI (),
117+ ww .Status (), float32 (ww .BytesWritten ())/ 1024 ,
118+ time .Since (start ).Milliseconds ())
119+ })
120+ })
121+ s .router .Use (middleware .Compress (5 ))
122+ s .router .Use (middleware .Recoverer )
123+ s .router .Use (cors .Handler (cors.Options {
124+ AllowCredentials : true ,
125+ AllowedHeaders : []string {"X-Requested-With" , "Content-Type" , "Authorization" , "Origin" },
126+ AllowedMethods : []string {"GET" , "POST" , "HEAD" , "OPTIONS" },
127+ AllowedOrigins : []string {"*" },
128+ }))
129+
109130 s .restAPIHandle = api .New ()
110131
111132 info := map [string ]any {}
@@ -117,11 +138,11 @@ func (s *Server) init() error {
117138 info ["hasOpenIDConnect" ] = true
118139 }
119140
120- s .router .HandleFunc ("/login" , func (rw http.ResponseWriter , r * http.Request ) {
141+ s .router .Get ("/login" , func (rw http.ResponseWriter , r * http.Request ) {
121142 rw .Header ().Add ("Content-Type" , "text/html; charset=utf-8" )
122143 cclog .Debugf ("##%v##" , info )
123144 web .RenderTemplate (rw , "login.tmpl" , & web.Page {Title : "Login" , Build : buildInfo , Infos : info })
124- }). Methods ( http . MethodGet )
145+ })
125146 s .router .HandleFunc ("/imprint" , func (rw http.ResponseWriter , r * http.Request ) {
126147 rw .Header ().Add ("Content-Type" , "text/html; charset=utf-8" )
127148 web .RenderTemplate (rw , "imprint.tmpl" , & web.Page {Title : "Imprint" , Build : buildInfo })
@@ -131,13 +152,6 @@ func (s *Server) init() error {
131152 web .RenderTemplate (rw , "privacy.tmpl" , & web.Page {Title : "Privacy" , Build : buildInfo })
132153 })
133154
134- secured := s .router .PathPrefix ("/" ).Subrouter ()
135- securedapi := s .router .PathPrefix ("/api" ).Subrouter ()
136- userapi := s .router .PathPrefix ("/userapi" ).Subrouter ()
137- configapi := s .router .PathPrefix ("/config" ).Subrouter ()
138- frontendapi := s .router .PathPrefix ("/frontend" ).Subrouter ()
139- metricstoreapi := s .router .PathPrefix ("/api" ).Subrouter ()
140-
141155 if ! config .Keys .DisableAuthentication {
142156 // Create login failure handler (used by both /login and /jwt-login)
143157 loginFailureHandler := func (rw http.ResponseWriter , r * http.Request , err error ) {
@@ -152,10 +166,10 @@ func (s *Server) init() error {
152166 })
153167 }
154168
155- s .router .Handle ("/login" , authHandle .Login (loginFailureHandler )). Methods ( http . MethodPost )
156- s .router .Handle ("/jwt-login" , authHandle .Login (loginFailureHandler ))
169+ s .router .Post ("/login" , authHandle .Login (loginFailureHandler ). ServeHTTP )
170+ s .router .HandleFunc ("/jwt-login" , authHandle .Login (loginFailureHandler ). ServeHTTP )
157171
158- s .router .Handle ("/logout" , authHandle .Logout (
172+ s .router .Post ("/logout" , authHandle .Logout (
159173 http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
160174 rw .Header ().Add ("Content-Type" , "text/html; charset=utf-8" )
161175 rw .WriteHeader (http .StatusOK )
@@ -166,86 +180,97 @@ func (s *Server) init() error {
166180 Build : buildInfo ,
167181 Infos : info ,
168182 })
169- }))).Methods (http .MethodPost )
170-
171- secured .Use (func (next http.Handler ) http.Handler {
172- return authHandle .Auth (
173- // On success;
174- next ,
175-
176- // On failure:
177- func (rw http.ResponseWriter , r * http.Request , err error ) {
178- rw .WriteHeader (http .StatusUnauthorized )
179- web .RenderTemplate (rw , "login.tmpl" , & web.Page {
180- Title : "Authentication failed - ClusterCockpit" ,
181- MsgType : "alert-danger" ,
182- Message : err .Error (),
183- Build : buildInfo ,
184- Infos : info ,
185- Redirect : r .RequestURI ,
183+ })).ServeHTTP )
184+ }
185+
186+ if flagDev {
187+ s .router .Handle ("/playground" , playground .Handler ("GraphQL playground" , "/query" ))
188+ s .router .Get ("/swagger/*" , httpSwagger .Handler (
189+ httpSwagger .URL ("http://" + config .Keys .Addr + "/swagger/doc.json" )))
190+ }
191+
192+ // Secured routes (require authentication)
193+ s .router .Group (func (secured chi.Router ) {
194+ if ! config .Keys .DisableAuthentication {
195+ secured .Use (func (next http.Handler ) http.Handler {
196+ return authHandle .Auth (
197+ next ,
198+ func (rw http.ResponseWriter , r * http.Request , err error ) {
199+ rw .WriteHeader (http .StatusUnauthorized )
200+ web .RenderTemplate (rw , "login.tmpl" , & web.Page {
201+ Title : "Authentication failed - ClusterCockpit" ,
202+ MsgType : "alert-danger" ,
203+ Message : err .Error (),
204+ Build : buildInfo ,
205+ Infos : info ,
206+ Redirect : r .RequestURI ,
207+ })
186208 })
187- })
188- })
209+ })
210+ }
189211
190- securedapi .Use (func (next http.Handler ) http.Handler {
191- return authHandle .AuthAPI (
192- // On success;
193- next ,
194- // On failure: JSON Response
195- onFailureResponse )
196- })
212+ secured .Handle ("/query" , graphQLServer )
197213
198- userapi .Use (func (next http.Handler ) http.Handler {
199- return authHandle .AuthUserAPI (
200- // On success;
201- next ,
202- // On failure: JSON Response
203- onFailureResponse )
214+ secured .HandleFunc ("/search" , func (rw http.ResponseWriter , r * http.Request ) {
215+ routerConfig .HandleSearchBar (rw , r , buildInfo )
204216 })
205217
206- metricstoreapi .Use (func (next http.Handler ) http.Handler {
207- return authHandle .AuthMetricStoreAPI (
208- // On success;
209- next ,
210- // On failure: JSON Response
211- onFailureResponse )
212- })
218+ routerConfig .SetupRoutes (secured , buildInfo )
219+ })
213220
214- configapi .Use (func (next http.Handler ) http.Handler {
215- return authHandle .AuthConfigAPI (
216- // On success;
217- next ,
218- // On failure: JSON Response
219- onFailureResponse )
221+ // API routes (JWT token auth)
222+ s .router .Route ("/api" , func (apiRouter chi.Router ) {
223+ // Main API routes with API auth
224+ apiRouter .Group (func (securedapi chi.Router ) {
225+ if ! config .Keys .DisableAuthentication {
226+ securedapi .Use (func (next http.Handler ) http.Handler {
227+ return authHandle .AuthAPI (next , onFailureResponse )
228+ })
229+ }
230+ s .restAPIHandle .MountAPIRoutes (securedapi )
220231 })
221232
222- frontendapi .Use (func (next http.Handler ) http.Handler {
223- return authHandle .AuthFrontendAPI (
224- // On success;
225- next ,
226- // On failure: JSON Response
227- onFailureResponse )
233+ // Metric store API routes with separate auth
234+ apiRouter .Group (func (metricstoreapi chi.Router ) {
235+ if ! config .Keys .DisableAuthentication {
236+ metricstoreapi .Use (func (next http.Handler ) http.Handler {
237+ return authHandle .AuthMetricStoreAPI (next , onFailureResponse )
238+ })
239+ }
240+ s .restAPIHandle .MountMetricStoreAPIRoutes (metricstoreapi )
228241 })
229- }
242+ })
230243
231- if flagDev {
232- s .router .Handle ("/playground" , playground .Handler ("GraphQL playground" , "/query" ))
233- s .router .PathPrefix ("/swagger/" ).Handler (httpSwagger .Handler (
234- httpSwagger .URL ("http://" + config .Keys .Addr + "/swagger/doc.json" ))).Methods (http .MethodGet )
235- }
236- secured .Handle ("/query" , graphQLServer )
244+ // User API routes
245+ s .router .Route ("/userapi" , func (userapi chi.Router ) {
246+ if ! config .Keys .DisableAuthentication {
247+ userapi .Use (func (next http.Handler ) http.Handler {
248+ return authHandle .AuthUserAPI (next , onFailureResponse )
249+ })
250+ }
251+ s .restAPIHandle .MountUserAPIRoutes (userapi )
252+ })
237253
238- // Send a searchId and then reply with a redirect to a user, or directly send query to job table for jobid and project.
239- secured .HandleFunc ("/search" , func (rw http.ResponseWriter , r * http.Request ) {
240- routerConfig .HandleSearchBar (rw , r , buildInfo )
254+ // Config API routes (uses Group with full paths to avoid shadowing
255+ // the /config page route that is registered in the secured group)
256+ s .router .Group (func (configapi chi.Router ) {
257+ if ! config .Keys .DisableAuthentication {
258+ configapi .Use (func (next http.Handler ) http.Handler {
259+ return authHandle .AuthConfigAPI (next , onFailureResponse )
260+ })
261+ }
262+ s .restAPIHandle .MountConfigAPIRoutes (configapi )
241263 })
242264
243- // Mount all /monitoring/... and /api/... routes.
244- routerConfig .SetupRoutes (secured , buildInfo )
245- s .restAPIHandle .MountAPIRoutes (securedapi )
246- s .restAPIHandle .MountUserAPIRoutes (userapi )
247- s .restAPIHandle .MountConfigAPIRoutes (configapi )
248- s .restAPIHandle .MountFrontendAPIRoutes (frontendapi )
265+ // Frontend API routes
266+ s .router .Route ("/frontend" , func (frontendapi chi.Router ) {
267+ if ! config .Keys .DisableAuthentication {
268+ frontendapi .Use (func (next http.Handler ) http.Handler {
269+ return authHandle .AuthFrontendAPI (next , onFailureResponse )
270+ })
271+ }
272+ s .restAPIHandle .MountFrontendAPIRoutes (frontendapi )
273+ })
249274
250275 if config .Keys .APISubjects != nil {
251276 s .natsAPIHandle = api .NewNatsAPI ()
@@ -254,28 +279,55 @@ func (s *Server) init() error {
254279 }
255280 }
256281
257- s .restAPIHandle .MountMetricStoreAPIRoutes (metricstoreapi )
282+ // 404 handler for pages and API routes
283+ notFoundHandler := func (rw http.ResponseWriter , r * http.Request ) {
284+ if strings .HasPrefix (r .URL .Path , "/api/" ) || strings .HasPrefix (r .URL .Path , "/userapi/" ) ||
285+ strings .HasPrefix (r .URL .Path , "/frontend/" ) || strings .HasPrefix (r .URL .Path , "/config/" ) {
286+ rw .Header ().Set ("Content-Type" , "application/json" )
287+ rw .WriteHeader (http .StatusNotFound )
288+ json .NewEncoder (rw ).Encode (map [string ]string {
289+ "status" : "Resource not found" ,
290+ "error" : "the requested endpoint does not exist" ,
291+ })
292+ return
293+ }
294+ rw .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
295+ rw .WriteHeader (http .StatusNotFound )
296+ web .RenderTemplate (rw , "404.tmpl" , & web.Page {
297+ Title : "Page Not Found" ,
298+ Build : buildInfo ,
299+ })
300+ }
258301
259302 if config .Keys .EmbedStaticFiles {
260303 if i , err := os .Stat ("./var/img" ); err == nil {
261304 if i .IsDir () {
262305 cclog .Info ("Use local directory for static images" )
263- s .router .PathPrefix ("/img/" ). Handler ( http .StripPrefix ("/img/" , http .FileServer (http .Dir ("./var/img" ))))
306+ s .router .Handle ("/img/*" , http .StripPrefix ("/img/" , http .FileServer (http .Dir ("./var/img" ))))
264307 }
265308 }
266- s .router .PathPrefix ("/" ).Handler (http .StripPrefix ("/" , web .ServeFiles ()))
309+ fileServer := http .StripPrefix ("/" , web .ServeFiles ())
310+ s .router .Handle ("/*" , http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
311+ if web .StaticFileExists (r .URL .Path ) {
312+ fileServer .ServeHTTP (rw , r )
313+ return
314+ }
315+ notFoundHandler (rw , r )
316+ }))
267317 } else {
268- s .router .PathPrefix ("/" ).Handler (http .FileServer (http .Dir (config .Keys .StaticFiles )))
318+ staticDir := http .Dir (config .Keys .StaticFiles )
319+ fileServer := http .FileServer (staticDir )
320+ s .router .Handle ("/*" , http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
321+ f , err := staticDir .Open (r .URL .Path )
322+ if err == nil {
323+ f .Close ()
324+ fileServer .ServeHTTP (rw , r )
325+ return
326+ }
327+ notFoundHandler (rw , r )
328+ }))
269329 }
270330
271- s .router .Use (handlers .CompressHandler )
272- s .router .Use (handlers .RecoveryHandler (handlers .PrintRecoveryStack (true )))
273- s .router .Use (handlers .CORS (
274- handlers .AllowCredentials (),
275- handlers .AllowedHeaders ([]string {"X-Requested-With" , "Content-Type" , "Authorization" , "Origin" }),
276- handlers .AllowedMethods ([]string {"GET" , "POST" , "HEAD" , "OPTIONS" }),
277- handlers .AllowedOrigins ([]string {"*" })))
278-
279331 return nil
280332}
281333
@@ -286,28 +338,14 @@ const (
286338)
287339
288340func (s * Server ) Start (ctx context.Context ) error {
289- handler := handlers .CustomLoggingHandler (io .Discard , s .router , func (_ io.Writer , params handlers.LogFormatterParams ) {
290- if strings .HasPrefix (params .Request .RequestURI , "/api/" ) {
291- cclog .Debugf ("%s %s (%d, %.02fkb, %dms)" ,
292- params .Request .Method , params .URL .RequestURI (),
293- params .StatusCode , float32 (params .Size )/ 1024 ,
294- time .Since (params .TimeStamp ).Milliseconds ())
295- } else {
296- cclog .Debugf ("%s %s (%d, %.02fkb, %dms)" ,
297- params .Request .Method , params .URL .RequestURI (),
298- params .StatusCode , float32 (params .Size )/ 1024 ,
299- time .Since (params .TimeStamp ).Milliseconds ())
300- }
301- })
302-
303341 // Use configurable timeouts with defaults
304342 readTimeout := time .Duration (defaultReadTimeout ) * time .Second
305343 writeTimeout := time .Duration (defaultWriteTimeout ) * time .Second
306344
307345 s .server = & http.Server {
308346 ReadTimeout : readTimeout ,
309347 WriteTimeout : writeTimeout ,
310- Handler : handler ,
348+ Handler : s . router ,
311349 Addr : config .Keys .Addr ,
312350 }
313351
0 commit comments