Skip to content

Commit 6a3f12d

Browse files
veb: fix handling of default CorsOptions.allowed_headers (#24703)
1 parent 99c39ab commit 6a3f12d

2 files changed

Lines changed: 168 additions & 11 deletions

File tree

‎vlib/veb/middleware.v‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ interface HasBeforeRequest {
177177
}
178178

179179
pub const cors_safelisted_response_headers = [http.CommonHeader.cache_control, .content_language,
180-
.content_length, .content_type, .expires, .last_modified, .pragma].map(it.str())
180+
.content_length, .content_type, .expires, .last_modified, .pragma].map(it.str()).join(',')
181181

182182
// CorsOptions is used to set CORS response headers.
183183
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#the_http_response_headers
@@ -190,7 +190,7 @@ pub:
190190
// ;`Access-Control-Allow-Credentials`
191191
allow_credentials bool
192192
// allowed HTTP headers for a cross-origin request; `Access-Control-Allow-Headers`
193-
allowed_headers []string = ['*']
193+
allowed_headers []string
194194
// allowed HTTP methods for a cross-origin request; `Access-Control-Allow-Methods`
195195
allowed_methods []http.Method
196196
// indicate if clients are able to access other headers than the "CORS-safelisted"
@@ -225,7 +225,7 @@ pub fn (options &CorsOptions) set_headers(mut ctx Context) {
225225
} else if _ := ctx.req.header.get(.access_control_request_headers) {
226226
// a server must respond with `Access-Control-Allow-Headers` if
227227
// `Access-Control-Request-Headers` is present in a preflight request
228-
ctx.set_header(.access_control_allow_headers, cors_safelisted_response_headers.join(','))
228+
ctx.set_header(.access_control_allow_headers, cors_safelisted_response_headers)
229229
}
230230

231231
if options.allowed_methods.len > 0 {
@@ -260,6 +260,10 @@ pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
260260
ctx.set_header(.access_control_allow_origin, origin)
261261
ctx.set_header(.vary, 'Origin, Access-Control-Request-Headers')
262262

263+
if options.allow_credentials {
264+
ctx.set_header(.access_control_allow_credentials, 'true')
265+
}
266+
263267
// validate request method
264268
if ctx.req.method !in options.allowed_methods {
265269
ctx.res.set_status(.method_not_allowed)
@@ -305,18 +309,12 @@ pub fn (options &CorsOptions) validate_request(mut ctx Context) bool {
305309
pub fn cors[T](options CorsOptions) MiddlewareOptions[T] {
306310
return MiddlewareOptions[T]{
307311
handler: fn [options] [T](mut ctx T) bool {
308-
if ctx.req.method == .options {
309-
// preflight request
312+
if ctx.req.method == .options { // preflight
310313
options.set_headers(mut ctx.Context)
311314
ctx.text('ok')
312315
return false
313316
} else {
314-
// check if there is a cross-origin request
315-
if options.validate_request(mut ctx.Context) == false {
316-
return false
317-
}
318-
// no cross-origin request / valid cross-origin request
319-
return true
317+
return options.validate_request(mut ctx.Context)
320318
}
321319
}
322320
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import veb
2+
import net.http
3+
import time
4+
import os
5+
6+
const base_port = 13013
7+
const exit_after = time.second * 10
8+
const allowed_origin = 'https://vlang.io'
9+
10+
fn get_port_and_url(test_number int) (int, string) {
11+
p := base_port + test_number
12+
return p, 'http://localhost:${p}'
13+
}
14+
15+
pub struct Context {
16+
veb.Context
17+
}
18+
19+
pub struct App {
20+
veb.Middleware[Context]
21+
mut:
22+
started chan bool
23+
}
24+
25+
pub fn (mut app App) before_accept_loop() {
26+
app.started <- true
27+
}
28+
29+
pub fn (app &App) index(mut ctx Context) veb.Result {
30+
return ctx.text('index')
31+
}
32+
33+
fn setup(port int, o veb.CorsOptions) ! {
34+
os.chdir(os.dir(@FILE))!
35+
go fn () {
36+
time.sleep(exit_after)
37+
assert false, 'timeout reached!'
38+
exit(1)
39+
}()
40+
41+
mut app := &App{}
42+
app.use(veb.cors[Context](o))
43+
44+
go veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2)
45+
// app startup time
46+
_ := <-app.started
47+
}
48+
49+
fn test_no_user_provided_allowed_headers() {
50+
port, localserver := get_port_and_url(1)
51+
setup(port, veb.CorsOptions{
52+
origins: [allowed_origin]
53+
})!
54+
55+
x := http.fetch(http.FetchConfig{
56+
url: localserver
57+
method: http.Method.options
58+
header: http.new_header_from_map({
59+
http.CommonHeader.origin: allowed_origin
60+
})
61+
})!
62+
63+
assert x.status() == http.Status.ok
64+
if header := x.header.get(.access_control_allow_headers) {
65+
assert false, 'Header should not be set'
66+
}
67+
}
68+
69+
fn test_user_provided_allowed_header() {
70+
port, localserver := get_port_and_url(2)
71+
setup(port, veb.CorsOptions{
72+
origins: [allowed_origin]
73+
allowed_headers: ['content-type']
74+
})!
75+
76+
x := http.fetch(http.FetchConfig{
77+
url: localserver
78+
method: http.Method.options
79+
header: http.new_header_from_map({
80+
http.CommonHeader.origin: allowed_origin
81+
})
82+
})!
83+
84+
assert x.status() == http.Status.ok
85+
if header := x.header.get(.access_control_allow_headers) {
86+
assert header == 'content-type'
87+
} else {
88+
assert false, 'Header not set'
89+
}
90+
}
91+
92+
fn test_user_provided_allowed_header_wildcard() {
93+
port, localserver := get_port_and_url(3)
94+
setup(port, veb.CorsOptions{
95+
origins: [allowed_origin]
96+
allowed_headers: ['*']
97+
})!
98+
99+
x := http.fetch(http.FetchConfig{
100+
url: localserver
101+
method: http.Method.options
102+
header: http.new_header_from_map({
103+
http.CommonHeader.origin: allowed_origin
104+
})
105+
})!
106+
107+
assert x.status() == http.Status.ok
108+
if header := x.header.get(.access_control_allow_headers) {
109+
assert header == '*'
110+
} else {
111+
assert false, 'Header not set'
112+
}
113+
}
114+
115+
fn test_request_has_access_control_request_headers() {
116+
port, localserver := get_port_and_url(4)
117+
setup(port, veb.CorsOptions{
118+
origins: [allowed_origin]
119+
})!
120+
121+
x := http.fetch(http.FetchConfig{
122+
url: localserver
123+
method: http.Method.options
124+
header: http.new_header_from_map({
125+
http.CommonHeader.origin: allowed_origin
126+
http.CommonHeader.access_control_request_headers: 'any-value'
127+
})
128+
})!
129+
130+
assert x.status() == http.Status.ok
131+
if header := x.header.get(http.CommonHeader.access_control_allow_headers) {
132+
assert header == veb.cors_safelisted_response_headers
133+
} else {
134+
assert false, 'Header not set'
135+
}
136+
}
137+
138+
fn test_allow_credentials_non_preflight() {
139+
port, localserver := get_port_and_url(5)
140+
setup(port, veb.CorsOptions{
141+
origins: [allowed_origin]
142+
allowed_methods: [http.Method.get]
143+
allow_credentials: true
144+
})!
145+
146+
x := http.fetch(http.FetchConfig{
147+
url: localserver
148+
header: http.new_header_from_map({
149+
http.CommonHeader.origin: allowed_origin
150+
})
151+
})!
152+
153+
assert x.status() == http.Status.ok
154+
if header := x.header.get(http.CommonHeader.access_control_allow_credentials) {
155+
assert header == 'true'
156+
} else {
157+
assert false, 'Header not set'
158+
}
159+
}

0 commit comments

Comments
 (0)