1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-20 14:31:09 +02:00
pocketbase/apis/serve.go

299 lines
7.6 KiB
Go
Raw Normal View History

2023-04-20 05:05:41 +03:00
package apis
import (
"context"
2023-04-20 05:05:41 +03:00
"crypto/tls"
2024-09-29 19:23:19 +03:00
"errors"
2023-04-20 05:05:41 +03:00
"log"
"net"
"net/http"
"path/filepath"
"strings"
2023-12-08 21:16:48 +02:00
"sync"
2023-04-20 05:05:41 +03:00
"time"
"github.com/fatih/color"
"github.com/pocketbase/pocketbase/core"
2024-09-29 19:23:19 +03:00
"github.com/pocketbase/pocketbase/tools/hook"
2023-08-25 11:16:31 +03:00
"github.com/pocketbase/pocketbase/tools/list"
2024-09-29 19:23:19 +03:00
"github.com/pocketbase/pocketbase/ui"
2023-04-20 05:05:41 +03:00
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)
// ServeConfig defines a configuration struct for apis.Serve().
type ServeConfig struct {
// ShowStartBanner indicates whether to show or hide the server start console message.
2023-04-20 05:05:41 +03:00
ShowStartBanner bool
2024-09-29 19:23:19 +03:00
// DashboardPath specifies the route path to the superusers dashboard interface
// (default to "/_/{path...}").
//
// Note: Must include the "{path...}" wildcard parameter.
DashboardPath string
// HttpAddr is the TCP address to listen for the HTTP server (eg. "127.0.0.1:80").
HttpAddr string
2024-09-29 19:23:19 +03:00
// HttpsAddr is the TCP address to listen for the HTTPS server (eg. "127.0.0.1:443").
HttpsAddr string
2023-08-25 11:16:31 +03:00
// 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
2023-04-20 05:05:41 +03:00
}
// Serve starts a new app web server.
2023-08-20 18:31:56 +03:00
//
// NB! The app should be bootstrapped before starting the web server.
//
// Example:
//
// app.Bootstrap()
// apis.Serve(app, apis.ServeConfig{
// HttpAddr: "127.0.0.1:8080",
// ShowStartBanner: false,
// })
2024-09-29 19:23:19 +03:00
func Serve(app core.App, config ServeConfig) error {
if len(config.AllowedOrigins) == 0 {
config.AllowedOrigins = []string{"*"}
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
if config.DashboardPath == "" {
config.DashboardPath = "/_/{path...}"
} else if !strings.HasSuffix(config.DashboardPath, "{path...}") {
return errors.New("invalid dashboard path - missing {path...} wildcard")
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
// ensure that the latest migrations are applied before starting the server
err := app.RunAllMigrations()
if err != nil {
return err
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
pbRouter, err := NewRouter(app)
2023-04-20 05:05:41 +03:00
if err != nil {
2024-09-29 19:23:19 +03:00
return err
2023-04-20 05:05:41 +03:00
}
pbRouter.Bind(CORSWithConfig(CORSConfig{
AllowOrigins: config.AllowedOrigins,
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}))
2024-09-29 19:23:19 +03:00
pbRouter.BindFunc(installerRedirect(app, config.DashboardPath))
pbRouter.GET(config.DashboardPath, Static(ui.DistDirFS, false)).
BindFunc(dashboardRemoveInstallerParam()).
BindFunc(dashboardCacheControl()).
Bind(Gzip())
2023-04-20 05:05:41 +03:00
// start http server
// ---
mainAddr := config.HttpAddr
if config.HttpsAddr != "" {
mainAddr = config.HttpsAddr
2023-04-20 05:05:41 +03:00
}
2023-08-25 11:16:31 +03:00
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 {
2024-09-29 19:23:19 +03:00
pbRouter.Bind(wwwRedirect(wwwRedirects))
2023-08-25 11:16:31 +03:00
}
2023-04-20 05:05:41 +03:00
2023-05-29 16:57:50 +03:00
certManager := &autocert.Manager{
2023-04-20 05:05:41 +03:00
Prompt: autocert.AcceptTOS,
2024-09-29 19:23:19 +03:00
Cache: autocert.DirCache(filepath.Join(app.DataDir(), core.LocalAutocertCacheDirName)),
2023-08-25 11:16:31 +03:00
HostPolicy: autocert.HostWhitelist(hostNames...),
2023-04-20 05:05:41 +03:00
}
// base request context used for cancelling long running requests
// like the SSE connections
baseCtx, cancelBaseCtx := context.WithCancel(context.Background())
defer cancelBaseCtx()
server := &http.Server{
2023-04-20 05:05:41 +03:00
TLSConfig: &tls.Config{
2023-08-25 11:16:31 +03:00
MinVersion: tls.VersionTLS12,
2023-04-20 05:05:41 +03:00
GetCertificate: certManager.GetCertificate,
NextProtos: []string{acme.ALPNProto},
},
2024-09-29 19:23:19 +03:00
// higher defaults to accommodate large file uploads/downloads
WriteTimeout: 3 * time.Minute,
ReadTimeout: 3 * time.Minute,
2023-04-20 05:05:41 +03:00
ReadHeaderTimeout: 30 * time.Second,
2024-09-29 19:23:19 +03:00
Addr: mainAddr,
BaseContext: func(l net.Listener) context.Context {
return baseCtx
},
2024-09-29 19:23:19 +03:00
ErrorLog: log.New(&serverErrorLogWriter{app: app}, "", 0),
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
serveEvent := new(core.ServeEvent)
serveEvent.App = app
serveEvent.Router = pbRouter
serveEvent.Server = server
serveEvent.CertManager = certManager
var listener net.Listener
// graceful shutdown
// ---------------------------------------------------------------
// WaitGroup to block until server.ShutDown() returns because Serve and similar methods exit immediately.
// Note that the WaitGroup would do nothing if the app.OnTerminate() hook isn't triggered.
var wg sync.WaitGroup
// try to gracefully shutdown the server on app termination
app.OnTerminate().Bind(&hook.Handler[*core.TerminateEvent]{
Id: "pbGracefulShutdown",
Func: func(te *core.TerminateEvent) error {
cancelBaseCtx()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
wg.Add(1)
_ = server.Shutdown(ctx)
if te.IsRestart {
// wait for execve and other handlers up to 3 seconds before exit
time.AfterFunc(3*time.Second, func() {
wg.Done()
})
} else {
wg.Done()
}
return te.Next()
},
Priority: -9999,
})
// wait for the graceful shutdown to complete before exit
defer func() {
wg.Wait()
if listener != nil {
_ = listener.Close()
}
}()
// ---------------------------------------------------------------
// trigger the OnServe hook and start the tcp listener
serveHookErr := app.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {
handler, err := e.Router.BuildMux()
if err != nil {
return err
}
e.Server.Handler = handler
addr := e.Server.Addr
// fallback similar to the std Server.ListenAndServe/ListenAndServeTLS
if addr == "" {
if config.HttpsAddr != "" {
addr = ":https"
} else {
addr = ":http"
}
}
var lnErr error
listener, lnErr = net.Listen("tcp", addr)
return lnErr
})
if serveHookErr != nil {
return serveHookErr
2023-04-20 05:05:41 +03:00
}
if listener == nil {
return errors.New("The OnServe finalizer wasn't invoked. Did you forget to call the ServeEvent.Next() method?")
}
if config.ShowStartBanner {
2023-04-20 05:05:41 +03:00
schema := "http"
2023-08-25 11:16:31 +03:00
addr := server.Addr
if config.HttpsAddr != "" {
2023-04-20 05:05:41 +03:00
schema = "https"
2023-08-25 11:16:31 +03:00
if len(config.CertificateDomains) > 0 {
addr = config.CertificateDomains[0]
}
2023-04-20 05:05:41 +03:00
}
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()),
2023-08-25 11:16:31 +03:00
color.CyanString("%s://%s", schema, addr),
2023-04-20 05:05:41 +03:00
)
regular := color.New()
2023-08-25 11:16:31 +03:00
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))
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
var serveErr error
if config.HttpsAddr != "" {
if config.HttpAddr != "" {
2024-09-29 19:23:19 +03:00
// start an additional HTTP server for redirecting the traffic to the HTTPS version
go http.ListenAndServe(config.HttpAddr, certManager.HTTPHandler(nil))
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
// start HTTPS server
serveErr = server.ServeTLS(listener, "", "")
} else {
// OR start HTTP server
serveErr = server.Serve(listener)
}
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
return serveErr
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
return nil
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
type serverErrorLogWriter struct {
app core.App
2023-04-20 05:05:41 +03:00
}
2024-09-29 19:23:19 +03:00
func (s *serverErrorLogWriter) Write(p []byte) (int, error) {
s.app.Logger().Debug(strings.TrimSpace(string(p)))
2023-04-20 05:05:41 +03:00
2024-09-29 19:23:19 +03:00
return len(p), nil
2023-04-20 05:05:41 +03:00
}