mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 05:12:25 -05:00 
			
		
		
		
	* [chore] Refactor HTML templates and CSS * eslint * ignore "Local" * rss tests * fiddle with OG just a tiny bit * dick around with polls a bit more so SR stops saying "clickable" * remove break * oh lord * don't lazy load avatar * fix ogmeta tests * clean up some cruft * catch remaining calls to c.HTML * fix error rendering + stack overflow in tag * allow templating attributes * fix indent * set aria-hidden on status complementary content, since it's already present in the label anyway * tidy up templating calls a little * try to make styling a bit more consistent + readable * fix up some remaining CSS issues * fix up reports
		
			
				
	
	
		
			279 lines
		
	
	
	
		
			8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			279 lines
		
	
	
	
		
			8 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
 | |
| }
 | |
| 
 | |
| // 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() {
 | |
| 	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 err != nil {
 | |
| 		log.Fatal(nil, err)
 | |
| 	}
 | |
| 
 | |
| 	// 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)
 | |
| 		}
 | |
| 	}()
 | |
| }
 | |
| 
 | |
| // Stop shuts down the router nicely
 | |
| 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()
 | |
| 
 | |
| 	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
 | |
| }
 |