Files
remark42/backend/app/rest/api/ssl.go
T
Dmitry VerkhoturovandUmputun bb6d1450f1 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.
2026-06-30 16:26:03 -05:00

168 lines
5.0 KiB
Go

package api
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"net/url"
"strings"
"time"
log "github.com/go-pkgz/lgr"
R "github.com/go-pkgz/rest"
"github.com/go-pkgz/routegroup"
"golang.org/x/crypto/acme/autocert"
)
// sslMode defines ssl mode for rest server
type sslMode int8
const (
// None defines to run http server only
None sslMode = iota
// Static defines to run both https and http server. Redirect http to https
Static
// Auto defines to run both https and http server. Redirect http to https. Https server with autocert support
Auto
)
// SSLConfig holds all ssl params for rest server
type SSLConfig struct {
SSLMode sslMode
Cert string
Key string
Port int
ACMELocation string
ACMEEmail string
}
// httpToHTTPSRouter creates new router which does redirect from http to https server
// with default middlewares. Used in 'static' ssl mode.
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())
return router
}
// httpChallengeRouter creates new router which performs ACME "http-01" challenge response
// 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) http.Handler {
log.Printf("[DEBUG] create http-challenge routes")
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()))
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)
if err != nil {
log.Printf("[WARN] failed to build redirect URL, %s", err)
http.Error(w, "invalid redirect URL", http.StatusInternalServerError)
return
}
http.Redirect(w, r, newURL, http.StatusTemporaryRedirect)
})
}
func (s *Rest) redirectURL(r *http.Request) (string, error) {
baseURL, err := url.Parse(s.RemarkURL)
if err != nil {
return "", fmt.Errorf("parse remark URL: %w", err)
}
if baseURL.Scheme != "http" && baseURL.Scheme != "https" || baseURL.Host == "" {
return "", fmt.Errorf("remark URL must be absolute HTTP(S) URL")
}
basePath := strings.TrimRight(baseURL.Path, "/")
requestPath := "/" + strings.TrimLeft(r.URL.Path, "/")
baseURL.Path = basePath + requestPath
baseURL.RawQuery = r.URL.RawQuery
baseURL.Fragment = ""
return baseURL.String(), nil
}
func (s *Rest) makeAutocertManager() *autocert.Manager {
return &autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache(s.SSLConfig.ACMELocation),
HostPolicy: autocert.HostWhitelist(s.getRemarkHost()),
Email: s.SSLConfig.ACMEEmail,
}
}
// makeHTTPSAutoCertServer makes https server with autocert mode (LE support)
func (s *Rest) makeHTTPSAutocertServer(address string, port int, router http.Handler, m *autocert.Manager) *http.Server {
server := s.makeHTTPServer(address, port, router)
cfg := s.makeTLSConfig()
cfg.GetCertificate = m.GetCertificate
server.TLSConfig = cfg
return server
}
// makeHTTPSServer makes https server for static mode
func (s *Rest) makeHTTPSServer(address string, port int, router http.Handler) *http.Server {
server := s.makeHTTPServer(address, port, router)
server.TLSConfig = s.makeTLSConfig()
return server
}
// getRemarkHost returns hostname for remark server.
// For example for remarkURL https://remark.com:443 it should return remark.com
func (s *Rest) getRemarkHost() string {
u, err := url.Parse(s.RemarkURL)
if err != nil {
return ""
}
return u.Hostname()
}
func (s *Rest) makeTLSConfig() *tls.Config {
return &tls.Config{
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.CurveP256,
tls.X25519,
tls.CurveP384,
},
}
}