From c89c68a4dc96cc94e2256b5e24d7490cc2692932 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Fri, 25 Aug 2023 11:16:31 +0300 Subject: [PATCH] poc of serve domain args --- CHANGELOG.md | 2 ++ apis/serve.go | 67 +++++++++++++++++++++++++++++++++++++++++++++------ cmd/serve.go | 30 ++++++++++++++++------- 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c395733f..19f3d1d3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## v0.18.0 - WIP +- (@todo docs) Simplified the `serve` command when used with a domain name. + - Added new `SmtpConfig.LocalName` option to specify a custom domain name (or IP address) for the initial EHLO/HELO exchange ([#3097](https://github.com/pocketbase/pocketbase/discussions/3097)). _This is usually required for verification purposes only by some SMTP providers, such as on-premise [Gmail SMTP-relay](https://support.google.com/a/answer/2956491)._ diff --git a/apis/serve.go b/apis/serve.go index 223cc2e0c..c5d6384d3 100644 --- a/apis/serve.go +++ b/apis/serve.go @@ -11,11 +11,13 @@ import ( "time" "github.com/fatih/color" + "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/migrations" "github.com/pocketbase/pocketbase/migrations/logs" + "github.com/pocketbase/pocketbase/tools/list" "github.com/pocketbase/pocketbase/tools/migrate" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" @@ -26,12 +28,20 @@ type ServeConfig struct { // ShowStartBanner indicates whether to show or hide the server start console message. ShowStartBanner bool - // HttpAddr is the HTTP server address to bind (eg. `127.0.0.1:80`). + // HttpAddr is the TCP address to listen for the HTTP server (eg. `127.0.0.1:80`). HttpAddr string - // HttpsAddr is the HTTPS server address to bind (eg. `127.0.0.1:443`). + // HttpsAddr is the TCP address to listen for the HTTPS server (eg. `127.0.0.1:443`). HttpsAddr string + // Optional domains list to use when issuing the TLS certificate. + // + // If not set, the host from the bound server address will be used. + // + // For convenience, for each "non-www" domain a "www" entry and + // redirect will be automatically added. + CertificateDomains []string + // AllowedOrigins is an optional list of CORS origins (default to "*"). AllowedOrigins []string } @@ -85,16 +95,53 @@ func Serve(app core.App, config ServeConfig) (*http.Server, error) { mainAddr = config.HttpsAddr } - mainHost, _, _ := net.SplitHostPort(mainAddr) + var wwwRedirects []string + + // extract the host names for the certificate host policy + hostNames := config.CertificateDomains + if len(hostNames) == 0 { + host, _, _ := net.SplitHostPort(mainAddr) + hostNames = append(hostNames, host) + } + for _, host := range hostNames { + if strings.HasPrefix(host, "www.") { + continue // explicitly set www host + } + + wwwHost := "www." + host + if !list.ExistInSlice(wwwHost, hostNames) { + hostNames = append(hostNames, wwwHost) + wwwRedirects = append(wwwRedirects, wwwHost) + } + } + + // implicit www->non-www redirect(s) + if len(wwwRedirects) > 0 { + router.Pre(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + host := c.Request().Host + + if strings.HasPrefix(host, "www.") && list.ExistInSlice(host, wwwRedirects) { + return c.Redirect( + http.StatusTemporaryRedirect, + (c.Scheme() + "://" + host[4:] + c.Request().RequestURI), + ) + } + + return next(c) + } + }) + } certManager := &autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(filepath.Join(app.DataDir(), ".autocert_cache")), - HostPolicy: autocert.HostWhitelist(mainHost, "www."+mainHost), + HostPolicy: autocert.HostWhitelist(hostNames...), } server := &http.Server{ TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, GetCertificate: certManager.GetCertificate, NextProtos: []string{acme.ALPNProto}, }, @@ -117,8 +164,14 @@ func Serve(app core.App, config ServeConfig) (*http.Server, error) { if config.ShowStartBanner { schema := "http" + addr := server.Addr + if config.HttpsAddr != "" { schema = "https" + + if len(config.CertificateDomains) > 0 { + addr = config.CertificateDomains[0] + } } date := new(strings.Builder) @@ -128,12 +181,12 @@ func Serve(app core.App, config ServeConfig) (*http.Server, error) { bold.Printf( "%s Server started at %s\n", strings.TrimSpace(date.String()), - color.CyanString("%s://%s", schema, server.Addr), + color.CyanString("%s://%s", schema, addr), ) regular := color.New() - regular.Printf("├─ REST API: %s\n", color.CyanString("%s://%s/api/", schema, server.Addr)) - regular.Printf("└─ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, server.Addr)) + regular.Printf("├─ REST API: %s\n", color.CyanString("%s://%s/api/", schema, addr)) + regular.Printf("└─ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, addr)) } // try to gracefully shutdown the server on app termination diff --git a/cmd/serve.go b/cmd/serve.go index 2b900af9e..76ba59cab 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -17,14 +17,26 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { var httpsAddr string command := &cobra.Command{ - Use: "serve", - Short: "Starts the web server (default to 127.0.0.1:8090)", + Use: "serve [domain(s)]", + Args: cobra.ArbitraryArgs, + Short: "Starts the web server (default to 127.0.0.1:8090 if no domain is specified)", Run: func(command *cobra.Command, args []string) { + // set default listener addresses if at least one domain is specified + if len(args) > 0 { + if httpAddr == "" { + httpAddr = "0.0.0.0:80" + } + if httpsAddr == "" { + httpsAddr = "0.0.0.0:443" + } + } + _, err := apis.Serve(app, apis.ServeConfig{ - HttpAddr: httpAddr, - HttpsAddr: httpsAddr, - ShowStartBanner: showStartBanner, - AllowedOrigins: allowedOrigins, + HttpAddr: httpAddr, + HttpsAddr: httpsAddr, + ShowStartBanner: showStartBanner, + AllowedOrigins: allowedOrigins, + CertificateDomains: args, }) if err != http.ErrServerClosed { @@ -43,15 +55,15 @@ func NewServeCommand(app core.App, showStartBanner bool) *cobra.Command { command.PersistentFlags().StringVar( &httpAddr, "http", - "127.0.0.1:8090", - "api HTTP server address", + "", + "TCP address to listen for the HTTP server\n(if domain args are specified - default to 0.0.0.0:80, otherwise - default to 127.0.0.1:8090)", ) command.PersistentFlags().StringVar( &httpsAddr, "https", "", - "api HTTPS server address (auto TLS via Let's Encrypt)\nthe incoming --http address traffic also will be redirected to this address", + "TCP address to listen for the HTTPS server\n(if domain args are specified - default to 0.0.0.0:443, otherwise - default to empty string, aka. no TLS)\nThe incoming HTTP traffic also will be auto redirected to the HTTPS version", ) return command