//! request handles properly the incoming request //! it will parse the request according to the HTTP message specifications //! see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages //! NOTE: only few parts of the specification has been implemented use json; use lazy_static::lazy_static; use regex::Regex; use std::collections::VecDeque; type RequestParts = (String, VecDeque, String); const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; const NULL_CHAR: &'static str = "\0"; // TODO: put this const in a conf file ? const HTTP_METHODS: [&'static str; 1] = ["POST"]; const HTTP_TARGETS: [&'static str; 3] = ["/validate/", "/get/", "/refresh/"]; lazy_static! { static ref HTTP_VERSION_REGEX: Regex = Regex::new("^HTTP/(1.1|1.0|2)$").unwrap(); } #[derive(Debug)] pub enum HTTPVersion { Http1_0, Http1_1, Http2, Unknown, } impl Into for HTTPVersion { fn into(self) -> String { match self { Self::Http1_0 => "HTTP/1.0".to_string(), Self::Http1_1 => "HTTP/1.1".to_string(), Self::Http2 => "HTTP/2".to_string(), Self::Unknown => "UNKNOWN".to_string(), } } } // TODO: not really satifying... could accept `String` too impl From<&String> for HTTPVersion { fn from(http_version: &String) -> Self { match http_version.as_str() { "HTTP/1.0" => Self::Http1_0, "HTTP/1.1" => Self::Http1_1, "HTTP/2" => Self::Http2, _ => Self::Unknown, } } } #[derive(Debug)] pub struct HTTPStartLine { pub method: String, pub target: String, pub version: HTTPVersion, } impl HTTPStartLine { fn new(method: String, target: String, version: HTTPVersion) -> Self { HTTPStartLine { method, target, version, } } fn parse(start_line: &str) -> Result { // declare a new `start_line` var to borrow to &str `start_line` let start_line = start_line.to_string(); let parts: Vec<&str> = start_line.split(" ").collect(); if parts.len() < 3 { return Err("unable to parse the start correctly"); } let method = parts[0].to_string(); let target = parts[1].to_string(); let version = parts[2].to_string(); if !Self::check_method(&method) { return Err("method validation failed, bad method"); } if !Self::check_target(&target) { return Err("target validation failed, unvalid target"); } if !Self::check_version(&version) { return Err("http version validation failed, unknown version"); } Ok(HTTPStartLine::new( method, target, HTTPVersion::from(&version), )) } /// checks if the start_line method is in a predefined HTTP method list fn check_method(method: &String) -> bool { for m in HTTP_METHODS.iter() { if m.to_string() == *method { return true; } } false } /// checks if the start_line target is in a predefined HTTP target whitelist fn check_target(target: &String) -> bool { for t in HTTP_TARGETS.iter() { if t.to_string() == *target { return true; } } false } fn check_version(version: &String) -> bool { HTTP_VERSION_REGEX.is_match(version) } pub fn is_valid(&self) -> bool { return self.method != "" && self.target != ""; } } impl Default for HTTPStartLine { fn default() -> Self { HTTPStartLine { method: "".to_string(), target: "".to_string(), version: HTTPVersion::Unknown, } } } impl Into for HTTPStartLine { fn into(self) -> String { let version: String = self.version.into(); return format!("{} {} {}", self.method, self.target, version); } } /// represents an HTTP request body /// for simplicity, only json body is accepted #[derive(Debug)] pub struct HTTPBody { data: json::JsonValue, } impl HTTPBody { fn new(data: json::JsonValue) -> HTTPBody { HTTPBody { data } } pub fn get_data(&self) -> &json::JsonValue { &self.data } } impl TryFrom for HTTPBody { type Error = String; fn try_from(body: String) -> Result { let body = body.replace(NULL_CHAR, ""); match json::parse(&body) { Ok(v) => Ok(HTTPBody::new(v)), Err(e) => Err(format!( "error occurred during request body parsing err={}", e )), } } } /// Represents an HTTP request (headers are not parsed) #[derive(Debug)] pub struct HTTPRequest { pub start_line: HTTPStartLine, pub body: Option, } impl HTTPRequest { // associated function to build a new HTTPRequest fn new(start_line: HTTPStartLine, body: Option) -> Self { HTTPRequest { start_line, body } } /// split correctly the incoming request in order to get : /// * start_line /// * headers /// * data (if exists) fn get_request_parts(request: &str) -> Result { // separate the body part from the start_line and the headers let mut request_parts: VecDeque = request .split(HTTP_REQUEST_SEPARATOR) .map(|r| r.to_string()) .collect(); if request_parts.len() < 3 { return Err("request has no enough informations to be correctly parsed".to_string()); } let start_line = request_parts.pop_front().unwrap(); let body = request_parts.pop_back().unwrap(); Ok((start_line, request_parts, body)) } /// parse the request by spliting the incoming request with the separator `\r\n` fn parse(request: &str) -> Result { let request = request.to_string(); 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) => eprintln!("error occurred while parsing start_line err={}", e), } let body = HTTPBody::try_from(rp.2); match body { Ok(v) => request.body = Some(v), Err(e) => eprintln!("error occurred during body parsing err={}", e), } return Ok(request); } Err(e) => { return Err(format!("error occurred getting request parts err={}", e)); } } } pub fn is_valid(&self) -> bool { return self.start_line.is_valid(); } } impl Default for HTTPRequest { fn default() -> Self { HTTPRequest { start_line: HTTPStartLine::default(), body: None, } } } impl From<&str> for HTTPRequest { fn from(request: &str) -> Self { match Self::parse(request) { Ok(v) => v, Err(v) => { eprintln!("{}", format!("[ERR]: {v}")); return HTTPRequest::default(); } } } } pub fn handle_request(request: &str) -> HTTPRequest { HTTPRequest::from(request) } #[test] fn test_handle_request() { struct Expect { start_line: String, body: Option, is_valid: bool, } let test_cases: [(String, Expect); 11] = [ ( "POST /get/ HTTP/1.1\r\n\r\n".to_string(), Expect { start_line: "POST /get/ HTTP/1.1".to_string(), body: None, is_valid: true, }, ), ( "POST /refresh/ HTTP/2\r\n\r\n".to_string(), Expect { start_line: "POST /refresh/ HTTP/2".to_string(), body: None, is_valid: true, }, ), ( "POST /validate/ HTTP/1.0\r\n\r\n".to_string(), Expect { start_line: "POST /validate/ HTTP/1.0".to_string(), body: None, is_valid: true, }, ), ( "GET / HTTP/1.1\r\n\r\n".to_string(), Expect { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, }, ), // intentionally add HTTP with no version number ( "OPTIONS /admin/2 HTTP/\r\nContent-Type: application/json\r\n".to_string(), Expect { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, }, ), ( "POST HTTP".to_string(), Expect { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, } ), ( "".to_string(), Expect { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, } ), ( "fjlqskjd /oks?id=65 HTTP/2\r\n\r\n".to_string(), Expect { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, } ), ( " ".to_string(), Expect { start_line: " UNKNOWN".to_string(), body: None, is_valid: false, } ), ( r#"lm //// skkss\r\ndkldklkdl\r\n"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}""#.to_string(), Expect { start_line: " UNKNOWN".to_string(), body: Some(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#.to_string()), is_valid: false, } ), ( format!("{}\r\nuselessheaders\r\n{}", "POST /refresh/ HTTP/1.1", r#"{"access_token": "toto", "refresh_token": "tutu"}"#), Expect { start_line: "POST /refresh/ HTTP/1.1".to_string(), body: Some(r#"{"access_token":"toto","refresh_token":"tutu"}"#.to_string()), is_valid: true, } ), ]; for (request, expect) in test_cases { let http_request = HTTPRequest::from(request.as_str()); println!("{:?}", http_request); assert_eq!(expect.is_valid, http_request.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.data.dump()) } None => continue, } } } #[test] fn test_http_body() { let test_cases: [(&str, bool); 3] = [ ("hello, how are you ?", false), ("qsdfqsdffqsdffsq", false), ( r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#, true, ), ]; for (body, is_valid) in test_cases { match HTTPBody::try_from(body.to_string()) { Ok(_) => assert!(is_valid), Err(_) => assert!(!is_valid), } } } #[test] fn test_http_method() { let test_cases: Vec<(String, bool)> = vec![ ("POST".to_string(), true), ("POST ".to_string(), false), ("GET".to_string(), false), ("get".to_string(), false), ("qsdqsfqsf/".to_string(), false), ("OPTIONS".to_string(), false), ]; for (method, is_valid) in test_cases { assert_eq!(is_valid, HTTPStartLine::check_method(&method)); } }