2024-08-17 21:57:14 +02:00
package bootstrap
import (
2025-04-06 06:04:08 -07:00
"context"
2026-04-19 14:04:22 +02:00
"crypto/tls"
2025-05-17 00:36:58 +02:00
"errors"
2025-04-28 18:13:50 +09:00
"fmt"
2025-07-27 03:03:52 +02:00
"log/slog"
2025-03-06 17:42:12 +01:00
"net"
2025-04-28 18:13:50 +09:00
"net/http"
2025-06-19 00:41:57 +08:00
"os"
2025-06-27 22:24:22 +02:00
"strings"
2026-04-19 14:04:22 +02:00
"sync"
"sync/atomic"
2024-08-17 21:57:14 +02:00
"time"
2026-04-19 14:04:22 +02:00
"github.com/fsnotify/fsnotify"
2025-09-14 17:26:21 +02:00
sloggin "github.com/gin-contrib/slog"
2024-08-17 21:57:14 +02:00
"github.com/gin-gonic/gin"
2026-07-02 23:36:45 -07:00
"github.com/italypaleale/francis/builtin/ratelimit"
"github.com/italypaleale/go-kit/servicerunner"
2025-05-05 15:59:44 +02:00
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
2025-05-03 14:25:22 -07:00
"gorm.io/gorm"
2025-07-27 03:03:52 +02:00
"github.com/pocket-id/pocket-id/backend/frontend"
2025-02-05 18:08:01 +01:00
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/controller"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
2026-07-05 14:14:39 -07:00
"github.com/pocket-id/pocket-id/backend/internal/tracing"
2025-03-06 17:42:12 +01:00
"github.com/pocket-id/pocket-id/backend/internal/utils/systemd"
2024-08-17 21:57:14 +02:00
)
2025-03-29 15:11:25 -07:00
// This is used to register additional controllers for tests
2025-05-03 14:25:22 -07:00
var registerTestControllers [ ] func ( apiGroup * gin . RouterGroup , db * gorm . DB , svc * services )
2025-03-29 15:11:25 -07:00
2026-07-02 23:36:45 -07:00
func initRouter ( db * gorm . DB , svc * services , rateLimitServices map [ string ] * ratelimit . RateLimitService ) ( servicerunner . Service , error ) {
2026-04-19 18:36:34 +02:00
r , err := initEngine ( )
if err != nil {
return nil , err
}
2026-07-02 23:36:45 -07:00
err = registerRoutes ( r , db , svc , rateLimitServices )
2026-04-26 15:11:35 +03:00
if err != nil {
return nil , err
}
2026-04-19 18:36:34 +02:00
serverConfig , err := initServer ( r )
if err != nil {
return nil , err
}
runFn := func ( ctx context . Context ) error {
return runServer ( ctx , serverConfig )
}
return runFn , nil
}
type serverConfig struct {
addr string
certProvider * tlsCertProvider
listener net . Listener
server * http . Server
tlsConfig * tls . Config
}
func initEngine ( ) ( * gin . Engine , error ) {
setGinMode ( )
r := gin . New ( )
initLogger ( r )
configureEngine ( r )
registerGlobalMiddleware ( r )
return r , nil
}
func setGinMode ( ) {
2024-08-17 21:57:14 +02:00
// Set the appropriate Gin mode based on the environment
switch common . EnvConfig . AppEnv {
2025-11-16 09:25:06 -08:00
case common . AppEnvProduction :
2024-08-17 21:57:14 +02:00
gin . SetMode ( gin . ReleaseMode )
2025-11-16 09:25:06 -08:00
case common . AppEnvDevelopment :
2024-08-17 21:57:14 +02:00
gin . SetMode ( gin . DebugMode )
2025-11-16 09:25:06 -08:00
case common . AppEnvTest :
2024-08-17 21:57:14 +02:00
gin . SetMode ( gin . TestMode )
}
2026-04-19 18:36:34 +02:00
}
2024-08-17 21:57:14 +02:00
2026-04-19 18:36:34 +02:00
func configureEngine ( r * gin . Engine ) {
2025-05-17 00:36:58 +02:00
if ! common . EnvConfig . TrustProxy {
_ = r . SetTrustedProxies ( nil )
}
2026-03-27 01:44:31 +08:00
if common . EnvConfig . TrustedPlatform != "" {
r . TrustedPlatform = common . EnvConfig . TrustedPlatform
}
2026-07-05 14:14:39 -07:00
r . Use ( otelgin . Middleware (
common . Name ,
otelgin . WithFilter ( shouldTraceRequest ) ) ,
)
}
// shouldTraceRequest reports whether an incoming request should be traced.
// It traces only requests handled by real backend routes (the API, the OIDC/OAuth endpoints, and the well-known documents).
// Everything else falls through to the frontend NoRoute handler, which serves the SPA shell and static assets; tracing those would produce noisy, unparented spans named just "GET" with an empty http.route.
func shouldTraceRequest ( r * http . Request ) bool {
p := r . URL . Path
switch {
case strings . HasPrefix ( p , "/api/" ) ,
strings . HasPrefix ( p , "/.well-known/" ) ,
p == "/authorize" :
return true
default :
return false
2025-05-05 15:59:44 +02:00
}
2026-04-19 18:36:34 +02:00
}
2025-05-05 15:59:44 +02:00
2026-04-19 18:36:34 +02:00
func registerGlobalMiddleware ( r * gin . Engine ) {
2025-12-05 11:17:13 +01:00
r . Use ( middleware . HeadMiddleware ( ) )
2025-11-30 18:29:35 +01:00
r . Use ( middleware . NewCacheControlMiddleware ( ) . Add ( ) )
2024-08-23 17:04:19 +02:00
r . Use ( middleware . NewCorsMiddleware ( ) . Add ( ) )
2025-09-07 20:45:06 +02:00
r . Use ( middleware . NewCspMiddleware ( ) . Add ( ) )
2024-10-28 18:11:54 +01:00
r . Use ( middleware . NewErrorHandlerMiddleware ( ) . Add ( ) )
2026-04-19 18:36:34 +02:00
}
2024-08-23 17:04:19 +02:00
2026-07-02 23:36:45 -07:00
func registerRoutes ( r * gin . Engine , db * gorm . DB , svc * services , rateLimitServices map [ string ] * ratelimit . RateLimitService ) error {
2026-04-26 15:11:35 +03:00
2026-06-22 18:42:02 +02:00
err := frontend . RegisterFrontend ( r )
2026-04-26 15:11:35 +03:00
if errors . Is ( err , frontend . ErrFrontendNotIncluded ) {
slog . Warn ( "Frontend is not included in the build. Skipping frontend registration." )
} else if err != nil {
return fmt . Errorf ( "failed to register frontend: %w" , err )
}
// Initialize middleware for specific routes
2026-06-29 12:11:18 +02:00
authMiddleware := middleware . NewAuthMiddleware ( svc . apiKeyModule , svc . userService , svc . jwtService )
2024-08-17 21:57:14 +02:00
fileSizeLimitMiddleware := middleware . NewFileSizeLimitMiddleware ( )
2026-07-02 23:36:45 -07:00
rateLimitMiddleware := middleware . NewRateLimitMiddleware ( rateLimitServices )
apiRateLimitMiddleware := rateLimitMiddleware . Add ( middleware . RateLimitAPI )
2026-01-24 20:29:50 +01:00
apiGroup := r . Group ( "/api" , apiRateLimitMiddleware )
2026-06-22 18:42:02 +02:00
baseGroup := r . Group ( "/" , apiRateLimitMiddleware )
2026-06-29 12:11:18 +02:00
svc . apiKeyModule . RegisterRoutes ( apiGroup ,
authMiddleware . WithAdminNotRequired ( ) . Add ( ) ,
authMiddleware . WithAdminNotRequired ( ) . WithApiKeyAuthDisabled ( ) . Add ( ) ,
)
2026-06-29 14:02:20 +02:00
svc . webauthnModule . RegisterRoutes ( apiGroup ,
authMiddleware . WithAdminNotRequired ( ) . Add ( ) ,
2026-07-02 23:36:45 -07:00
rateLimitMiddleware . Add ( middleware . RateLimitWebauthnLogin ) ,
rateLimitMiddleware . Add ( middleware . RateLimitWebauthnReauthenticate ) ,
2026-06-29 14:02:20 +02:00
)
2026-06-22 18:42:02 +02:00
controller . NewOidcController ( apiGroup , authMiddleware , fileSizeLimitMiddleware , svc . oidcService )
2026-07-02 23:36:45 -07:00
controller . NewUserController ( apiGroup , authMiddleware , rateLimitMiddleware , svc . userService , svc . oneTimeAccessService , svc . webauthnModule , svc . appConfigService )
2025-05-03 14:25:22 -07:00
controller . NewAppConfigController ( apiGroup , authMiddleware , svc . appConfigService , svc . emailService , svc . ldapService )
2025-09-20 21:42:52 +02:00
controller . NewAppImagesController ( apiGroup , authMiddleware , svc . appImagesService )
2025-05-03 14:25:22 -07:00
controller . NewAuditLogController ( apiGroup , svc . auditLogService , authMiddleware )
controller . NewUserGroupController ( apiGroup , authMiddleware , svc . userGroupService )
controller . NewCustomClaimController ( apiGroup , authMiddleware , svc . customClaimService )
2026-02-22 12:39:19 -06:00
controller . NewVersionController ( apiGroup , authMiddleware , svc . versionService )
2026-01-02 17:54:20 +01:00
controller . NewScimController ( apiGroup , authMiddleware , svc . scimService )
2026-06-30 20:53:31 +02:00
svc . userSignUpModule . RegisterRoutes ( apiGroup ,
authMiddleware . Add ( ) ,
2026-07-02 23:36:45 -07:00
rateLimitMiddleware . Add ( middleware . RateLimitSignup ) ,
2026-06-30 20:53:31 +02:00
)
2024-08-17 21:57:14 +02:00
2026-06-22 18:42:02 +02:00
optionalBrowserAuth := authMiddleware . WithAdminNotRequired ( ) . WithSuccessOptional ( ) . WithApiKeyAuthDisabled ( ) . Add ( )
browserAuth := authMiddleware . WithAdminNotRequired ( ) . WithApiKeyAuthDisabled ( ) . Add ( )
svc . oidcModule . RegisterRoutes ( baseGroup , apiGroup , optionalBrowserAuth , browserAuth )
2026-04-19 18:36:34 +02:00
registerTestRoutes ( apiGroup , db , svc )
2024-08-17 21:57:14 +02:00
2025-05-03 14:25:22 -07:00
controller . NewWellKnownController ( baseGroup , svc . jwtService )
2024-08-17 21:57:14 +02:00
2026-04-19 18:36:34 +02:00
// These are not rate-limited.
2025-05-06 13:14:18 -07:00
controller . NewHealthzController ( r )
2026-04-26 15:11:35 +03:00
2026-07-05 14:14:39 -07:00
// Receives OTLP trace payloads from the browser SPA (POST /internal/telemetry/traces) and forwards them to the collector, when trace export is enabled.
// Outside /api, so it's unauthenticated and not traced, but it is rate-limited.
tracing . NewTelemetryController ( r , rateLimitMiddleware . Add ( middleware . RateLimitInternal ) )
2026-04-26 15:11:35 +03:00
return nil
2026-04-19 18:36:34 +02:00
}
2025-05-06 13:14:18 -07:00
2026-04-19 18:36:34 +02:00
func registerTestRoutes ( apiGroup * gin . RouterGroup , db * gorm . DB , svc * services ) {
if common . EnvConfig . AppEnv . IsProduction ( ) {
return
}
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
for _ , f := range registerTestControllers {
f ( apiGroup , db , svc )
}
}
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
func initServer ( r * gin . Engine ) ( * serverConfig , error ) {
protocols , tlsConfig , certProvider , err := initServerProtocols ( )
if err != nil {
return nil , err
}
2026-04-19 14:04:22 +02:00
2026-05-29 09:18:29 +02:00
var socketFn func ( ) ( * socket , error )
2026-05-29 11:14:49 +02:00
switch {
case common . EnvConfig . SystemdSocket :
2026-05-29 09:18:29 +02:00
socketFn = systemdSocket
2026-05-29 11:14:49 +02:00
case common . EnvConfig . UnixSocket != "" :
2026-05-29 09:18:29 +02:00
socketFn = unixSocket
2026-05-29 11:14:49 +02:00
default :
2026-05-29 09:18:29 +02:00
socketFn = tcpSocket
2026-04-19 18:36:34 +02:00
}
2026-04-19 14:04:22 +02:00
2026-05-29 09:18:29 +02:00
socket , err := socketFn ( )
if err != nil {
2026-04-19 18:36:34 +02:00
return nil , err
}
2026-05-29 09:18:29 +02:00
addr := socket . addr
listener := socket . listener
server := newHTTPServer ( r , protocols )
return & serverConfig { addr , certProvider , listener , server , tlsConfig } , nil
2026-04-19 18:36:34 +02:00
}
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
func initServerProtocols ( ) ( * http . Protocols , * tls . Config , * tlsCertProvider , error ) {
protocols := new ( http . Protocols )
protocols . SetHTTP1 ( true )
if common . EnvConfig . TLSCertFile == "" || common . EnvConfig . TLSKeyFile == "" {
2026-04-19 14:04:22 +02:00
protocols . SetUnencryptedHTTP2 ( true )
2026-04-19 18:36:34 +02:00
return protocols , nil , nil , nil
2026-04-19 14:04:22 +02:00
}
2026-01-24 18:24:34 +01:00
2026-04-19 18:36:34 +02:00
protocols . SetHTTP2 ( true )
certProvider , err := newCertProvider ( common . EnvConfig . TLSCertFile , common . EnvConfig . TLSKeyFile )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "failed to load TLS certificate: %w" , err )
}
tlsConfig := & tls . Config {
GetCertificate : certProvider . GetCertificate ,
MinVersion : tls . VersionTLS13 ,
NextProtos : [ ] string { "h2" } ,
}
slog . Info ( "TLS enabled" )
return protocols , tlsConfig , certProvider , nil
}
func newHTTPServer ( r * gin . Engine , protocols * http . Protocols ) * http . Server {
return & http . Server {
2025-04-28 18:13:50 +09:00
MaxHeaderBytes : 1 << 20 ,
ReadHeaderTimeout : 10 * time . Second ,
2026-04-19 18:36:34 +02:00
Protocols : protocols ,
2026-05-18 23:04:08 +02:00
Handler : http . HandlerFunc ( func ( w http . ResponseWriter , req * http . Request ) {
2025-12-05 11:17:13 +01:00
// HEAD requests don't get matched by Gin routes, so we convert them to GET
// middleware.HeadMiddleware will convert them back to HEAD later
if req . Method == http . MethodHead {
req . Method = http . MethodGet
ctx := context . WithValue ( req . Context ( ) , middleware . IsHeadRequestCtxKey { } , true )
req = req . WithContext ( ctx )
}
r . ServeHTTP ( w , req )
2026-05-18 23:04:08 +02:00
} ) ,
2025-04-28 18:13:50 +09:00
}
2026-04-19 18:36:34 +02:00
}
func runServer ( ctx context . Context , config * serverConfig ) error {
slog . Info ( "Server listening" , slog . String ( "addr" , config . addr ) , slog . Bool ( "tls" , config . tlsConfig != nil ) )
certWatcher , err := startCertWatcher ( ctx , config . certProvider )
if err != nil {
return err
2025-06-19 00:41:57 +08:00
}
2026-04-19 18:36:34 +02:00
defer closeCertWatcher ( certWatcher )
2025-06-19 00:41:57 +08:00
2026-04-19 18:36:34 +02:00
startHTTPServer ( config )
notifySystemdReady ( )
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
<- ctx . Done ( )
2026-04-21 09:53:06 -07:00
// We do not pass the context because it's already been canceled
//nolint:contextcheck
return shutdownServer ( config . server )
2026-04-19 18:36:34 +02:00
}
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
func startCertWatcher ( ctx context . Context , certProvider * tlsCertProvider ) ( * fsnotify . Watcher , error ) {
if certProvider == nil {
return nil , nil
}
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
certWatcher , err := fsnotify . NewWatcher ( )
if err != nil {
return nil , fmt . Errorf ( "failed to create certificate watcher: %w" , err )
}
2025-05-03 14:25:22 -07:00
2026-04-19 18:36:34 +02:00
if err := certWatcher . Add ( common . EnvConfig . TLSCertFile ) ; err != nil {
certWatcher . Close ( )
return nil , fmt . Errorf ( "failed to watch TLS certificate: %w" , err )
}
if err := certWatcher . Add ( common . EnvConfig . TLSKeyFile ) ; err != nil {
certWatcher . Close ( )
return nil , fmt . Errorf ( "failed to watch TLS key: %w" , err )
}
2025-05-03 14:25:22 -07:00
2026-04-19 18:36:34 +02:00
go certProvider . StartWatching ( ctx , certWatcher )
return certWatcher , nil
}
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
func closeCertWatcher ( certWatcher * fsnotify . Watcher ) {
if certWatcher != nil {
certWatcher . Close ( )
}
}
2025-05-03 14:25:22 -07:00
2026-04-19 18:36:34 +02:00
func startHTTPServer ( config * serverConfig ) {
go func ( ) {
defer config . listener . Close ( )
2025-04-28 18:13:50 +09:00
2026-04-19 18:36:34 +02:00
listener := config . listener
if config . tlsConfig != nil {
listener = tls . NewListener ( config . listener , config . tlsConfig )
2025-04-28 18:13:50 +09:00
}
2026-04-19 18:36:34 +02:00
srvErr := config . server . Serve ( listener )
2025-05-03 14:25:22 -07:00
2026-04-19 18:36:34 +02:00
if srvErr != http . ErrServerClosed {
slog . Error ( "Error starting app server" , "error" , srvErr )
os . Exit ( 1 )
2026-04-19 14:04:22 +02:00
}
2026-04-19 18:36:34 +02:00
} ( )
}
2026-04-19 14:04:22 +02:00
2026-04-19 18:36:34 +02:00
func notifySystemdReady ( ) {
err := systemd . SdNotifyReady ( )
if err != nil {
// Log the error only
slog . Warn ( "Unable to notify systemd that the service is ready" , "error" , err )
}
}
2026-04-21 09:53:06 -07:00
func shutdownServer ( srv * http . Server ) error {
// Note we use the background context here as ctx has been canceled already
shutdownCtx , shutdownCancel := context . WithTimeout ( context . Background ( ) , 5 * time . Second )
shutdownErr := srv . Shutdown ( shutdownCtx ) //nolint:contextcheck
2026-04-19 18:36:34 +02:00
shutdownCancel ( )
if shutdownErr != nil {
// Log the error only (could be context canceled)
slog . Warn ( "App server shutdown error" , "error" , shutdownErr )
2024-08-17 21:57:14 +02:00
}
2025-04-28 18:13:50 +09:00
2026-04-19 18:36:34 +02:00
return nil
2024-08-17 21:57:14 +02:00
}
2025-09-14 17:26:21 +02:00
func initLogger ( r * gin . Engine ) {
loggerSkipPathsPrefix := [ ] string {
2025-09-20 21:42:52 +02:00
"GET /api/application-images/logo" ,
"GET /api/application-images/background" ,
"GET /api/application-images/favicon" ,
2025-12-17 16:20:22 +01:00
"GET /api/application-images/email" ,
2025-09-14 17:26:21 +02:00
"GET /_app" ,
"GET /fonts" ,
"GET /healthz" ,
"HEAD /healthz" ,
}
r . Use ( sloggin . SetLogger (
sloggin . WithLogger ( func ( _ * gin . Context , _ * slog . Logger ) * slog . Logger {
return slog . Default ( )
} ) ,
sloggin . WithSkipper ( func ( c * gin . Context ) bool {
for _ , prefix := range loggerSkipPathsPrefix {
if strings . HasPrefix ( c . Request . Method + " " + c . Request . URL . String ( ) , prefix ) {
return true
}
}
return false
} ) ,
) )
}
2026-04-19 14:04:22 +02:00
// tlsCertProvider holds certificates that can be dynamically reloaded
type tlsCertProvider struct {
certMutex sync . RWMutex
cert * tls . Certificate
certFile string
keyFile string
forceReload atomic . Bool
}
// GetCertificate implements tls.GetCertificate interface for dynamic certificate loading
func ( p * tlsCertProvider ) GetCertificate ( _ * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
if p . forceReload . Load ( ) {
p . certMutex . Lock ( )
p . forceReload . Store ( false )
p . certMutex . Unlock ( )
}
p . certMutex . RLock ( )
defer p . certMutex . RUnlock ( )
return p . cert , nil
}
// newCertProvider creates a new certificate provider with initial certificates loaded
func newCertProvider ( certFile , keyFile string ) ( * tlsCertProvider , error ) {
cert , err := tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
return nil , err
}
return & tlsCertProvider {
cert : & cert ,
certFile : certFile ,
keyFile : keyFile ,
} , nil
}
// reloadCertificate reloads the certificate from disk
func ( p * tlsCertProvider ) reloadCertificate ( ) error {
cert , err := tls . LoadX509KeyPair ( p . certFile , p . keyFile )
if err != nil {
return fmt . Errorf ( "failed to reload TLS certificate: %w" , err )
}
p . certMutex . Lock ( )
p . cert = & cert
p . certMutex . Unlock ( )
return nil
}
// StartWatching begins monitoring the certificate files for changes with debouncing
func ( p * tlsCertProvider ) StartWatching ( ctx context . Context , watcher * fsnotify . Watcher ) {
debounceDuration := 1 * time . Second
reloadTimer := time . NewTimer ( debounceDuration )
reloadTimer . Stop ( )
for {
select {
case <- ctx . Done ( ) :
return
case event , ok := <- watcher . Events :
if ! ok {
return
}
// Only process write/rename events for certificate/key files
if event . Has ( fsnotify . Write | fsnotify . Rename ) {
// Reset the debounce timer whenever we get a relevant event
reloadTimer . Stop ( )
// Drain the channel if there's a pending value
select {
case <- reloadTimer . C :
default :
}
reloadTimer . Reset ( debounceDuration )
slog . Debug ( "TLS file change detected, debouncing" , slog . String ( "path" , event . Name ) )
}
case <- reloadTimer . C :
// Timer fired - no more events in 500ms, so reload
slog . Info ( "Reloading TLS certificate" )
if err := p . reloadCertificate ( ) ; err != nil {
slog . Error ( "Failed to reload TLS certificate" , "error" , err )
continue
}
p . forceReload . Store ( true )
slog . Info ( "TLS certificate reloaded successfully" )
case err , ok := <- watcher . Errors :
if ! ok {
return
}
slog . Error ( "Certificate watcher error" , "error" , err )
}
}
}