searcherside/handlers/cli/server.go

112 lines
3.2 KiB
Go

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
}