package apis import ( "context" "crypto/tls" "datamanager/backend/core" "github.com/fatih/color" "github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "github.com/pocketbase/pocketbase/tools/list" "golang.org/x/crypto/acme" "log" "log/slog" "net" "net/http" "strings" "sync" "time" ) // ServeConfig defines a configuration struct for apis.Serve(). type ServeConfig struct { // ShowStartBanner indicates whether to show or hide the server start console message. ShowStartBanner bool // HttpAddr is the TCP address to listen for the HTTP server (eg. `127.0.0.1:80`). HttpAddr string // 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 } func Serve(app core.App, config ServeConfig) (*http.Server, error) { if len(config.AllowedOrigins) == 0 { config.AllowedOrigins = []string{"*"} } router, err := InitApi(app) if err != nil { return nil, err } // configure cors router.Use(middleware.CORSWithConfig(middleware.CORSConfig{ Skipper: middleware.DefaultSkipper, AllowOrigins: config.AllowedOrigins, AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, })) // start http server // --- mainAddr := config.HttpAddr if config.HttpsAddr != "" { mainAddr = config.HttpsAddr } 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) } }) } // base request context used for cancelling long running requests // like the SSE connections baseCtx, cancelBaseCtx := context.WithCancel(context.Background()) defer cancelBaseCtx() server := &http.Server{ TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, NextProtos: []string{acme.ALPNProto}, }, ReadTimeout: 10 * time.Minute, ReadHeaderTimeout: 30 * time.Second, // WriteTimeout: 60 * time.Second, // breaks sse! Handler: router, Addr: mainAddr, BaseContext: func(l net.Listener) context.Context { return baseCtx }, } 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) log.New(date, "", log.LstdFlags).Print() bold := color.New(color.Bold).Add(color.FgGreen) _, _ = bold.Printf( "%s Server started at %s\n", strings.TrimSpace(date.String()), color.CyanString("%s://%s", schema, addr), ) } // WaitGroup to block until server.ShutDown() returns because Serve and similar methods exit immediately. // Note that the WaitGroup would not do anything if the app.OnTerminate() hook isn't triggered. var wg sync.WaitGroup // wait for the graceful shutdown to complete before exit defer wg.Wait() // --- // @todo consider removing the server return value because it is // not really useful when combined with the blocking serve calls // --- // start HTTPS server if config.HttpsAddr != "" { // if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version if config.HttpAddr != "" { go func() { err := http.ListenAndServe(config.HttpAddr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) })) if err != nil { slog.Error("Failed to start HTTP server for redirect: %v", err) } }() } return server, server.ListenAndServeTLS("", "") } // OR start HTTP server return server, server.ListenAndServe() }