mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-02-10 04:33:47 +02:00
295 lines
7.4 KiB
Go
295 lines
7.4 KiB
Go
package apis
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/pocketbase/pocketbase/tools/hook"
|
|
"github.com/pocketbase/pocketbase/tools/list"
|
|
"github.com/pocketbase/pocketbase/ui"
|
|
"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.
|
|
ShowStartBanner bool
|
|
|
|
// 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
|
|
|
|
// 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
|
|
}
|
|
|
|
// Serve starts a new app web server.
|
|
//
|
|
// 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,
|
|
// })
|
|
func Serve(app core.App, config ServeConfig) error {
|
|
if len(config.AllowedOrigins) == 0 {
|
|
config.AllowedOrigins = []string{"*"}
|
|
}
|
|
|
|
if config.DashboardPath == "" {
|
|
config.DashboardPath = "/_/{path...}"
|
|
} else if !strings.HasSuffix(config.DashboardPath, "{path...}") {
|
|
return errors.New("invalid dashboard path - missing {path...} wildcard")
|
|
}
|
|
|
|
// ensure that the latest migrations are applied before starting the server
|
|
err := app.RunAllMigrations()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pbRouter, err := NewRouter(app)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pbRouter.Bind(CORSWithConfig(CORSConfig{
|
|
AllowOrigins: config.AllowedOrigins,
|
|
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
|
}))
|
|
|
|
pbRouter.BindFunc(installerRedirect(app, config.DashboardPath))
|
|
|
|
pbRouter.GET(config.DashboardPath, Static(ui.DistDirFS, false)).
|
|
BindFunc(dashboardRemoveInstallerParam()).
|
|
BindFunc(dashboardCacheControl()).
|
|
Bind(Gzip())
|
|
|
|
// 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 {
|
|
pbRouter.Bind(wwwRedirect(wwwRedirects))
|
|
}
|
|
|
|
certManager := &autocert.Manager{
|
|
Prompt: autocert.AcceptTOS,
|
|
Cache: autocert.DirCache(filepath.Join(app.DataDir(), core.LocalAutocertCacheDirName)),
|
|
HostPolicy: autocert.HostWhitelist(hostNames...),
|
|
}
|
|
|
|
// 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,
|
|
GetCertificate: certManager.GetCertificate,
|
|
NextProtos: []string{acme.ALPNProto},
|
|
},
|
|
// higher defaults to accommodate large file uploads/downloads
|
|
WriteTimeout: 3 * time.Minute,
|
|
ReadTimeout: 3 * time.Minute,
|
|
ReadHeaderTimeout: 30 * time.Second,
|
|
Addr: mainAddr,
|
|
BaseContext: func(l net.Listener) context.Context {
|
|
return baseCtx
|
|
},
|
|
ErrorLog: log.New(&serverErrorLogWriter{app: app}, "", 0),
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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),
|
|
)
|
|
|
|
regular := color.New()
|
|
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))
|
|
}
|
|
|
|
var serveErr error
|
|
if config.HttpsAddr != "" {
|
|
if config.HttpAddr != "" {
|
|
// start an additional HTTP server for redirecting the traffic to the HTTPS version
|
|
go http.ListenAndServe(config.HttpAddr, certManager.HTTPHandler(nil))
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type serverErrorLogWriter struct {
|
|
app core.App
|
|
}
|
|
|
|
func (s *serverErrorLogWriter) Write(p []byte) (int, error) {
|
|
s.app.Logger().Debug(strings.TrimSpace(string(p)))
|
|
|
|
return len(p), nil
|
|
}
|