mirror of
https://github.com/vee1e/krply.git
synced 2026-09-01 09:49:51 +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
|
||||
}
|
||||
|
||||
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)))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
httpSrv := &http.Server{Addr: *listen, Handler: srv.Handler()}
|
||||
httpSrv := &http.Server{Addr: listenAddr, Handler: srv.Handler()}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
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()
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ type Server struct {
|
|||
planMu sync.RWMutex
|
||||
plans map[string]*replay.Plan
|
||||
planTimes map[string]time.Time
|
||||
|
||||
corsOrigins []string
|
||||
}
|
||||
|
||||
// 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() {
|
||||
static = os.DirFS("web/dist")
|
||||
}
|
||||
origins := strings.Split(os.Getenv("KRPLY_CORS_ORIGINS"), ",")
|
||||
if len(origins) == 1 && origins[0] == "" {
|
||||
origins = []string{"*"}
|
||||
}
|
||||
return &Server{
|
||||
store: store,
|
||||
mat: mat,
|
||||
planner: planner,
|
||||
metrics: m,
|
||||
version: version,
|
||||
static: static,
|
||||
plans: map[string]*replay.Plan{},
|
||||
planTimes: map[string]time.Time{},
|
||||
store: store,
|
||||
mat: mat,
|
||||
planner: planner,
|
||||
metrics: m,
|
||||
version: version,
|
||||
static: static,
|
||||
plans: map[string]*replay.Plan{},
|
||||
planTimes: map[string]time.Time{},
|
||||
corsOrigins: origins,
|
||||
}, 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.
|
||||
func (s *Server) Handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
|
@ -95,7 +133,7 @@ func (s *Server) Handler() http.Handler {
|
|||
mux.HandleFunc("POST /v1/replay-runs", s.handleReplayRun)
|
||||
mux.HandleFunc("GET /metrics", s.handleMetrics)
|
||||
mux.HandleFunc("/", s.handleStatic)
|
||||
return mux
|
||||
return s.cors(mux)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
ts := newTestServer(t, seedStore(t))
|
||||
var clusters []queryv1.Cluster
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue