188 lines
5.1 KiB
Go
188 lines
5.1 KiB
Go
package cors
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Policy is immutable after New and safe to share between concurrent requests.
|
|
type Policy struct {
|
|
enabled, wildcard, credentials bool
|
|
origins, methods, headers map[string]struct{}
|
|
allowMethods, allowHeaders, exposeHeaders, maxAge string
|
|
}
|
|
|
|
// New validates and snapshots policy. It performs no network or DNS operations.
|
|
func New(c Config) (*Policy, error) {
|
|
if err := c.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
p := &Policy{enabled: c.Enabled, credentials: c.AllowCredentials}
|
|
if !c.Enabled {
|
|
return p, nil
|
|
}
|
|
p.origins = make(map[string]struct{}, len(c.AllowedOrigins))
|
|
for _, origin := range c.AllowedOrigins {
|
|
if origin == "*" {
|
|
p.wildcard = true
|
|
continue
|
|
}
|
|
normalized, _ := normalizeOrigin(origin)
|
|
p.origins[normalized] = struct{}{}
|
|
}
|
|
methods := c.AllowedMethods
|
|
if len(methods) == 0 {
|
|
methods = []string{http.MethodGet, http.MethodHead, http.MethodPost}
|
|
}
|
|
p.methods, p.allowMethods = compileList(methods, false)
|
|
p.headers, p.allowHeaders = compileList(c.AllowedHeaders, true)
|
|
_, p.exposeHeaders = compileList(c.ExposedHeaders, true)
|
|
if c.MaxAge > 0 {
|
|
p.maxAge = strconv.FormatInt(int64(c.MaxAge/time.Second), 10)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func compileList(values []string, header bool) (map[string]struct{}, string) {
|
|
set := make(map[string]struct{}, len(values))
|
|
list := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
key := value
|
|
if header {
|
|
key = strings.ToLower(value)
|
|
value = http.CanonicalHeaderKey(value)
|
|
}
|
|
if _, exists := set[key]; exists {
|
|
continue
|
|
}
|
|
set[key] = struct{}{}
|
|
list = append(list, value)
|
|
}
|
|
return set, strings.Join(list, ", ")
|
|
}
|
|
|
|
// Wrap installs CORS outside routing/authentication. Only actual CORS preflights
|
|
// are short-circuited. Actual requests always continue through normal handlers;
|
|
// an unlisted Origin receives no permission to read the response in a browser.
|
|
// The original ResponseWriter, Request and body pass through without buffering.
|
|
func (p *Policy) Wrap(next http.Handler) http.Handler {
|
|
if next == nil {
|
|
panic("cors: nil handler")
|
|
}
|
|
if p == nil {
|
|
panic("cors: nil policy")
|
|
}
|
|
if !p.enabled {
|
|
return next
|
|
}
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
h := w.Header()
|
|
vary(h, "Origin")
|
|
origins := r.Header.Values("Origin")
|
|
// An OPTIONS request without Origin or Access-Control-Request-Method is
|
|
// ordinary application traffic, including user-defined OPTIONS handlers.
|
|
requestedMethods := r.Header.Values("Access-Control-Request-Method")
|
|
preflight := r.Method == http.MethodOptions && len(origins) > 0 && len(requestedMethods) > 0
|
|
if preflight {
|
|
vary(h, "Access-Control-Request-Method")
|
|
vary(h, "Access-Control-Request-Headers")
|
|
}
|
|
if len(origins) != 1 || !p.allowsOrigin(origins[0]) {
|
|
if preflight {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if preflight {
|
|
if len(requestedMethods) != 1 || !p.allowsMethod(requestedMethods[0]) || !p.allowsHeaders(r.Header.Values("Access-Control-Request-Headers")) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
p.allowOrigin(h, origins[0])
|
|
h.Set("Access-Control-Allow-Methods", p.allowMethods)
|
|
if p.allowHeaders != "" {
|
|
h.Set("Access-Control-Allow-Headers", p.allowHeaders)
|
|
}
|
|
if p.maxAge != "" {
|
|
h.Set("Access-Control-Max-Age", p.maxAge)
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
// Match the actual method too: this grants response access, not permission
|
|
// to execute a side effect. Authentication/CSRF checks remain in the app.
|
|
if p.allowsMethod(r.Method) {
|
|
p.allowOrigin(h, origins[0])
|
|
if p.exposeHeaders != "" {
|
|
h.Set("Access-Control-Expose-Headers", p.exposeHeaders)
|
|
}
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func (p *Policy) allowsOrigin(origin string) bool {
|
|
if !p.wildcard {
|
|
_, ok := p.origins[origin]
|
|
return ok
|
|
}
|
|
normalized, ok := normalizeOrigin(origin)
|
|
return ok && normalized == origin
|
|
}
|
|
func (p *Policy) allowsMethod(method string) bool { _, ok := p.methods[method]; return ok }
|
|
func (p *Policy) allowOrigin(h http.Header, origin string) {
|
|
if p.wildcard {
|
|
origin = "*"
|
|
}
|
|
h.Set("Access-Control-Allow-Origin", origin)
|
|
if p.credentials {
|
|
h.Set("Access-Control-Allow-Credentials", "true")
|
|
}
|
|
}
|
|
func (p *Policy) allowsHeaders(lines []string) bool {
|
|
count, size := 0, 0
|
|
for _, line := range lines {
|
|
size += len(line)
|
|
if size > 8192 {
|
|
return false
|
|
}
|
|
// Parse a header list without allocating an attacker-sized split slice.
|
|
for {
|
|
name, rest, more := strings.Cut(line, ",")
|
|
name = strings.Trim(name, " \t")
|
|
count++
|
|
if count > 128 || !token(name) {
|
|
return false
|
|
}
|
|
if _, ok := p.headers[strings.ToLower(name)]; !ok {
|
|
return false
|
|
}
|
|
if !more {
|
|
break
|
|
}
|
|
line = rest
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func vary(h http.Header, name string) {
|
|
for _, line := range h.Values("Vary") {
|
|
for {
|
|
v, rest, more := strings.Cut(line, ",")
|
|
v = strings.TrimSpace(v)
|
|
if v == "*" || strings.EqualFold(v, name) {
|
|
return
|
|
}
|
|
if !more {
|
|
break
|
|
}
|
|
line = rest
|
|
}
|
|
}
|
|
h.Add("Vary", name)
|
|
}
|