From bb6d1450f199bc403f642a42c3446b7db84f14a1 Mon Sep 17 00:00:00 2001 From: Dmitry Verkhoturov Date: Tue, 30 Jun 2026 21:50:49 +0100 Subject: [PATCH] Migrate ssl.go TLS routers from chi to routegroup First step of the go-chi -> go-pkgz/routegroup migration. The HTTP->HTTPS redirect and ACME http-01 challenge routers are small, self-contained http.Handlers separate from the main API router, so they move cleanly: - chi.NewRouter() -> routegroup.New(http.NewServeMux()) - middleware.Throttle -> rest.Throttle (same concurrency-limit semantics) - middleware.Timeout -> local timeout helper (context deadline, mirrors chi) - drop middleware.RealIP: these routers do redirect/challenge only, with no per-IP logic, so the spoofable header trust is simply removed here - return http.Handler instead of chi.Router (callers already take http.Handler) chi stays a dependency (still used by the main API router); this only removes its use from ssl.go. go test -race, vet, golangci-lint and govulncheck clean. --- backend/app/rest/api/ssl.go | 49 ++++++++++++++++++++++---------- backend/app/rest/api/ssl_test.go | 26 +++++++++++++++++ backend/go.mod | 2 +- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/backend/app/rest/api/ssl.go b/backend/app/rest/api/ssl.go index f1beaeab..f163ec17 100644 --- a/backend/app/rest/api/ssl.go +++ b/backend/app/rest/api/ssl.go @@ -1,6 +1,7 @@ package api import ( + "context" "crypto/tls" "fmt" "net/http" @@ -8,12 +9,10 @@ import ( "strings" "time" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" log "github.com/go-pkgz/lgr" - "golang.org/x/crypto/acme/autocert" - R "github.com/go-pkgz/rest" + "github.com/go-pkgz/routegroup" + "golang.org/x/crypto/acme/autocert" ) // sslMode defines ssl mode for rest server @@ -42,13 +41,13 @@ type SSLConfig struct { // httpToHTTPSRouter creates new router which does redirect from http to https server // with default middlewares. Used in 'static' ssl mode. -func (s *Rest) httpToHTTPSRouter() chi.Router { - log.Printf("[DEBUG] create https-to-http redirect routes") - router := chi.NewRouter() - router.Use(middleware.RealIP, R.Recoverer(log.Default())) - router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) +func (s *Rest) httpToHTTPSRouter() http.Handler { + log.Printf("[DEBUG] create http-to-https redirect routes") + router := routegroup.New(http.NewServeMux()) + router.Use(R.Recoverer(log.Default())) + router.Use(R.Throttle(1000), timeout(60*time.Second)) - router.Handle("/*", s.redirectHandler()) + router.Handle("/", s.redirectHandler()) return router } @@ -56,16 +55,36 @@ func (s *Rest) httpToHTTPSRouter() chi.Router { // with default middlewares. This part is necessary to obtain certificate from LE. // If it receives not a acme challenge it performs redirect to https server. // Used in 'auto' ssl mode. -func (s *Rest) httpChallengeRouter(m *autocert.Manager) chi.Router { +func (s *Rest) httpChallengeRouter(m *autocert.Manager) http.Handler { log.Printf("[DEBUG] create http-challenge routes") - router := chi.NewRouter() - router.Use(middleware.RealIP, R.Recoverer(log.Default())) - router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) + router := routegroup.New(http.NewServeMux()) + router.Use(R.Recoverer(log.Default())) + router.Use(R.Throttle(1000), timeout(60*time.Second)) - router.Handle("/*", m.HTTPHandler(s.redirectHandler())) + router.Handle("/", m.HTTPHandler(s.redirectHandler())) return router } +// timeout returns a middleware matching chi's middleware.Timeout: it sets a +// deadline on the request context and writes 504 Gateway Timeout if the +// deadline is exceeded. The 504 is sent once the downstream handler returns +// after observing the canceled context; a handler that ignores r.Context() +// is not aborted. +func timeout(d time.Duration) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), d) + defer func() { + cancel() + if ctx.Err() == context.DeadlineExceeded { + w.WriteHeader(http.StatusGatewayTimeout) + } + }() + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + func (s *Rest) redirectHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { newURL, err := s.redirectURL(r) diff --git a/backend/app/rest/api/ssl_test.go b/backend/app/rest/api/ssl_test.go index d177c8ba..cc8f8efd 100644 --- a/backend/app/rest/api/ssl_test.go +++ b/backend/app/rest/api/ssl_test.go @@ -8,11 +8,37 @@ import ( "net/http/httptest" "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestTimeout(t *testing.T) { + t.Run("fast handler passes through and gets a deadline", func(t *testing.T) { + var gotDeadline bool + h := timeout(time.Second)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, gotDeadline = r.Context().Deadline() + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("ok")) + })) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody)) + assert.True(t, gotDeadline, "request context should carry a deadline") + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, "ok", rec.Body.String()) + }) + + t.Run("deadline exceeded writes 504", func(t *testing.T) { + h := timeout(10*time.Millisecond)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + <-r.Context().Done() // honor the context: return only once the deadline fires + })) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", http.NoBody)) + assert.Equal(t, http.StatusGatewayTimeout, rec.Code) + }) +} + func TestSSL_Redirect(t *testing.T) { rest := Rest{RemarkURL: "https://localhost:443"} diff --git a/backend/go.mod b/backend/go.mod index e9f6b73a..61ffb839 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -16,6 +16,7 @@ require ( github.com/go-pkgz/notify v1.3.0 github.com/go-pkgz/repeater/v2 v2.2.0 github.com/go-pkgz/rest v1.21.0 + github.com/go-pkgz/routegroup v1.6.0 github.com/go-pkgz/syncs v1.3.2 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 @@ -48,7 +49,6 @@ require ( github.com/go-pkgz/email v0.6.0 // indirect github.com/go-pkgz/expirable-cache/v3 v3.1.0 // indirect github.com/go-pkgz/repeater v1.2.0 // indirect - github.com/go-pkgz/routegroup v1.6.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect