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.
168 lines
5.0 KiB
Go
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,
|
|
},
|
|
}
|
|
}
|