package cli import ( "context" "crypto/rand" "errors" "fmt" "log/slog" "net" "net/http" "path/filepath" "time" "github.com/go-chi/chi/v5" "code.icb4dc0.de/prskr/searcherside/core/services" v1 "code.icb4dc0.de/prskr/searcherside/handlers/api/v1" "code.icb4dc0.de/prskr/searcherside/infrastructure/api" "code.icb4dc0.de/prskr/searcherside/internal/flags" "code.icb4dc0.de/prskr/searcherside/internal/logging" ) const ( jwtSecretLength = 64 ) type ServerHandler struct { ListenAddress string `env:"LISTEN_ADDRESS" name:"listen-address" short:"a" help:"Listen address" default:":3000"` DataDirectory string `env:"DATA_DIRECTORY" name:"data-directory" short:"d" help:"Data directory" default:"${CWD}/data"` Config struct { ReadHeaderTimeout time.Duration `env:"HTTP_READ_HEADER_TIMEOUT" name:"read-header-timeout" help:"Read header timeout" default:"5s"` ShutDownTimeout time.Duration `env:"HTTP_SHUTDOWN_TIMEOUT" name:"shutdown-timeout" help:"Shutdown timeout" default:"5s"` ParseMaxMemoryBytes int64 `env:"HTTP_PARSE_MAX_MEMORY_BYTES" name:"parse-max-memory-bytes" help:"Parse max memory bytes" default:"33554432"` } `embed:"" prefix:"http."` Auth struct { JwtSecret flags.HexString `env:"AUTH_JWT_SECRET" name:"jwt-secret" help:"JWT secret"` } `embed:"" prefix:"auth."` } func (h *ServerHandler) Run(ctx context.Context, logger *slog.Logger) error { indexCurator, err := services.NewFileIndexCurator( filepath.Join(h.DataDirectory, "searcherside.json"), services.BleveIndexer{DataDirectory: h.DataDirectory}, services.TarZSTIndexArchiver{DataDirectory: h.DataDirectory}, ) if err != nil { logger.Error("Failed to create index curator", logging.Error(err)) return err } secret, err := h.jwtSecret() if err != nil { return err } r := chi.NewRouter() r.Use(api.LoggingMiddleware) r.Route("/api/v1", func(r chi.Router) { indexHandler := v1.IndexHandler{ MaxMemoryBytes: h.Config.ParseMaxMemoryBytes, Indexer: indexCurator, } searchHandler := v1.SearchHandler{ Curator: indexCurator, } v1.Mount(r, secret, indexHandler, searchHandler) }) srv := http.Server{ Addr: h.ListenAddress, Handler: r, ReadHeaderTimeout: h.Config.ReadHeaderTimeout, BaseContext: func(listener net.Listener) context.Context { return logging.ContextWithLogger(ctx, logger) }, } logger.Info("Starting server", slog.String("address", h.ListenAddress)) go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error("Failed to start server", logging.Error(err)) } }() <-ctx.Done() logger.Info("Shutting down server") shutdownCtx, cancel := context.WithTimeout(context.Background(), h.Config.ShutDownTimeout) if err := srv.Shutdown(shutdownCtx); err != nil { logger.Error("Failed to shutdown server", logging.Error(err)) } cancel() return nil } func (h *ServerHandler) jwtSecret() ([]byte, error) { if len(h.Auth.JwtSecret) == 0 { h.Auth.JwtSecret = make([]byte, jwtSecretLength) if n, err := rand.Read(h.Auth.JwtSecret); err != nil { return nil, err } else if n != jwtSecretLength { return nil, fmt.Errorf("expected to read %d random bytes but got %d", jwtSecretLength, n) } } return h.Auth.JwtSecret, nil }