@@ -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 {
@@ -117,11 +117,11 @@ func (s *Server) init() error {
117117 info ["hasOpenIDConnect" ] = true
118118 }
119119
120- s .router .HandleFunc ("/login" , func (rw http.ResponseWriter , r * http.Request ) {
120+ s .router .Get ("/login" , func (rw http.ResponseWriter , r * http.Request ) {
121121 rw .Header ().Add ("Content-Type" , "text/html; charset=utf-8" )
122122 cclog .Debugf ("##%v##" , info )
123123 web .RenderTemplate (rw , "login.tmpl" , & web.Page {Title : "Login" , Build : buildInfo , Infos : info })
124- }). Methods ( http . MethodGet )
124+ })
125125 s .router .HandleFunc ("/imprint" , func (rw http.ResponseWriter , r * http.Request ) {
126126 rw .Header ().Add ("Content-Type" , "text/html; charset=utf-8" )
127127 web .RenderTemplate (rw , "imprint.tmpl" , & web.Page {Title : "Imprint" , Build : buildInfo })
@@ -131,13 +131,6 @@ func (s *Server) init() error {
131131 web .RenderTemplate (rw , "privacy.tmpl" , & web.Page {Title : "Privacy" , Build : buildInfo })
132132 })
133133
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-
141134 if ! config .Keys .DisableAuthentication {
142135 // Create login failure handler (used by both /login and /jwt-login)
143136 loginFailureHandler := func (rw http.ResponseWriter , r * http.Request , err error ) {
@@ -152,10 +145,10 @@ func (s *Server) init() error {
152145 })
153146 }
154147
155- s .router .Handle ("/login" , authHandle .Login (loginFailureHandler )). Methods ( http . MethodPost )
156- s .router .Handle ("/jwt-login" , authHandle .Login (loginFailureHandler ))
148+ s .router .Post ("/login" , authHandle .Login (loginFailureHandler ). ServeHTTP )
149+ s .router .HandleFunc ("/jwt-login" , authHandle .Login (loginFailureHandler ). ServeHTTP )
157150
158- s .router .Handle ("/logout" , authHandle .Logout (
151+ s .router .Post ("/logout" , authHandle .Logout (
159152 http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
160153 rw .Header ().Add ("Content-Type" , "text/html; charset=utf-8" )
161154 rw .WriteHeader (http .StatusOK )
@@ -166,86 +159,83 @@ func (s *Server) init() error {
166159 Build : buildInfo ,
167160 Infos : info ,
168161 })
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 ,
186- })
187- })
188- })
162+ })).ServeHTTP )
163+ }
189164
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- })
165+ if flagDev {
166+ s .router .Handle ("/playground" , playground .Handler ("GraphQL playground" , "/query" ))
167+ s .router .Get ("/swagger/*" , httpSwagger .Handler (
168+ httpSwagger .URL ("http://" + config .Keys .Addr + "/swagger/doc.json" )))
169+ }
197170
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 )
204- })
171+ // Secured routes (require authentication)
172+ s .router .Group (func (secured chi.Router ) {
173+ if ! config .Keys .DisableAuthentication {
174+ secured .Use (func (next http.Handler ) http.Handler {
175+ return authHandle .Auth (
176+ next ,
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 ,
186+ })
187+ })
188+ })
189+ }
205190
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- })
191+ secured .Handle ("/query" , graphQLServer )
213192
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 )
193+ secured .HandleFunc ("/search" , func (rw http.ResponseWriter , r * http.Request ) {
194+ routerConfig .HandleSearchBar (rw , r , buildInfo )
220195 })
221196
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 )
228- })
229- }
197+ routerConfig .SetupRoutes (secured , buildInfo )
198+ })
230199
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 )
200+ // API routes (JWT token auth)
201+ s .router .Route ("/api" , func (securedapi chi.Router ) {
202+ if ! config .Keys .DisableAuthentication {
203+ securedapi .Use (func (next http.Handler ) http.Handler {
204+ return authHandle .AuthAPI (next , onFailureResponse )
205+ })
206+ }
207+ s .restAPIHandle .MountAPIRoutes (securedapi )
208+ })
237209
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 )
210+ // User API routes
211+ s .router .Route ("/userapi" , func (userapi chi.Router ) {
212+ if ! config .Keys .DisableAuthentication {
213+ userapi .Use (func (next http.Handler ) http.Handler {
214+ return authHandle .AuthUserAPI (next , onFailureResponse )
215+ })
216+ }
217+ s .restAPIHandle .MountUserAPIRoutes (userapi )
241218 })
242219
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 )
220+ // Config API routes
221+ s .router .Route ("/config" , func (configapi chi.Router ) {
222+ if ! config .Keys .DisableAuthentication {
223+ configapi .Use (func (next http.Handler ) http.Handler {
224+ return authHandle .AuthConfigAPI (next , onFailureResponse )
225+ })
226+ }
227+ s .restAPIHandle .MountConfigAPIRoutes (configapi )
228+ })
229+
230+ // Frontend API routes
231+ s .router .Route ("/frontend" , func (frontendapi chi.Router ) {
232+ if ! config .Keys .DisableAuthentication {
233+ frontendapi .Use (func (next http.Handler ) http.Handler {
234+ return authHandle .AuthFrontendAPI (next , onFailureResponse )
235+ })
236+ }
237+ s .restAPIHandle .MountFrontendAPIRoutes (frontendapi )
238+ })
249239
250240 if config .Keys .APISubjects != nil {
251241 s .natsAPIHandle = api .NewNatsAPI ()
@@ -254,27 +244,57 @@ func (s *Server) init() error {
254244 }
255245 }
256246
257- s .restAPIHandle .MountMetricStoreAPIRoutes (metricstoreapi )
247+ // Metric store API routes (mounted under /api but with different auth)
248+ s .router .Route ("/api" , func (metricstoreapi chi.Router ) {
249+ if ! config .Keys .DisableAuthentication {
250+ metricstoreapi .Use (func (next http.Handler ) http.Handler {
251+ return authHandle .AuthMetricStoreAPI (next , onFailureResponse )
252+ })
253+ }
254+ s .restAPIHandle .MountMetricStoreAPIRoutes (metricstoreapi )
255+ })
256+
257+ // Custom 404 handler for unmatched routes
258+ s .router .NotFound (func (rw http.ResponseWriter , r * http.Request ) {
259+ if strings .HasPrefix (r .URL .Path , "/api/" ) || strings .HasPrefix (r .URL .Path , "/userapi/" ) ||
260+ strings .HasPrefix (r .URL .Path , "/frontend/" ) || strings .HasPrefix (r .URL .Path , "/config/" ) {
261+ rw .Header ().Set ("Content-Type" , "application/json" )
262+ rw .WriteHeader (http .StatusNotFound )
263+ json .NewEncoder (rw ).Encode (map [string ]string {
264+ "status" : "Resource not found" ,
265+ "error" : "the requested endpoint does not exist" ,
266+ })
267+ return
268+ }
269+ rw .WriteHeader (http .StatusNotFound )
270+ web .RenderTemplate (rw , "message.tmpl" , & web.Page {
271+ Title : "Not Found" ,
272+ MsgType : "alert-warning" ,
273+ Message : "The requested page was not found." ,
274+ Build : buildInfo ,
275+ })
276+ })
258277
259278 if config .Keys .EmbedStaticFiles {
260279 if i , err := os .Stat ("./var/img" ); err == nil {
261280 if i .IsDir () {
262281 cclog .Info ("Use local directory for static images" )
263- s .router .PathPrefix ("/img/" ). Handler ( http .StripPrefix ("/img/" , http .FileServer (http .Dir ("./var/img" ))))
282+ s .router .Handle ("/img/*" , http .StripPrefix ("/img/" , http .FileServer (http .Dir ("./var/img" ))))
264283 }
265284 }
266- s .router .PathPrefix ("/" ). Handler ( http .StripPrefix ("/" , web .ServeFiles ()))
285+ s .router .Handle ("/*" , http .StripPrefix ("/" , web .ServeFiles ()))
267286 } else {
268- s .router .PathPrefix ("/" ). Handler ( http .FileServer (http .Dir (config .Keys .StaticFiles )))
287+ s .router .Handle ("/*" , http .FileServer (http .Dir (config .Keys .StaticFiles )))
269288 }
270289
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 {"*" })))
290+ s .router .Use (middleware .Compress (5 ))
291+ s .router .Use (middleware .Recoverer )
292+ s .router .Use (cors .Handler (cors.Options {
293+ AllowCredentials : true ,
294+ AllowedHeaders : []string {"X-Requested-With" , "Content-Type" , "Authorization" , "Origin" },
295+ AllowedMethods : []string {"GET" , "POST" , "HEAD" , "OPTIONS" },
296+ AllowedOrigins : []string {"*" },
297+ }))
278298
279299 return nil
280300}
@@ -286,18 +306,17 @@ const (
286306)
287307
288308func (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 {
309+ // Add request logging middleware
310+ s .router .Use (func (next http.Handler ) http.Handler {
311+ return http .HandlerFunc (func (rw http.ResponseWriter , r * http.Request ) {
312+ start := time .Now ()
313+ ww := middleware .NewWrapResponseWriter (rw , r .ProtoMajor )
314+ next .ServeHTTP (ww , r )
296315 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- }
316+ r . Method , r .URL .RequestURI (),
317+ ww . Status () , float32 (ww . BytesWritten () )/ 1024 ,
318+ time .Since (start ).Milliseconds ())
319+ })
301320 })
302321
303322 // Use configurable timeouts with defaults
@@ -307,7 +326,7 @@ func (s *Server) Start(ctx context.Context) error {
307326 s .server = & http.Server {
308327 ReadTimeout : readTimeout ,
309328 WriteTimeout : writeTimeout ,
310- Handler : handler ,
329+ Handler : s . router ,
311330 Addr : config .Keys .Addr ,
312331 }
313332
0 commit comments