Ctrl K
ComunidadGuías…

engine/serve.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package engine

import (
	"context"
	"crypto/tls"
	"errors"
	"net"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/caddyserver/certmagic"
	"github.com/sirupsen/logrus"
	"github.com/xyproto/algernon/utils"
	"github.com/xyproto/env/v2"
	"golang.org/x/net/http2"
)

// limitBodyMiddleware wraps req.Body in http.MaxBytesReader, sharing the
// largeFileSize cap so no handler can allocate unbounded memory.
func (ac *Config) limitBodyMiddleware(next http.Handler) http.Handler {
	limit := int64(ac.largeFileSize)
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Body != nil {
			r.Body = http.MaxBytesReader(w, r.Body, limit)
		}
		next.ServeHTTP(w, r)
	})
}

// isBindError returns true if the error is a fatal port binding error
// (permission denied, address already in use, etc).
func isBindError(err error) bool {
	if opErr, ok := errors.AsType[*net.OpError](err); ok {
		return opErr.Op == "listen"
	}
	return false
}

// List of functions to run at shutdown
var (
	shutdownFunctions  []func()
	serverServingMutex sync.Mutex
	completed          atomic.Bool
)

// AtShutdown adds a function to the list of functions that will be ran at shutdown
func AtShutdown(shutdownFunction func()) {
	serverServingMutex.Lock()
	defer serverServingMutex.Unlock()
	shutdownFunctions = append(shutdownFunctions, shutdownFunction)
}

// guardHandler wraps the given handler with the middleware that every
// server must use, including the HTTP/3 (QUIC) servers
func (ac *Config) guardHandler(handler http.Handler) http.Handler {
	// Cap request bodies before any handler reads them. 0 means unlimited.
	if ac.largeFileSize > 0 {
		handler = ac.limitBodyMiddleware(handler)
	}
	// Check permissions for every route, not just the ones in RegisterHandlers
	handler = ac.permissionMiddleware(handler)
	// Canonicalize the request path before anything else looks at it
	return canonicalPathMiddleware(handler)
}

// NewGracefulServer creates a new graceful server configuration
func (ac *Config) NewGracefulServer(handler http.Handler, http2support bool, addr string) *GracefulServer {
	handler = ac.guardHandler(handler)
	// Server configuration
	s := &http.Server{
		Addr:    addr,
		Handler: handler, // Use the provided http.Handler (e.g. httprouter)
		// The timeout values are also the maximum time it can take
		// for a complete page of Server-Sent Events (SSE).
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      time.Duration(ac.writeTimeout) * time.Second,
		MaxHeaderBytes:    1 << 20,
	}
	if http2support {
		// Enable HTTP/2 support
		http2.ConfigureServer(s, nil)
	}
	gracefulServer := &GracefulServer{
		Server:  s,
		Timeout: ac.shutdownTimeout,
	}
	// Handle ctrl-c: run the shutdown functions
	gracefulServer.ShutdownInitiated = ac.GenerateShutdownFunction(gracefulServer)
	return gracefulServer
}

// GenerateShutdownFunction generates a function that will run the postponed
// shutdown functions.  Note that gracefulServer can be nil. It's only used for
// finding out if the server was interrupted (ctrl-c or killed, SIGINT/SIGTERM)
func (ac *Config) GenerateShutdownFunction(gracefulServer *GracefulServer) func() {
	return func() {
		if completed.Load() {
			// The shutdown functions have already been called
			return
		}

		if ac.verboseMode {
			logrus.Info("Initiating shutdown")
		}

		// Call the shutdown functions in chronological order (FIFO)
		for _, shutdownFunction := range shutdownFunctions {
			serverServingMutex.Lock()
			shutdownFunction()
			serverServingMutex.Unlock()
		}

		completed.Store(true)

		if ac.verboseMode {
			logrus.Info("Shutdown complete")
		}

		serverServingMutex.Lock()
		defer serverServingMutex.Unlock()

		// Forced shutdown
		if gracefulServer != nil {
			if gracefulServer.Interrupted() {
				ac.fatalExit(errors.New("Interrupted"))
			}
		}

		// Close the log files last, so that the shutdown functions above
		// could still log to them
		ac.closeLogs()

		// One final flush
		os.Stdout.Sync()
	}
}

// configureCertMagic sets CertMagic package-level defaults from ac.
// See certStorageDir for where the certificates are placed.
func (ac *Config) configureCertMagic() {
	certStorageDir := certStorageDir()
	if volatileCertStorage(certStorageDir) {
		logrus.Warnf("Storing certificates in %s, which may be cleared at boot. New certificates would then be requested for every boot, which counts against the Let's Encrypt rate limits. Set %s to a directory that persists.", certStorageDir, xdgDataHomeEnvVar)
	} else if ac.verboseMode {
		logrus.Info("Storing certificates in " + certStorageDir)
	}
	defaultEmail := env.StrAlt("LOGNAME", "USER", "root") + "@localhost"
	if len(ac.serve.certMagicDomains) > 0 {
		defaultEmail = "webmaster@" + ac.serve.certMagicDomains[0]
	}
	// Let CertMagic log with logrus instead of with the default zap logger,
	// which logs the time as a floating point number of seconds
	certmagic.Default.Logger = certMagicLogger()
	certmagic.DefaultACME.Logger = certmagic.Default.Logger
	certmagic.DefaultACME.Email = env.Str("EMAIL", defaultEmail)
	// TODO: Find a way for Algernon users to agree on this manually
	certmagic.DefaultACME.Agreed = true
	certmagic.Default.Storage = &certmagic.FileStorage{Path: certStorageDir}
	if ac.serve.useCertMagicStaging {
		certmagic.DefaultACME.CA = certmagic.LetsEncryptStagingCA
	}
	// Point the ACME challenge solvers at the configured listen ports, so an
	// external 80->httpAddr and 443->httpsAddr redirect (firewall, reverse proxy)
	// can deliver the challenge without needing privileged-port binds locally.
	if p := portFromAddr(ac.serve.httpAddr); p != 0 {
		certmagic.DefaultACME.AltHTTPPort = p
	}
	if p := portFromAddr(ac.serve.httpsAddr); p != 0 {
		certmagic.DefaultACME.AltTLSALPNPort = p
	}
}

// portFromAddr returns the port number from a [host][:port] address, or 0.
func portFromAddr(addr string) int {
	if addr == "" {
		return 0
	}
	_, portStr, err := net.SplitHostPort(addr)
	if err != nil {
		return 0
	}
	p, err := strconv.Atoi(portStr)
	if err != nil {
		return 0
	}
	return p
}

// Serve HTTP, HTTP/2 and/or HTTPS. Returns an error if unable to serve, or nil when done serving.
func (ac *Config) Serve(handler http.Handler, done, ready chan bool) error {
	// If we are not writing internal logs to a file, reduce the verbosity
	http2.VerboseLogs = (ac.internalLogFilename != os.DevNull)

	if ac.onlyLuaMode {
		ready <- true // Send a "ready" message to the REPL
		<-done        // Wait for a "done" message from the REPL (or just keep waiting)
		// Serve nothing
		return nil // Done
	}

	// When --domain is specified, refuse to start if TLS is required but no certificates
	// are available and Let's Encrypt is not enabled.
	if ac.serverAddDomain && !ac.serve.useCertMagic {
		needsTLS := ac.serve.httpsAddr != ""
		for _, ps := range ac.serve.portSettings {
			if ps.TLS {
				needsTLS = true
				break
			}
		}
		if needsTLS {
			if _, err := os.Stat(ac.serve.serverCert); err != nil {
				ac.fatalExit(errors.New("TLS certificate not found: " + ac.serve.serverCert + " (use --cert, --letsencrypt, or remove --https-addr)"))
			}
			if _, err := os.Stat(ac.serve.serverKey); err != nil {
				ac.fatalExit(errors.New("TLS key not found: " + ac.serve.serverKey + " (use --key, --letsencrypt, or remove --https-addr)"))
			}
		}
	}

	// If explicit port settings are configured (from SetPorts in Lua), use them
	if len(ac.serve.portSettings) > 0 {
		return ac.servePortSettings(handler, done, ready)
	}

	// If --http-addr and/or --https-addr are set, convert to portSettings and use them
	if ac.serve.httpAddr != "" || ac.serve.httpsAddr != "" {
		if ac.serve.httpAddr != "" {
			ac.serve.portSettings = append(ac.serve.portSettings, PortSetting{Addr: ac.serve.httpAddr, Protocol: "http", TLS: false})
		}
		if ac.serve.httpsAddr != "" {
			ac.serve.portSettings = append(ac.serve.portSettings, PortSetting{Addr: ac.serve.httpsAddr, Protocol: "http2", TLS: true})
		}
		return ac.servePortSettings(handler, done, ready)
	}

	// Channel to wait and see if we should just serve regular HTTP instead
	justServeRegularHTTP := make(chan bool)

	var (
		servingHTTPS atomic.Bool
		servingHTTP  atomic.Bool
	)

	// Goroutine that wait for a message to just serve regular HTTP, if needed
	go func() {
		<-justServeRegularHTTP // Wait for a message to just serve regular HTTP
		logrus.Info("Serving HTTP on http://" + utils.HostPortToURL(ac.serverAddr) + "/")

		servingHTTP.Store(true)
		HTTPserver := ac.NewGracefulServer(handler, false, ac.serverAddr)
		// Open the URL before the serving has started, in a short delay
		if ac.openURLAfterServing && ac.luaServerFilename != "" {
			go func() {
				time.Sleep(delayBeforeLaunchingBrowser)
				ac.OpenURL(ac.serverHost, ac.serverAddr, false)
			}()
		}
		// Start serving. Shut down gracefully at exit.
		if err := HTTPserver.ListenAndServe(); err != nil {
			servingHTTP.Store(false)

			// If we can't serve regular HTTP on port 80, give up
			ac.fatalExit(err)
		}
	}()

	// Decide which protocol to listen to
	switch {
	case ac.serve.useCertMagic:
		if len(ac.serve.certMagicDomains) == 0 {
			logrus.Warnln("Found no directories looking like domains in the given directory.")
		} else if len(ac.serve.certMagicDomains) == 1 {
			logrus.Infof("Serving one domain with CertMagic: %s", ac.serve.certMagicDomains[0])
		} else {
			logrus.Infof("Serving %d domains with CertMagic: %s", len(ac.serve.certMagicDomains), strings.Join(ac.serve.certMagicDomains, ", "))
		}
		servingHTTPS.Store(true)
		// TODO: Look at "Advanced use" at https://github.com/caddyserver/certmagic#examples
		// Listen for HTTP and HTTPS requests, for specific domain(s)
		go func() {
			ac.configureCertMagic()
			if err := ac.serveCertMagic(handler); err != nil {
				servingHTTPS.Store(false)
				if isBindError(err) {
					ac.fatalExit(err)
				}
				logrus.Error(err)
			}
		}()
	case ac.serveJustQUIC: // Just serve HTTP/3 (QUIC), but fallback to HTTP
		logrus.Info("Serving HTTP/3 (QUIC) on https://" + utils.HostPortToURL(ac.serverAddr) + "/")
		servingHTTPS.Store(true)
		// Start serving over QUIC
		go ac.ListenAndServeQUIC(handler, justServeRegularHTTP, &servingHTTPS)
	case ac.productionMode && !ac.serve.portConfigFromCLI:
		// Listen for both HTTPS+HTTP/2 and HTTP requests, on different ports
		logrus.Info("Serving HTTP/2 on https://" + utils.HostPortToURL(utils.JoinHostPort(ac.serverHost, ":443")) + "/")
		servingHTTPS.Store(true)
		go func() {
			// Start serving. Shut down gracefully at exit.
			// Listen for HTTPS + HTTP/2 requests
			HTTPS2server := ac.NewGracefulServer(handler, true, utils.JoinHostPort(ac.serverHost, ":443"))
			// Start serving. Shut down gracefully at exit.
			if err := HTTPS2server.ListenAndServeTLS(ac.serve.serverCert, ac.serve.serverKey); err != nil {
				servingHTTPS.Store(false)
				if isBindError(err) {
					ac.fatalExit(err)
				}
				logrus.Error(err)
			}
		}()
		logrus.Info("Serving HTTP on http://" + utils.HostPortToURL(utils.JoinHostPort(ac.serverHost, ":80")) + "/")
		servingHTTP.Store(true)
		go func() {
			if ac.serve.redirectHTTP {
				// Redirect HTTP to HTTPS
				redirectFunc := func(w http.ResponseWriter, req *http.Request) {
					http.Redirect(w, req, "https://"+req.Host+req.URL.String(), http.StatusMovedPermanently)
				}
				if err := http.ListenAndServe(utils.JoinHostPort(ac.serverHost, ":80"), http.HandlerFunc(redirectFunc)); err != nil {
					servingHTTP.Store(false)
					// If we can't serve regular HTTP on port 80, give up
					ac.fatalExit(err)
				}
			} else {
				// Don't redirect, but serve the same contents as the HTTPS server as HTTP on port 80
				HTTPserver := ac.NewGracefulServer(handler, false, utils.JoinHostPort(ac.serverHost, ":80"))
				if err := HTTPserver.ListenAndServe(); err != nil {
					servingHTTP.Store(false)
					// If we can't serve regular HTTP on port 80, give up
					ac.fatalExit(err)
				}
			}
		}()
	case ac.serveJustHTTP2: // It's unusual to serve HTTP/2 without HTTPS
		logrus.Warn("Serving HTTP/2 without HTTPS (not recommended!) on http://" + utils.HostPortToURL(ac.serverAddr) + "/")
		servingHTTPS.Store(true)
		go func() {
			// Listen for HTTP/2 requests
			HTTP2server := ac.NewGracefulServer(handler, true, ac.serverAddr)
			// Start serving. Shut down gracefully at exit.
			if err := HTTP2server.ListenAndServe(); err != nil {
				servingHTTPS.Store(false)
				ac.fatalExit(err)
			}
		}()
	case !ac.serveJustHTTP2 && !ac.serveJustHTTP:
		logrus.Info("Serving HTTP/2 on https://" + utils.HostPortToURL(ac.serverAddr) + "/")
		servingHTTPS.Store(true)
		// Listen for HTTPS + HTTP/2 requests
		HTTPS2server := ac.NewGracefulServer(handler, true, ac.serverAddr)
		// Start serving. Shut down gracefully at exit.
		go func() {
			if err := HTTPS2server.ListenAndServeTLS(ac.serve.serverCert, ac.serve.serverKey); err != nil {
				servingHTTPS.Store(false)
				if isBindError(err) {
					ac.fatalExit(err)
				}
				logrus.Errorf("%s. Not serving HTTP/2.", err)
				logrus.Info("Use the -t flag for serving regular HTTP.")
				// If HTTPS failed (perhaps the key + cert are missing),
				// serve plain HTTP instead
				justServeRegularHTTP <- true
			}
		}()
	default:
		servingHTTP.Store(true)
		justServeRegularHTTP <- true
	}

	// Wait just a tiny bit
	time.Sleep(20 * time.Millisecond)

	ready <- true // Send a "ready" message to the REPL

	// Open the URL, if specified
	if ac.openURLAfterServing {
		if !servingHTTP.Load() && !servingHTTPS.Load() {
			ac.fatalExit(errors.New("serving neither over http:// nor over https://"))
		}
		// Open the https:// URL if both http:// and https:// are being served
		ac.OpenURL(ac.serverHost, ac.serverAddr, servingHTTPS.Load())
	}

	<-done // Wait for a "done" message from the REPL (or just keep waiting)

	return nil // Done serving
}

// serveCertMagic serves HTTPS on port 443 and redirects HTTP on port 80 to
// HTTPS, for all CertMagic domains. Unlike certmagic.HTTPS, it starts serving
// right away and obtains the certificates in the background, so that a domain
// that fails to get a certificate does not stop the other domains from being
// served. CertMagic keeps retrying the failed domains.
func (ac *Config) serveCertMagic(handler http.Handler) error {
	cfg := certmagic.NewDefault()
	if err := cfg.ManageAsync(context.Background(), ac.serve.certMagicDomains); err != nil {
		return err
	}

	tlsConfig := cfg.TLSConfig()
	tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...)
	httpsLn, err := tls.Listen("tcp", ":"+strconv.Itoa(certmagic.HTTPSPort), tlsConfig)
	if err != nil {
		return err
	}
	httpLn, err := net.Listen("tcp", ":"+strconv.Itoa(certmagic.HTTPPort))
	if err != nil {
		httpsLn.Close()
		return err
	}

	// Answer the HTTP challenge, and redirect everything else to HTTPS
	var redirectHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		host := req.Host
		if h, _, err := net.SplitHostPort(host); err == nil {
			host = h
		}
		w.Header().Set("Connection", "close")
		http.Redirect(w, req, "https://"+host+req.URL.RequestURI(), http.StatusMovedPermanently)
	})
	if len(cfg.Issuers) > 0 {
		if am, ok := cfg.Issuers[0].(*certmagic.ACMEIssuer); ok {
			redirectHandler = am.HTTPChallengeHandler(redirectHandler)
		}
	}

	// The same timeouts as certmagic.HTTPS uses
	httpServer := &http.Server{
		Handler:           redirectHandler,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       5 * time.Second,
		WriteTimeout:      5 * time.Second,
		IdleTimeout:       5 * time.Second,
	}
	httpsServer := &http.Server{
		Handler:           handler,
		ReadHeaderTimeout: 10 * time.Second,
		ReadTimeout:       30 * time.Second,
		WriteTimeout:      2 * time.Minute,
		IdleTimeout:       5 * time.Minute,
	}
	go func() {
		if err := httpServer.Serve(httpLn); err != nil && !errors.Is(err, http.ErrServerClosed) {
			logrus.Error(err)
		}
	}()
	return httpsServer.Serve(httpsLn)
}

// servePortSettings starts listeners based on the explicit portSettings configuration.
func (ac *Config) servePortSettings(handler http.Handler, done, ready chan bool) error {
	// When using CertMagic, set up managed TLS and the HTTP challenge handler.
	var (
		cmCfg      *certmagic.Config
		acmeIssuer *certmagic.ACMEIssuer
	)
	if ac.serve.useCertMagic {
		ac.configureCertMagic()
		cmCfg = certmagic.NewDefault()
		if err := cmCfg.ManageAsync(context.Background(), ac.serve.certMagicDomains); err != nil {
			logrus.Errorf("CertMagic setup failed, falling back to cert/key files: %v", err)
			cmCfg = nil
		} else if len(cmCfg.Issuers) > 0 {
			if am, ok := cmCfg.Issuers[0].(*certmagic.ACMEIssuer); ok {
				acmeIssuer = am
			}
		}
	}

	for _, ps := range ac.serve.portSettings {
		switch ps.Protocol {
		case "http":
			if ps.TLS {
				logrus.Infof("Serving HTTPS on https://%s/", utils.HostPortToURL(ps.Addr))
				if cmCfg != nil {
					go func() {
						tlsCfg := cmCfg.TLSConfig()
						tlsCfg.NextProtos = append([]string{"h2", "http/1.1"}, tlsCfg.NextProtos...)
						srv := ac.NewGracefulServer(handler, false, ps.Addr)
						if err := srv.ListenAndServeTLSConfig(tlsCfg); err != nil {
							if isBindError(err) {
								ac.fatalExit(err)
							}
							logrus.Error(err)
						}
					}()
				} else {
					go func() {
						srv := ac.NewGracefulServer(handler, false, ps.Addr)
						if err := srv.ListenAndServeTLS(ac.serve.serverCert, ac.serve.serverKey); err != nil {
							if isBindError(err) {
								ac.fatalExit(err)
							}
							logrus.Error(err)
						}
					}()
				}
			} else {
				logrus.Infof("Serving HTTP on http://%s/", utils.HostPortToURL(ps.Addr))
				go func() {
					if ac.serve.redirectHTTP && ac.serve.httpsAddr != "" {
						redirectFunc := func(w http.ResponseWriter, req *http.Request) {
							http.Redirect(w, req, "https://"+req.Host+req.URL.String(), http.StatusMovedPermanently)
						}
						var h http.Handler = http.HandlerFunc(redirectFunc)
						if acmeIssuer != nil {
							h = acmeIssuer.HTTPChallengeHandler(h)
						}
						if err := http.ListenAndServe(ps.Addr, h); err != nil {
							ac.fatalExit(err)
						}
					} else {
						h := handler
						if acmeIssuer != nil {
							h = acmeIssuer.HTTPChallengeHandler(h)
						}
						srv := ac.NewGracefulServer(h, false, ps.Addr)
						if err := srv.ListenAndServe(); err != nil {
							ac.fatalExit(err)
						}
					}
				}()
			}
		case "http2":
			if ps.TLS {
				logrus.Infof("Serving HTTP/2 on https://%s/", utils.HostPortToURL(ps.Addr))
				if cmCfg != nil {
					go func() {
						tlsCfg := cmCfg.TLSConfig()
						tlsCfg.NextProtos = append([]string{"h2", "http/1.1"}, tlsCfg.NextProtos...)
						srv := ac.NewGracefulServer(handler, true, ps.Addr)
						if err := srv.ListenAndServeTLSConfig(tlsCfg); err != nil {
							if isBindError(err) {
								ac.fatalExit(err)
							}
							logrus.Error(err)
						}
					}()
				} else {
					go func() {
						srv := ac.NewGracefulServer(handler, true, ps.Addr)
						if err := srv.ListenAndServeTLS(ac.serve.serverCert, ac.serve.serverKey); err != nil {
							if isBindError(err) {
								ac.fatalExit(err)
							}
							logrus.Error(err)
						}
					}()
				}
			} else {
				logrus.Infof("Serving HTTP/2 (h2c) on http://%s/", utils.HostPortToURL(ps.Addr))
				go func() {
					srv := ac.NewGracefulServer(handler, true, ps.Addr)
					if err := srv.ListenAndServe(); err != nil {
						ac.fatalExit(err)
					}
				}()
			}
		case "http3":
			if ps.TLS {
				logrus.Infof("Serving HTTP/3 (QUIC) on https://%s/", utils.HostPortToURL(ps.Addr))
			} else {
				logrus.Infof("Serving HTTP/3 (QUIC, no TLS) on %s/", utils.HostPortToURL(ps.Addr))
			}
			go ac.serveQUICPortSetting(handler, ps)
		case "event":
			logrus.Infof("Event server (SSE) on %s", utils.HostPortToURL(ps.Addr))
			ac.eventAddr = ps.Addr
			ac.separateEventServer = true
		default:
			logrus.Errorf("SetPorts: unknown protocol %q for %s", ps.Protocol, ps.Addr)
		}
	}

	// Wait just a tiny bit for listeners to start
	time.Sleep(20 * time.Millisecond)

	ready <- true // Send a "ready" message to the REPL

	// Open the URL, if specified
	if ac.openURLAfterServing {
		hasTLS := false
		firstAddr := ""
		for _, ps := range ac.serve.portSettings {
			if ps.Protocol == "event" {
				continue
			}
			if firstAddr == "" {
				firstAddr = ps.Addr
			}
			if ps.TLS {
				hasTLS = true
				break
			}
		}
		if firstAddr != "" {
			ac.OpenURL(ac.serverHost, firstAddr, hasTLS)
		}
	}

	<-done // Wait for a "done" message from the REPL (or just keep waiting)

	return nil // Done serving
}