Compare commits
	
		
			No commits in common. "master" and "v0.3.1" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										597
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										597
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "simple-auth" | name = "simple-auth" | ||||||
| version = "0.3.2" | version = "0.3.1" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
| @ -15,9 +15,6 @@ jwt-simple = "0.11.1" | |||||||
| simple_logger = "4.0.0" | simple_logger = "4.0.0" | ||||||
| log = "0.4.17" | log = "0.4.17" | ||||||
| base64 = "0.13.1" | base64 = "0.13.1" | ||||||
| serde_json = "1.0" |  | ||||||
| 
 |  | ||||||
| http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.6" } |  | ||||||
| 
 | 
 | ||||||
| # 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" | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ cargo build --release | |||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Configuration | ## Configuration | ||||||
|  | 
 | ||||||
| ### Store | ### Store | ||||||
| The store represents the credentials. For now, this a `.txt` file with plain passwords. You have to create one like: | The store represents the credentials. For now, this a `.txt` file with plain passwords. You have to create one like: | ||||||
| ```txt | ```txt | ||||||
| @ -71,7 +72,7 @@ cargo test | |||||||
| * set the following env variables: | * set the following env variables: | ||||||
| ```bash | ```bash | ||||||
| export SIMPLE_AUTH_URL="http://<url>:<port>" | export SIMPLE_AUTH_URL="http://<url>:<port>" | ||||||
| export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THIS ONE IN PRODUCTION ! | export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THE ONE IN PRODUCTION ! | ||||||
| ``` | ``` | ||||||
| * run the server (if no one is running remotly) | * run the server (if no one is running remotly) | ||||||
| * run curl tests | * run curl tests | ||||||
| @ -79,14 +80,14 @@ export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THIS ONE IN PROD | |||||||
| cd tests/bash/ | cd tests/bash/ | ||||||
| ./curling.bash && echo "passed" | ./curling.bash && echo "passed" | ||||||
| ``` | ``` | ||||||
| * run python tests | * run python requests tests | ||||||
| ```bash | ```bash | ||||||
| # create a python venv | # create a python venv | ||||||
| cd tests/python | cd tests/python | ||||||
| python3 -m venv venv | python3 -m venv venv | ||||||
| source venv/bin/activate | source venv/bin/activate | ||||||
| 
 | 
 | ||||||
| # install the requirements | # intall the requirements | ||||||
| pip install -r requirements | pip install -r requirements | ||||||
| 
 | 
 | ||||||
| # launch the tests | # launch the tests | ||||||
| @ -96,5 +97,5 @@ python -m unittest | |||||||
| ## Documentation | ## Documentation | ||||||
| ```bash | ```bash | ||||||
| # add the '--open' arg to open the doc on a browser | # add the '--open' arg to open the doc on a browser | ||||||
| cargo doc -r --no-deps | cargo doc --no-deps | ||||||
| ``` | ``` | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| //! **config** module provides `Config` struct to load and validate `.ini` file.
 | //! provides `Config` struct to load and validate `.ini` file
 | ||||||
| 
 |  | ||||||
| mod config; | mod config; | ||||||
| 
 | 
 | ||||||
| pub use config::Config; | pub use config::Config; | ||||||
|  | |||||||
							
								
								
									
										93
									
								
								src/http/message.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/http/message.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | |||||||
|  | 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()); | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/http/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/http/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | //! 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; | ||||||
							
								
								
									
										416
									
								
								src/http/request.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								src/http/request.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,416 @@ | |||||||
|  | //! 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), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										172
									
								
								src/http/response.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/http/response.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,172 @@ | |||||||
|  | //! 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,32 +1,29 @@ | |||||||
| use http::{HTTPRequest, HTTPResponse, JSONMessage}; | //! router aims to handle correctly the request corresponding to the target
 | ||||||
| use json::JsonValue; | //! it implements all the logic to build an `HTTPResponse`
 | ||||||
| 
 | 
 | ||||||
|  | use base64; | ||||||
|  | use json; | ||||||
|  | 
 | ||||||
|  | use super::{HTTPMessage, HTTPRequest, HTTPResponse}; | ||||||
| use crate::config::Config; | use crate::config::Config; | ||||||
| use crate::jwt::JWTSigner; | use crate::jwt::JWTSigner; | ||||||
| use crate::message::{JWTMessage, ValidationMessage}; | use crate::stores::{FileStore, Store}; | ||||||
| use crate::stores::{Credentials, FileStore, Store}; |  | ||||||
| 
 | 
 | ||||||
| // TODO: must be mapped with corresponding handler
 | // TODO: must be mapped with corresponding handler
 | ||||||
| const GET_ROUTE: &'static str = "/get/"; | 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 { | ||||||
|     match request.get_body() { |         Some(ref b) => { | ||||||
|         Some(d) => { |             let credentials = store.is_auth(&b.get_data()).await; | ||||||
|             let credentials = Credentials::from(d); |             if credentials.is_none() { | ||||||
|             if credentials.is_empty() { |  | ||||||
|                 log::error!("unable to parse the credentials correctly from the incoming request"); |  | ||||||
|                 return HTTPResponse::as_400(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if !store.is_auth(&credentials).await { |  | ||||||
|                 return HTTPResponse::as_403(); |                 return HTTPResponse::as_403(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -34,16 +31,16 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H | |||||||
|                 match JWTSigner::new(config).await { |                 match JWTSigner::new(config).await { | ||||||
|                     Ok(s) => s, |                     Ok(s) => s, | ||||||
|                     Err(e) => { |                     Err(e) => { | ||||||
|                         let message = JSONMessage::error(&e); |                         let message = HTTPMessage::error(&e); | ||||||
|                         return HTTPResponse::as_500(message); |                         return HTTPResponse::as_500(message); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }; |             }; | ||||||
| 
 | 
 | ||||||
|             match jwt_signer.sign(credentials) { |             match jwt_signer.sign(credentials.unwrap().email) { | ||||||
|                 Ok(t) => send_token(&t), |                 Ok(t) => HTTPResponse::send_token(&t), | ||||||
|                 Err(e) => { |                 Err(e) => { | ||||||
|                     let message = JSONMessage::error(&e); |                     let message = HTTPMessage::error(&e); | ||||||
|                     return HTTPResponse::as_500(message); |                     return HTTPResponse::as_500(message); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @ -52,10 +49,10 @@ async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> H | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// handle_validate 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(); | ||||||
|     } |     } | ||||||
| @ -64,10 +61,11 @@ 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 = ValidationMessage::default(); |                 let mut message = HTTPMessage::default(); | ||||||
|                 message.set_reason("no token provided in the request body"); |                 message.put("valid", "false"); | ||||||
|  |                 message.put("reason", "no token provided in the request body"); | ||||||
|  |                 let json = message.try_into().unwrap(); | ||||||
| 
 | 
 | ||||||
|                 let json = json::parse(&serde_json::to_string(&message).unwrap()).unwrap(); |  | ||||||
|                 return HTTPResponse::as_200(Some(json)); |                 return HTTPResponse::as_200(Some(json)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @ -77,29 +75,30 @@ 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 = JSONMessage::error(&e); |                 let message = HTTPMessage::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 = ValidationMessage::default(); |     let mut message = HTTPMessage::default(); | ||||||
|     match jwt_signer.validate(&token) { |     match jwt_signer.validate(&token) { | ||||||
|         Ok(()) => { |         Ok(()) => { | ||||||
|             message.set_valid(true); |             message.put("valid", "true"); | ||||||
|         } |         } | ||||||
|         Err(e) => { |         Err(e) => { | ||||||
|             message.set_reason(&e); |             message.put("valid", "false"); | ||||||
|  |             message.put("reason", &e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let json: JsonValue = json::parse(&serde_json::to_string(&message).unwrap()).unwrap(); |     let json: json::JsonValue = message.try_into().unwrap(); | ||||||
|     HTTPResponse::as_200(Some(json)) |     HTTPResponse::as_200(Some(json)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// handle_public_key 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(); | ||||||
|     } |     } | ||||||
| @ -108,7 +107,7 @@ async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &st | |||||||
|         match JWTSigner::new(config).await { |         match JWTSigner::new(config).await { | ||||||
|             Ok(s) => s, |             Ok(s) => s, | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 let message = JSONMessage::error(&e); |                 let message = HTTPMessage::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,18 +115,25 @@ async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &st | |||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let public_key = jwt_signer.get_public_key(); |     let public_key = jwt_signer.get_public_key(); | ||||||
|     let message = serde_json::to_string(&JWTMessage::with_pubkey(public_key)).unwrap(); |  | ||||||
| 
 | 
 | ||||||
|     HTTPResponse::as_200(Some(json::parse(&message).unwrap())) |     let mut message = HTTPMessage::default(); | ||||||
|  |     message.put("pubkey", &base64::encode(public_key)); | ||||||
|  | 
 | ||||||
|  |     let json = message.try_into().unwrap(); | ||||||
|  |     HTTPResponse::as_200(Some(json)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub struct Router; | pub struct Router; | ||||||
| 
 | 
 | ||||||
| impl Router { | impl Router { | ||||||
|     /// route routes the request to the corresponding handler
 |     /// routes the request to the corresponding handling method
 | ||||||
|     pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse { |     pub async fn route(&self, request_str: &str, addr: String, config: Config) -> HTTPResponse { | ||||||
|         let request = HTTPRequest::from(request_str); |         let mut request = HTTPRequest::from(request_str); | ||||||
|         match request.get_target() { |         request.set_addr(addr); | ||||||
|  | 
 | ||||||
|  |         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, | ||||||
| @ -136,27 +142,20 @@ impl Router { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// send_token generates an HTTPResponse with the new token
 | // this MUST be used like a Singleton
 | ||||||
| pub fn send_token(jwt_message: &str) -> HTTPResponse { |  | ||||||
|     let message = if jwt_message != "" { |  | ||||||
|         jwt_message |  | ||||||
|     } else { |  | ||||||
|         r#"{"token": "error.generation.token"}"# |  | ||||||
|     }; |  | ||||||
|     HTTPResponse::as_200(Some(json::parse(message).unwrap())) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // 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 http::HTTPStatusCode; |     use super::HTTPStatusCode; | ||||||
| 
 | 
 | ||||||
|     let router: &Router = &ROUTER; |     let router: &Router = &ROUTER; | ||||||
|     let config: Config = Config::default(); |     let config: Config = Config::default(); | ||||||
|     let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; |     let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; | ||||||
| 
 | 
 | ||||||
|     let response: HTTPResponse = router.route(request_str, config).await; |     let response: HTTPResponse = router.route(request_str, "".to_string(), config).await; | ||||||
|     assert_eq!(HTTPStatusCode::Http400, response.get_status_code()); |     assert_eq!( | ||||||
|  |         HTTPStatusCode::Http400, | ||||||
|  |         response.status_line.get_status_code() | ||||||
|  |     ); | ||||||
| } | } | ||||||
| @ -5,9 +5,6 @@ use serde::{Deserialize, Serialize}; | |||||||
| use std::collections::HashSet; | use std::collections::HashSet; | ||||||
| use tokio::fs; | use tokio::fs; | ||||||
| 
 | 
 | ||||||
| use crate::message::JWTMessage; |  | ||||||
| use crate::stores::Credentials; |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize)] | #[derive(Serialize, Deserialize)] | ||||||
| struct JWTCustomClaims { | struct JWTCustomClaims { | ||||||
|     email: String, |     email: String, | ||||||
| @ -61,8 +58,8 @@ impl JWTSigner { | |||||||
|         verification_options |         verification_options | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// sign builds and signs the token
 |     /// builds and signs the token
 | ||||||
|     pub fn sign(&self, credentials: Credentials) -> Result<String, String> { |     pub fn sign(&self, email: String) -> Result<String, String> { | ||||||
|         let jwt_key = { |         let jwt_key = { | ||||||
|             match RS384KeyPair::from_pem(&self.private_key) { |             match RS384KeyPair::from_pem(&self.private_key) { | ||||||
|                 Ok(k) => k, |                 Ok(k) => k, | ||||||
| @ -72,18 +69,13 @@ impl JWTSigner { | |||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|         let mut claims = Claims::with_custom_claims( |         let mut claims = Claims::with_custom_claims( | ||||||
|             JWTCustomClaims { |             JWTCustomClaims { email }, | ||||||
|                 email: credentials.get_email(), |  | ||||||
|             }, |  | ||||||
|             Duration::from_hours(self.exp_time), |             Duration::from_hours(self.exp_time), | ||||||
|         ); |         ); | ||||||
|         claims.issuer = Some(self.issuer.clone()); |         claims.issuer = Some(self.issuer.clone()); | ||||||
| 
 | 
 | ||||||
|         match jwt_key.sign(claims) { |         match jwt_key.sign(claims) { | ||||||
|             Ok(token) => { |             Ok(token) => Ok(token), | ||||||
|                 // TODO: need to generate the refresh token
 |  | ||||||
|                 return Ok(serde_json::to_string(&JWTMessage::with_access(token)).unwrap()); |  | ||||||
|             } |  | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 return Err(format!("unable to sign the token details={}", e)); |                 return Err(format!("unable to sign the token details={}", e)); | ||||||
|             } |             } | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| //! **jwt** module aims to read `.pem` files and sign/validate the token.
 | //! simple module to read `.pem` files and sign the token
 | ||||||
| 
 |  | ||||||
| mod jwt; | mod jwt; | ||||||
| 
 | 
 | ||||||
| pub use jwt::JWTSigner; | pub use jwt::JWTSigner; | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,8 +1,8 @@ | |||||||
| mod config; | mod config; | ||||||
|  | mod http; | ||||||
| mod jwt; | mod jwt; | ||||||
| mod message; |  | ||||||
| mod router; |  | ||||||
| mod stores; | mod stores; | ||||||
|  | mod utils; | ||||||
| 
 | 
 | ||||||
| use clap::Parser; | use clap::Parser; | ||||||
| use configparser::ini::Ini; | use configparser::ini::Ini; | ||||||
| @ -12,8 +12,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)] | ||||||
| @ -67,7 +67,7 @@ async fn main() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// handle_connection parses the incoming request and builds an HTTP response
 | /// parses the incoming request (partial spec implementation) and build an HTTP response
 | ||||||
| async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) { | async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) { | ||||||
|     log::info!("client connected: {}", addr); |     log::info!("client connected: {}", addr); | ||||||
| 
 | 
 | ||||||
| @ -90,7 +90,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, config).await; |     let response = ROUTER.route(request_string, addr.clone(), 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(); | ||||||
|  | |||||||
| @ -1,48 +0,0 @@ | |||||||
| use serde::Serialize; |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize)] |  | ||||||
| /// JWTMessage aims to have a generic struct to build JSON HTTP response message with JWT informations
 |  | ||||||
| pub struct JWTMessage { |  | ||||||
|     #[serde(skip_serializing_if = "String::is_empty")] |  | ||||||
|     access_token: String, |  | ||||||
|     #[serde(skip_serializing_if = "String::is_empty")] |  | ||||||
|     refresh_token: String, |  | ||||||
|     #[serde(skip_serializing_if = "String::is_empty")] |  | ||||||
|     pubkey: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl JWTMessage { |  | ||||||
|     pub fn with_access(access_token: String) -> Self { |  | ||||||
|         JWTMessage { |  | ||||||
|             access_token: access_token, |  | ||||||
|             refresh_token: "".to_string(), |  | ||||||
|             pubkey: "".to_string(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn with_pubkey(pubkey: String) -> Self { |  | ||||||
|         JWTMessage { |  | ||||||
|             access_token: "".to_string(), |  | ||||||
|             refresh_token: "".to_string(), |  | ||||||
|             pubkey: base64::encode(pubkey), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Default)] |  | ||||||
| /// ValidationMessage aims to build a JSON HTTP response body for JWT validation
 |  | ||||||
| pub struct ValidationMessage { |  | ||||||
|     valid: bool, |  | ||||||
|     #[serde(skip_serializing_if = "String::is_empty")] |  | ||||||
|     reason: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl ValidationMessage { |  | ||||||
|     pub fn set_valid(&mut self, valid: bool) { |  | ||||||
|         self.valid = valid; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn set_reason(&mut self, reason: &str) { |  | ||||||
|         self.reason = reason.to_string(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| //! **message** module holds all structs to manage JSON response body for the authentication.
 |  | ||||||
| 
 |  | ||||||
| mod message; |  | ||||||
| 
 |  | ||||||
| pub use message::{JWTMessage, ValidationMessage}; |  | ||||||
| @ -1,4 +0,0 @@ | |||||||
| //! **router** module includes all the handlers to get and validate JWT.
 |  | ||||||
| 
 |  | ||||||
| mod router; |  | ||||||
| pub use router::ROUTER; |  | ||||||
| @ -1,9 +1,10 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
|  | use json; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
| 
 | 
 | ||||||
| use super::store::{Credentials, Store}; | use super::store::{Credentials, Store}; | ||||||
| 
 | 
 | ||||||
| /// FileStore references a credentials store file
 | /// references a credentials store file
 | ||||||
| pub struct FileStore { | pub struct FileStore { | ||||||
|     path: String, |     path: String, | ||||||
|     credentials: Vec<Credentials>, |     credentials: Vec<Credentials>, | ||||||
| @ -17,9 +18,8 @@ impl FileStore { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// parse_contents loads and reads the file asynchonously
 |     /// loads and reads the file asynchonously
 | ||||||
|     ///
 |     /// parses the file line by line to retrieve the credentials
 | ||||||
|     /// It parses the file line by line to retrieve the credentials
 |  | ||||||
|     async fn parse_contents(&mut self) { |     async fn parse_contents(&mut self) { | ||||||
|         let contents = tokio::fs::read_to_string(&self.path).await; |         let contents = tokio::fs::read_to_string(&self.path).await; | ||||||
|         let mut credentials: Vec<Credentials> = vec![]; |         let mut credentials: Vec<Credentials> = vec![]; | ||||||
| @ -47,31 +47,41 @@ impl FileStore { | |||||||
|         self.credentials = credentials; |         self.credentials = credentials; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// auth checks if the credentials exist in the `FileStore`
 |     /// checks if the credentials exist in the `FileStore`
 | ||||||
|     fn auth(&self, email: String, password: String) -> bool { |     fn auth(&self, email: String, password: String) -> Option<Credentials> { | ||||||
|         let credentials: Vec<&Credentials> = self |         let credentials: Vec<&Credentials> = self | ||||||
|             .credentials |             .credentials | ||||||
|             .iter() |             .iter() | ||||||
|             .filter(|x| *x.get_email() == email && *x.get_password() == password) |             .filter(|x| x.email == email && x.password == password) | ||||||
|             .collect(); |             .collect(); | ||||||
|         if credentials.len() == 1 { |         if credentials.len() == 1 { | ||||||
|             return true; |             // no need to store the password again
 | ||||||
|  |             return Some(Credentials::new( | ||||||
|  |                 credentials[0].email.clone(), | ||||||
|  |                 "".to_string(), | ||||||
|  |             )); | ||||||
|         } |         } | ||||||
|         false |         None | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl Store for FileStore { | impl Store for FileStore { | ||||||
|     async fn is_auth(&mut self, credentials: &Credentials) -> bool { |     async fn is_auth(&mut self, data: &json::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); | ||||||
|             return false; |             return None; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let credentials = Credentials::from(data); | ||||||
|  |         if credentials.is_empty() { | ||||||
|  |             log::error!("unable to parse the credentials correctly from the incoming request"); | ||||||
|  |             return None; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         self.parse_contents().await; |         self.parse_contents().await; | ||||||
|         self.auth(credentials.get_email(), credentials.get_password()) |         self.auth(credentials.email, credentials.password) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -80,14 +90,13 @@ async fn test_store() { | |||||||
|     use std::env; |     use std::env; | ||||||
| 
 | 
 | ||||||
|     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); |     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); | ||||||
|  |     // TODO: path::Path should be better
 | ||||||
|     let store_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "store.txt"); |     let store_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "store.txt"); | ||||||
| 
 | 
 | ||||||
|     let mut store = FileStore::new(store_path); |     let mut store = FileStore::new(store_path); | ||||||
| 
 | 
 | ||||||
|     let data = json::parse(r#"{"email": "toto@toto.fr", "password": "tata"}"#).unwrap(); |     let data = json::parse(r#"{"email": "toto@toto.fr", "password": "tata"}"#).unwrap(); | ||||||
|     let credentials = Credentials::from(&data); |     let credentials = store.is_auth(&data).await; | ||||||
|     assert_eq!(credentials.get_email(), "toto@toto.fr"); |     assert_eq!(false, credentials.is_none()); | ||||||
| 
 |     assert_eq!(credentials.unwrap().email, "toto@toto.fr"); | ||||||
|     let is_auth = store.is_auth(&credentials).await; |  | ||||||
|     assert_eq!(true, is_auth); |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| //! **store** module lists interfaces available to check credentials. Each store must implement the trait `is_auth`.
 | //! store module lists interfaces available to check request credentials
 | ||||||
| //!
 | //! each store must implement the trait `is_auth`
 | ||||||
| //! For now one store is available:
 | //! two stores are available :
 | ||||||
| //! * `FileStore`: credentials stored in a text file (like **/etc/passwd**)
 | //! * `FileStore`: credentials stored in a text file (like **/etc/passwd**)
 | ||||||
|  | //! * `DBStore`: credentials stored in a database (TODO)
 | ||||||
| 
 | 
 | ||||||
| mod file; | mod file; | ||||||
| mod store; | mod store; | ||||||
|  | |||||||
| @ -1,55 +1,47 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use json::JsonValue; | use json; | ||||||
| use serde::Deserialize; | 
 | ||||||
|  | use crate::utils::extract_json_value; | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| pub trait Store { | pub trait Store { | ||||||
|     async fn is_auth(&mut self, data: &Credentials) -> bool; |     async fn is_auth(&mut self, data: &json::JsonValue) -> Option<Credentials>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Default, Debug, Deserialize)] | #[derive(Default, Debug)] | ||||||
| pub struct Credentials { | pub struct Credentials { | ||||||
|     email: String, |     pub email: String, | ||||||
|     password: String, |     pub password: String, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Credentials represents the incoming user credentials for authentication checking
 |  | ||||||
| impl Credentials { | impl Credentials { | ||||||
|     pub fn new(email: String, password: String) -> Self { |     pub fn new(email: String, password: String) -> Self { | ||||||
|         Credentials { email, password } |         Credentials { email, password } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn get_email(&self) -> String { |  | ||||||
|         self.email.clone() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_password(&self) -> String { |  | ||||||
|         self.password.clone() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn is_empty(&self) -> bool { |     pub fn is_empty(&self) -> bool { | ||||||
|         self.email == "" || self.password == "" |         self.email == "" || self.password == "" | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO: could be less restrictive with `From<&str>`
 | 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 res = serde_json::from_str(&data.dump()); |         match data { | ||||||
|         match res { |             json::JsonValue::Object(ref d) => { | ||||||
|             Ok(c) => c, |                 credentials.email = extract_json_value(&d, "email").unwrap_or("".to_string()); | ||||||
|             Err(e) => { |                 credentials.password = extract_json_value(&d, "password").unwrap_or("".to_string()); | ||||||
|                 log::warn!("unable to deserialize credentials: {}", e); |  | ||||||
|                 return Credentials::default(); |  | ||||||
|             } |             } | ||||||
|  |             _ => return credentials, | ||||||
|         } |         } | ||||||
|  |         credentials | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[test] | #[test] | ||||||
| fn test_credentials() { | fn test_credentials() { | ||||||
|     struct Expect { |     struct Expect { | ||||||
|         data: JsonValue, |         data: json::JsonValue, | ||||||
|         is_empty: bool, |         is_empty: bool, | ||||||
|     } |     } | ||||||
|     let test_cases: [Expect; 2] = [ |     let test_cases: [Expect; 2] = [ | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | //! includes utility function, that's all !
 | ||||||
|  | mod utils; | ||||||
|  | 
 | ||||||
|  | pub use utils::extract_json_value; | ||||||
							
								
								
									
										30
									
								
								src/utils/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/utils/utils.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | 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), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -16,31 +16,15 @@ fi | |||||||
| for i in {0..10} | for i in {0..10} | ||||||
| do | do | ||||||
| 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"username":"toto", "password":"tutu"}') | 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"username":"toto", "password":"tutu"}') | ||||||
| 	if [ $http_response != "400" ] |  | ||||||
| 	then |  | ||||||
| 		echo "bad http status code : ${http_response}, expect 400" |  | ||||||
| 		exit 1 |  | ||||||
| 	fi |  | ||||||
| 
 |  | ||||||
| 	if [ "$(cat response.txt | jq -r '.error')" != "bad request" ] |  | ||||||
| 	then |  | ||||||
| 		echo "bad data returned, expect : bad request" |  | ||||||
| 		exit 1 |  | ||||||
| 	fi |  | ||||||
| done |  | ||||||
| 
 |  | ||||||
| for i in {0..10} |  | ||||||
| do |  | ||||||
| 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"email":"toto", "password":"tutu"}') |  | ||||||
| 	if [ $http_response != "403" ] | 	if [ $http_response != "403" ] | ||||||
| 	then | 	then | ||||||
| 		echo "bad http status code : ${http_response}, expect 403" | 		echo "bad http status code : ${http_response}, expect 200" | ||||||
| 		exit 1 | 		exit 1 | ||||||
| 	fi | 	fi | ||||||
| 
 | 
 | ||||||
| 	if [ "$(cat response.txt | jq -r '.error')" != "url forbidden" ] | 	if [ "$(cat response.txt | jq -r '.error')" != "invalid credentials" ] | ||||||
| 	then | 	then | ||||||
| 		echo "bad data returned, expect : url forbidden" | 		echo "bad data returned, expect : invalid credentials" | ||||||
| 		exit 1 | 		exit 1 | ||||||
| 	fi | 	fi | ||||||
| done | done | ||||||
| @ -51,7 +35,7 @@ do | |||||||
| 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/ge/ -d '{"username":"toto", "password":"tutu"}') | 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/ge/ -d '{"username":"toto", "password":"tutu"}') | ||||||
| 	if [ $http_response != "404" ] | 	if [ $http_response != "404" ] | ||||||
| 	then | 	then | ||||||
| 		echo "bad http status code : ${http_response}, expect 404" | 		echo "bad http status code : ${http_response}, expect 400" | ||||||
| 		exit 1 | 		exit 1 | ||||||
| 	fi | 	fi | ||||||
| done | done | ||||||
| @ -62,7 +46,7 @@ do | |||||||
| 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/pubkey/) | 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/pubkey/) | ||||||
| 	if [ $http_response != "200" ] | 	if [ $http_response != "200" ] | ||||||
| 	then | 	then | ||||||
| 		echo "bad http status code : ${http_response}, expect 200" | 		echo "bad http status code : ${http_response}, expect 400" | ||||||
| 		exit 1 | 		exit 1 | ||||||
| 	fi | 	fi | ||||||
| done | done | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ class TestResponse(TestCase): | |||||||
|         self.assertEqual(resp.status_code, 200, "bad status code returned") |         self.assertEqual(resp.status_code, 200, "bad status code returned") | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
| 
 | 
 | ||||||
|         token = resp.json()["access_token"] |         token = resp.json()["token"] | ||||||
|         jwt_decoded = jwt.decode( |         jwt_decoded = jwt.decode( | ||||||
|             token, |             token, | ||||||
|             pubkey or self.pub_key, |             pubkey or self.pub_key, | ||||||
| @ -48,14 +48,14 @@ class TestResponse(TestCase): | |||||||
|         ) |         ) | ||||||
|         self.assertEqual(resp.status_code, 200, "bad status code returned") |         self.assertEqual(resp.status_code, 200, "bad status code returned") | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
|         self.assertEqual(resp.json()["valid"], False, "bad status returned") |         self.assertEqual(resp.json()["valid"], "false", "bad status returned") | ||||||
|         self.assertEqual(resp.json()["reason"], "no token provided in the request body") |         self.assertEqual(resp.json()["reason"], "no token provided in the request body") | ||||||
| 
 | 
 | ||||||
|     def test_validate_target_empty_token(self): |     def test_validate_target_empty_token(self): | ||||||
|         resp = requests.post(URL + "/validate/", json={"tutu": "tutu", "token": ""}) |         resp = requests.post(URL + "/validate/", json={"tutu": "tutu", "token": ""}) | ||||||
|         self.assertEqual(resp.status_code, 200, "bad status code returned") |         self.assertEqual(resp.status_code, 200, "bad status code returned") | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
|         self.assertEqual(resp.json()["valid"], False, "bad status returned") |         self.assertEqual(resp.json()["valid"], "false", "bad status returned") | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             resp.json()["reason"], |             resp.json()["reason"], | ||||||
|             "token validation failed details=JWT compact encoding error", |             "token validation failed details=JWT compact encoding error", | ||||||
| @ -67,7 +67,7 @@ class TestResponse(TestCase): | |||||||
|         resp = requests.post(URL + "/validate/", json={"token": token}) |         resp = requests.post(URL + "/validate/", json={"token": token}) | ||||||
|         self.assertEqual(resp.status_code, 200, "bad status code returned") |         self.assertEqual(resp.status_code, 200, "bad status code returned") | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |         self.assertIsNotNone(resp.json(), "response data can't be empty") | ||||||
|         self.assertEqual(resp.json()["valid"], True, "bad status returned") |         self.assertEqual(resp.json()["valid"], "true", "bad status returned") | ||||||
| 
 | 
 | ||||||
|     # TODO: must be updated after implementing `/refresh/` url handler |     # TODO: must be updated after implementing `/refresh/` url handler | ||||||
|     def test_refresh_target(self): |     def test_refresh_target(self): | ||||||
| @ -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"], | ||||||
|             "url not found", |             "the url requested does not exist", | ||||||
|             "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"], | ||||||
|             "bad request", |             "the incoming request is not valid", | ||||||
|             "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"], | ||||||
|             "url forbidden", |             "invalid credentials", | ||||||
|             "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"], | ||||||
|             "url not found", |             "the url requested does not exist", | ||||||
|             "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"], | ||||||
|             "bad request", |             "the incoming request is not valid", | ||||||
|             "invalid error message returned", |             "invalid error message returned", | ||||||
|         ) |         ) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user