summaryrefslogtreecommitdiff
path: root/internal/router
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-11-13 19:48:51 +0100
committerLibravatar GitHub <noreply@github.com>2023-11-13 19:48:51 +0100
commit8d0c017cf205bcf57630c91b53079001deed4d36 (patch)
treecf2240e3dff137df2871c9b93548a885833e9a52 /internal/router
parent[chore] update otel -> v1.20.0 (#2358) (diff)
downloadgotosocial-8d0c017cf205bcf57630c91b53079001deed4d36.tar.xz
[feature/performance] Wrap incoming HTTP requests in timeout handler (#2353)
* deinterface router, start messing about with deadlines * weeeee * thanks linter (thinter) * write Connection: close when timing out requests * update wording * don't replace req * don't bother with fancy Cause functions (I'll use them one day...)
Diffstat (limited to 'internal/router')
-rw-r--r--internal/router/attach.go8
-rw-r--r--internal/router/router.go326
-rw-r--r--internal/router/timeout.go61
3 files changed, 246 insertions, 149 deletions
diff --git a/internal/router/attach.go b/internal/router/attach.go
index 40e5992a4..4b4fd7ac4 100644
--- a/internal/router/attach.go
+++ b/internal/router/attach.go
@@ -19,18 +19,18 @@ package router
import "github.com/gin-gonic/gin"
-func (r *router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes {
+func (r *Router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes {
return r.engine.Use(handlers...)
}
-func (r *router) AttachNoRouteHandler(handler gin.HandlerFunc) {
+func (r *Router) AttachNoRouteHandler(handler gin.HandlerFunc) {
r.engine.NoRoute(handler)
}
-func (r *router) AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
+func (r *Router) AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return r.engine.Group(relativePath, handlers...)
}
-func (r *router) AttachHandler(method string, path string, handler gin.HandlerFunc) {
+func (r *Router) AttachHandler(method string, path string, handler gin.HandlerFunc) {
r.engine.Handle(method, path, handler)
}
diff --git a/internal/router/router.go b/internal/router/router.go
index b6991f97f..f71dc97ef 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -29,6 +29,7 @@ import (
"codeberg.org/gruf/go-debug"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log"
"golang.org/x/crypto/acme/autocert"
)
@@ -42,94 +43,120 @@ const (
maxMultipartMemory = int64(8 * bytesize.MiB)
)
-// Router provides the REST interface for gotosocial, using gin.
-type Router interface {
- // Attach global gin middlewares to this router.
- AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes
- // AttachGroup attaches the given handlers into a group with the given relativePath as
- // base path for that group. It then returns the *gin.RouterGroup so that the caller
- // can add any extra middlewares etc specific to that group, as desired.
- AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup
- // Attach a single gin handler to the router with the given method and path.
- // To make middleware management easier, AttachGroup should be preferred where possible.
- // However, this function can be used for attaching single handlers that only require
- // global middlewares.
- AttachHandler(method string, path string, handler gin.HandlerFunc)
-
- // Attach 404 NoRoute handler
- AttachNoRouteHandler(handler gin.HandlerFunc)
- // Start the router
- Start()
- // Stop the router
- Stop(ctx context.Context) error
+// Router provides the HTTP REST
+// interface for GoToSocial, using gin.
+type Router struct {
+ engine *gin.Engine
+ srv *http.Server
}
-// router fulfils the Router interface using gin and logrus
-type router struct {
- engine *gin.Engine
- srv *http.Server
- certManager *autocert.Manager
+// New returns a new Router, which wraps
+// an http server and gin handler engine.
+//
+// The router's Attach functions should be
+// used *before* the router is Started.
+//
+// When the router's work is finished, Stop
+// should be called on it to close connections
+// gracefully.
+//
+// The provided context will be used as the base
+// context for all requests passing through the
+// underlying http.Server, so this should be a
+// long-running context.
+func New(ctx context.Context) (*Router, error) {
+ // TODO: make this configurable?
+ gin.SetMode(gin.ReleaseMode)
+
+ // Create the engine here -- this is the core
+ // request routing handler for GoToSocial.
+ engine := gin.New()
+ engine.MaxMultipartMemory = maxMultipartMemory
+ engine.HandleMethodNotAllowed = true
+
+ // Set up client IP forwarding via
+ // trusted x-forwarded-* headers.
+ trustedProxies := config.GetTrustedProxies()
+ if err := engine.SetTrustedProxies(trustedProxies); err != nil {
+ return nil, err
+ }
+
+ // Attach functions used by HTML templating,
+ // and load HTML templates into the engine.
+ LoadTemplateFunctions(engine)
+ if err := LoadTemplates(engine); err != nil {
+ return nil, err
+ }
+
+ // Use the passed-in cmd context as the base context for the
+ // server, since we'll never want the server to live past the
+ // `server start` command anyway.
+ baseCtx := func(_ net.Listener) context.Context { return ctx }
+
+ addr := fmt.Sprintf("%s:%d",
+ config.GetBindAddress(),
+ config.GetPort(),
+ )
+
+ // Wrap the gin engine handler in our
+ // own timeout handler, to ensure we
+ // don't keep very slow requests around.
+ handler := timeoutHandler{engine}
+
+ s := &http.Server{
+ Addr: addr,
+ Handler: handler,
+ ReadTimeout: readTimeout,
+ ReadHeaderTimeout: readHeaderTimeout,
+ WriteTimeout: writeTimeout,
+ IdleTimeout: idleTimeout,
+ BaseContext: baseCtx,
+ }
+
+ return &Router{
+ engine: engine,
+ srv: s,
+ }, nil
}
-// Start starts the router nicely. It will serve two handlers if letsencrypt is enabled, and only the web/API handler if letsencrypt is not enabled.
-func (r *router) Start() {
- // listen is the server start function, by
- // default pointing to regular HTTP listener,
- // but updated to TLS if LetsEncrypt is enabled.
- listen := r.srv.ListenAndServe
-
- // During config validation we already checked that both Chain and Key are set
- // so we can forego checking for both here
- if chain := config.GetTLSCertificateChain(); chain != "" {
- pkey := config.GetTLSCertificateKey()
- cer, err := tls.LoadX509KeyPair(chain, pkey)
- if err != nil {
- log.Fatalf(
- nil,
- "tls: failed to load keypair from %s and %s, ensure they are PEM-encoded and can be read by this process: %s",
- chain, pkey, err,
- )
- }
- r.srv.TLSConfig = &tls.Config{
- MinVersion: tls.VersionTLS12,
- Certificates: []tls.Certificate{cer},
- }
- // TLS is enabled, update the listen function
- listen = func() error { return r.srv.ListenAndServeTLS("", "") }
+// Start starts the router nicely.
+//
+// It will serve two handlers if letsencrypt is enabled,
+// and only the web/API handler if letsencrypt is not enabled.
+func (r *Router) Start() {
+ var (
+ // listen is the server start function.
+ // By default this points to a regular
+ // HTTP listener, but will be changed to
+ // TLS if custom certs or LE are enabled.
+ listen func() error
+ err error
+
+ certFile = config.GetTLSCertificateChain()
+ keyFile = config.GetTLSCertificateKey()
+ leEnabled = config.GetLetsEncryptEnabled()
+ )
+
+ switch {
+ // TLS with custom certs.
+ case certFile != "":
+ // During config validation we already checked
+ // that either both or neither of Chain and Key
+ // are set, so we can forego checking again here.
+ listen, err = r.customTLS(certFile, keyFile)
+
+ // TLS with letsencrypt.
+ case leEnabled:
+ listen, err = r.letsEncryptTLS()
+
+ // Default listen. TLS must
+ // be handled by reverse proxy.
+ default:
+ listen = r.srv.ListenAndServe
}
- if config.GetLetsEncryptEnabled() {
- // LetsEncrypt support is enabled
-
- // Prepare an HTTPS-redirect handler for LetsEncrypt fallback
- redirect := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- target := "https://" + r.Host + r.URL.Path
- if len(r.URL.RawQuery) > 0 {
- target += "?" + r.URL.RawQuery
- }
- http.Redirect(rw, r, target, http.StatusTemporaryRedirect)
- })
-
- go func() {
- // Take our own copy of HTTP server
- // with updated autocert manager endpoint
- srv := (*r.srv) //nolint
- srv.Handler = r.certManager.HTTPHandler(redirect)
- srv.Addr = fmt.Sprintf("%s:%d",
- config.GetBindAddress(),
- config.GetLetsEncryptPort(),
- )
-
- // Start the LetsEncrypt autocert manager HTTP server.
- log.Infof(nil, "letsencrypt listening on %s", srv.Addr)
- if err := srv.ListenAndServe(); err != nil &&
- err != http.ErrServerClosed {
- log.Fatalf(nil, "letsencrypt: listen: %s", err)
- }
- }()
-
- // TLS is enabled, update the listen function
- listen = func() error { return r.srv.ListenAndServeTLS("", "") }
+ if err != nil {
+ log.Fatal(nil, err)
}
// Pass the server handler through a debug pprof middleware handler.
@@ -154,7 +181,7 @@ func (r *router) Start() {
}
// Stop shuts down the router nicely
-func (r *router) Stop(ctx context.Context) error {
+func (r *Router) Stop(ctx context.Context) error {
log.Infof(nil, "shutting down http router with %s grace period", shutdownTimeout)
timeout, cancel := context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
@@ -167,78 +194,87 @@ func (r *router) Stop(ctx context.Context) error {
return nil
}
-// New returns a new Router.
-//
-// The router's Attach functions should be used *before* the router is Started.
-//
-// When the router's work is finished, Stop should be called on it to close connections gracefully.
-//
-// The provided context will be used as the base context for all requests passing
-// through the underlying http.Server, so this should be a long-running context.
-func New(ctx context.Context) (Router, error) {
- gin.SetMode(gin.TestMode)
-
- // create the actual engine here -- this is the core request routing handler for gts
- engine := gin.New()
- engine.MaxMultipartMemory = maxMultipartMemory
- engine.HandleMethodNotAllowed = true
-
- // set up IP forwarding via x-forward-* headers.
- trustedProxies := config.GetTrustedProxies()
- if err := engine.SetTrustedProxies(trustedProxies); err != nil {
+// customTLS modifies the router's underlying
+// http server to use custom TLS cert/key pair.
+func (r *Router) customTLS(
+ certFile string,
+ keyFile string,
+) (func() error, error) {
+ // Load certificates from disk.
+ cer, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err != nil {
+ err = gtserror.Newf(
+ "failed to load keypair from %s and %s, ensure they are "+
+ "PEM-encoded and can be read by this process: %w",
+ certFile, keyFile, err,
+ )
return nil, err
}
- // set template functions
- LoadTemplateFunctions(engine)
-
- // load templates onto the engine
- if err := LoadTemplates(engine); err != nil {
- return nil, err
+ // Override server's TLSConfig.
+ r.srv.TLSConfig = &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ Certificates: []tls.Certificate{cer},
}
- // use the passed-in command context as the base context for the server,
- // since we'll never want the server to live past the command anyway
- baseCtx := func(_ net.Listener) context.Context {
- return ctx
+ // Update listen function to use custom TLS.
+ listen := func() error { return r.srv.ListenAndServeTLS("", "") }
+ return listen, nil
+}
+
+// letsEncryptTLS modifies the router's underlying http
+// server to use LetsEncrypt via an ACME Autocert manager.
+//
+// It also starts a listener on the configured LetsEncrypt
+// port to validate LE requests.
+func (r *Router) letsEncryptTLS() (func() error, error) {
+ acm := &autocert.Manager{
+ Prompt: autocert.AcceptTOS,
+ HostPolicy: autocert.HostWhitelist(config.GetHost()),
+ Cache: autocert.DirCache(config.GetLetsEncryptCertDir()),
+ Email: config.GetLetsEncryptEmailAddress(),
}
- bindAddress := config.GetBindAddress()
- port := config.GetPort()
- addr := fmt.Sprintf("%s:%d", bindAddress, port)
+ // Override server's TLSConfig.
+ r.srv.TLSConfig = acm.TLSConfig()
- s := &http.Server{
- Addr: addr,
- Handler: engine, // use gin engine as handler
- ReadTimeout: readTimeout,
- ReadHeaderTimeout: readHeaderTimeout,
- WriteTimeout: writeTimeout,
- IdleTimeout: idleTimeout,
- BaseContext: baseCtx,
+ // Prepare a fallback handler for LetsEncrypt.
+ //
+ // This will redirect all non-LetsEncrypt http
+ // reqs to https, preserving path and query params.
+ var fallback http.HandlerFunc = func(
+ w http.ResponseWriter,
+ r *http.Request,
+ ) {
+ // Rewrite target to https.
+ target := "https://" + r.Host + r.URL.Path
+ if len(r.URL.RawQuery) > 0 {
+ target += "?" + r.URL.RawQuery
+ }
+
+ http.Redirect(w, r, target, http.StatusTemporaryRedirect)
}
- // We need to spawn the underlying server slightly differently depending on whether lets encrypt is enabled or not.
- // In either case, the gin engine will still be used for routing requests.
- leEnabled := config.GetLetsEncryptEnabled()
-
- var m *autocert.Manager
- if leEnabled {
- // le IS enabled, so roll up an autocert manager for handling letsencrypt requests
- host := config.GetHost()
- leCertDir := config.GetLetsEncryptCertDir()
- leEmailAddress := config.GetLetsEncryptEmailAddress()
- m = &autocert.Manager{
- Prompt: autocert.AcceptTOS,
- HostPolicy: autocert.HostWhitelist(host),
- Cache: autocert.DirCache(leCertDir),
- Email: leEmailAddress,
+ // Take our own copy of the HTTP server,
+ // and update it to serve LetsEncrypt
+ // requests via the autocert manager.
+ leSrv := (*r.srv) //nolint:govet
+ leSrv.Handler = acm.HTTPHandler(fallback)
+ leSrv.Addr = fmt.Sprintf("%s:%d",
+ config.GetBindAddress(),
+ config.GetLetsEncryptPort(),
+ )
+
+ go func() {
+ // Start the LetsEncrypt autocert manager HTTP server.
+ log.Infof(nil, "letsencrypt listening on %s", leSrv.Addr)
+ if err := leSrv.ListenAndServe(); err != nil &&
+ err != http.ErrServerClosed {
+ log.Fatalf(nil, "letsencrypt: listen: %s", err)
}
- s.TLSConfig = m.TLSConfig()
- }
+ }()
- return &router{
- engine: engine,
- srv: s,
- certManager: m,
- }, nil
+ // Update listen function to use LetsEncrypt TLS.
+ listen := func() error { return r.srv.ListenAndServeTLS("", "") }
+ return listen, nil
}
diff --git a/internal/router/timeout.go b/internal/router/timeout.go
new file mode 100644
index 000000000..0f1ef869b
--- /dev/null
+++ b/internal/router/timeout.go
@@ -0,0 +1,61 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package router
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+const requestTimeout = 10 * time.Minute
+
+type timeoutHandler struct {
+ *gin.Engine
+}
+
+// ServeHTTP wraps the embedded Gin engine's ServeHTTP
+// function with an injected context which times out
+// non-upgraded inbound requests after 10 minutes.
+func (th timeoutHandler) ServeHTTP(
+ w http.ResponseWriter,
+ r *http.Request,
+) {
+ if upgr := r.Header.Get("Upgrade"); upgr != "" {
+ // Upgrade to wss (probably).
+ // Leave well enough alone.
+ th.Engine.ServeHTTP(w, r)
+ return
+ }
+
+ // Create timeout ctx.
+ toCtx, cancelCtx := context.WithTimeout(
+ r.Context(),
+ requestTimeout,
+ )
+ defer cancelCtx()
+
+ // Serve the request using a shallow copy
+ // with the new context, without replacing
+ // the underlying request, since the latter
+ // may be used later outside of the Gin
+ // engine for post-request cleanup tasks.
+ th.Engine.ServeHTTP(w, r.WithContext(toCtx))
+}