use std::{collections::HashMap, string::FromUtf8Error}; use crate::utils::{ bytehex2val, AMP_ASCII, DOUBLE_NEW_LINES_UNIX, DOUBLE_NEW_LINES_WIN, EQL_ASCII, PCT_ASCII, QSM_ASCII, }; #[derive(Debug, PartialEq, Eq)] pub struct HttpRequest { pub http: String, pub verb: String, pub path: String, pub headers: HashMap, pub query_args: HashMap, pub body: Vec, } impl TryFrom<&[u8]> for HttpRequest { type Error = FromUtf8Error; fn try_from(value: &[u8]) -> Result { build_request_from_buffer(value) } } #[test] fn pct_decode_test_1() { let expected = "http://user:p@5S@example.com"; let escaped = "http://user:p%405S@example.com"; assert_eq!( String::from_utf8(pct_decode(escaped.as_bytes())).unwrap(), expected, ) } #[test] fn pct_decode_test_2() { let expected = " "; let escaped = "%20"; assert_eq!( String::from_utf8(pct_decode(escaped.as_bytes())).unwrap(), expected, ) } #[test] fn build_request_from_buffer_test_1() { let expected = HttpRequest { http: "HTTP/1.1".to_owned(), verb: "GET".to_owned(), path: "/path".to_owned(), headers: vec![ ("host".to_owned(), "example.com".to_owned()), ("accept-encoding".to_owned(), "gzip".to_owned()), ( "content-type".to_owned(), "application/json; encoding=utf-8".to_owned(), ), ("content-length".to_owned(), "16".to_owned()), ] .into_iter() .collect(), query_args: vec![ ("email".to_owned(), "someone@example.com".to_owned()), ("send".to_owned(), "no".to_owned()), ] .into_iter() .collect(), body: "{\"breaks\": true}".as_bytes().to_owned(), }; let escaped = [ "GET /path?email=someone%40example.com&send=yes&send=no HTTP/1.1", "Host:example.com", "Accept-Encoding: gzip", "Content-Type:\t\t\tapplication/json; encoding=utf-8 \t", "Content-Length: 16", "", "{\"breaks\": true}", ] .join("\r\n"); let built = build_request_from_buffer(escaped.as_bytes()); assert_eq!(Ok(expected), built) } fn pct_decode(buffer_in: &[u8]) -> Vec { let mut buffer_out = Vec::::with_capacity(buffer_in.len()); let mut i = 0; while i < buffer_in.len() { let mut value = buffer_in[i]; if buffer_in[i] == PCT_ASCII && i + 2 < buffer_in.len() && let Some(higher) = bytehex2val(buffer_in[i + 1]) && let Some(lower) = bytehex2val(buffer_in[i + 2]) { value = (higher << 4) + lower; i += 3; } else { i += 1; } if value == 0 { break; } buffer_out.push(value); } buffer_out } fn build_request_from_buffer(buffer: &[u8]) -> Result { let mut first_line_end = buffer.iter().position(|&s| s == b'\n').unwrap_or(0); if first_line_end > 0 && buffer[first_line_end - 1] == b'\r' { first_line_end -= 1; } let first_line = String::from_utf8( buffer .iter() .take(first_line_end) .copied() .collect::>(), )?; let http_verb: String = first_line.split(' ').collect::>()[0].to_string(); let http_version: String = first_line .chars() .rev() .collect::() .split(' ') .collect::>()[0] .chars() .rev() .collect::(); let http_path_string: String = String::from( &first_line[(http_verb.len() + 1)..(first_line.len() - http_version.len() - 1)], ); let http_path = { let http_path_bytes = http_path_string .as_bytes() .iter() .take_while(|u| **u != QSM_ASCII) .copied() .collect::>(); String::from_utf8(pct_decode(&http_path_bytes))? }; let http_query_map = { let http_query_bytes = http_path_string .as_bytes() .iter() .skip_while(|u| **u != QSM_ASCII) .skip(1) .copied() .collect::>(); http_query_bytes .split(|x| *x == AMP_ASCII) .filter(|x| { String::from_utf8(x.to_vec()) .map(|y| !y.trim().is_empty()) .unwrap_or(false) }) .filter(|x| x.contains(&EQL_ASCII)) .flat_map(|x| -> Result<(String, String), FromUtf8Error> { let i = x.iter().position(|p| *p == EQL_ASCII).unwrap_or(0); let (k, v) = x.split_at(i); Ok(( String::from_utf8(pct_decode(k))?, String::from_utf8(pct_decode(&v[1..]))?, )) }) .collect::>() }; let mut body: &[u8] = b""; let mut header: String = "".to_string(); for i in 0..buffer.len() { let buffer_part = buffer.split_at(i); let sz = if buffer_part.1.starts_with(&DOUBLE_NEW_LINES_UNIX) { DOUBLE_NEW_LINES_UNIX.len() } else if buffer_part.1.starts_with(&DOUBLE_NEW_LINES_WIN) { DOUBLE_NEW_LINES_WIN.len() } else { 0 }; if sz > 0 { header = String::from_utf8(buffer_part.0.to_vec())?; body = &buffer_part.1[sz..]; break; } } let headers: HashMap = header .lines() .skip(1) .filter_map(|x: &str| { x.find(':').map(|y| { let (bef, aft) = x.split_at(y); (bef.to_string().to_lowercase(), aft[1..].trim().to_string()) }) }) .collect(); Ok(HttpRequest { http: http_version, verb: http_verb, path: http_path, headers, query_args: http_query_map, body: body.to_vec(), }) }