package main import ( "context" "errors" "fmt" "log/slog" "net/http" "net/url" "slices" "time" "gitea.dwysokinski.me/Kichiyaki/chiclientip" "gitea.dwysokinski.me/twhelp/core/internal/adapter" "gitea.dwysokinski.me/twhelp/core/internal/app" "gitea.dwysokinski.me/twhelp/core/internal/chislog" "gitea.dwysokinski.me/twhelp/core/internal/health" "gitea.dwysokinski.me/twhelp/core/internal/port" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/realclientip/realclientip-go" "github.com/urfave/cli/v2" ) var ( apiServerPortFlag = &cli.UintFlag{ Name: "api.port", EnvVars: []string{"API_PORT"}, Value: 9234, //nolint:gomnd } apiServerHandlerTimeoutFlag = &cli.DurationFlag{ Name: "api.handlerTimeout", EnvVars: []string{"API_HANDLER_TIMEOUT"}, Value: 5 * time.Second, //nolint:gomnd Usage: "https://pkg.go.dev/net/http#TimeoutHandler", } apiServerReadTimeoutFlag = &cli.DurationFlag{ Name: "api.readTimeout", EnvVars: []string{"API_READ_TIMEOUT"}, Value: 5 * time.Second, //nolint:gomnd } apiServerReadHeaderTimeoutFlag = &cli.DurationFlag{ Name: "api.readHeaderTimeout", EnvVars: []string{"API_READ_HEADER_TIMEOUT"}, Value: time.Second, } apiServerWriteTimeoutFlag = &cli.DurationFlag{ Name: "api.writeTimeout", EnvVars: []string{"API_WRITE_TIMEOUT"}, Value: 10 * time.Second, //nolint:gomnd } apiServerIdleTimeoutFlag = &cli.DurationFlag{ Name: "api.idleTimeout", EnvVars: []string{"API_IDLE_TIMEOUT"}, Value: 180 * time.Second, //nolint:gomnd } apiServerShutdownTimeoutFlag = &cli.DurationFlag{ Name: "api.shutdownTimeout", EnvVars: []string{"API_SHUTDOWN_TIMEOUT"}, Value: 10 * time.Second, //nolint:gomnd } apiServerOpenAPIEnabledFlag = &cli.BoolFlag{ Name: "api.openApi.enabled", EnvVars: []string{"API_OPENAPI_ENABLED"}, Value: true, } apiServerOpenAPISwaggerEnabledFlag = &cli.BoolFlag{ Name: "api.openApi.swaggerEnabled", EnvVars: []string{"API_OPENAPI_SWAGGER_ENABLED"}, Value: true, } apiServerOpenAPIServersFlag = &cli.StringSliceFlag{ Name: "api.openApi.servers", EnvVars: []string{"API_OPENAPI_SERVERS"}, } apiServerFlags = []cli.Flag{ apiServerPortFlag, apiServerHandlerTimeoutFlag, apiServerReadTimeoutFlag, apiServerReadHeaderTimeoutFlag, apiServerWriteTimeoutFlag, apiServerIdleTimeoutFlag, apiServerOpenAPIEnabledFlag, apiServerOpenAPISwaggerEnabledFlag, apiServerOpenAPIServersFlag, } ) const ( apiBasePath = "/api" metaBasePath = "/_meta" ) var cmdServe = &cli.Command{ Name: "serve", Usage: "Run the HTTP server", Flags: slices.Concat(apiServerFlags, dbFlags), Action: func(c *cli.Context) error { logger := loggerFromCtx(c.Context) // deps bunDB, err := newBunDBFromFlags(c) if err != nil { return err } defer closeBunDB(bunDB, logger) // adapters versionRepo := adapter.NewVersionBunRepository(bunDB) serverRepo := adapter.NewServerBunRepository(bunDB) tribeRepo := adapter.NewTribeBunRepository(bunDB) playerRepo := adapter.NewPlayerBunRepository(bunDB) villageRepo := adapter.NewVillageBunRepository(bunDB) ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB) tribeChangeRepo := adapter.NewTribeChangeBunRepository(bunDB) tribeSnapshotRepo := adapter.NewTribeSnapshotBunRepository(bunDB) playerSnapshotRepo := adapter.NewPlayerSnapshotBunRepository(bunDB) // services versionSvc := app.NewVersionService(versionRepo) serverSvc := app.NewServerService(serverRepo, nil, nil) tribeSvc := app.NewTribeService(tribeRepo, nil, nil) tribeChangeSvc := app.NewTribeChangeService(tribeChangeRepo) playerSvc := app.NewPlayerService(playerRepo, tribeChangeSvc, nil, nil) villageSvc := app.NewVillageService(villageRepo, nil, nil) ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil) tribeSnapshotSvc := app.NewTribeSnapshotService(tribeSnapshotRepo, tribeSvc, nil) playerSnapshotSvc := app.NewPlayerSnapshotService(playerSnapshotRepo, playerSvc, nil) // health h := health.New() server, err := newHTTPServer( httpServerConfig{ port: c.Uint(apiServerPortFlag.Name), readTimeout: c.Duration(apiServerReadTimeoutFlag.Name), readHeaderTimeout: c.Duration(apiServerReadHeaderTimeoutFlag.Name), writeTimeout: c.Duration(apiServerWriteTimeoutFlag.Name), idleTimeout: c.Duration(apiServerIdleTimeoutFlag.Name), }, func(r chi.Router) error { oapiCfg, oapiCfgErr := newOpenAPIConfigFromFlags(c) if oapiCfgErr != nil { return oapiCfgErr } r.Group(func(r chi.Router) { r.Use(middleware.Recoverer) r.Mount(metaBasePath, port.NewMetaHTTPHandler(h)) }) r.Group(func(r chi.Router) { r.Use(newAPIMiddlewares(c, logger)...) r.Mount(apiBasePath, port.NewAPIHTTPHandler( versionSvc, serverSvc, tribeSvc, playerSvc, villageSvc, ennoblementSvc, tribeChangeSvc, tribeSnapshotSvc, playerSnapshotSvc, port.WithOpenAPIConfig(oapiCfg), )) }) return nil }, ) if err != nil { return fmt.Errorf("couldn't construct HTTP server: %w", err) } idleConnsClosed := make(chan struct{}) go func() { waitForShutdownSignal(c.Context) logger.Debug("received shutdown signal") ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerShutdownTimeoutFlag.Name)) defer cancel() if err := server.Shutdown(ctx); err != nil { logger.Warn("shutdown failed", slog.Any("error", err)) } close(idleConnsClosed) }() logger.Info("Server is listening on the addr "+server.Addr, slog.String("addr", server.Addr)) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("ListenAndServe failed: %w", err) } <-idleConnsClosed return nil }, } func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) { rawURLs := c.StringSlice(apiServerOpenAPIServersFlag.Name) servers := make([]port.OpenAPIConfigServer, 0, len(rawURLs)) for _, raw := range rawURLs { u, err := url.Parse(raw) if err != nil { return port.OpenAPIConfig{}, err } servers = append(servers, port.OpenAPIConfigServer{ URL: u, }) } return port.OpenAPIConfig{ Enabled: c.Bool(apiServerOpenAPIEnabledFlag.Name), SwaggerEnabled: c.Bool(apiServerOpenAPISwaggerEnabledFlag.Name), BasePath: apiBasePath, Servers: servers, }, nil } func newAPIMiddlewares(c *cli.Context, logger *slog.Logger) chi.Middlewares { return chi.Middlewares{ chiclientip.ClientIP( realclientip.NewChainStrategy( realclientip.Must(realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For")), realclientip.RemoteAddrStrategy{}, ), ), chislog.Logger(logger, chislog.WithIPExtractor(func(r *http.Request) string { clientIP, _ := chiclientip.ClientIPFromContext(r.Context()) return clientIP })), middleware.Recoverer, func(next http.Handler) http.Handler { return http.TimeoutHandler(next, c.Duration(apiServerHandlerTimeoutFlag.Name), "Timeout") }, } }