Skip to content

Commit b7113f8

Browse files
committed
fasthttp: chunked body fixes
1 parent 625f638 commit b7113f8

3 files changed

Lines changed: 145 additions & 81 deletions

File tree

vlib/fasthttp/fasthttp_bsd.c.v

Lines changed: 9 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -261,34 +261,6 @@ fn send_request_timeout(fd int) {
261261
C.send(fd, status_408_response.data, status_408_response.len, send_flags)
262262
}
263263

264-
// chunked_body_complete checks whether the combined read_buf + read_extra
265-
// data ends with the chunked transfer encoding terminator \r\n0\r\n\r\n.
266-
@[direct_array_access]
267-
fn chunked_body_complete(c &Conn) bool {
268-
terminator := [u8(`\r`), `\n`, `0`, `\r`, `\n`, `\r`, `\n`]
269-
total := c.read_len + c.read_extra.len
270-
if total < 7 {
271-
return false
272-
}
273-
// Get the last 7 bytes from the combined data
274-
mut tail := [7]u8{}
275-
start := total - 7
276-
for i := 0; i < 7; i++ {
277-
pos := start + i
278-
if pos < c.read_len {
279-
tail[i] = c.read_buf[pos]
280-
} else {
281-
tail[i] = c.read_extra[pos - c.read_len]
282-
}
283-
}
284-
for i := 0; i < 7; i++ {
285-
if tail[i] != terminator[i] {
286-
return false
287-
}
288-
}
289-
return true
290-
}
291-
292264
fn handle_write(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) {
293265
if send_pending(c_ptr) {
294266
return
@@ -463,8 +435,9 @@ fn handle_read(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr
463435

464436
// Enforce the configured header limit without capping large request bodies.
465437
mut header_end := -1
438+
mut full_data := []u8{}
466439
if c.read_extra.len > 0 {
467-
full_data := c.get_full_request_data()
440+
full_data = c.get_full_request_data()
468441
header_end = find_header_end_in_buf(full_data.data, full_data.len)
469442
} else {
470443
header_end = find_header_end_in_buf(&c.read_buf[0], c.read_len)
@@ -483,38 +456,14 @@ fn handle_read(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr
483456

484457
// Check if the full body has been received.
485458
if c.read_extra.len > 0 {
486-
// Large request spilling into dynamic buffer.
487-
// Headers are in read_buf; check for chunked encoding.
488-
if has_chunked_transfer_encoding_in_buf(&c.read_buf[0], if c.read_len < buf_size {
489-
c.read_len
490-
} else {
491-
buf_size
492-
})
493-
{
494-
// For chunked, check if the tail of the combined data ends with
495-
// the terminator \r\n0\r\n\r\n (7 bytes). The terminator could
496-
// span the boundary between read_buf and read_extra.
497-
if !chunked_body_complete(c) {
498-
elapsed_ns := time.sys_mono_now() - c.read_start
499-
timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000
500-
if elapsed_ns >= timeout_ns {
501-
send_request_timeout(c.fd)
502-
close_conn(server, kq, c_ptr, mut clients)
503-
}
504-
return
505-
}
506-
} else {
507-
// Non-chunked large requests spill into the dynamic overflow buffer too.
508-
full_data := c.get_full_request_data()
509-
if !has_complete_body(full_data.data, full_data.len) {
510-
elapsed_ns := time.sys_mono_now() - c.read_start
511-
timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000
512-
if elapsed_ns >= timeout_ns {
513-
send_request_timeout(c.fd)
514-
close_conn(server, kq, c_ptr, mut clients)
515-
}
516-
return
459+
if !has_complete_body(full_data.data, full_data.len) {
460+
elapsed_ns := time.sys_mono_now() - c.read_start
461+
timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000
462+
if elapsed_ns >= timeout_ns {
463+
send_request_timeout(c.fd)
464+
close_conn(server, kq, c_ptr, mut clients)
517465
}
466+
return
518467
}
519468
} else if !has_complete_body(&c.read_buf[0], c.read_len) {
520469
// Body not complete yet - check for timeout

vlib/fasthttp/request_parser.v

Lines changed: 124 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ fn find_header_end_in_buf(buf &u8, buf_len int) int {
172172
// - there is no Content-Length header and no chunked encoding (body not expected)
173173
// - Content-Length is 0
174174
// - enough body bytes have been received
175-
// - chunked encoding is complete (0\r\n\r\n terminator found)
175+
// - chunked encoding is complete (the zero-size chunk and trailers were parsed)
176176
// Returns false only when more body data is expected.
177177
@[direct_array_access]
178178
fn has_complete_body(buf &u8, buf_len int) bool {
@@ -182,26 +182,7 @@ fn has_complete_body(buf &u8, buf_len int) bool {
182182
}
183183
// Check for Transfer-Encoding: chunked header (case-insensitive)
184184
if has_chunked_transfer_encoding_in_buf(buf, header_end) {
185-
// For chunked encoding, look for the terminating chunk: "\r\n0\r\n\r\n"
186-
// (preceding chunk delimiter + zero-size chunk + empty trailer section)
187-
// Also check for "0\r\n\r\n" right at the body start (degenerate empty-body case)
188-
unsafe {
189-
if buf_len >= header_end + 5 && buf[header_end] == `0` && buf[header_end + 1] == `\r`
190-
&& buf[header_end + 2] == `\n` && buf[header_end + 3] == `\r`
191-
&& buf[header_end + 4] == `\n` {
192-
return true
193-
}
194-
if buf_len >= header_end + 7 {
195-
for i := header_end; i <= buf_len - 7; i++ {
196-
if buf[i] == `\r` && buf[i + 1] == `\n` && buf[i + 2] == `0`
197-
&& buf[i + 3] == `\r` && buf[i + 4] == `\n` && buf[i + 5] == `\r`
198-
&& buf[i + 6] == `\n` {
199-
return true
200-
}
201-
}
202-
}
203-
}
204-
return false
185+
return has_complete_chunked_body(buf, buf_len, header_end)
205186
}
206187
content_length := parse_content_length_from_buf(buf, header_end)
207188
if content_length <= 0 {
@@ -211,6 +192,128 @@ fn has_complete_body(buf &u8, buf_len int) bool {
211192
return body_received >= content_length
212193
}
213194

195+
@[direct_array_access]
196+
fn has_complete_chunked_body(buf &u8, buf_len int, body_start int) bool {
197+
mut pos := body_start
198+
for {
199+
lf_pos := find_line_lf_in_buf(buf, buf_len, pos)
200+
if lf_pos < 0 {
201+
return false
202+
}
203+
mut line_end := lf_pos
204+
unsafe {
205+
if line_end > pos && buf[line_end - 1] == `\r` {
206+
line_end--
207+
}
208+
}
209+
mut size_end := line_end
210+
for i := pos; i < line_end; i++ {
211+
unsafe {
212+
if buf[i] == `;` {
213+
size_end = i
214+
break
215+
}
216+
}
217+
}
218+
mut size_start := pos
219+
for size_start < size_end {
220+
unsafe {
221+
if buf[size_start] != ` ` && buf[size_start] != `\t` {
222+
break
223+
}
224+
}
225+
size_start++
226+
}
227+
for size_end > size_start {
228+
unsafe {
229+
if buf[size_end - 1] != ` ` && buf[size_end - 1] != `\t` {
230+
break
231+
}
232+
}
233+
size_end--
234+
}
235+
if size_start == size_end {
236+
return true
237+
}
238+
mut chunk_size := 0
239+
for i := size_start; i < size_end; i++ {
240+
digit := chunked_hex_digit_value(unsafe { buf[i] })
241+
if digit < 0 {
242+
return true
243+
}
244+
if chunk_size > (max_int - digit) / 16 {
245+
return true
246+
}
247+
chunk_size = chunk_size * 16 + digit
248+
}
249+
pos = lf_pos + 1
250+
if chunk_size == 0 {
251+
return has_complete_chunked_trailers(buf, buf_len, pos)
252+
}
253+
if chunk_size > buf_len - pos {
254+
return false
255+
}
256+
data_end := pos + chunk_size
257+
if data_end + 2 > buf_len {
258+
return false
259+
}
260+
unsafe {
261+
if buf[data_end] != `\r` || buf[data_end + 1] != `\n` {
262+
return true
263+
}
264+
}
265+
pos = data_end + 2
266+
}
267+
return false
268+
}
269+
270+
@[direct_array_access]
271+
fn has_complete_chunked_trailers(buf &u8, buf_len int, start int) bool {
272+
mut pos := start
273+
for {
274+
lf_pos := find_line_lf_in_buf(buf, buf_len, pos)
275+
if lf_pos < 0 {
276+
return false
277+
}
278+
mut line_end := lf_pos
279+
unsafe {
280+
if line_end > pos && buf[line_end - 1] == `\r` {
281+
line_end--
282+
}
283+
}
284+
if line_end == pos {
285+
return true
286+
}
287+
pos = lf_pos + 1
288+
}
289+
return false
290+
}
291+
292+
@[direct_array_access]
293+
fn find_line_lf_in_buf(buf &u8, buf_len int, start int) int {
294+
for i := start; i < buf_len; i++ {
295+
unsafe {
296+
if buf[i] == `\n` {
297+
return i
298+
}
299+
}
300+
}
301+
return -1
302+
}
303+
304+
fn chunked_hex_digit_value(ch u8) int {
305+
if ch >= `0` && ch <= `9` {
306+
return int(ch - `0`)
307+
}
308+
if ch >= `a` && ch <= `f` {
309+
return int(ch - `a` + 10)
310+
}
311+
if ch >= `A` && ch <= `F` {
312+
return int(ch - `A` + 10)
313+
}
314+
return -1
315+
}
316+
214317
// has_chunked_transfer_encoding_in_buf scans the header bytes for a
215318
// "Transfer-Encoding:" header whose value contains "chunked" (case-insensitive).
216319
@[direct_array_access]

vlib/fasthttp/request_parser_test.v

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,15 @@ fn test_has_complete_body_with_complete_chunked_body() {
113113
'POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n'.bytes()
114114
assert has_complete_body(buffer.data, buffer.len)
115115
}
116+
117+
fn test_has_complete_body_with_incomplete_chunk_data_containing_terminator_bytes() {
118+
buffer :=
119+
'POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n20\r\nabc\r\n0\r\n\r\n'.bytes()
120+
assert !has_complete_body(buffer.data, buffer.len)
121+
}
122+
123+
fn test_has_complete_body_with_complete_chunk_data_containing_terminator_bytes() {
124+
buffer :=
125+
'POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nabc\r\n0\r\n\r\ndef\r\n0\r\n\r\n'.bytes()
126+
assert has_complete_body(buffer.data, buffer.len)
127+
}

0 commit comments

Comments
 (0)