209 lines
6.0 KiB
Rust
209 lines
6.0 KiB
Rust
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<String, String>,
|
|
pub query_args: HashMap<String, String>,
|
|
pub body: Vec<u8>,
|
|
}
|
|
|
|
impl TryFrom<&[u8]> for HttpRequest {
|
|
type Error = FromUtf8Error;
|
|
|
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
|
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<u8> {
|
|
let mut buffer_out = Vec::<u8>::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<HttpRequest, FromUtf8Error> {
|
|
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::<Vec<u8>>(),
|
|
)?;
|
|
|
|
let http_verb: String = first_line.split(' ').collect::<Vec<&str>>()[0].to_string();
|
|
let http_version: String = first_line
|
|
.chars()
|
|
.rev()
|
|
.collect::<String>()
|
|
.split(' ')
|
|
.collect::<Vec<&str>>()[0]
|
|
.chars()
|
|
.rev()
|
|
.collect::<String>();
|
|
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::<Vec<u8>>();
|
|
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::<Vec<u8>>();
|
|
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::<HashMap<String, String>>()
|
|
};
|
|
|
|
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<String, String> = 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(),
|
|
})
|
|
}
|