static-site-server-rs/src/http_request.rs

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(),
})
}