-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcorset.go
305 lines (256 loc) · 9.41 KB
/
corset.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package corset
import (
"net/http"
"strconv"
"strings"
)
const (
// Set by server and specifies the allowed origin. Must be a single value, or a wildcard for allow all origins
allowOriginsHeader = "Access-Control-Allow-Origin"
// Set by server and specifies the allowed methods May be multiple values
allowMethodsHeader = "Access-Control-Allow-Methods"
// Set by server and specifies the allowed headers May be multiple values
allowHeadersHeader = "Access-Control-Allow-Headers"
// Set by server and specifies whether the client may send credentials. The client may still send credentials if the request was
// not preceded by a Preflight and the client specified `withCredentials`
allowCredentialsHeader = "Access-Control-Allow-Credentials"
// Set by server and specifies which non-simple response headers may be visible to the client
exposeHeadersHeader = "Access-Control-Expose-Headers"
// Set by server and specifies how long, in seconds, a response can stay in the browser's cache before another Preflight is made
maxAgeHeader = "Access-Control-Max-Age"
// Sent via Preflight when the client is using a non-simple HTTP method
requestMethodHeader = "Access-Control-Request-Method"
// Sent via Preflight when the client has set additional headers. May be multiple values
requestHeadersHeader = "Access-Control-Request-Headers"
// Specifies the origin of the request or response
originHeader = "Origin"
// Set by server and tells proxy servers to take into account the Origin header when deciding whether to send cached content
varyHeader = "Vary"
)
var (
// Default allowed headers. Defaults to the "Origin" header, though this should be included automatically
defaultAllowedHeaders = []string{originHeader}
// Default allowed methods. Defaults to simple methods (those that do not trigger a Preflight)
defaultAllowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead}
)
// CorsetOptions represents configurable options that are available to the consumer
type CorsetOptions struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
AllowCredentials bool
UseOptionsPassthrough bool
MaxAge int
ExposeHeaders []string
}
// Corset represents a Corset middleware object
type Corset struct {
allowedOrigins []string
allowedMethods []string
allowedHeaders []string
allowCredentials bool
maxAge int
exposedHeaders []string
// Set to true when allowed origins contains a "*"
allowAllOrigins bool
// Set to true when allowed headers contains a "*"
allowAllHeaders bool
useOptionsPassthrough bool
}
// NewCorset initializes a new Corset middleware object
func NewCorset(opts CorsetOptions) *Corset {
c := &Corset{
allowCredentials: opts.AllowCredentials,
maxAge: opts.MaxAge,
exposedHeaders: opts.ExposeHeaders,
useOptionsPassthrough: opts.UseOptionsPassthrough,
}
// Register origins: if no given origins, default to allow all e.g. "*"
if len(opts.AllowedOrigins) == 0 {
c.allowAllOrigins = true
} else {
// For each origin, convert to lowercase and append
for _, origin := range opts.AllowedOrigins {
origin := strings.ToLower(origin)
// If wildcard origin, override and set to allow all e.g. "*"
if origin == "*" {
c.allowAllOrigins = true
break
}
c.allowedOrigins = append(c.allowedOrigins, origin)
}
// Append "null" to allow list to support testing / requests from files, redirects, etc
// Note: Used for redirects because the browser should not expose the origin of the new server; redirects are followed automatically
c.allowedOrigins = append(c.allowedOrigins, "null")
}
// Register headers: if no given headers, default to those allowed per the spec
// Although these headers are allowed by default, we add them anyway for the sake of consistency
if len(opts.AllowedHeaders) == 0 {
c.allowedHeaders = defaultAllowedHeaders
} else {
for _, header := range opts.AllowedHeaders {
header := strings.ToLower(header)
if header == "*" {
c.allowAllHeaders = true
break
}
c.allowedHeaders = append(c.allowedHeaders, http.CanonicalHeaderKey(header))
}
c.allowedHeaders = append(c.allowedHeaders, originHeader)
}
if len(opts.AllowedMethods) == 0 {
c.allowedMethods = defaultAllowedMethods
} else {
for _, method := range opts.AllowedMethods {
c.allowedMethods = append(c.allowedMethods, strings.ToUpper(method))
}
}
return c
}
// handleRequest handles actual HTTP requests subsequent to or standalone from Preflight requests
func (c *Corset) handleRequest(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin := r.Header.Get(originHeader)
// Set the "vary" header to prevent proxy servers from sending cached responses for one client to another
headers.Add(varyHeader, "Origin")
// If no origin was specified, this is not a valid CORS request
if origin == "" {
return
}
// If the origin is not in the allow list, deny
if !c.isOriginAllowed(origin) {
// @todo 403
return
}
if c.allowAllOrigins {
// If all origins are allowed, use the wildcard value
headers.Set(allowOriginsHeader, "*")
} else {
// Otherwise, set the origin to the request origin
headers.Set(allowOriginsHeader, origin)
}
// If we've exposed headers, set them
// If the consumer specified headers that are exposed by default, we'll still include them - this is spec compliant
if len(c.exposedHeaders) > 0 {
headers.Set(exposeHeadersHeader, strings.Join(c.exposedHeaders, ", "))
}
// Allow the client to send credentials. If making an XHR request, the client must set `withCredentials` to `true`
if c.allowCredentials {
headers.Set(allowCredentialsHeader, "true")
}
}
// handlePreflightRequest handles Preflight requests
func (c *Corset) handlePreflightRequest(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin := r.Header.Get(originHeader)
// Set the "vary" header to prevent proxy servers from sending cached responses for one client to another
headers.Add(varyHeader, originHeader)
headers.Add(varyHeader, requestMethodHeader)
headers.Add(varyHeader, requestHeadersHeader)
// If no origin was specified, this is not a valid CORS request
if origin == "" {
return
}
// If the origin is not in the allow list, deny
if !c.isOriginAllowed(origin) {
return
}
// Validate the method; this is the crux of the Preflight
requestMethod := r.Header.Get(requestMethodHeader)
if !c.isMethodAllowed(requestMethod) {
return
}
// Validate request headers. Preflights are also used when requests include additional headers from the client
requestHeaders := deriveHeaders(r)
if !c.areHeadersAllowed(requestHeaders) {
return
}
if c.allowAllOrigins {
// If all origins are allowed, use the wildcard value
headers.Set(allowOriginsHeader, "*")
} else {
// Otherwise, set the origin to the request origin.
headers.Set(allowOriginsHeader, origin)
}
// Set the allowed methods, as a Preflight may have been sent if the client included non-simple methods
headers.Set(allowMethodsHeader, requestMethod)
// Set the allowed headers, as a Preflight may have been sent if the client included non-simple headers
if len(requestHeaders) > 0 {
headers.Set(allowHeadersHeader, strings.Join(c.allowedHeaders, ", "))
}
// Allow the client to send credentials. If making an XHR request, the client must set `withCredentials` to `true`
if c.allowCredentials {
headers.Set(allowCredentialsHeader, "true")
}
// Set the Max Age. This is only necessary for Preflights given the Max Age refers to server-suggested duration,
// in seconds, a response should stay in the browser's cache before another Preflight is made.
if c.maxAge > 0 {
headers.Set(maxAgeHeader, strconv.Itoa(c.maxAge))
}
}
// Handler initializes the Corset middleware and applies the CORS spec, as configured by the consumer, on the request
func (c *Corset) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isPreflightRequest(r) {
c.handlePreflightRequest(w, r)
if c.useOptionsPassthrough {
h.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusNoContent)
}
} else {
c.handleRequest(w, r)
h.ServeHTTP(w, r)
}
})
}
// isOriginAllowed determines whether the given origin is allowed per the user-defined allow list
func (c *Corset) isOriginAllowed(origin string) bool {
if c.allowAllOrigins {
return true
}
origin = strings.ToLower(origin)
for _, allowedOrigin := range c.allowedOrigins {
// @todo regex
if origin == allowedOrigin {
return true
}
}
return false
}
// isMethodAllowed determines whether the given method is allowed per the user-defined allow list
func (c *Corset) isMethodAllowed(method string) bool {
if len(c.allowedMethods) == 0 {
return false
}
method = strings.ToUpper(method)
if method == http.MethodOptions {
return true
}
for _, allowedMethod := range c.allowedMethods {
if method == allowedMethod {
return true
}
}
return false
}
// areHeadersAllowed determines whether the given headers are allowed per the user-defined allow list
func (c *Corset) areHeadersAllowed(headers []string) bool {
if c.allowAllHeaders || len(headers) == 0 {
return true
}
for _, header := range headers {
header = http.CanonicalHeaderKey(header)
allowsHeader := false
for _, allowedHeader := range c.allowedHeaders {
if header == allowedHeader {
allowsHeader = true
break
}
}
if !allowsHeader {
return false
}
}
return true
}