421 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			421 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! 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>, 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<String> 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<Self, &str> {
 | |
|         // 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<String> 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<String> for HTTPBody {
 | |
|     type Error = String;
 | |
|     fn try_from(body: String) -> Result<HTTPBody, Self::Error> {
 | |
|         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<HTTPBody>,
 | |
| }
 | |
| 
 | |
| impl HTTPRequest {
 | |
|     // associated function to build a new HTTPRequest
 | |
|     fn new(start_line: HTTPStartLine, body: Option<HTTPBody>) -> 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<RequestParts, String> {
 | |
|         // separate the body part from the start_line and the headers
 | |
|         let mut request_parts: VecDeque<String> = 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<HTTPRequest, String> {
 | |
|         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<String>,
 | |
|         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));
 | |
|     }
 | |
| }
 |