Skip to content

Commit f6aa40d

Browse files
committed
Migrate from gorilla to chi web framework. add 404 handler
1 parent c920c57 commit f6aa40d

File tree

9 files changed

+252
-226
lines changed

9 files changed

+252
-226
lines changed

cmd/cc-backend/server.go

Lines changed: 129 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -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
5252
type 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

288308
func (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

go.mod

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,20 @@ require (
1717
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
1818
github.com/coreos/go-oidc/v3 v3.17.0
1919
github.com/expr-lang/expr v1.17.7
20+
github.com/go-chi/chi/v5 v5.2.5
21+
github.com/go-chi/cors v1.2.2
2022
github.com/go-co-op/gocron/v2 v2.19.0
2123
github.com/go-ldap/ldap/v3 v3.4.12
2224
github.com/golang-jwt/jwt/v5 v5.3.0
2325
github.com/golang-migrate/migrate/v4 v4.19.1
2426
github.com/google/gops v0.3.28
25-
github.com/gorilla/handlers v1.5.2
26-
github.com/gorilla/mux v1.8.1
2727
github.com/gorilla/sessions v1.4.0
2828
github.com/influxdata/line-protocol/v2 v2.2.1
2929
github.com/jmoiron/sqlx v1.4.0
3030
github.com/joho/godotenv v1.5.1
3131
github.com/linkedin/goavro/v2 v2.14.1
3232
github.com/mattn/go-sqlite3 v1.14.33
33+
github.com/parquet-go/parquet-go v0.27.0
3334
github.com/qustavo/sqlhooks/v2 v2.1.0
3435
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
3536
github.com/stretchr/testify v1.11.1
@@ -64,7 +65,6 @@ require (
6465
github.com/aws/smithy-go v1.24.0 // indirect
6566
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
6667
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
67-
github.com/felixge/httpsnoop v1.0.4 // indirect
6868
github.com/fsnotify/fsnotify v1.9.0 // indirect
6969
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
7070
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
@@ -81,7 +81,6 @@ require (
8181
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
8282
github.com/goccy/go-yaml v1.19.0 // indirect
8383
github.com/golang/snappy v0.0.4 // indirect
84-
github.com/google/go-cmp v0.7.0 // indirect
8584
github.com/google/uuid v1.6.0 // indirect
8685
github.com/gorilla/securecookie v1.1.2 // indirect
8786
github.com/gorilla/websocket v1.5.3 // indirect
@@ -99,7 +98,6 @@ require (
9998
github.com/oapi-codegen/runtime v1.1.1 // indirect
10099
github.com/parquet-go/bitpack v1.0.0 // indirect
101100
github.com/parquet-go/jsonlite v1.0.0 // indirect
102-
github.com/parquet-go/parquet-go v0.27.0 // indirect
103101
github.com/pierrec/lz4/v4 v4.1.21 // indirect
104102
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
105103
github.com/prometheus/common v0.67.4 // indirect

0 commit comments

Comments
 (0)