Merge branch 'feature/http-destructuration' into develop
This commit is contained in:
		
						commit
						141a79c409
					
				
							
								
								
									
										596
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										596
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -16,6 +16,8 @@ simple_logger = "4.0.0" | |||||||
| log = "0.4.17" | log = "0.4.17" | ||||||
| base64 = "0.13.1" | base64 = "0.13.1" | ||||||
| 
 | 
 | ||||||
|  | http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.3" } | ||||||
|  | 
 | ||||||
| # useful for tests (embedded files should be delete in release ?) | # useful for tests (embedded files should be delete in release ?) | ||||||
| #rust-embed="6.4.1" | #rust-embed="6.4.1" | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,93 +0,0 @@ | |||||||
| use json; |  | ||||||
| use std::collections::HashMap; |  | ||||||
| 
 |  | ||||||
| const JSON_DELIMITER: &'static str = ","; |  | ||||||
| 
 |  | ||||||
| /// `HashMap` wrapper, represents the JSON response body
 |  | ||||||
| pub struct HTTPMessage { |  | ||||||
|     message: HashMap<String, String>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for HTTPMessage { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         HTTPMessage { |  | ||||||
|             message: HashMap::new(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// try to convert `HTTPMessage` in `json::JsonValue`
 |  | ||||||
| impl TryInto<json::JsonValue> for HTTPMessage { |  | ||||||
|     type Error = String; |  | ||||||
|     fn try_into(self) -> Result<json::JsonValue, Self::Error> { |  | ||||||
|         let message = format!(r#"{{{}}}"#, self.build_json()); |  | ||||||
|         match json::parse(&message) { |  | ||||||
|             Ok(r) => Ok(r), |  | ||||||
|             Err(e) => Err(format!( |  | ||||||
|                 "unable to parse the HTTPMessage correctly: {}, err={}", |  | ||||||
|                 message, e |  | ||||||
|             )), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl HTTPMessage { |  | ||||||
|     pub fn put(&mut self, key: &str, value: &str) { |  | ||||||
|         self.message.insert(key.to_string(), value.to_string()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// associated function to build an HTTPMessage error
 |  | ||||||
|     pub fn error(message: &str) -> Option<json::JsonValue> { |  | ||||||
|         let mut http_message = HTTPMessage::default(); |  | ||||||
|         http_message.put("error", message); |  | ||||||
| 
 |  | ||||||
|         match message.try_into() { |  | ||||||
|             Ok(m) => Some(m), |  | ||||||
|             Err(e) => { |  | ||||||
|                 eprintln!( |  | ||||||
|                     "unable to parse the message: {} into JSON, err={}", |  | ||||||
|                     message, e |  | ||||||
|                 ); |  | ||||||
|                 return None; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// loops over all the HashMap keys, builds a JSON key value for each one and join them with `JSON_DELIMITER`
 |  | ||||||
|     fn build_json(self) -> String { |  | ||||||
|         let unstruct: Vec<String> = self |  | ||||||
|             .message |  | ||||||
|             .keys() |  | ||||||
|             .map(|k| format!(r#""{}":{:?}"#, k, self.message.get(k).unwrap())) |  | ||||||
|             .collect(); |  | ||||||
|         let joined = unstruct.join(JSON_DELIMITER); |  | ||||||
|         joined |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_message() { |  | ||||||
|     let mut http_message = HTTPMessage::default(); |  | ||||||
|     http_message.put("email", "toto@toto.fr"); |  | ||||||
|     http_message.put("password", "tata"); |  | ||||||
| 
 |  | ||||||
|     let mut json_result: Result<json::JsonValue, String> = http_message.try_into(); |  | ||||||
|     assert!(json_result.is_ok()); |  | ||||||
| 
 |  | ||||||
|     let mut json = json_result.unwrap(); |  | ||||||
|     assert!(json.has_key("email")); |  | ||||||
|     assert!(json.has_key("password")); |  | ||||||
| 
 |  | ||||||
|     let empty_http_message = HTTPMessage::default(); |  | ||||||
|     json_result = empty_http_message.try_into(); |  | ||||||
|     assert!(json_result.is_ok()); |  | ||||||
| 
 |  | ||||||
|     json = json_result.unwrap(); |  | ||||||
|     assert_eq!("{}", json.dump().to_string()); |  | ||||||
| 
 |  | ||||||
|     let mut bad_http_message = HTTPMessage::default(); |  | ||||||
|     bad_http_message.put("\"", ""); |  | ||||||
| 
 |  | ||||||
|     json_result = bad_http_message.try_into(); |  | ||||||
|     assert!(json_result.is_err()); |  | ||||||
| } |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| //! http module includes tools to parse an HTTP request and build and HTTP response
 |  | ||||||
| 
 |  | ||||||
| pub mod message; |  | ||||||
| pub mod request; |  | ||||||
| pub mod response; |  | ||||||
| pub mod router; |  | ||||||
| 
 |  | ||||||
| pub use message::HTTPMessage; |  | ||||||
| pub use request::{HTTPRequest, HTTPVersion}; |  | ||||||
| pub use response::{HTTPResponse, HTTPStatusCode}; |  | ||||||
| pub use router::ROUTER; |  | ||||||
| @ -1,416 +0,0 @@ | |||||||
| //! 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; |  | ||||||
| 
 |  | ||||||
| use crate::utils::extract_json_value; |  | ||||||
| 
 |  | ||||||
| type RequestParts = (String, VecDeque<String>, String); |  | ||||||
| 
 |  | ||||||
| const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; |  | ||||||
| const NULL_CHAR: &'static str = "\0"; |  | ||||||
| 
 |  | ||||||
| lazy_static! { |  | ||||||
|     static ref HTTP_VERSION_REGEX: Regex = Regex::new("^HTTP/(1.1|1.0|2)$").unwrap(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, Copy, Clone)] |  | ||||||
| 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 { |  | ||||||
|     method: String, |  | ||||||
|     target: String, |  | ||||||
|     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_version(&version) { |  | ||||||
|             return Err("http version validation failed, unknown version"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(HTTPStartLine::new( |  | ||||||
|             method, |  | ||||||
|             target, |  | ||||||
|             HTTPVersion::from(&version), |  | ||||||
|         )) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn check_version(version: &String) -> bool { |  | ||||||
|         HTTP_VERSION_REGEX.is_match(version) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn is_valid(&self) -> bool { |  | ||||||
|         return self.method != "" && self.target != ""; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_target(&self) -> String { |  | ||||||
|         self.target.clone() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 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!("during request body parsing details={}", e)), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// Represents an HTTP request (headers are not parsed)
 |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub struct HTTPRequest { |  | ||||||
|     pub start_line: HTTPStartLine, |  | ||||||
|     pub body: Option<HTTPBody>, |  | ||||||
|     // includes the client IP + port (should be in the headers)
 |  | ||||||
|     pub addr: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl HTTPRequest { |  | ||||||
|     /// 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) => 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) => { |  | ||||||
|                 return Err(e); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// retrieve value in `HTTPBody` (returns None if empty or does not exist)
 |  | ||||||
|     pub fn get_body_value(&self, key: &str) -> Option<String> { |  | ||||||
|         match self.body { |  | ||||||
|             Some(ref b) => match &b.data { |  | ||||||
|                 json::JsonValue::Object(d) => extract_json_value(&d, key), |  | ||||||
|                 _ => None, |  | ||||||
|             }, |  | ||||||
|             None => None, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_method(&self) -> String { |  | ||||||
|         self.start_line.method.clone() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[allow(dead_code)] |  | ||||||
|     pub fn is_valid(&self) -> bool { |  | ||||||
|         return self.start_line.is_valid(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn set_addr(&mut self, addr: String) { |  | ||||||
|         self.addr = addr; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for HTTPRequest { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         HTTPRequest { |  | ||||||
|             start_line: HTTPStartLine::default(), |  | ||||||
|             body: None, |  | ||||||
|             addr: "".to_string(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl From<&str> for HTTPRequest { |  | ||||||
|     fn from(request: &str) -> Self { |  | ||||||
|         match Self::parse(request) { |  | ||||||
|             Ok(v) => v, |  | ||||||
|             Err(e) => { |  | ||||||
|                 log::error!("{}", e); |  | ||||||
|                 return HTTPRequest::default(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_request() { |  | ||||||
|     struct Expect { |  | ||||||
|         start_line: String, |  | ||||||
|         body: Option<String>, |  | ||||||
|         is_valid: bool, |  | ||||||
|         has_token: bool, |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let test_cases: [(String, Expect); 12] = [ |  | ||||||
|         ( |  | ||||||
|             "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, |  | ||||||
|                 has_token: false, |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "POST /refresh/ HTTP/2\r\n\r\n".to_string(), |  | ||||||
|             Expect { |  | ||||||
|                 start_line: "POST /refresh/ HTTP/2".to_string(), |  | ||||||
|                 body: None, |  | ||||||
|                 is_valid: true, |  | ||||||
|                 has_token: false, |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "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, |  | ||||||
|                 has_token: false, |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "GET / HTTP/1.1\r\n\r\n".to_string(), |  | ||||||
|             Expect { |  | ||||||
|                 start_line: "GET / HTTP/1.1".to_string(), |  | ||||||
|                 body: None, |  | ||||||
|                 is_valid: true, |  | ||||||
|                 has_token: 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, |  | ||||||
|                 has_token: false, |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "POST HTTP".to_string(), |  | ||||||
|             Expect { |  | ||||||
|                 start_line: "  UNKNOWN".to_string(), |  | ||||||
|                 body: None, |  | ||||||
|                 is_valid: false, |  | ||||||
|                 has_token: false |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "".to_string(), |  | ||||||
|             Expect { |  | ||||||
|                 start_line: "  UNKNOWN".to_string(), |  | ||||||
|                 body: None, |  | ||||||
|                 is_valid: false, |  | ||||||
|                 has_token: false |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "fjlqskjd /oks?id=65 HTTP/2\r\n\r\n".to_string(), |  | ||||||
|             Expect { |  | ||||||
|                 start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(), |  | ||||||
|                 body: None, |  | ||||||
|                 is_valid: true, |  | ||||||
|                 has_token: false |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             "   ".to_string(), |  | ||||||
|             Expect { |  | ||||||
|                 start_line: "  UNKNOWN".to_string(), |  | ||||||
|                 body: None, |  | ||||||
|                 is_valid: false, |  | ||||||
|                 has_token: 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, |  | ||||||
|                 has_token: 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, |  | ||||||
|                 has_token: false |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|         ( |  | ||||||
|             format!("{}\r\nuselessheaders\r\n{}", "POST /get/ HTTP/1.1", r#"{"token": "toto", "refresh_token": "tutu"}"#), |  | ||||||
|             Expect { |  | ||||||
|                 start_line: "POST /get/ HTTP/1.1".to_string(), |  | ||||||
|                 body: Some(r#"{"token":"toto","refresh_token":"tutu"}"#.to_string()), |  | ||||||
|                 is_valid: true, |  | ||||||
|                 has_token: true |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     for (request, expect) in test_cases { |  | ||||||
|         let http_request = HTTPRequest::from(request.as_str()); |  | ||||||
|         assert_eq!(expect.is_valid, http_request.is_valid()); |  | ||||||
| 
 |  | ||||||
|         let token = http_request.get_body_value("token"); |  | ||||||
|         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, |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         match expect.has_token { |  | ||||||
|             true => assert!(token.is_some()), |  | ||||||
|             false => assert!(token.is_none()), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[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), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,172 +0,0 @@ | |||||||
| //! response handles the incoming request parsed `HTTPRequest`
 |  | ||||||
| //! it will build an HTTPResponse corresponding to the HTTP message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
 |  | ||||||
| //! NOTE: only few parts of the specification has been implemented
 |  | ||||||
| 
 |  | ||||||
| use super::{HTTPMessage, HTTPVersion}; |  | ||||||
| use json; |  | ||||||
| 
 |  | ||||||
| #[derive(Debug, PartialEq, Clone)] |  | ||||||
| pub enum HTTPStatusCode { |  | ||||||
|     Http200, |  | ||||||
|     Http400, |  | ||||||
|     Http403, |  | ||||||
|     Http404, |  | ||||||
|     Http500, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Into<String> for HTTPStatusCode { |  | ||||||
|     fn into(self) -> String { |  | ||||||
|         match self { |  | ||||||
|             Self::Http200 => "200".to_string(), |  | ||||||
|             Self::Http400 => "400".to_string(), |  | ||||||
|             Self::Http404 => "404".to_string(), |  | ||||||
|             Self::Http403 => "403".to_string(), |  | ||||||
|             Self::Http500 => "500".to_string(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub struct HTTPStatusLine { |  | ||||||
|     version: HTTPVersion, |  | ||||||
|     status_code: HTTPStatusCode, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for HTTPStatusLine { |  | ||||||
|     fn default() -> HTTPStatusLine { |  | ||||||
|         HTTPStatusLine { |  | ||||||
|             version: HTTPVersion::Http1_1, |  | ||||||
|             status_code: HTTPStatusCode::Http400, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Into<String> for HTTPStatusLine { |  | ||||||
|     fn into(self) -> String { |  | ||||||
|         let version: String = self.version.into(); |  | ||||||
|         let status_code: String = self.status_code.into(); |  | ||||||
|         format! {"{} {}", version, status_code} |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl HTTPStatusLine { |  | ||||||
|     pub fn set_status_code(&mut self, code: HTTPStatusCode) { |  | ||||||
|         self.status_code = code; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[allow(dead_code)] |  | ||||||
|     pub fn get_status_code(&self) -> HTTPStatusCode { |  | ||||||
|         self.status_code.clone() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// represents an HTTP response (headers are not parsed)
 |  | ||||||
| /// NOTE: for simplicity, only JSON body are accepted
 |  | ||||||
| pub struct HTTPResponse { |  | ||||||
|     pub status_line: HTTPStatusLine, |  | ||||||
|     body: json::JsonValue, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for HTTPResponse { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         HTTPResponse { |  | ||||||
|             status_line: HTTPStatusLine::default(), |  | ||||||
|             body: json::parse(r#"{"error": "the incoming request is not valid"}"#).unwrap(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Into<String> for HTTPResponse { |  | ||||||
|     fn into(self) -> String { |  | ||||||
|         // move `self.body` into a new var
 |  | ||||||
|         let b = self.body; |  | ||||||
|         let body: String = json::stringify(b); |  | ||||||
| 
 |  | ||||||
|         let status_line: String = self.status_line.into(); |  | ||||||
|         format!( |  | ||||||
|             "{}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}", |  | ||||||
|             status_line, |  | ||||||
|             body.len(), |  | ||||||
|             body |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl HTTPResponse { |  | ||||||
|     pub fn as_500(message: Option<json::JsonValue>) -> Self { |  | ||||||
|         let mut response = Self::default(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http500); |  | ||||||
| 
 |  | ||||||
|         response.body = { |  | ||||||
|             match message { |  | ||||||
|                 Some(m) => m, |  | ||||||
|                 None => json::parse(r#"{"error": "unexpected error occurred"}"#).unwrap(), |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_404() -> Self { |  | ||||||
|         let mut response = Self::default(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http404); |  | ||||||
|         response.body = json::parse(r#"{"error": "the url requested does not exist"}"#).unwrap(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_403() -> Self { |  | ||||||
|         let mut response = HTTPResponse { |  | ||||||
|             status_line: HTTPStatusLine::default(), |  | ||||||
|             body: json::parse(r#"{"error": "invalid credentials"}"#).unwrap(), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http403); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// wrap the `Self::default()` associated func (not really clear)
 |  | ||||||
|     pub fn as_400() -> Self { |  | ||||||
|         Self::default() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn as_200(message: Option<json::JsonValue>) -> Self { |  | ||||||
|         let mut response = Self::default(); |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|             .status_line |  | ||||||
|             .set_status_code(HTTPStatusCode::Http200); |  | ||||||
| 
 |  | ||||||
|         response.body = { |  | ||||||
|             match message { |  | ||||||
|                 Some(m) => m, |  | ||||||
|                 None => json::parse(r#"{"status": "ok"}"#).unwrap(), |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         response |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// builds an HTTP 200 response with the generated JWT
 |  | ||||||
|     pub fn send_token(token: &str) -> Self { |  | ||||||
|         let mut http_message = HTTPMessage::default(); |  | ||||||
|         http_message.put("token", token); |  | ||||||
| 
 |  | ||||||
|         let message = { |  | ||||||
|             match http_message.try_into() { |  | ||||||
|                 Ok(m) => m, |  | ||||||
|                 Err(_e) => json::parse(r#"{"token": "error.generation.token"}"#).unwrap(), |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         HTTPResponse::as_200(Some(message)) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,8 +1,7 @@ | |||||||
| mod config; | mod config; | ||||||
| mod http; |  | ||||||
| mod jwt; | mod jwt; | ||||||
|  | mod router; | ||||||
| mod stores; | mod stores; | ||||||
| mod utils; |  | ||||||
| 
 | 
 | ||||||
| use clap::Parser; | use clap::Parser; | ||||||
| use configparser::ini::Ini; | use configparser::ini::Ini; | ||||||
| @ -12,8 +11,8 @@ use tokio::{ | |||||||
|     time::{timeout, Duration}, |     time::{timeout, Duration}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | use crate::router::ROUTER; | ||||||
| use config::Config; | use config::Config; | ||||||
| use http::ROUTER; |  | ||||||
| 
 | 
 | ||||||
| #[derive(Parser)] | #[derive(Parser)] | ||||||
| #[clap(author, version, about, long_about = None)] | #[clap(author, version, about, long_about = None)] | ||||||
| @ -90,7 +89,7 @@ async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let request_string = std::str::from_utf8(&message).unwrap(); |     let request_string = std::str::from_utf8(&message).unwrap(); | ||||||
|     let response = ROUTER.route(request_string, addr.clone(), config).await; |     let response = ROUTER.route(request_string, config).await; | ||||||
|     let response_str: String = response.into(); |     let response_str: String = response.into(); | ||||||
| 
 | 
 | ||||||
|     stream.write(response_str.as_bytes()).await.unwrap(); |     stream.write(response_str.as_bytes()).await.unwrap(); | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								src/router/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/router/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | //! router module includes all the handlers to get and validate JWT
 | ||||||
|  | 
 | ||||||
|  | mod router; | ||||||
|  | pub use router::ROUTER; | ||||||
| @ -2,9 +2,9 @@ | |||||||
| //! it implements all the logic to build an `HTTPResponse`
 | //! it implements all the logic to build an `HTTPResponse`
 | ||||||
| 
 | 
 | ||||||
| use base64; | use base64; | ||||||
| use json; | use http::{HTTPRequest, HTTPResponse, JSONMessage}; | ||||||
|  | use json::JsonValue; | ||||||
| 
 | 
 | ||||||
| use super::{HTTPMessage, HTTPRequest, HTTPResponse}; |  | ||||||
| use crate::config::Config; | use crate::config::Config; | ||||||
| use crate::jwt::JWTSigner; | use crate::jwt::JWTSigner; | ||||||
| use crate::stores::{FileStore, Store}; | use crate::stores::{FileStore, Store}; | ||||||
| @ -14,15 +14,16 @@ const GET_ROUTE: &'static str = "/get/"; | |||||||
| const VALIDATE_ROUTE: &'static str = "/validate/"; | const VALIDATE_ROUTE: &'static str = "/validate/"; | ||||||
| const PUBKEY_ROUTE: &'static str = "/pubkey/"; | const PUBKEY_ROUTE: &'static str = "/pubkey/"; | ||||||
| 
 | 
 | ||||||
| async fn handle_get(request: HTTPRequest, config: Config, method: &str) -> HTTPResponse { | async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { | ||||||
|     if method.trim().to_lowercase() != "post" { |     if method.trim().to_lowercase() != "post" { | ||||||
|         return HTTPResponse::as_400(); |         return HTTPResponse::as_400(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let mut store = FileStore::new(config.filestore_path.clone()); |     let mut store = FileStore::new(config.filestore_path.clone()); | ||||||
|     match &request.body { | 
 | ||||||
|         Some(ref b) => { |     match request.get_body() { | ||||||
|             let credentials = store.is_auth(&b.get_data()).await; |         Some(d) => { | ||||||
|  |             let credentials = store.is_auth(d).await; | ||||||
|             if credentials.is_none() { |             if credentials.is_none() { | ||||||
|                 return HTTPResponse::as_403(); |                 return HTTPResponse::as_403(); | ||||||
|             } |             } | ||||||
| @ -31,16 +32,16 @@ async fn handle_get(request: HTTPRequest, config: Config, method: &str) -> HTTPR | |||||||
|                 match JWTSigner::new(config).await { |                 match JWTSigner::new(config).await { | ||||||
|                     Ok(s) => s, |                     Ok(s) => s, | ||||||
|                     Err(e) => { |                     Err(e) => { | ||||||
|                         let message = HTTPMessage::error(&e); |                         let message = JSONMessage::error(&e); | ||||||
|                         return HTTPResponse::as_500(message); |                         return HTTPResponse::as_500(message); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             match jwt_signer.sign(credentials.unwrap().email) { |             match jwt_signer.sign(credentials.unwrap().email) { | ||||||
|                 Ok(t) => HTTPResponse::send_token(&t), |                 Ok(t) => send_token(&t), | ||||||
|                 Err(e) => { |                 Err(e) => { | ||||||
|                     let message = HTTPMessage::error(&e); |                     let message = JSONMessage::error(&e); | ||||||
|                     return HTTPResponse::as_500(message); |                     return HTTPResponse::as_500(message); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -52,7 +53,11 @@ async fn handle_get(request: HTTPRequest, config: Config, method: &str) -> HTTPR | |||||||
| /// validates the token by checking:
 | /// validates the token by checking:
 | ||||||
| /// * expiration time
 | /// * expiration time
 | ||||||
| /// * signature
 | /// * signature
 | ||||||
| async fn handle_validate(request: HTTPRequest, config: Config, method: &str) -> HTTPResponse { | async fn handle_validate( | ||||||
|  |     request: HTTPRequest<'_>, | ||||||
|  |     config: Config, | ||||||
|  |     method: &str, | ||||||
|  | ) -> HTTPResponse { | ||||||
|     if request.get_method().trim().to_lowercase() != method { |     if request.get_method().trim().to_lowercase() != method { | ||||||
|         return HTTPResponse::as_400(); |         return HTTPResponse::as_400(); | ||||||
|     } |     } | ||||||
| @ -61,7 +66,7 @@ async fn handle_validate(request: HTTPRequest, config: Config, method: &str) -> | |||||||
|         match request.get_body_value("token") { |         match request.get_body_value("token") { | ||||||
|             Some(t) => t, |             Some(t) => t, | ||||||
|             None => { |             None => { | ||||||
|                 let mut message = HTTPMessage::default(); |                 let mut message = JSONMessage::default(); | ||||||
|                 message.put("valid", "false"); |                 message.put("valid", "false"); | ||||||
|                 message.put("reason", "no token provided in the request body"); |                 message.put("reason", "no token provided in the request body"); | ||||||
|                 let json = message.try_into().unwrap(); |                 let json = message.try_into().unwrap(); | ||||||
| @ -75,14 +80,14 @@ async fn handle_validate(request: HTTPRequest, config: Config, method: &str) -> | |||||||
|         match JWTSigner::new(config).await { |         match JWTSigner::new(config).await { | ||||||
|             Ok(s) => s, |             Ok(s) => s, | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 let message = HTTPMessage::error(&e); |                 let message = JSONMessage::error(&e); | ||||||
|                 let json = message.try_into().unwrap(); |                 let json = message.try_into().unwrap(); | ||||||
|                 return HTTPResponse::as_500(Some(json)); |                 return HTTPResponse::as_500(Some(json)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let mut message = HTTPMessage::default(); |     let mut message = JSONMessage::default(); | ||||||
|     match jwt_signer.validate(&token) { |     match jwt_signer.validate(&token) { | ||||||
|         Ok(()) => { |         Ok(()) => { | ||||||
|             message.put("valid", "true"); |             message.put("valid", "true"); | ||||||
| @ -93,12 +98,16 @@ async fn handle_validate(request: HTTPRequest, config: Config, method: &str) -> | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let json: json::JsonValue = message.try_into().unwrap(); |     let json: JsonValue = message.try_into().unwrap(); | ||||||
|     HTTPResponse::as_200(Some(json)) |     HTTPResponse::as_200(Some(json)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// returns the JWT public key in base64 encoded
 | /// returns the JWT public key in base64 encoded
 | ||||||
| async fn handle_public_key(request: HTTPRequest, config: Config, method: &str) -> HTTPResponse { | async fn handle_public_key( | ||||||
|  |     request: HTTPRequest<'_>, | ||||||
|  |     config: Config, | ||||||
|  |     method: &str, | ||||||
|  | ) -> HTTPResponse { | ||||||
|     if request.get_method().trim().to_lowercase() != method { |     if request.get_method().trim().to_lowercase() != method { | ||||||
|         return HTTPResponse::as_400(); |         return HTTPResponse::as_400(); | ||||||
|     } |     } | ||||||
| @ -107,7 +116,7 @@ async fn handle_public_key(request: HTTPRequest, config: Config, method: &str) - | |||||||
|         match JWTSigner::new(config).await { |         match JWTSigner::new(config).await { | ||||||
|             Ok(s) => s, |             Ok(s) => s, | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 let message = HTTPMessage::error(&e); |                 let message = JSONMessage::error(&e); | ||||||
|                 let json = message.try_into().unwrap(); |                 let json = message.try_into().unwrap(); | ||||||
|                 return HTTPResponse::as_500(Some(json)); |                 return HTTPResponse::as_500(Some(json)); | ||||||
|             } |             } | ||||||
| @ -116,7 +125,7 @@ async fn handle_public_key(request: HTTPRequest, config: Config, method: &str) - | |||||||
| 
 | 
 | ||||||
|     let public_key = jwt_signer.get_public_key(); |     let public_key = jwt_signer.get_public_key(); | ||||||
| 
 | 
 | ||||||
|     let mut message = HTTPMessage::default(); |     let mut message = JSONMessage::default(); | ||||||
|     message.put("pubkey", &base64::encode(public_key)); |     message.put("pubkey", &base64::encode(public_key)); | ||||||
| 
 | 
 | ||||||
|     let json = message.try_into().unwrap(); |     let json = message.try_into().unwrap(); | ||||||
| @ -127,13 +136,9 @@ pub struct Router; | |||||||
| 
 | 
 | ||||||
| impl Router { | impl Router { | ||||||
|     /// routes the request to the corresponding handling method
 |     /// routes the request to the corresponding handling method
 | ||||||
|     pub async fn route(&self, request_str: &str, addr: String, config: Config) -> HTTPResponse { |     pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse { | ||||||
|         let mut request = HTTPRequest::from(request_str); |         let request = HTTPRequest::from(request_str); | ||||||
|         request.set_addr(addr); |         match request.get_target() { | ||||||
| 
 |  | ||||||
|         let target = request.start_line.get_target(); |  | ||||||
| 
 |  | ||||||
|         match target.as_str() { |  | ||||||
|             GET_ROUTE => handle_get(request, config, "post").await, |             GET_ROUTE => handle_get(request, config, "post").await, | ||||||
|             VALIDATE_ROUTE => handle_validate(request, config, "post").await, |             VALIDATE_ROUTE => handle_validate(request, config, "post").await, | ||||||
|             PUBKEY_ROUTE => handle_public_key(request, config, "get").await, |             PUBKEY_ROUTE => handle_public_key(request, config, "get").await, | ||||||
| @ -142,12 +147,27 @@ impl Router { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// send_token generates an HTTPResponse with the new token
 | ||||||
|  | pub fn send_token(token: &str) -> HTTPResponse { | ||||||
|  |     let mut message = JSONMessage::default(); | ||||||
|  |     message.put("token", token); | ||||||
|  | 
 | ||||||
|  |     let json = { | ||||||
|  |         match message.try_into() { | ||||||
|  |             Ok(m) => m, | ||||||
|  |             Err(_e) => json::parse(r#"{"token": "error.generation.token"}"#).unwrap(), | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     HTTPResponse::as_200(Some(json)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // this MUST be used like a Singleton
 | // this MUST be used like a Singleton
 | ||||||
| pub const ROUTER: Router = Router {}; | pub const ROUTER: Router = Router {}; | ||||||
| 
 | 
 | ||||||
| #[tokio::test] | #[tokio::test] | ||||||
| async fn test_route() { | async fn test_route() { | ||||||
|     use super::HTTPStatusCode; |     use http::HTTPStatusCode; | ||||||
| 
 | 
 | ||||||
|     let router: &Router = &ROUTER; |     let router: &Router = &ROUTER; | ||||||
|     let config: Config = Config::default(); |     let config: Config = Config::default(); | ||||||
| @ -1,5 +1,5 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use json; | use json::JsonValue; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
| 
 | 
 | ||||||
| use super::store::{Credentials, Store}; | use super::store::{Credentials, Store}; | ||||||
| @ -67,7 +67,7 @@ impl FileStore { | |||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl Store for FileStore { | impl Store for FileStore { | ||||||
|     async fn is_auth(&mut self, data: &json::JsonValue) -> Option<Credentials> { |     async fn is_auth(&mut self, data: &JsonValue) -> Option<Credentials> { | ||||||
|         // ensure that the store file already exists even after its instanciation
 |         // ensure that the store file already exists even after its instanciation
 | ||||||
|         if !Path::new(&self.path).is_file() { |         if !Path::new(&self.path).is_file() { | ||||||
|             log::error!("{} path referencing file store does not exist", self.path); |             log::error!("{} path referencing file store does not exist", self.path); | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use json; | use json::JsonValue; | ||||||
| 
 | 
 | ||||||
| use crate::utils::extract_json_value; | use http::extract_json_value; | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| pub trait Store { | pub trait Store { | ||||||
|     async fn is_auth(&mut self, data: &json::JsonValue) -> Option<Credentials>; |     async fn is_auth(&mut self, data: &JsonValue) -> Option<Credentials>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Default, Debug)] | #[derive(Default, Debug)] | ||||||
| @ -24,11 +24,11 @@ impl Credentials { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl From<&json::JsonValue> for Credentials { | impl From<&JsonValue> for Credentials { | ||||||
|     fn from(data: &json::JsonValue) -> Self { |     fn from(data: &JsonValue) -> Self { | ||||||
|         let mut credentials = Credentials::default(); |         let mut credentials = Credentials::default(); | ||||||
|         match data { |         match data { | ||||||
|             json::JsonValue::Object(ref d) => { |             JsonValue::Object(ref d) => { | ||||||
|                 credentials.email = extract_json_value(&d, "email").unwrap_or("".to_string()); |                 credentials.email = extract_json_value(&d, "email").unwrap_or("".to_string()); | ||||||
|                 credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); |                 credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); | ||||||
|             } |             } | ||||||
| @ -41,7 +41,7 @@ impl From<&json::JsonValue> for Credentials { | |||||||
| #[test] | #[test] | ||||||
| fn test_credentials() { | fn test_credentials() { | ||||||
|     struct Expect { |     struct Expect { | ||||||
|         data: json::JsonValue, |         data: JsonValue, | ||||||
|         is_empty: bool, |         is_empty: bool, | ||||||
|     } |     } | ||||||
|     let test_cases: [Expect; 2] = [ |     let test_cases: [Expect; 2] = [ | ||||||
|  | |||||||
| @ -1,4 +0,0 @@ | |||||||
| //! includes utility function, that's all !
 |  | ||||||
| mod utils; |  | ||||||
| 
 |  | ||||||
| pub use utils::extract_json_value; |  | ||||||
| @ -1,30 +0,0 @@ | |||||||
| use json::object::Object; |  | ||||||
| 
 |  | ||||||
| /// extracts JSON value from a key
 |  | ||||||
| pub fn extract_json_value(data: &Object, key: &str) -> Option<String> { |  | ||||||
|     match data.get(key) { |  | ||||||
|         Some(u) => match u.as_str() { |  | ||||||
|             Some(s) => return Some(s.to_string()), |  | ||||||
|             None => None, |  | ||||||
|         }, |  | ||||||
|         None => None, |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_extract_json_value() { |  | ||||||
|     let test_cases: [(json::JsonValue, bool, bool); 3] = [ |  | ||||||
|         (json::parse(r#"{"test": ""}"#).unwrap(), true, true), |  | ||||||
|         (json::parse(r#"{}"#).unwrap(), true, false), |  | ||||||
|         (json::parse(r#"[]"#).unwrap(), false, false), |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     for (value, is_valid, has_key) in test_cases { |  | ||||||
|         match value { |  | ||||||
|             json::JsonValue::Object(d) => { |  | ||||||
|                 assert_eq!(has_key, extract_json_value(&d, "test").is_some()); |  | ||||||
|             } |  | ||||||
|             _ => assert!(!is_valid), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -22,7 +22,7 @@ do | |||||||
| 		exit 1 | 		exit 1 | ||||||
| 	fi | 	fi | ||||||
| 
 | 
 | ||||||
| 	if [ "$(cat response.txt | jq -r '.error')" != "invalid credentials" ] | 	if [ "$(cat response.txt | jq -r '.error')" != "url forbidden" ] | ||||||
| 	then | 	then | ||||||
| 		echo "bad data returned, expect : invalid credentials" | 		echo "bad data returned, expect : invalid credentials" | ||||||
| 		exit 1 | 		exit 1 | ||||||
|  | |||||||
| @ -78,7 +78,7 @@ class TestResponse(TestCase): | |||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             resp.json()["error"], |             resp.json()["error"], | ||||||
|             "the url requested does not exist", |             "url not found", | ||||||
|             "bad status returned", |             "bad status returned", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| @ -88,7 +88,7 @@ class TestResponse(TestCase): | |||||||
|         self.assertIsNotNone(resp.json(), "response data must not be empty") |         self.assertIsNotNone(resp.json(), "response data must not be empty") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             resp.json()["error"], |             resp.json()["error"], | ||||||
|             "the incoming request is not valid", |             "bad request", | ||||||
|             "invalid error message returned", |             "invalid error message returned", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| @ -98,7 +98,7 @@ class TestResponse(TestCase): | |||||||
|         self.assertIsNotNone(resp.json(), "response data must not be empty") |         self.assertIsNotNone(resp.json(), "response data must not be empty") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             resp.json()["error"], |             resp.json()["error"], | ||||||
|             "invalid credentials", |             "url forbidden", | ||||||
|             "invalid error message returned", |             "invalid error message returned", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| @ -110,7 +110,7 @@ class TestResponse(TestCase): | |||||||
|         self.assertIsNotNone(resp.json(), "response data must not be empty") |         self.assertIsNotNone(resp.json(), "response data must not be empty") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             resp.json()["error"], |             resp.json()["error"], | ||||||
|             "the url requested does not exist", |             "url not found", | ||||||
|             "invalid error message returned", |             "invalid error message returned", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| @ -133,6 +133,6 @@ class TestResponse(TestCase): | |||||||
|         self.assertIsNotNone(resp.json(), "response data must not be empty") |         self.assertIsNotNone(resp.json(), "response data must not be empty") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             resp.json()["error"], |             resp.json()["error"], | ||||||
|             "the incoming request is not valid", |             "bad request", | ||||||
|             "invalid error message returned", |             "invalid error message returned", | ||||||
|         ) |         ) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user