From 0c7b0bec12c5f5f75c52ccee4a9cb5d9f83e0b4f Mon Sep 17 00:00:00 2001 From: landrigun Date: Wed, 15 Feb 2023 12:18:29 +0000 Subject: [PATCH] impl http request + fix few tests and scope issues --- Cargo.toml | 1 + src/body.rs | 2 + src/lib.rs | 2 + src/request.rs | 203 +++++++++++++++++++++++++++++++++++++++++++++- src/start_line.rs | 18 +++- 5 files changed, 222 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9a9480b..4b8e633 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ edition = "2021" [dependencies] json = "0.12.4" lazy_static = "1.4.0" +log = "0.4.17" regex = "1" diff --git a/src/body.rs b/src/body.rs index d300526..af48615 100644 --- a/src/body.rs +++ b/src/body.rs @@ -1,3 +1,5 @@ +use json; + const NULL_CHAR: &'static str = "\0"; #[derive(Debug)] diff --git a/src/lib.rs b/src/lib.rs index c3bbea0..29e830f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,11 @@ //! NOTE: only few parts of the specification has been implemented mod body; +mod request; mod start_line; mod version; pub use body::HTTPBody; +pub use request::HTTPRequest; pub use start_line::HTTPStartLine; pub use version::HTTPVersion; diff --git a/src/request.rs b/src/request.rs index c3678ea..c7bd800 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,6 +1,205 @@ use std::collections::VecDeque; -type RequestParts = (String, VecDeque, String); +use crate::{HTTPBody, HTTPStartLine}; + +type RequestParts<'a> = (&'a str, VecDeque<&'a str>, &'a str); const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; -const NULL_CHAR: &'static str = "\0"; + +/// HTTPRequest represents an HTTP request (headers are not parsed) +#[derive(Debug)] +pub struct HTTPRequest<'a> { + pub start_line: HTTPStartLine<'a>, + pub body: Option, + // includes the client IP + port (should be in the headers) + pub addr: String, +} + +impl<'a> HTTPRequest<'a> { + /// get_request_parts splits correctly the incoming request in order to get: + /// * start_line + /// * headers + /// * data (if exists) + fn get_request_parts(request: &str) -> Result { + let mut request_parts: VecDeque<&str> = request.split(HTTP_REQUEST_SEPARATOR).collect(); + + if request_parts.len() < 3 { + return Err("request has no enough informations to be correctly parsed"); + } + + let start_line = request_parts.pop_front().unwrap(); + let body = request_parts.pop_back().unwrap(); + + Ok((start_line, request_parts, body)) + } + + fn parse(request: &str) -> Result { + match HTTPRequest::get_request_parts(&request) { + Ok(rp) => { + let mut request = HTTPRequest::default(); + + let start_line = HTTPStartLine::parse(rp.0); + match start_line { + Ok(v) => request.start_line = v, + Err(e) => log::error!("while parsing start_line details={}", e), + } + + let body = HTTPBody::try_from(rp.2); + match body { + Ok(v) => request.body = Some(v), + Err(e) => log::warn!("{}", e), + } + + return Ok(request); + } + Err(e) => Err(e.to_string()), + } + } +} + +impl<'a> Default for HTTPRequest<'a> { + fn default() -> Self { + HTTPRequest { + start_line: HTTPStartLine::default(), + body: None, + addr: "".to_string(), + } + } +} + +impl<'a> From<&'a str> for HTTPRequest<'a> { + fn from(request: &'a str) -> Self { + match Self::parse(request) { + Ok(v) => v, + Err(e) => { + log::error!("{}", e); + return HTTPRequest::default(); + } + } + } +} + +#[test] +fn test_request() { + struct Expect<'a> { + start_line: &'a str, + body: Option<&'a str>, + is_valid: bool, + } + + let test_cases: [(&str, Expect); 12] = [ + ( + "POST /get/ HTTP/1.1\r\n\r\n", + Expect { + start_line: "POST /get/ HTTP/1.1", + body: None, + is_valid: true, + }, + ), + ( + "POST /refresh/ HTTP/2\r\n\r\n", + Expect { + start_line: "POST /refresh/ HTTP/2", + body: None, + is_valid: true, + }, + ), + ( + "POST /validate/ HTTP/1.0\r\n\r\n", + Expect { + start_line: "POST /validate/ HTTP/1.0", + body: None, + is_valid: true, + }, + ), + ( + "GET / HTTP/1.1\r\n\r\n", + Expect { + start_line: "GET / HTTP/1.1", + body: None, + is_valid: true, + }, + ), + // intentionally add HTTP with no version number + ( + "OPTIONS /admin/2 HTTP/\r\nContent-Type: application/json\r\n", + Expect { + start_line: " UNKNOWN", + body: None, + is_valid: false, + }, + ), + ( + "POST HTTP", + Expect { + start_line: " UNKNOWN", + body: None, + is_valid: false, + } + ), + ( + "", + Expect { + start_line: " UNKNOWN", + body: None, + is_valid: false, + } + ), + ( + "fjlqskjd /oks?id=65 HTTP/2\r\n\r\n", + Expect { + start_line: "fjlqskjd /oks?id=65 HTTP/2", + body: None, + is_valid: true, + } + ), + ( + " ", + Expect { + start_line: " UNKNOWN", + body: None, + is_valid: false, + } + ), + ( + r#"lm //// skkss\r\ndkldklkdl\r\n"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}""#, + Expect { + start_line: " UNKNOWN", + body: Some(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#), + is_valid: false, + } + ), + ( + "POST /refresh/ HTTP/1.1\r\nuselessheaders\r\n\"{\"access_token\": \"toto\", \"refresh_token\": \"tutu\"}", + Expect { + start_line: "POST /refresh/ HTTP/1.1", + body: Some(r#"{"access_token":"toto","refresh_token":"tutu"}"#), + is_valid: true, + } + ), + ( + "POST /get/ HTTP/1.1\r\nuselessheaders\r\n\"{\"token\": \"toto\", \"refresh_token\": \"tutu\"}", + Expect { + start_line: "POST /get/ HTTP/1.1", + body: Some(r#"{"token":"toto","refresh_token":"tutu"}"#), + is_valid: true, + } + ), + ]; + + for (request, expect) in test_cases { + let http_request = HTTPRequest::from(request); + println!("{}", request); + assert_eq!(expect.is_valid, http_request.start_line.is_valid()); + + let start_line: String = http_request.start_line.into(); + assert_eq!(expect.start_line, start_line); + + match http_request.body { + Some(v) => { + assert_eq!(expect.body.unwrap(), v.get_data().dump()) + } + None => continue, + } + } +} diff --git a/src/start_line.rs b/src/start_line.rs index 733cfca..24315da 100644 --- a/src/start_line.rs +++ b/src/start_line.rs @@ -1,7 +1,7 @@ use lazy_static::lazy_static; use regex::Regex; -use crate::version::HTTPVersion; +use crate::HTTPVersion; lazy_static! { static ref HTTP_VERSION_REGEX: Regex = Regex::new("^HTTP/(1.1|1.0|2)$").unwrap(); @@ -23,7 +23,7 @@ impl<'a> HTTPStartLine<'a> { } } - fn parse(start_line: &'a str) -> Result { + pub fn parse(start_line: &'a str) -> Result { let parts: Vec<&str> = start_line.split(" ").collect(); if parts.len() != 3 { return Err("unable to parse the start correctly"); @@ -42,11 +42,25 @@ impl<'a> HTTPStartLine<'a> { )) } + pub fn is_valid(&self) -> bool { + return self.method != "" && self.target != "" && self.version != HTTPVersion::Unknown; + } + fn check_version(version: &str) -> bool { HTTP_VERSION_REGEX.is_match(version) } } +impl<'a> Default for HTTPStartLine<'a> { + fn default() -> Self { + HTTPStartLine { + method: "", + target: "", + version: HTTPVersion::Unknown, + } + } +} + impl<'a> Into for HTTPStartLine<'a> { fn into(self) -> String { let version: String = self.version.into();