410 lines
9.6 KiB
V
410 lines
9.6 KiB
V
module fasthttp
|
|
|
|
const empty_space = u8(` `)
|
|
const cr_char = u8(0x0d)
|
|
const lf_char = u8(0x0a)
|
|
|
|
// libc memchr is AVX2-accelerated via glibc IFUNC
|
|
@[inline]
|
|
fn find_byte(buf &u8, len int, c u8) int {
|
|
unsafe {
|
|
p := C.memchr(buf, c, len)
|
|
if p == voidptr(nil) {
|
|
return -1
|
|
}
|
|
return int(&u8(p) - buf)
|
|
}
|
|
}
|
|
|
|
// parse_http1_request_line parses the request line of an HTTP/1.1 request.
|
|
// spec: https://datatracker.ietf.org/doc/rfc9112/
|
|
// request-line is the start-line for for requests
|
|
// According to RFC 9112, the request line is structured as:
|
|
// `request-line = method SP request-target SP HTTP-version`
|
|
// where:
|
|
// METHOD is the HTTP method (e.g., GET, POST)
|
|
// SP is a single space character
|
|
// REQUEST-TARGET is the path or resource being requested
|
|
// HTTP-VERSION is the version of HTTP being used (e.g., HTTP/1.1)
|
|
// CRLF is a carriage return followed by a line feed
|
|
// returns the position after the CRLF on success
|
|
@[direct_array_access]
|
|
pub fn parse_http1_request_line(mut req HttpRequest) !int {
|
|
unsafe {
|
|
buf := &req.buffer[0]
|
|
len := req.buffer.len
|
|
|
|
if len < 12 {
|
|
return error('Too short')
|
|
}
|
|
|
|
// METHOD
|
|
pos1 := find_byte(buf, len, empty_space)
|
|
if pos1 <= 0 {
|
|
return error('Invalid method')
|
|
}
|
|
req.method = Slice{0, pos1}
|
|
|
|
// PATH - skip any extra spaces
|
|
mut pos2 := pos1 + 1
|
|
for pos2 < len && buf[pos2] == empty_space {
|
|
pos2++
|
|
}
|
|
if pos2 >= len {
|
|
return error('Missing path')
|
|
}
|
|
|
|
path_start := pos2
|
|
space_pos := find_byte(buf + pos2, len - pos2, empty_space)
|
|
cr_pos := find_byte(buf + pos2, len - pos2, cr_char)
|
|
|
|
if space_pos < 0 && cr_pos < 0 {
|
|
return error('Invalid request line')
|
|
}
|
|
|
|
// pick earliest delimiter
|
|
mut path_len := 0
|
|
mut delim_pos := 0
|
|
if space_pos >= 0 && (cr_pos < 0 || space_pos < cr_pos) {
|
|
path_len = space_pos
|
|
delim_pos = pos2 + space_pos
|
|
} else {
|
|
path_len = cr_pos
|
|
delim_pos = pos2 + cr_pos
|
|
}
|
|
|
|
req.path = Slice{path_start, path_len}
|
|
|
|
// VERSION
|
|
if buf[delim_pos] == cr_char {
|
|
// No HTTP version specified
|
|
req.version = Slice{delim_pos, 0}
|
|
} else {
|
|
version_start := delim_pos + 1
|
|
cr := find_byte(buf + version_start, len - version_start, cr_char)
|
|
if cr < 0 {
|
|
return error('Invalid HTTP request line: Missing CR')
|
|
}
|
|
req.version = Slice{version_start, cr}
|
|
delim_pos = version_start + cr
|
|
}
|
|
|
|
// Validate CRLF
|
|
if delim_pos + 1 >= len || buf[delim_pos + 1] != lf_char {
|
|
return error('Invalid CRLF')
|
|
}
|
|
|
|
return delim_pos + 2 // Return position after CRLF
|
|
}
|
|
}
|
|
|
|
// decode_http_request parses a raw HTTP request from the given byte buffer
|
|
pub fn decode_http_request(buffer []u8) !HttpRequest {
|
|
mut req := HttpRequest{
|
|
buffer: buffer
|
|
}
|
|
|
|
// header_start is the byte index immediately after the request line's \r\n
|
|
header_start := parse_http1_request_line(mut req)!
|
|
|
|
// Find the end of the header block (\r\n\r\n)
|
|
mut body_start := -1
|
|
for i := header_start; i <= buffer.len - 4; i++ {
|
|
if buffer[i] == cr_char && buffer[i + 1] == lf_char && buffer[i + 2] == cr_char
|
|
&& buffer[i + 3] == lf_char {
|
|
body_start = i + 4
|
|
|
|
// The header fields slice covers everything from header_start
|
|
// up to (but not including) the final double CRLF
|
|
req.header_fields = Slice{
|
|
start: header_start
|
|
len: i - header_start
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if body_start != -1 {
|
|
req.body = Slice{
|
|
start: body_start
|
|
len: buffer.len - body_start
|
|
}
|
|
} else {
|
|
// If no body delimiter found, assume headers go to end or body is missing
|
|
req.header_fields = Slice{header_start, buffer.len - header_start - 2}
|
|
req.body = Slice{0, 0}
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
// Helper function to convert Slice to string for debugging
|
|
fn (slice Slice) to_string(buffer []u8) string {
|
|
if slice.len <= 0 {
|
|
return ''
|
|
}
|
|
return buffer[slice.start..slice.start + slice.len].bytestr()
|
|
}
|
|
|
|
@[direct_array_access]
|
|
fn find_header_end_in_buf(buf &u8, buf_len int) int {
|
|
for i := 0; i < buf_len - 1; i++ {
|
|
unsafe {
|
|
if buf[i] == `\n` {
|
|
if i + 1 < buf_len && buf[i + 1] == `\n` {
|
|
return i + 2
|
|
}
|
|
if i + 2 < buf_len && buf[i + 1] == `\r` && buf[i + 2] == `\n` {
|
|
return i + 3
|
|
}
|
|
}
|
|
if i + 3 < buf_len && buf[i] == `\r` && buf[i + 1] == `\n` && buf[i + 2] == `\r`
|
|
&& buf[i + 3] == `\n` {
|
|
return i + 4
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// has_complete_body checks if a raw HTTP request buffer contains the full body
|
|
// as indicated by the Content-Length or Transfer-Encoding headers. Returns true if:
|
|
// - 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 (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 {
|
|
header_end := find_header_end_in_buf(buf, buf_len)
|
|
if header_end < 0 {
|
|
return false // headers not complete yet
|
|
}
|
|
// Check for Transfer-Encoding: chunked header (case-insensitive)
|
|
if has_chunked_transfer_encoding_in_buf(buf, header_end) {
|
|
return has_complete_chunked_body(buf, buf_len, header_end)
|
|
}
|
|
content_length := parse_content_length_from_buf(buf, header_end)
|
|
if content_length <= 0 {
|
|
return true // no content-length or zero: body complete
|
|
}
|
|
body_received := buf_len - header_end
|
|
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]
|
|
fn has_chunked_transfer_encoding_in_buf(buf &u8, header_end int) bool {
|
|
te_lower := 'transfer-encoding:'
|
|
for i := 0; i < header_end - te_lower.len; i++ {
|
|
unsafe {
|
|
if buf[i] != `\n` {
|
|
continue
|
|
}
|
|
pos := i + 1
|
|
if pos + te_lower.len > header_end {
|
|
continue
|
|
}
|
|
mut matched := true
|
|
for j := 0; j < te_lower.len; j++ {
|
|
mut ch := buf[pos + j]
|
|
if ch >= `A` && ch <= `Z` {
|
|
ch = ch + 32
|
|
}
|
|
if ch != te_lower[j] {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if matched {
|
|
chunked_str := 'chunked'
|
|
for val_start := pos + te_lower.len; val_start < header_end - chunked_str.len; val_start++ {
|
|
if buf[val_start] == `\r` || buf[val_start] == `\n` {
|
|
break
|
|
}
|
|
mut cmatch := true
|
|
for k := 0; k < chunked_str.len; k++ {
|
|
mut ch2 := buf[val_start + k]
|
|
if ch2 >= `A` && ch2 <= `Z` {
|
|
ch2 = ch2 + 32
|
|
}
|
|
if ch2 != chunked_str[k] {
|
|
cmatch = false
|
|
break
|
|
}
|
|
}
|
|
if cmatch {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parse_content_length_from_buf scans the header bytes for a Content-Length header
|
|
// and returns its integer value, or -1 if not found.
|
|
@[direct_array_access]
|
|
fn parse_content_length_from_buf(buf &u8, header_end int) int {
|
|
cl_lower := 'content-length:'
|
|
for i := 0; i < header_end - cl_lower.len; i++ {
|
|
unsafe {
|
|
if buf[i] != `\n` {
|
|
continue
|
|
}
|
|
pos := i + 1
|
|
if pos + cl_lower.len > header_end {
|
|
continue
|
|
}
|
|
mut matched := true
|
|
for j := 0; j < cl_lower.len; j++ {
|
|
mut ch := buf[pos + j]
|
|
if ch >= `A` && ch <= `Z` {
|
|
ch = ch + 32
|
|
}
|
|
if ch != cl_lower[j] {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if matched {
|
|
mut start := pos + cl_lower.len
|
|
for start < header_end && buf[start] == ` ` {
|
|
start++
|
|
}
|
|
mut val := 0
|
|
for start < header_end && buf[start] >= `0` && buf[start] <= `9` {
|
|
val = val * 10 + int(buf[start] - `0`)
|
|
start++
|
|
}
|
|
return val
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|