thread-master/apps/backend/internal/middleware/cors.go

89 lines
2.5 KiB
Go

package middleware
import (
"encoding/json"
"net/http"
"strings"
)
// CorsMiddleware — browser CORS for Harbor Desk FE.
// Note: cannot combine Access-Control-Allow-Origin: * with Allow-Credentials: true
// (browsers reject). FE uses Bearer token (no cookies), so no credentials header.
type CorsMiddleware struct {
allowedOrigins map[string]struct{}
}
func NewCorsMiddleware(origins []string) *CorsMiddleware {
allowed := make(map[string]struct{}, len(origins))
for _, origin := range origins {
origin = strings.TrimRight(strings.TrimSpace(origin), "/")
if origin != "" {
allowed[origin] = struct{}{}
}
}
return &CorsMiddleware{allowedOrigins: allowed}
}
func (m *CorsMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !m.setHeader(w, r) {
writeOriginDenied(w)
return
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next(w, r)
}
}
func (m *CorsMiddleware) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !m.setHeader(w, r) {
writeOriginDenied(w)
return
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
w.WriteHeader(http.StatusNotFound)
})
}
func writeOriginDenied(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
_ = json.NewEncoder(w).Encode(map[string]any{
"code": 403003, "message": "origin is not allowed", "data": nil, "error": nil,
})
}
func (m *CorsMiddleware) setHeader(w http.ResponseWriter, r *http.Request) bool {
origin := strings.TrimRight(strings.TrimSpace(r.Header.Get("Origin")), "/")
if origin != "" {
if _, ok := m.allowedOrigins[origin]; !ok {
return false
}
w.Header().Set("Access-Control-Allow-Origin", origin)
appendVary(w, "Origin")
}
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-Id, device_id")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
w.Header().Set("Access-Control-Max-Age", "600")
w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
return true
}
func appendVary(w http.ResponseWriter, value string) {
for _, existing := range w.Header().Values("Vary") {
for _, part := range strings.Split(existing, ",") {
if strings.EqualFold(strings.TrimSpace(part), value) {
return
}
}
}
w.Header().Add("Vary", value)
}