Skip to content

Commit bfb9b65

Browse files
authored
net.http,veb: fix detection of the headers/body boundary in parse_request_head_str (fix #26091) (#26112)
1 parent 8a39f8c commit bfb9b65

File tree

3 files changed

+64
-17
lines changed

3 files changed

+64
-17
lines changed

vlib/net/http/request.v

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -435,34 +435,41 @@ pub fn parse_request_head(mut reader io.BufferedReader) !Request {
435435

436436
// parse_request_head parses *only* the header of a raw HTTP request into a Request object
437437
pub fn parse_request_head_str(s string) !Request {
438-
// TODO called by veb twice!?
439-
pos0 := s.index('\n') or { 0 }
440-
lines := s.split('\n')
438+
pos0 := s.index('\n') or { return error('malformed request: no request line found') }
441439
line0 := s[..pos0].trim_space()
442-
443440
method, target, version := parse_request_line(line0)!
444441

445442
// headers
446443
mut header := new_header()
447-
for i := 1; i < lines.len; i++ {
448-
mut line := lines[i].trim_right('\r')
444+
// split by newline and skip the first line (request line)
445+
lines := s[pos0 + 1..].split('\n')
446+
447+
for line_raw in lines {
448+
line := line_raw.trim_right('\r')
449+
450+
// IMPORTANT: HTTP headers end at the first empty line.
451+
// If we hit this, we are now at the body, so we stop parsing headers.
452+
if line == '' {
453+
break
454+
}
455+
449456
if !line.contains(':') {
450457
continue
451458
}
452-
// key, value := parse_header(line)!
459+
453460
mut pos := parse_header_fast(line)!
454461
key := line.substr_unsafe(0, pos)
455-
for pos < line.len - 1 && line[pos + 1].is_space() {
456-
// Skip space or tab in value name
457-
pos++
462+
463+
// Skip space or tab after the colon
464+
mut val_start := pos + 1
465+
for val_start < line.len && line[val_start].is_space() {
466+
val_start++
458467
}
459-
if pos + 1 < line.len {
460-
value := line.substr_unsafe(pos + 1, line.len)
461-
_, _ = key, value
462-
// println('key,value=${key},${value}')
468+
469+
if val_start < line.len {
470+
value := line.substr_unsafe(val_start, line.len)
463471
header.add_custom(key, value)!
464472
}
465-
// header.coerce(canonicalize: true)
466473
}
467474

468475
mut request_cookies := map[string]string{}
@@ -480,6 +487,20 @@ pub fn parse_request_head_str(s string) !Request {
480487
}
481488
}
482489

490+
// parse_request_str parses a raw HTTP request string into a Request object.
491+
pub fn parse_request_str(s string) !Request {
492+
mut request := parse_request_head_str(s)!
493+
494+
delim := '\r\n\r\n'
495+
body_pos := s.index(delim) or { -1 }
496+
497+
if body_pos != -1 {
498+
request.data = s[body_pos + delim.len..]
499+
}
500+
501+
return request
502+
}
503+
483504
fn parse_request_line(line string) !(Method, urllib.URL, Version) {
484505
// println('S=${s}')
485506
words := line.split(' ')

vlib/net/http/request_test.v

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,32 @@ fn test_parse_request_head_str_post_with_headers() {
250250
assert req.header.custom_values('Content-Type') == ['application/json']
251251
}
252252

253+
fn test_parse_request_head_str_post_with_headers_and_body() {
254+
s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}'
255+
req := http.parse_request_head_str(s) or {
256+
assert false, 'did not parse: ${err}'
257+
return
258+
}
259+
assert req.method == .post
260+
assert req.url == '/index'
261+
assert req.version == .v1_1
262+
assert req.host == 'localhost:9008'
263+
assert req.header.custom_values('User-Agent') == ['curl/7.68.0']
264+
assert req.header.custom_values('Accept') == ['*/*']
265+
assert req.header.custom_values('Content-Type') == ['application/json']
266+
assert req.header.custom_values('Connection') == ['keep-alive']
267+
assert req.data == ''
268+
}
269+
270+
fn test_parse_request_head_post_with_headers_and_body() {
271+
s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}'
272+
req := http.parse_request_str(s) or {
273+
assert false, 'did not parse: ${err}'
274+
return
275+
}
276+
assert req.data == '{"username": "test"}'
277+
}
278+
253279
fn test_parse_request_head_str_with_spaces_in_header_values() {
254280
s := 'GET /path HTTP/1.1\r\nX-Custom-Header: value with spaces\r\n\r\n'
255281
req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') }

vlib/veb/veb_d_new_veb.v

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ fn parallel_request_handler[A, X](req fasthttp.HttpRequest) ![]u8 {
7676
client_fd := req.client_conn_fd
7777

7878
// Parse the raw request bytes into a standard `http.Request`.
79-
req2 := http.parse_request_head_str(s.clone()) or {
79+
req2 := http.parse_request_str(s.clone()) or {
8080
eprintln('[veb] Failed to parse request: ${err}')
8181
println('s=')
8282
println(s)
8383
println('==============')
84-
return http_ok_response // tiny_bad_request_response
84+
return 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
8585
}
8686
// Create and populate the `veb.Context`.
8787
completed_context := handle_request_and_route[A, X](mut global_app, req2, client_fd,

0 commit comments

Comments
 (0)