// 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 . package router import ( "context" "crypto/tls" "fmt" "net" "net/http" "time" "codeberg.org/gruf/go-bytesize" "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" ) const ( readTimeout = 60 * time.Second writeTimeout = 30 * time.Second idleTimeout = 30 * time.Second readHeaderTimeout = 30 * time.Second shutdownTimeout = 30 * time.Second maxMultipartMemory = int64(8 * bytesize.MiB) ) // Router provides the HTTP REST // interface for GoToSocial, using gin. type Router struct { engine *gin.Engine srv *http.Server } // 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. 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() error { 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) if err != nil { return err } // TLS with letsencrypt. case leEnabled: listen, err = r.letsEncryptTLS() if err != nil { return err } // Default listen. TLS must // be handled by reverse proxy. default: listen = r.srv.ListenAndServe } // Pass the server handler through a debug pprof middleware handler. // For standard production builds this will be a no-op, but when the // "debug" or "debugenv" build-tag is set pprof stats will be served // at the standard "/debug/pprof" URL. r.srv.Handler = debug.WithPprof(r.srv.Handler) if debug.DEBUG { // Profiling requires timeouts longer than 30s, so reset these. log.Warn(nil, "resetting http.Server{} timeout to support profiling") r.srv.ReadTimeout = 0 r.srv.WriteTimeout = 0 } // Start the main listener. go func() { log.Infof(nil, "listening on %s", r.srv.Addr) if err := listen(); err != nil && err != http.ErrServerClosed { log.Fatalf(nil, "listen: %s", err) } }() return nil } // Stop shuts down the router nicely. func (r *Router) Stop() error { log.Infof(nil, "shutting down http router with %s grace period", shutdownTimeout) timeout, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() if err := r.srv.Shutdown(timeout); err != nil { return fmt.Errorf("error shutting down http router: %s", err) } log.Info(nil, "http router closed connections and shut down gracefully") return 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 } // Override server's TLSConfig. r.srv.TLSConfig = &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cer}, } // 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(), } // Override server's TLSConfig. r.srv.TLSConfig = acm.TLSConfig() // 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) } // 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) } }() // Update listen function to use LetsEncrypt TLS. listen := func() error { return r.srv.ListenAndServeTLS("", "") } return listen, nil }