more platform stuff

This commit is contained in:
mats committed 2026-07-18 23:28:51 +02:00
1 parent 4b9a882018
commit fc5eefbe5e
7 files changed
+756

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
// Package httpclient provides shared HTTP client helpers used across the
// monorepo: client IP extraction, CORS middleware, and related utilities.
package httpclient
import (
"net"
"net/http"
"strings"
)
// ClientIP extracts the best-effort client IP from a request by checking
// common reverse-proxy and CDN headers in priority order, falling back to
// the TCP remote address when no header is present. It does NOT respect
// query-parameter overrides (those belong at the handler level — see
// slask-finder/cmd/stores for the ?ip= debug pattern).
//
// Priority order: CF-Connecting-IP → X-Real-IP → X-Forwarded-For (first
// entry) → RemoteAddr (with port stripped when present).
func ClientIP(r *http.Request) string {
for _, h := range []string{"CF-Connecting-IP", "X-Real-IP", "X-Forwarded-For"} {
v := r.Header.Get(h)
if v == "" {
continue
}
// X-Forwarded-For may be a comma-separated list; take the first.
if h == "X-Forwarded-For" {
if i := strings.IndexByte(v, ','); i >= 0 {
v = v[:i]
}
}
return strings.TrimSpace(v)
}
// Fall back to the TCP remote address, stripping the port when present.
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
return host
}
return r.RemoteAddr
}
+101
View File
@@ -0,0 +1,101 @@
package httpclient
import (
"net/http"
)
type corsConfig struct {
origin string // "" = mirror request Origin; "*" = wildcard (no credentials)
methods string
headers string
exposeHeaders string
}
// CORSOption customises the CORS middleware.
type CORSOption func(*corsConfig)
// WithOrigin sets the Access-Control-Allow-Origin value. Use "*" for public
// APIs that don't need credentials. The default (empty string) mirrors the
// request's Origin header and enables credentials.
func WithOrigin(origin string) CORSOption {
return func(c *corsConfig) { c.origin = origin }
}
// WithMethods sets Access-Control-Allow-Methods. Default: "GET, POST, PUT, OPTIONS".
func WithMethods(methods string) CORSOption {
return func(c *corsConfig) { c.methods = methods }
}
// WithHeaders sets Access-Control-Allow-Headers. Default: "*".
func WithHeaders(headers string) CORSOption {
return func(c *corsConfig) { c.headers = headers }
}
// WithExposeHeaders sets Access-Control-Expose-Headers. Default: empty.
func WithExposeHeaders(headers string) CORSOption {
return func(c *corsConfig) { c.exposeHeaders = headers }
}
// CORS returns middleware that sets CORS response headers and handles
// preflight (OPTIONS) requests. By default it mirrors the request Origin,
// enables credentials, and allows GET/POST/PUT/OPTIONS with any request
// headers.
//
// Use WithOrigin("*") for public APIs that don't need credentials (per the
// CORS spec, credentials and wildcard origin are mutually exclusive).
//
// Common patterns:
//
// // Public API (wildcard origin, no credentials):
// mux := http.NewServeMux()
// handler := httpclient.CORS(
// httpclient.WithOrigin("*"),
// httpclient.WithMethods("GET, POST, OPTIONS"),
// )(mux)
//
// // Authenticated API (origin reflection, credentials):
// handler := httpclient.CORS()(mux)
func CORS(opts ...CORSOption) func(http.Handler) http.Handler {
cfg := corsConfig{
methods: "GET, POST, PUT, OPTIONS",
headers: "*",
}
for _, o := range opts {
o(&cfg)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
setCORSHeaders(w, r, cfg)
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Max-Age", "86400")
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
func setCORSHeaders(w http.ResponseWriter, r *http.Request, cfg corsConfig) {
origin := cfg.origin
if origin == "" {
origin = r.Header.Get("Origin")
}
if origin == "" {
return
}
w.Header().Set("Access-Control-Allow-Origin", origin)
// Per spec, credentials can't be used with wildcard origin.
if origin != "*" {
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
if cfg.exposeHeaders != "" {
w.Header().Set("Access-Control-Expose-Headers", cfg.exposeHeaders)
}
if cfg.methods != "" {
w.Header().Set("Access-Control-Allow-Methods", cfg.methods)
}
if cfg.headers != "" {
w.Header().Set("Access-Control-Allow-Headers", cfg.headers)
}
}