From b7113f8425b17fc689a617834a7e6083e3193572 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 26 Apr 2026 05:12:23 +0300 Subject: [PATCH] fasthttp: chunked body fixes --- vlib/fasthttp/fasthttp_bsd.c.v | 69 ++----------- vlib/fasthttp/request_parser.v | 145 ++++++++++++++++++++++++---- vlib/fasthttp/request_parser_test.v | 12 +++ 3 files changed, 145 insertions(+), 81 deletions(-) diff --git a/vlib/fasthttp/fasthttp_bsd.c.v b/vlib/fasthttp/fasthttp_bsd.c.v index e31c90fa3..3466c3f75 100644 --- a/vlib/fasthttp/fasthttp_bsd.c.v +++ b/vlib/fasthttp/fasthttp_bsd.c.v @@ -261,34 +261,6 @@ fn send_request_timeout(fd int) { C.send(fd, status_408_response.data, status_408_response.len, send_flags) } -// chunked_body_complete checks whether the combined read_buf + read_extra -// data ends with the chunked transfer encoding terminator \r\n0\r\n\r\n. -@[direct_array_access] -fn chunked_body_complete(c &Conn) bool { - terminator := [u8(`\r`), `\n`, `0`, `\r`, `\n`, `\r`, `\n`] - total := c.read_len + c.read_extra.len - if total < 7 { - return false - } - // Get the last 7 bytes from the combined data - mut tail := [7]u8{} - start := total - 7 - for i := 0; i < 7; i++ { - pos := start + i - if pos < c.read_len { - tail[i] = c.read_buf[pos] - } else { - tail[i] = c.read_extra[pos - c.read_len] - } - } - for i := 0; i < 7; i++ { - if tail[i] != terminator[i] { - return false - } - } - return true -} - fn handle_write(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { if send_pending(c_ptr) { return @@ -463,8 +435,9 @@ fn handle_read(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr // Enforce the configured header limit without capping large request bodies. mut header_end := -1 + mut full_data := []u8{} if c.read_extra.len > 0 { - full_data := c.get_full_request_data() + full_data = c.get_full_request_data() header_end = find_header_end_in_buf(full_data.data, full_data.len) } else { 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 // Check if the full body has been received. if c.read_extra.len > 0 { - // Large request spilling into dynamic buffer. - // Headers are in read_buf; check for chunked encoding. - if has_chunked_transfer_encoding_in_buf(&c.read_buf[0], if c.read_len < buf_size { - c.read_len - } else { - buf_size - }) - { - // For chunked, check if the tail of the combined data ends with - // the terminator \r\n0\r\n\r\n (7 bytes). The terminator could - // span the boundary between read_buf and read_extra. - if !chunked_body_complete(c) { - elapsed_ns := time.sys_mono_now() - c.read_start - timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000 - if elapsed_ns >= timeout_ns { - send_request_timeout(c.fd) - close_conn(server, kq, c_ptr, mut clients) - } - return - } - } else { - // Non-chunked large requests spill into the dynamic overflow buffer too. - full_data := c.get_full_request_data() - if !has_complete_body(full_data.data, full_data.len) { - elapsed_ns := time.sys_mono_now() - c.read_start - timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000 - if elapsed_ns >= timeout_ns { - send_request_timeout(c.fd) - close_conn(server, kq, c_ptr, mut clients) - } - return + if !has_complete_body(full_data.data, full_data.len) { + elapsed_ns := time.sys_mono_now() - c.read_start + timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000 + if elapsed_ns >= timeout_ns { + send_request_timeout(c.fd) + close_conn(server, kq, c_ptr, mut clients) } + return } } else if !has_complete_body(&c.read_buf[0], c.read_len) { // Body not complete yet - check for timeout diff --git a/vlib/fasthttp/request_parser.v b/vlib/fasthttp/request_parser.v index d5bb80352..c63fbd7d0 100644 --- a/vlib/fasthttp/request_parser.v +++ b/vlib/fasthttp/request_parser.v @@ -172,7 +172,7 @@ fn find_header_end_in_buf(buf &u8, buf_len int) int { // - there is no Content-Length header and no chunked encoding (body not expected) // - Content-Length is 0 // - enough body bytes have been received -// - chunked encoding is complete (0\r\n\r\n terminator found) +// - chunked encoding is complete (the zero-size chunk and trailers were parsed) // Returns false only when more body data is expected. @[direct_array_access] fn has_complete_body(buf &u8, buf_len int) bool { @@ -182,26 +182,7 @@ fn has_complete_body(buf &u8, buf_len int) bool { } // Check for Transfer-Encoding: chunked header (case-insensitive) if has_chunked_transfer_encoding_in_buf(buf, header_end) { - // For chunked encoding, look for the terminating chunk: "\r\n0\r\n\r\n" - // (preceding chunk delimiter + zero-size chunk + empty trailer section) - // Also check for "0\r\n\r\n" right at the body start (degenerate empty-body case) - unsafe { - if buf_len >= header_end + 5 && buf[header_end] == `0` && buf[header_end + 1] == `\r` - && buf[header_end + 2] == `\n` && buf[header_end + 3] == `\r` - && buf[header_end + 4] == `\n` { - return true - } - if buf_len >= header_end + 7 { - for i := header_end; i <= buf_len - 7; i++ { - if buf[i] == `\r` && buf[i + 1] == `\n` && buf[i + 2] == `0` - && buf[i + 3] == `\r` && buf[i + 4] == `\n` && buf[i + 5] == `\r` - && buf[i + 6] == `\n` { - return true - } - } - } - } - return false + return has_complete_chunked_body(buf, buf_len, header_end) } content_length := parse_content_length_from_buf(buf, header_end) if content_length <= 0 { @@ -211,6 +192,128 @@ fn has_complete_body(buf &u8, buf_len int) bool { return body_received >= content_length } +@[direct_array_access] +fn has_complete_chunked_body(buf &u8, buf_len int, body_start int) bool { + mut pos := body_start + for { + lf_pos := find_line_lf_in_buf(buf, buf_len, pos) + if lf_pos < 0 { + return false + } + mut line_end := lf_pos + unsafe { + if line_end > pos && buf[line_end - 1] == `\r` { + line_end-- + } + } + mut size_end := line_end + for i := pos; i < line_end; i++ { + unsafe { + if buf[i] == `;` { + size_end = i + break + } + } + } + mut size_start := pos + for size_start < size_end { + unsafe { + if buf[size_start] != ` ` && buf[size_start] != `\t` { + break + } + } + size_start++ + } + for size_end > size_start { + unsafe { + if buf[size_end - 1] != ` ` && buf[size_end - 1] != `\t` { + break + } + } + size_end-- + } + if size_start == size_end { + return true + } + mut chunk_size := 0 + for i := size_start; i < size_end; i++ { + digit := chunked_hex_digit_value(unsafe { buf[i] }) + if digit < 0 { + return true + } + if chunk_size > (max_int - digit) / 16 { + return true + } + chunk_size = chunk_size * 16 + digit + } + pos = lf_pos + 1 + if chunk_size == 0 { + return has_complete_chunked_trailers(buf, buf_len, pos) + } + if chunk_size > buf_len - pos { + return false + } + data_end := pos + chunk_size + if data_end + 2 > buf_len { + return false + } + unsafe { + if buf[data_end] != `\r` || buf[data_end + 1] != `\n` { + return true + } + } + pos = data_end + 2 + } + return false +} + +@[direct_array_access] +fn has_complete_chunked_trailers(buf &u8, buf_len int, start int) bool { + mut pos := start + for { + lf_pos := find_line_lf_in_buf(buf, buf_len, pos) + if lf_pos < 0 { + return false + } + mut line_end := lf_pos + unsafe { + if line_end > pos && buf[line_end - 1] == `\r` { + line_end-- + } + } + if line_end == pos { + return true + } + pos = lf_pos + 1 + } + return false +} + +@[direct_array_access] +fn find_line_lf_in_buf(buf &u8, buf_len int, start int) int { + for i := start; i < buf_len; i++ { + unsafe { + if buf[i] == `\n` { + return i + } + } + } + return -1 +} + +fn chunked_hex_digit_value(ch u8) int { + if ch >= `0` && ch <= `9` { + return int(ch - `0`) + } + if ch >= `a` && ch <= `f` { + return int(ch - `a` + 10) + } + if ch >= `A` && ch <= `F` { + return int(ch - `A` + 10) + } + return -1 +} + // has_chunked_transfer_encoding_in_buf scans the header bytes for a // "Transfer-Encoding:" header whose value contains "chunked" (case-insensitive). @[direct_array_access] diff --git a/vlib/fasthttp/request_parser_test.v b/vlib/fasthttp/request_parser_test.v index 7ef48a9b6..1c604770a 100644 --- a/vlib/fasthttp/request_parser_test.v +++ b/vlib/fasthttp/request_parser_test.v @@ -113,3 +113,15 @@ fn test_has_complete_body_with_complete_chunked_body() { '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() assert has_complete_body(buffer.data, buffer.len) } + +fn test_has_complete_body_with_incomplete_chunk_data_containing_terminator_bytes() { + buffer := + '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() + assert !has_complete_body(buffer.data, buffer.len) +} + +fn test_has_complete_body_with_complete_chunk_data_containing_terminator_bytes() { + buffer := + '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() + assert has_complete_body(buffer.data, buffer.len) +} -- 2.39.5