veylant/internal/middleware/cors.go
2026-02-23 13:35:04 +01:00

67 lines
2.3 KiB
Go

package middleware
import (
"net/http"
"strings"
)
// CORS returns a middleware that adds Cross-Origin Resource Sharing headers to
// every response and handles OPTIONS preflight requests. It is designed for the
// Veylant dashboard (React SPA) which calls the proxy from a different origin.
//
// allowedOrigins is the list of permitted origins (e.g. ["http://localhost:3000",
// "https://dashboard.veylant.ai"]). An empty list disables CORS (no headers added).
// Pass ["*"] to allow any origin (development only — never use in production).
func CORS(allowedOrigins []string) func(http.Handler) http.Handler {
if len(allowedOrigins) == 0 {
// No origins configured — identity middleware.
return func(next http.Handler) http.Handler { return next }
}
originSet := make(map[string]struct{}, len(allowedOrigins))
wildcard := false
for _, o := range allowedOrigins {
if o == "*" {
wildcard = true
}
originSet[o] = struct{}{}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Only set CORS headers when the request carries an Origin header.
if origin != "" {
_, allowed := originSet[origin]
if wildcard || allowed {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Add("Vary", "Origin")
}
}
// Handle preflight (OPTIONS) — respond 204 and do not forward.
if r.Method == http.MethodOptions && origin != "" {
requestedMethod := r.Header.Get("Access-Control-Request-Method")
requestedHeaders := r.Header.Get("Access-Control-Request-Headers")
if requestedMethod != "" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
if requestedHeaders != "" {
// Echo the requested headers back (normalised to title-case).
w.Header().Set("Access-Control-Allow-Headers", strings.ToTitle(requestedHeaders))
} else {
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-Id")
}
w.Header().Set("Access-Control-Max-Age", "86400") // 24 h preflight cache
w.WriteHeader(http.StatusNoContent)
return
}
}
next.ServeHTTP(w, r)
})
}
}