Files
photoprism/internal/server/start.go
2025-02-20 16:35:30 +01:00

273 lines
7.8 KiB
Go

package server
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"os"
"time"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/sync/errgroup"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/server/process"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/media/http/header"
"github.com/photoprism/photoprism/pkg/txt"
)
// Start the REST API server using the configuration provided
func Start(ctx context.Context, conf *config.Config) {
defer func() {
if err := recover(); err != nil {
log.Error(err)
}
}()
start := time.Now()
// Log the server process ID for troubleshooting purposes.
log.Infof("server: started as pid %d", process.ID)
// Set web server mode.
if conf.HttpMode() != "" {
gin.SetMode(conf.HttpMode())
} else if conf.Debug() == false {
gin.SetMode(gin.ReleaseMode)
}
// Create new router engine without standard middleware.
router := gin.New()
// Set proxy addresses from which headers related to the client and protocol can be trusted
if err := router.SetTrustedProxies(conf.TrustedProxies()); err != nil {
log.Warnf("server: %s", err)
}
// Register panic recovery middleware.
router.Use(Recovery())
// Register logger middleware if debug mode is enabled.
if conf.Debug() {
router.Use(Logger())
}
// Register compression middleware if enabled in the configuration.
switch conf.HttpCompression() {
case "br", "brotli":
log.Infof("server: brotli compression is currently not supported")
case "gzip":
router.Use(gzip.Gzip(
gzip.DefaultCompression,
gzip.WithExcludedExtensions([]string{
".png", ".gif", ".jpeg", ".jpg", ".webp", ".mp3", ".mp4", ".zip", ".gz",
}),
gzip.WithExcludedPaths([]string{
conf.BaseUri("/health"),
conf.BaseUri(config.ApiUri + "/t"),
conf.BaseUri(config.ApiUri + "/folders/t"),
conf.BaseUri(config.ApiUri + "/zip"),
conf.BaseUri(config.ApiUri + "/albums"),
conf.BaseUri(config.ApiUri + "/labels"),
conf.BaseUri(config.ApiUri + "/videos"),
}),
))
log.Infof("server: enabled gzip compression")
}
// Register security middleware.
router.Use(Security(conf))
// Create REST API router group.
APIv1 = router.Group(conf.BaseUri(config.ApiUri), Api(conf))
// Initialize package extensions.
Ext().Init(router, conf)
// Find and load templates.
router.LoadHTMLFiles(conf.TemplateFiles()...)
// Register application routes.
registerRoutes(router, conf)
// Register "GET /health" route so clients can perform health checks.
router.GET(conf.BaseUri("/health"), func(c *gin.Context) {
c.Header(header.CacheControl, header.CacheControlNoStore)
c.Header(header.AccessControlAllowOrigin, header.Any)
c.String(http.StatusOK, "OK")
})
// Create a new HTTP server instance with no read or write timeout, except for reading the headers:
// https://pkg.go.dev/net/http#Server
server := &http.Server{
ReadHeaderTimeout: time.Minute,
ReadTimeout: -1,
WriteTimeout: -1,
Handler: router,
}
var tlsErr error
var tlsManager *autocert.Manager
// Listen on a Unix domain socket instead of a TCP port?
if unixSocket := conf.HttpSocket(); unixSocket != nil {
var listener net.Listener
var unixAddr *net.UnixAddr
var err error
// Check if the Unix socket already exists and delete it if the force flag is set.
if fs.SocketExists(unixSocket.Path) {
if txt.Bool(unixSocket.Query().Get("force")) == false {
Fail("server: %s socket %s already exists", clean.Log(unixSocket.Scheme), clean.Log(unixSocket.Path))
return
} else if removeErr := os.Remove(unixSocket.Path); removeErr != nil {
Fail("server: %s socket %s already exists and cannot be deleted", clean.Log(unixSocket.Scheme), clean.Log(unixSocket.Path))
return
}
}
// Create a Unix socket and listen on it.
if unixAddr, err = net.ResolveUnixAddr(unixSocket.Scheme, unixSocket.Path); err != nil {
Fail("server: invalid %s socket (%s)", clean.Log(unixSocket.Scheme), err)
return
} else if listener, err = net.ListenUnix(unixSocket.Scheme, unixAddr); err != nil {
Fail("server: failed to listen on %s socket (%s)", clean.Log(unixSocket.Scheme), err)
return
} else {
// Update socket permissions?
if mode := unixSocket.Query().Get("mode"); mode == "" {
// Skip, no socket mode was specified.
} else if modeErr := os.Chmod(unixSocket.Path, fs.ParseMode(mode, fs.ModeSocket)); modeErr != nil {
log.Warnf(
"server: failed to change permissions of %s socket %s (%s)",
clean.Log(unixSocket.Scheme),
clean.Log(unixSocket.Path),
modeErr,
)
}
// Listen on Unix socket, which should be automatically closed and removed after use:
// https://pkg.go.dev/net#UnixListener.SetUnlinkOnClose.
server.Addr = listener.Addr().String()
log.Infof("server: listening on %s [%s]", unixSocket.Path, time.Since(start))
// Start Web server.
go StartHttp(server, listener)
}
} else if tlsManager, tlsErr = AutoTLS(conf); tlsErr == nil {
log.Infof("server: starting in auto tls mode")
tlsSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
tlsConfig := tlsManager.TLSConfig()
tlsConfig.MinVersion = tls.VersionTLS12
// Listen on HTTPS socket.
server.Addr = tlsSocket
server.TLSConfig = tlsConfig
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
// Start Web server.
go StartAutoTLS(server, tlsManager, conf)
} else if publicCert, privateKey := conf.TLS(); publicCert != "" && privateKey != "" {
log.Infof("server: starting in tls mode")
tlsSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
// Listen on HTTPS socket.
server.Addr = tlsSocket
server.TLSConfig = tlsConfig
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
// Start Web server.
go StartTLS(server, publicCert, privateKey)
} else {
log.Infof("server: %s", tlsErr)
tcpSocket := fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort())
if listener, err := net.Listen("tcp", tcpSocket); err != nil {
Fail("server: %s", err)
return
} else {
// Listen on HTTP socket.
server.Addr = tcpSocket
log.Infof("server: listening on %s [%s]", server.Addr, time.Since(start))
// Start Web server.
go StartHttp(server, listener)
}
}
// Graceful web server shutdown.
<-ctx.Done()
log.Info("server: shutting down")
err := server.Close()
if err != nil {
log.Errorf("server: shutdown failed (%s)", err)
}
}
// StartHttp starts the Web server in http mode.
func StartHttp(s *http.Server, l net.Listener) {
if err := s.Serve(l); err != nil {
if errors.Is(err, http.ErrServerClosed) {
log.Info("server: shutdown complete")
} else {
log.Errorf("server: %s", err)
}
}
}
// StartTLS starts the Web server in https mode.
func StartTLS(s *http.Server, httpsCert, privateKey string) {
if err := s.ListenAndServeTLS(httpsCert, privateKey); err != nil {
if errors.Is(err, http.ErrServerClosed) {
log.Info("server: shutdown complete")
} else {
log.Errorf("server: %s", err)
}
}
}
// StartAutoTLS starts the Web server with auto tls enabled.
func StartAutoTLS(s *http.Server, m *autocert.Manager, conf *config.Config) {
var g errgroup.Group
g.Go(func() error {
return http.ListenAndServe(fmt.Sprintf("%s:%d", conf.HttpHost(), conf.HttpPort()), m.HTTPHandler(http.HandlerFunc(redirect)))
})
g.Go(func() error {
return s.ListenAndServeTLS("", "")
})
if err := g.Wait(); err != nil {
if errors.Is(err, http.ErrServerClosed) {
log.Info("server: shutdown complete")
} else {
log.Errorf("server: %s", err)
}
}
}
func redirect(w http.ResponseWriter, req *http.Request) {
target := "https://" + req.Host + req.RequestURI
http.Redirect(w, req, target, httpsRedirect)
}