1855e43699
Adds a `GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS` env-driven allow-list on the public REST server so the dev UI on https://www.galaxy.lan can call https://api.galaxy.lan without the browser blocking the cross-origin response. Defaults to empty (no CORS) so the production posture stays closed. The middleware mounts before route classification and anti-abuse, so OPTIONS preflights never charge against per-class rate-limit buckets. `tools/dev-deploy/docker-compose.yml` opts the dev gateway into a single allowed origin (`https://www.galaxy.lan`); local-dev keeps the defaults because Vite proxies through the same origin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
1.7 KiB
Go
53 lines
1.7 KiB
Go
package restapi
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// withCORS returns a gin middleware that handles browser CORS preflight and
|
|
// attaches Access-Control-Allow-* response headers when the request's Origin
|
|
// is on the configured allow-list. Origins are compared exactly: scheme,
|
|
// host, and port must match. An empty allow-list disables the middleware —
|
|
// requests pass through untouched. Requests without an Origin header always
|
|
// pass through, the middleware only acts when a browser actually asks.
|
|
//
|
|
// The middleware mounts before the anti-abuse layer so OPTIONS preflights
|
|
// do not count against the rate-limit buckets for the eventual real call.
|
|
func withCORS(allowedOrigins []string) gin.HandlerFunc {
|
|
allowed := make(map[string]struct{}, len(allowedOrigins))
|
|
for _, origin := range allowedOrigins {
|
|
allowed[origin] = struct{}{}
|
|
}
|
|
if len(allowed) == 0 {
|
|
return func(c *gin.Context) { c.Next() }
|
|
}
|
|
return func(c *gin.Context) {
|
|
origin := c.GetHeader("Origin")
|
|
if origin == "" {
|
|
c.Next()
|
|
return
|
|
}
|
|
if _, ok := allowed[origin]; !ok {
|
|
c.Next()
|
|
return
|
|
}
|
|
c.Header("Access-Control-Allow-Origin", origin)
|
|
c.Header("Vary", "Origin")
|
|
c.Header("Access-Control-Allow-Credentials", "true")
|
|
if c.Request.Method == http.MethodOptions {
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
|
if reqHeaders := c.GetHeader("Access-Control-Request-Headers"); reqHeaders != "" {
|
|
c.Header("Access-Control-Allow-Headers", reqHeaders)
|
|
} else {
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
}
|
|
c.Header("Access-Control-Max-Age", "3600")
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|