Files
remark42/backend/app/rest/api/ssl.go
T
Dmitry VerkhoturovandUmputun b33025a76f feat(api): adopt enforcing rest.Timeout, drop local cooperative timeout
go-pkgz/rest v1.22.0 ships an enforcing Timeout middleware (net/http.TimeoutHandler
style): it runs the handler with a deadline and returns 504 at the deadline even if
the handler ignores the context - unlike the local cooperative timeout, which only
cancelled the context and never actually stopped a stuck handler.

Replace the local timeout with rest.Timeout on every route with a bounded response.
The streaming and long-polling routes are deliberately left without it, since the
enforcing timeout buffers the whole response in memory and aborts at the deadline:
- GET /api/v1/userdata and GET /api/v1/admin/export stream gzipped exports
- GET /api/v1/admin/wait long-polls for up to 15m
- POST /api/v1/admin/import[/form] and /remap ingest large uploads

Delete the local timeout middleware and its test; the enforcing behaviour is covered
by go-pkgz/rest. TestRouteTimeout locks the enforcing-vs-exempt contract in this build.
2026-07-03 15:40:10 -05:00

147 lines
4.2 KiB
Go

package api
import (
"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), R.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), R.Timeout(60*time.Second))
router.Handle("/", m.HTTPHandler(s.redirectHandler()))
return router
}
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,
},
}
}