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 }