Ctrl K
ComunidadGuías…

internal/middleware/cors.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package middleware

import (
	"net"
	"net/http"
	"net/url"
	"strings"

	"github.com/rs/cors"
)

// CORSMode describes which cross-origin policy CORS selected, for startup logs.
type CORSMode int

const (
	// CORSAllowList allows only the configured origins.
	CORSAllowList CORSMode = iota
	// CORSBlockAll rejects every cross-origin request (production default).
	CORSBlockAll
	// CORSAllowAll allows every origin (development default with an API key).
	CORSAllowAll
	// CORSLoopbackOnly allows only localhost origins (development default
	// without an API key).
	CORSLoopbackOnly
)

// NormalizeOrigins trims whitespace and trailing slashes and drops empty
// entries. Browsers send Origin without a trailing slash, so
// "https://ui.example.com/" in SPARROW_CORS_ALLOWED_ORIGINS would otherwise never match.
func NormalizeOrigins(origins []string) []string {
	var out []string
	for _, o := range origins {
		o = strings.TrimRight(strings.TrimSpace(o), "/")
		if o != "" {
			out = append(out, o)
		}
	}
	return out
}

// CORS builds the cross-origin policy for the HTTP server.
//
//   - origins set: only those origins may call the API (a separately hosted UI
//     must be listed here).
//   - origins empty, production: no cross-origin access. The embedded UI is
//     same-origin and does not need CORS.
//   - origins empty, auth on: all origins, for local development (e.g. the
//     vite dev server on :5173 talking to :8080). A page still needs the key.
//   - origins empty, auth off: localhost origins only. Without a key every
//     caller is root, so allowing all origins would let any website a
//     browser on the network visits drive the API.
//
// The UI authenticates with the X-API-Key header (admin) or an Authorization
// bearer token (portal), never cookies, so credentials are not allowed.
func CORS(origins []string, production, authEnabled bool) (func(http.Handler) http.Handler, CORSMode) {
	origins = NormalizeOrigins(origins)
	switch {
	case len(origins) > 0:
		return cors.New(cors.Options{
			AllowedOrigins: origins,
			AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions},
			AllowedHeaders: []string{"Authorization", "Content-Type", APIKeyHeader},
			MaxAge:         300,
		}).Handler, CORSAllowList
	case production:
		// An empty AllowedOrigins list means "allow all" in rs/cors, so deny
		// explicitly with an origin func.
		return cors.New(cors.Options{
			AllowOriginFunc: func(string) bool { return false },
		}).Handler, CORSBlockAll
	case authEnabled:
		return cors.AllowAll().Handler, CORSAllowAll
	default:
		return cors.New(cors.Options{
			AllowOriginFunc: isLoopbackOrigin,
			AllowedMethods:  []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions},
			AllowedHeaders:  []string{"Authorization", "Content-Type", APIKeyHeader},
			MaxAge:          300,
		}).Handler, CORSLoopbackOnly
	}
}

// isLoopbackOrigin reports whether origin is http(s) on localhost or a
// loopback address, at any port.
func isLoopbackOrigin(origin string) bool {
	u, err := url.Parse(origin)
	if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
		return false
	}
	host := u.Hostname()
	if strings.EqualFold(host, "localhost") {
		return true
	}
	ip := net.ParseIP(host)
	return ip != nil && ip.IsLoopback()
}