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.
This commit is contained in:
Dmitry Verkhoturov
2026-06-30 16:26:03 -05:00
committed by Umputun
parent 8318f89dde
commit bb6d1450f1
3 changed files with 61 additions and 16 deletions
+34 -15
View File
@@ -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)
+26
View File
@@ -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"}
+1 -1
View File
@@ -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