mirror of
https://github.com/vee1e/krply.git
synced 2026-09-01 17:57:03 +00:00
feat(api): add cors middleware with configurable origins
- wrap all routes with CORS, honoring KRPLY_CORS_ORIGINS (default *) - short-circuit preflight OPTIONS and echo allowed origins - honor the PORT env var for platform deployments (render etc)
This commit is contained in:
parent
6576672b1d
commit
9155ea248c
3 changed files with 92 additions and 11 deletions
|
|
@ -42,6 +42,17 @@ func run() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listenAddr := *listen
|
||||||
|
listenFlagSet := false
|
||||||
|
flag.Visit(func(f *flag.Flag) {
|
||||||
|
if f.Name == "listen" {
|
||||||
|
listenFlagSet = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if !listenFlagSet && os.Getenv("PORT") != "" {
|
||||||
|
listenAddr = ":" + os.Getenv("PORT")
|
||||||
|
}
|
||||||
|
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
@ -64,11 +75,11 @@ func run() error {
|
||||||
return fmt.Errorf("build api server: %w", err)
|
return fmt.Errorf("build api server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpSrv := &http.Server{Addr: *listen, Handler: srv.Handler()}
|
httpSrv := &http.Server{Addr: listenAddr, Handler: srv.Handler()}
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
slog.Info("krply-server listening", "addr", *listen, "store", *storePath, "version", version)
|
slog.Info("krply-server listening", "addr", listenAddr, "store", *storePath, "version", version)
|
||||||
errCh <- httpSrv.ListenAndServe()
|
errCh <- httpSrv.ListenAndServe()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ type Server struct {
|
||||||
planMu sync.RWMutex
|
planMu sync.RWMutex
|
||||||
plans map[string]*replay.Plan
|
plans map[string]*replay.Plan
|
||||||
planTimes map[string]time.Time
|
planTimes map[string]time.Time
|
||||||
|
|
||||||
|
corsOrigins []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer wires a Server onto the given store, materializer, planner, and
|
// NewServer wires a Server onto the given store, materializer, planner, and
|
||||||
|
|
@ -65,18 +67,54 @@ func NewServer(store storage.Store, mat *materialize.Materializer, planner *repl
|
||||||
if fi, err := os.Stat("web/dist"); err == nil && fi.IsDir() {
|
if fi, err := os.Stat("web/dist"); err == nil && fi.IsDir() {
|
||||||
static = os.DirFS("web/dist")
|
static = os.DirFS("web/dist")
|
||||||
}
|
}
|
||||||
|
origins := strings.Split(os.Getenv("KRPLY_CORS_ORIGINS"), ",")
|
||||||
|
if len(origins) == 1 && origins[0] == "" {
|
||||||
|
origins = []string{"*"}
|
||||||
|
}
|
||||||
return &Server{
|
return &Server{
|
||||||
store: store,
|
store: store,
|
||||||
mat: mat,
|
mat: mat,
|
||||||
planner: planner,
|
planner: planner,
|
||||||
metrics: m,
|
metrics: m,
|
||||||
version: version,
|
version: version,
|
||||||
static: static,
|
static: static,
|
||||||
plans: map[string]*replay.Plan{},
|
plans: map[string]*replay.Plan{},
|
||||||
planTimes: map[string]time.Time{},
|
planTimes: map[string]time.Time{},
|
||||||
|
corsOrigins: origins,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cors wraps the mux with CORS headers so the static web UI can query the API
|
||||||
|
// from a different origin. Origins are allowed when they match KRPLY_CORS_ORIGINS
|
||||||
|
// (comma-separated, "*" for any). Preflight OPTIONS requests are short-circuited.
|
||||||
|
func (s *Server) cors(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
if origin != "" {
|
||||||
|
if s.allowsOrigin(origin) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept")
|
||||||
|
w.Header().Set("Vary", "Origin")
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) allowsOrigin(origin string) bool {
|
||||||
|
for _, o := range s.corsOrigins {
|
||||||
|
if o == "*" || o == origin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Handler returns the HTTP handler with all routes registered.
|
// Handler returns the HTTP handler with all routes registered.
|
||||||
func (s *Server) Handler() http.Handler {
|
func (s *Server) Handler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
@ -95,7 +133,7 @@ func (s *Server) Handler() http.Handler {
|
||||||
mux.HandleFunc("POST /v1/replay-runs", s.handleReplayRun)
|
mux.HandleFunc("POST /v1/replay-runs", s.handleReplayRun)
|
||||||
mux.HandleFunc("GET /metrics", s.handleMetrics)
|
mux.HandleFunc("GET /metrics", s.handleMetrics)
|
||||||
mux.HandleFunc("/", s.handleStatic)
|
mux.HandleFunc("/", s.handleStatic)
|
||||||
return mux
|
return s.cors(mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
// planByID returns the registered plan and whether it exists.
|
// planByID returns the registered plan and whether it exists.
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,38 @@ func TestHealth(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCORS(t *testing.T) {
|
||||||
|
ts := newTestServer(t, storage.NewInMemory())
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, ts.URL+"/v1/health", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Origin", "https://krply.lverma.com")
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get health: %v", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if got := res.Header.Get("Access-Control-Allow-Origin"); got != "https://krply.lverma.com" {
|
||||||
|
t.Errorf("Access-Control-Allow-Origin = %q, want origin echoed", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, err := http.NewRequest(http.MethodOptions, ts.URL+"/v1/health", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new preflight request: %v", err)
|
||||||
|
}
|
||||||
|
pre.Header.Set("Origin", "https://krply.lverma.com")
|
||||||
|
pres, err := http.DefaultClient.Do(pre)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("preflight: %v", err)
|
||||||
|
}
|
||||||
|
defer pres.Body.Close()
|
||||||
|
if pres.StatusCode != http.StatusNoContent {
|
||||||
|
t.Errorf("preflight status = %d, want 204", pres.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClusters(t *testing.T) {
|
func TestClusters(t *testing.T) {
|
||||||
ts := newTestServer(t, seedStore(t))
|
ts := newTestServer(t, seedStore(t))
|
||||||
var clusters []queryv1.Cluster
|
var clusters []queryv1.Cluster
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue