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

298 lines
7.5 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"
"fmt"
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
// 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
// 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(CORS(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.GET("/_/{path...}", Static(ui.DistDirFS, false)).
BindFunc(func(e *core.RequestEvent) error {
// ingore root path
if e.Request.PathValue(StaticWildcardParam) != "" {
e.Response.Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400")
}
return e.Next()
}).
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?")
}
schema := "http"
addr := server.Addr
if config.HttpsAddr != "" {
schema = "https"
if len(config.CertificateDomains) > 0 {
addr = config.CertificateDomains[0]
2023-04-20 05:05:41 +03:00
}
}
baseURL := fmt.Sprintf("%s://%s", schema, addr)
dashboardURL := fmt.Sprintf("%s/_", baseURL)
2023-04-20 05:05:41 +03:00
if config.ShowStartBanner {
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()),
color.CyanString("%s", baseURL),
2023-04-20 05:05:41 +03:00
)
regular := color.New()
regular.Printf("├─ REST API: %s\n", color.CyanString("%s/api/", baseURL))
regular.Printf("└─ Dashboard: %s\n", color.CyanString("%s/", dashboardURL))
2023-04-20 05:05:41 +03:00
}
go func() {
installerErr := loadInstaller(app, dashboardURL)
if installerErr != nil {
app.Logger().Warn("Failed to initialize installer", "error", installerErr)
}
}()
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
}