mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-04 09:02:25 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			308 lines
		
	
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			308 lines
		
	
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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"
 | 
						|
	"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
 | 
						|
	leSrv  *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.Panicf(nil, "listen: %v", err)
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Stop shuts down the router nicely.
 | 
						|
func (r *Router) Stop() error {
 | 
						|
	ctx := context.Background()
 | 
						|
 | 
						|
	// Shut down "main" server.
 | 
						|
	if err := stopServer(ctx, r.srv, "http server"); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// Shut down letsencrypt
 | 
						|
	// server if enabled.
 | 
						|
	if r.leSrv != nil {
 | 
						|
		if err := stopServer(ctx, r.leSrv, "letsencrypt http server"); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func stopServer(
 | 
						|
	ctx context.Context,
 | 
						|
	s *http.Server,
 | 
						|
	name string,
 | 
						|
) error {
 | 
						|
	timeout, cancel := context.WithTimeout(ctx, shutdownTimeout)
 | 
						|
	defer cancel()
 | 
						|
 | 
						|
	log.Infof(nil, "shutting down %s with %s grace period", name, shutdownTimeout)
 | 
						|
	if err := s.Shutdown(timeout); err != nil {
 | 
						|
		return fmt.Errorf("error shutting down %s: %w", name, err)
 | 
						|
	}
 | 
						|
 | 
						|
	log.Infof(ctx, "%s closed connections and shut down gracefully", name)
 | 
						|
	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 sets r.leSrv and 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.
 | 
						|
	r.leSrv = new(http.Server) //nolint:gosec
 | 
						|
	*r.leSrv = (*r.srv)        //nolint:govet
 | 
						|
	r.leSrv.Handler = acm.HTTPHandler(fallback)
 | 
						|
	r.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", r.leSrv.Addr)
 | 
						|
		if err := r.leSrv.ListenAndServe(); err != nil &&
 | 
						|
			err != http.ErrServerClosed {
 | 
						|
			log.Panicf(nil, "letsencrypt: listen: %v", err)
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	// Update listen function to use LetsEncrypt TLS.
 | 
						|
	listen := func() error { return r.srv.ListenAndServeTLS("", "") }
 | 
						|
	return listen, nil
 | 
						|
}
 |