Compare commits
	
		
			No commits in common. "master" and "v0.1.0" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -4,4 +4,3 @@ simple-auth | |||||||
| 
 | 
 | ||||||
| tests/python/__pycache__ | tests/python/__pycache__ | ||||||
| tests/bash/response.txt | tests/bash/response.txt | ||||||
| tests/data/*.ini |  | ||||||
|  | |||||||
							
								
								
									
										626
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										626
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										12
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.toml
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "simple-auth" | name = "simple-auth" | ||||||
| version = "0.3.2" | version = "0.1.0" | ||||||
| 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 | ||||||
| @ -12,12 +12,6 @@ regex = "1" | |||||||
| tokio = { version = "1.21.1", features = ["full"] } | tokio = { version = "1.21.1", features = ["full"] } | ||||||
| async-trait = "0.1.57" | async-trait = "0.1.57" | ||||||
| jwt-simple = "0.11.1" | jwt-simple = "0.11.1" | ||||||
| simple_logger = "4.0.0" |  | ||||||
| log = "0.4.17" |  | ||||||
| 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" | ||||||
| @ -33,7 +27,3 @@ features = ["derive"] | |||||||
| [dependencies.async-std] | [dependencies.async-std] | ||||||
| version = "1.6" | version = "1.6" | ||||||
| features = ["attributes"] | features = ["attributes"] | ||||||
| 
 |  | ||||||
| [dependencies.serde] |  | ||||||
| version = "1.0" |  | ||||||
| features = ["derive"] |  | ||||||
|  | |||||||
							
								
								
									
										33
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								README.md
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
| # simple-auth | # simple-auth | ||||||
| 
 | 
 | ||||||
| A little web server providing JWT token for auth user. | A little web server providing JWT token for auth auser. | ||||||
| 
 | 
 | ||||||
| ## Build | ## Build | ||||||
| ```bash | ```bash | ||||||
| @ -8,11 +8,12 @@ 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 | ||||||
| # acts as a comment (only on a start line) | # acts as a comment (only on a start line) | ||||||
| <email>:<password> | <username>:<password> | ||||||
| ``` | ``` | ||||||
| **WARN**: the file should have a chmod to **600**. | **WARN**: the file should have a chmod to **600**. | ||||||
| 
 | 
 | ||||||
| @ -44,19 +45,9 @@ expiration_time = 2 # in hours | |||||||
| ```bash | ```bash | ||||||
| ./simple-auth <ini_path> | ./simple-auth <ini_path> | ||||||
| 
 | 
 | ||||||
| # get a JWT | curl http://<ip>:<port>/get/ -d '{"username":"<user>", "password":"<password>"}' | ||||||
| curl http://<ip>:<port>/get/ -d '{"email":"<email>", "password":"<password>"}' |  | ||||||
| # should returned | # should returned | ||||||
| {"token":"<header>.<payload>.<signature>"} | {"token":"<header>.<payload>.<signature>"} | ||||||
| 
 |  | ||||||
| # validate a JWT |  | ||||||
| curl http://<ip>:<port>/validate/ -d '{"token":"<header>.<payload>.<signature>"}' |  | ||||||
| # should returned (if valid) |  | ||||||
| {"valid":"true"} |  | ||||||
| 
 |  | ||||||
| # get the public key for local validation |  | ||||||
| curl http://<ip>:<port>/pubkey/ |  | ||||||
| {"pubkey":"<b64_encoded_public_key>"} |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## Test | ## Test | ||||||
| @ -67,34 +58,28 @@ cargo test | |||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### integration tests | ### integration tests | ||||||
| * do the **configuration** step for your env tests | * run the server locally or remotly (the URL must be changed if needed in `curling.bash` and `test_requests.py`) | ||||||
| * set the following env variables: |  | ||||||
| ```bash |  | ||||||
| export SIMPLE_AUTH_URL="http://<url>:<port>" |  | ||||||
| export SIMPLE_AUTH_PUB_KEY="<path_to_pem_pub_key>" # DO NOT USE THIS ONE IN PRODUCTION ! |  | ||||||
| ``` |  | ||||||
| * run the server (if no one is running remotly) |  | ||||||
| * run curl tests | * run curl tests | ||||||
| ```bash | ```bash | ||||||
| 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 | ||||||
| python -m unittest | python -m unitest | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ## 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,130 +0,0 @@ | |||||||
| use configparser::ini::Ini; |  | ||||||
| use std::str::FromStr; |  | ||||||
| 
 |  | ||||||
| #[derive(Clone)] |  | ||||||
| pub struct Config { |  | ||||||
|     pub jwt_exp_time: u64, |  | ||||||
|     pub jwt_issuer: String, |  | ||||||
|     pub jwt_priv_key: String, |  | ||||||
|     pub jwt_pub_key: String, |  | ||||||
|     pub filestore_path: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Default for Config { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         Config { |  | ||||||
|             jwt_exp_time: 0, |  | ||||||
|             jwt_issuer: "".to_string(), |  | ||||||
|             jwt_priv_key: "".to_string(), |  | ||||||
|             jwt_pub_key: "".to_string(), |  | ||||||
|             filestore_path: "".to_string(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl TryFrom<Ini> for Config { |  | ||||||
|     type Error = String; |  | ||||||
|     fn try_from(config: Ini) -> Result<Self, Self::Error> { |  | ||||||
|         let exp_time = config |  | ||||||
|             .get("jwt", "expiration_time") |  | ||||||
|             .unwrap_or("".to_string()); |  | ||||||
|         let jwt_exp_time = { |  | ||||||
|             match u64::from_str(&exp_time) { |  | ||||||
|                 Ok(v) => v, |  | ||||||
|                 Err(e) => { |  | ||||||
|                     log::error!( |  | ||||||
|                         "unable to convert JWT expiration time into u64 details={}", |  | ||||||
|                         e |  | ||||||
|                     ); |  | ||||||
|                     0 |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|         let config = Config { |  | ||||||
|             jwt_exp_time, |  | ||||||
|             jwt_issuer: config.get("jwt", "issuer").unwrap_or("".to_string()), |  | ||||||
|             jwt_pub_key: config.get("jwt", "public_key").unwrap_or("".to_string()), |  | ||||||
|             jwt_priv_key: config.get("jwt", "private_key").unwrap_or("".to_string()), |  | ||||||
|             filestore_path: config.get("store", "path").unwrap_or("".to_string()), |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         if !config.validate() { |  | ||||||
|             return Err("ini file configuration validation failed".to_string()); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(config) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Config { |  | ||||||
|     /// validates config ini file
 |  | ||||||
|     fn validate(&self) -> bool { |  | ||||||
|         if self.jwt_exp_time <= 0 { |  | ||||||
|             log::error!("invalid config parameter: JWT expiration time is negative or equals to 0"); |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if self.jwt_issuer == "" { |  | ||||||
|             log::error!("invalid config parameter: JWT issuer is empty"); |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if self.jwt_pub_key == "" { |  | ||||||
|             log::error!("invalid config parameter: JWT public key file path is empty"); |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if self.jwt_priv_key == "" { |  | ||||||
|             log::error!("invalid config parameter: JWT private key file path is empty"); |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if self.filestore_path == "" { |  | ||||||
|             log::error!("invalid config parameter: filestore path is empty"); |  | ||||||
|             return false; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         true |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_config() { |  | ||||||
|     use std::env; |  | ||||||
| 
 |  | ||||||
|     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); |  | ||||||
| 
 |  | ||||||
|     let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "config.ini"); |  | ||||||
|     let mut config = Ini::new(); |  | ||||||
|     let _r = config.load(config_path); |  | ||||||
| 
 |  | ||||||
|     let router_config = Config::try_from(config); |  | ||||||
|     assert!(router_config.is_ok()); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_bad_config() { |  | ||||||
|     use std::env; |  | ||||||
| 
 |  | ||||||
|     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); |  | ||||||
| 
 |  | ||||||
|     let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "bad_config.ini"); |  | ||||||
|     let mut config = Ini::new(); |  | ||||||
|     let _r = config.load(config_path); |  | ||||||
| 
 |  | ||||||
|     let router_config = Config::try_from(config); |  | ||||||
|     assert!(router_config.is_err()); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_bad_config_path() { |  | ||||||
|     use std::env; |  | ||||||
| 
 |  | ||||||
|     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); |  | ||||||
| 
 |  | ||||||
|     let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "con.ini"); |  | ||||||
|     let mut config = Ini::new(); |  | ||||||
| 
 |  | ||||||
|     let result = config.load(config_path); |  | ||||||
|     assert!(result.is_err()); |  | ||||||
| } |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| //! **config** module provides `Config` struct to load and validate `.ini` file.
 |  | ||||||
| 
 |  | ||||||
| mod config; |  | ||||||
| 
 |  | ||||||
| pub use config::Config; |  | ||||||
							
								
								
									
										9
									
								
								src/http/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/http/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | //! http module includes tools to parse an HTTP request and build and HTTP response
 | ||||||
|  | 
 | ||||||
|  | pub mod request; | ||||||
|  | pub mod response; | ||||||
|  | pub mod router; | ||||||
|  | 
 | ||||||
|  | pub use request::HTTPRequest; | ||||||
|  | pub use response::{HTTPResponse, HTTPStatusCode}; | ||||||
|  | pub use router::{Config, ROUTER}; | ||||||
							
								
								
									
										370
									
								
								src/http/request.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										370
									
								
								src/http/request.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,370 @@ | |||||||
|  | //! request handles properly the incoming request
 | ||||||
|  | //! it will parse the request according to the HTTP message specifications
 | ||||||
|  | //! see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
 | ||||||
|  | //! NOTE: only few parts of the specification has been implemented
 | ||||||
|  | 
 | ||||||
|  | use json; | ||||||
|  | use lazy_static::lazy_static; | ||||||
|  | use regex::Regex; | ||||||
|  | use std::collections::VecDeque; | ||||||
|  | 
 | ||||||
|  | type RequestParts = (String, VecDeque<String>, String); | ||||||
|  | 
 | ||||||
|  | const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; | ||||||
|  | const NULL_CHAR: &'static str = "\0"; | ||||||
|  | 
 | ||||||
|  | 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!( | ||||||
|  |                 "error occurred during request body parsing err={}", | ||||||
|  |                 e | ||||||
|  |             )), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Represents an HTTP request (headers are not parsed)
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct HTTPRequest { | ||||||
|  |     pub start_line: HTTPStartLine, | ||||||
|  |     pub body: Option<HTTPBody>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl HTTPRequest { | ||||||
|  |     /// split correctly the incoming request in order to get :
 | ||||||
|  |     /// * start_line
 | ||||||
|  |     /// * headers
 | ||||||
|  |     /// * data (if exists)
 | ||||||
|  |     fn get_request_parts(request: &str) -> Result<RequestParts, String> { | ||||||
|  |         // separate the body part from the start_line and the headers
 | ||||||
|  |         let mut request_parts: VecDeque<String> = request | ||||||
|  |             .split(HTTP_REQUEST_SEPARATOR) | ||||||
|  |             .map(|r| r.to_string()) | ||||||
|  |             .collect(); | ||||||
|  | 
 | ||||||
|  |         if request_parts.len() < 3 { | ||||||
|  |             return Err("request has no enough informations to be correctly parsed".to_string()); | ||||||
|  |         } | ||||||
|  |         let start_line = request_parts.pop_front().unwrap(); | ||||||
|  |         let body = request_parts.pop_back().unwrap(); | ||||||
|  | 
 | ||||||
|  |         Ok((start_line, request_parts, body)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// parse the request by spliting the incoming request with the separator `\r\n`
 | ||||||
|  |     fn parse(request: &str) -> Result<HTTPRequest, String> { | ||||||
|  |         let request = request.to_string(); | ||||||
|  | 
 | ||||||
|  |         match HTTPRequest::get_request_parts(&request) { | ||||||
|  |             Ok(rp) => { | ||||||
|  |                 let mut request = HTTPRequest::default(); | ||||||
|  | 
 | ||||||
|  |                 let start_line = HTTPStartLine::parse(&rp.0); | ||||||
|  |                 match start_line { | ||||||
|  |                     Ok(v) => request.start_line = v, | ||||||
|  |                     Err(e) => eprintln!("error occurred while parsing start_line err={}", e), | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 let body = HTTPBody::try_from(rp.2); | ||||||
|  |                 match body { | ||||||
|  |                     Ok(v) => request.body = Some(v), | ||||||
|  |                     Err(e) => eprintln!("error occurred during body parsing err={}", e), | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return Ok(request); | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 return Err(format!("error occurred getting request parts err={}", e)); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[allow(dead_code)] | ||||||
|  |     pub fn is_valid(&self) -> bool { | ||||||
|  |         return self.start_line.is_valid(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Default for HTTPRequest { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         HTTPRequest { | ||||||
|  |             start_line: HTTPStartLine::default(), | ||||||
|  |             body: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<&str> for HTTPRequest { | ||||||
|  |     fn from(request: &str) -> Self { | ||||||
|  |         match Self::parse(request) { | ||||||
|  |             Ok(v) => v, | ||||||
|  |             Err(v) => { | ||||||
|  |                 eprintln!("{}", format!("[ERR]: {v}")); | ||||||
|  |                 return HTTPRequest::default(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn test_request() { | ||||||
|  |     struct Expect { | ||||||
|  |         start_line: String, | ||||||
|  |         body: Option<String>, | ||||||
|  |         is_valid: bool, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let test_cases: [(String, Expect); 11] = [ | ||||||
|  |         ( | ||||||
|  |             "POST /get/ HTTP/1.1\r\n\r\n".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "POST /get/ HTTP/1.1".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: true, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "POST /refresh/ HTTP/2\r\n\r\n".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "POST /refresh/ HTTP/2".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: true, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "POST /validate/ HTTP/1.0\r\n\r\n".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "POST /validate/ HTTP/1.0".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: true, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "GET / HTTP/1.1\r\n\r\n".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "GET / HTTP/1.1".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: true, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         // intentionally add HTTP with no version number
 | ||||||
|  |         ( | ||||||
|  |             "OPTIONS /admin/2 HTTP/\r\nContent-Type: application/json\r\n".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "  UNKNOWN".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: false, | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "POST HTTP".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "  UNKNOWN".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: false, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "  UNKNOWN".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: false, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "fjlqskjd /oks?id=65 HTTP/2\r\n\r\n".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: true, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             "   ".to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "  UNKNOWN".to_string(), | ||||||
|  |                 body: None, | ||||||
|  |                 is_valid: false, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             r#"lm //// skkss\r\ndkldklkdl\r\n"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}""#.to_string(), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "  UNKNOWN".to_string(), | ||||||
|  |                 body: Some(r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#.to_string()), | ||||||
|  |                 is_valid: false, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         ( | ||||||
|  |             format!("{}\r\nuselessheaders\r\n{}", "POST /refresh/ HTTP/1.1", r#"{"access_token": "toto", "refresh_token": "tutu"}"#), | ||||||
|  |             Expect { | ||||||
|  |                 start_line: "POST /refresh/ HTTP/1.1".to_string(), | ||||||
|  |                 body: Some(r#"{"access_token":"toto","refresh_token":"tutu"}"#.to_string()), | ||||||
|  |                 is_valid: true, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     for (request, expect) in test_cases { | ||||||
|  |         let http_request = HTTPRequest::from(request.as_str()); | ||||||
|  |         println!("{:?}", http_request); | ||||||
|  |         assert_eq!(expect.is_valid, http_request.is_valid()); | ||||||
|  | 
 | ||||||
|  |         let start_line: String = http_request.start_line.into(); | ||||||
|  |         assert_eq!(expect.start_line, start_line); | ||||||
|  | 
 | ||||||
|  |         match http_request.body { | ||||||
|  |             Some(v) => { | ||||||
|  |                 assert_eq!(expect.body.unwrap(), v.data.dump()) | ||||||
|  |             } | ||||||
|  |             None => continue, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn test_http_body() { | ||||||
|  |     let test_cases: [(&str, bool); 3] = [ | ||||||
|  |         ("hello, how are you ?", false), | ||||||
|  |         ("qsdfqsdffqsdffsq", false), | ||||||
|  |         ( | ||||||
|  |             r#"{"access_token":"AAAAAAAAAAAA.BBBBBBBBBB.CCCCCCCCCC","refresh_token": "DDDDDDDDDDD.EEEEEEEEEEE.FFFFF"}"#, | ||||||
|  |             true, | ||||||
|  |         ), | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     for (body, is_valid) in test_cases { | ||||||
|  |         match HTTPBody::try_from(body.to_string()) { | ||||||
|  |             Ok(_) => assert!(is_valid), | ||||||
|  |             Err(_) => assert!(!is_valid), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										145
									
								
								src/http/response.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/http/response.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | |||||||
|  | //! 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 crate::http::request::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() -> Self { | ||||||
|  |         let mut response = Self::default(); | ||||||
|  | 
 | ||||||
|  |         response | ||||||
|  |             .status_line | ||||||
|  |             .set_status_code(HTTPStatusCode::Http500); | ||||||
|  |         response.body = 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() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // TODO: need to be adjust to accept `json::JsonValue`
 | ||||||
|  |     pub fn as_200(token: String) -> Self { | ||||||
|  |         let mut response = Self::default(); | ||||||
|  | 
 | ||||||
|  |         response | ||||||
|  |             .status_line | ||||||
|  |             .set_status_code(HTTPStatusCode::Http200); | ||||||
|  | 
 | ||||||
|  |         response.body = json::parse(format!(r#"{{"token": "{}"}}"#, token).as_str()).unwrap(); | ||||||
|  | 
 | ||||||
|  |         response | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										251
									
								
								src/http/router.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/http/router.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,251 @@ | |||||||
|  | //! router aims to handle correctly the request corresponding to the target
 | ||||||
|  | //! it implements all the logic to build an `HTTPResponse`
 | ||||||
|  | 
 | ||||||
|  | use super::{HTTPRequest, HTTPResponse}; | ||||||
|  | use crate::stores::FileStore; | ||||||
|  | use crate::stores::Store; | ||||||
|  | use configparser::ini::Ini; | ||||||
|  | use jwt_simple::prelude::*; | ||||||
|  | use lazy_static::lazy_static; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::future::Future; | ||||||
|  | use std::pin::Pin; | ||||||
|  | use std::str::FromStr; | ||||||
|  | 
 | ||||||
|  | type FuturePinned<HTTPResponse> = Pin<Box<dyn Future<Output = HTTPResponse>>>; | ||||||
|  | type Handler = fn(HTTPRequest, Config) -> FuturePinned<HTTPResponse>; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Config { | ||||||
|  |     jwt_exp_time: u64, | ||||||
|  |     jwt_issuer: String, | ||||||
|  |     jwt_priv_key: String, | ||||||
|  |     jwt_pub_key: String, | ||||||
|  |     filestore_path: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Default for Config { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Config { | ||||||
|  |             jwt_exp_time: 0, | ||||||
|  |             jwt_issuer: "".to_string(), | ||||||
|  |             jwt_priv_key: "".to_string(), | ||||||
|  |             jwt_pub_key: "".to_string(), | ||||||
|  |             filestore_path: "".to_string(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TryFrom<Ini> for Config { | ||||||
|  |     type Error = String; | ||||||
|  |     fn try_from(config: Ini) -> Result<Self, Self::Error> { | ||||||
|  |         let exp_time = config | ||||||
|  |             .get("jwt", "expiration_time") | ||||||
|  |             .unwrap_or("".to_string()); | ||||||
|  |         let jwt_exp_time = { | ||||||
|  |             match u64::from_str(&exp_time) { | ||||||
|  |                 Ok(v) => v, | ||||||
|  |                 Err(e) => { | ||||||
|  |                     eprintln!("unable to convert JWT expiration time into u64 err={}", e); | ||||||
|  |                     0 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         let config = Config { | ||||||
|  |             jwt_exp_time, | ||||||
|  |             jwt_issuer: config.get("jwt", "issuer").unwrap_or("".to_string()), | ||||||
|  |             jwt_pub_key: config.get("jwt", "public_key").unwrap_or("".to_string()), | ||||||
|  |             jwt_priv_key: config.get("jwt", "private_key").unwrap_or("".to_string()), | ||||||
|  |             filestore_path: config.get("store", "path").unwrap_or("".to_string()), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if !config.validate() { | ||||||
|  |             return Err("ini file configuration validation failed".to_string()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         Ok(config) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Config { | ||||||
|  |     /// validates config ini file
 | ||||||
|  |     fn validate(&self) -> bool { | ||||||
|  |         if self.jwt_exp_time <= 0 { | ||||||
|  |             eprintln!("invalid config parameter: JWT expiration time is negative or equals to 0"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if self.jwt_issuer == "" { | ||||||
|  |             eprintln!("invalid config parameter: JWT issuer is empty"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // TODO: check if the file exists and rights are ok
 | ||||||
|  |         if self.jwt_pub_key == "" { | ||||||
|  |             eprintln!("invalid config parameter: JWT public key file path is empty"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // TODO: check if the file exists and rights are ok
 | ||||||
|  |         if self.jwt_priv_key == "" { | ||||||
|  |             eprintln!("invalid config parameter: JWT private key file path is empty"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if self.filestore_path == "" { | ||||||
|  |             eprintln!("invalid config parameter: filestore path is empty"); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         true | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn handle_get(request: HTTPRequest, config: Config) -> FuturePinned<HTTPResponse> { | ||||||
|  |     Box::pin(async move { | ||||||
|  |         let mut store = FileStore::new(config.filestore_path); | ||||||
|  |         match &request.body { | ||||||
|  |             Some(ref b) => { | ||||||
|  |                 let is_auth = store.is_auth(&b.get_data()).await; | ||||||
|  |                 if !is_auth { | ||||||
|  |                     return HTTPResponse::as_403(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 let priv_key_content = { | ||||||
|  |                     match std::fs::read_to_string(config.jwt_priv_key) { | ||||||
|  |                         Ok(c) => c, | ||||||
|  |                         Err(e) => { | ||||||
|  |                             eprintln!("error while reading JWT priv key content err={}", e); | ||||||
|  |                             "".to_string() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                 let jwt_key = { | ||||||
|  |                     match RS384KeyPair::from_pem(priv_key_content.as_str()) { | ||||||
|  |                         Ok(k) => k, | ||||||
|  |                         // TODO: set error in the message body
 | ||||||
|  |                         Err(e) => { | ||||||
|  |                             eprintln!("error occurred while getting private key err={}", e); | ||||||
|  |                             return HTTPResponse::as_500(); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |                 let mut claims = Claims::create(Duration::from_hours(config.jwt_exp_time)); | ||||||
|  |                 claims.issuer = Some(config.jwt_issuer); | ||||||
|  | 
 | ||||||
|  |                 match jwt_key.sign(claims) { | ||||||
|  |                     Ok(token) => HTTPResponse::as_200(token), | ||||||
|  |                     // TODO: set the error in the message body
 | ||||||
|  |                     Err(e) => { | ||||||
|  |                         eprintln!("error occurred while signing the token err={}", e); | ||||||
|  |                         return HTTPResponse::as_500(); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             None => HTTPResponse::as_400(), | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// validates the token by checking:
 | ||||||
|  | /// * expiration time
 | ||||||
|  | fn handle_validate(request: HTTPRequest, _config: Config) -> FuturePinned<HTTPResponse> { | ||||||
|  |     Box::pin(async move { | ||||||
|  |         match &request.body { | ||||||
|  |             Some(ref _b) => { | ||||||
|  |                 // TODO: impl the JWT validation
 | ||||||
|  |                 HTTPResponse::as_200("header.payload.signature".to_string()) | ||||||
|  |             } | ||||||
|  |             None => HTTPResponse::as_400(), | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | lazy_static! { | ||||||
|  |     /// defines the map between the URL and its associated callback
 | ||||||
|  |     /// each authorized targets must implement a function returning `FuturePinned<HTTPResponse>`
 | ||||||
|  |     // TODO: a macro should be implemented to mask the implementation details
 | ||||||
|  |     static ref HTTP_METHODS: HashMap<&'static str, Handler> = | ||||||
|  |         HashMap::from( | ||||||
|  |             [ | ||||||
|  |                 ("/get/", handle_get as Handler), | ||||||
|  |                 ("/validate/", handle_validate as Handler) | ||||||
|  |             ] | ||||||
|  |         ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct Router; | ||||||
|  | 
 | ||||||
|  | impl Router { | ||||||
|  |     pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse { | ||||||
|  |         let request = HTTPRequest::from(request_str); | ||||||
|  |         let target = request.start_line.get_target(); | ||||||
|  | 
 | ||||||
|  |         match HTTP_METHODS.get(target.as_str()) { | ||||||
|  |             Some(f) => f(request, config).await, | ||||||
|  |             None => HTTPResponse::as_404(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // this MUST be used like a Singleton
 | ||||||
|  | pub const ROUTER: Router = Router {}; | ||||||
|  | 
 | ||||||
|  | #[tokio::test] | ||||||
|  | async fn test_route() { | ||||||
|  |     use super::HTTPStatusCode; | ||||||
|  | 
 | ||||||
|  |     let router: &Router = &ROUTER; | ||||||
|  |     let config: Config = Config::default(); | ||||||
|  |     let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; | ||||||
|  | 
 | ||||||
|  |     let response: HTTPResponse = router.route(request_str, config).await; | ||||||
|  |     assert_eq!( | ||||||
|  |         HTTPStatusCode::Http400, | ||||||
|  |         response.status_line.get_status_code() | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn test_config() { | ||||||
|  |     use std::env; | ||||||
|  | 
 | ||||||
|  |     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); | ||||||
|  | 
 | ||||||
|  |     // TODO: path::Path should be better
 | ||||||
|  |     let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "config.ini"); | ||||||
|  |     let mut config = Ini::new(); | ||||||
|  |     let _r = config.load(config_path); | ||||||
|  | 
 | ||||||
|  |     let router_config = Config::try_from(config); | ||||||
|  |     assert!(router_config.is_ok()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn test_bad_config() { | ||||||
|  |     use std::env; | ||||||
|  | 
 | ||||||
|  |     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); | ||||||
|  | 
 | ||||||
|  |     // TODO: path::Path should be better
 | ||||||
|  |     let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "bad_config.ini"); | ||||||
|  |     let mut config = Ini::new(); | ||||||
|  |     let _r = config.load(config_path); | ||||||
|  | 
 | ||||||
|  |     let router_config = Config::try_from(config); | ||||||
|  |     assert!(router_config.is_err()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn test_bad_config_path() { | ||||||
|  |     use std::env; | ||||||
|  | 
 | ||||||
|  |     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); | ||||||
|  | 
 | ||||||
|  |     // TODO: path::Path should be better
 | ||||||
|  |     let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "con.ini"); | ||||||
|  |     let mut config = Ini::new(); | ||||||
|  | 
 | ||||||
|  |     let result = config.load(config_path); | ||||||
|  |     assert!(result.is_err()); | ||||||
|  | } | ||||||
							
								
								
									
										132
									
								
								src/jwt/jwt.rs
									
									
									
									
									
								
							
							
						
						
									
										132
									
								
								src/jwt/jwt.rs
									
									
									
									
									
								
							| @ -1,132 +0,0 @@ | |||||||
| use crate::config::Config; |  | ||||||
| use jwt_simple::common::VerificationOptions; |  | ||||||
| use jwt_simple::prelude::*; |  | ||||||
| use serde::{Deserialize, Serialize}; |  | ||||||
| use std::collections::HashSet; |  | ||||||
| use tokio::fs; |  | ||||||
| 
 |  | ||||||
| use crate::message::JWTMessage; |  | ||||||
| use crate::stores::Credentials; |  | ||||||
| 
 |  | ||||||
| #[derive(Serialize, Deserialize)] |  | ||||||
| struct JWTCustomClaims { |  | ||||||
|     email: String, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub struct JWTSigner { |  | ||||||
|     private_key: String, |  | ||||||
|     public_key: String, |  | ||||||
|     issuer: String, |  | ||||||
|     exp_time: u64, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl JWTSigner { |  | ||||||
|     // NOTE: could be included in a Trait: `TryFrom` but difficult to handle with async
 |  | ||||||
|     pub async fn new(config: Config) -> Result<Self, String> { |  | ||||||
|         let mut jwt_signer = JWTSigner { |  | ||||||
|             private_key: "".to_string(), |  | ||||||
|             public_key: "".to_string(), |  | ||||||
|             issuer: config.jwt_issuer, |  | ||||||
|             exp_time: config.jwt_exp_time, |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         match fs::read_to_string(config.jwt_priv_key).await { |  | ||||||
|             Ok(c) => { |  | ||||||
|                 jwt_signer.private_key = c; |  | ||||||
|             } |  | ||||||
|             Err(e) => { |  | ||||||
|                 return Err(format!("unable to read the private key details={}", e)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         match fs::read_to_string(config.jwt_pub_key).await { |  | ||||||
|             Ok(c) => { |  | ||||||
|                 jwt_signer.public_key = c; |  | ||||||
|             } |  | ||||||
|             Err(e) => { |  | ||||||
|                 return Err(format!("unable to read the public key details={}", e)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(jwt_signer) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn get_verification_options(&self) -> VerificationOptions { |  | ||||||
|         let mut verification_options = VerificationOptions::default(); |  | ||||||
| 
 |  | ||||||
|         let mut issuers: HashSet<String> = HashSet::new(); |  | ||||||
|         issuers.insert(self.issuer.clone()); |  | ||||||
|         verification_options.allowed_issuers = Some(issuers); |  | ||||||
| 
 |  | ||||||
|         verification_options |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// sign builds and signs the token
 |  | ||||||
|     pub fn sign(&self, credentials: Credentials) -> Result<String, String> { |  | ||||||
|         let jwt_key = { |  | ||||||
|             match RS384KeyPair::from_pem(&self.private_key) { |  | ||||||
|                 Ok(k) => k, |  | ||||||
|                 Err(e) => { |  | ||||||
|                     return Err(format!("unable to load the private key details={}", e)); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|         let mut claims = Claims::with_custom_claims( |  | ||||||
|             JWTCustomClaims { |  | ||||||
|                 email: credentials.get_email(), |  | ||||||
|             }, |  | ||||||
|             Duration::from_hours(self.exp_time), |  | ||||||
|         ); |  | ||||||
|         claims.issuer = Some(self.issuer.clone()); |  | ||||||
| 
 |  | ||||||
|         match jwt_key.sign(claims) { |  | ||||||
|             Ok(token) => { |  | ||||||
|                 // TODO: need to generate the refresh token
 |  | ||||||
|                 return Ok(serde_json::to_string(&JWTMessage::with_access(token)).unwrap()); |  | ||||||
|             } |  | ||||||
|             Err(e) => { |  | ||||||
|                 return Err(format!("unable to sign the token details={}", e)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn validate(&self, token: &str) -> Result<(), String> { |  | ||||||
|         let verification_options = self.get_verification_options(); |  | ||||||
|         match RS384PublicKey::from_pem(&self.public_key) { |  | ||||||
|             Ok(key) => { |  | ||||||
|                 if let Err(e) = |  | ||||||
|                     key.verify_token::<NoCustomClaims>(token, Some(verification_options)) |  | ||||||
|                 { |  | ||||||
|                     return Err(format!("token validation failed details={}", e)); |  | ||||||
|                 } |  | ||||||
|                 Ok(()) |  | ||||||
|             } |  | ||||||
|             Err(e) => Err(format!( |  | ||||||
|                 "token validation failed, can't read the public key details={}", |  | ||||||
|                 e |  | ||||||
|             )), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     pub fn get_public_key(&self) -> String { |  | ||||||
|         self.public_key.clone() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[tokio::test] |  | ||||||
| async fn test_signer() { |  | ||||||
|     use configparser::ini::Ini; |  | ||||||
|     use std::env; |  | ||||||
| 
 |  | ||||||
|     let root_path = env::var("CARGO_MANIFEST_DIR").unwrap(); |  | ||||||
| 
 |  | ||||||
|     let config_path = format!("{}/{}/{}/{}", root_path, "tests", "data", "config.ini"); |  | ||||||
|     let mut config = Ini::new(); |  | ||||||
|     let _r = config.load(config_path); |  | ||||||
| 
 |  | ||||||
|     let router_config = Config::try_from(config); |  | ||||||
|     assert!(router_config.is_ok()); |  | ||||||
| 
 |  | ||||||
|     let jwt_signer = JWTSigner::new(router_config.unwrap()); |  | ||||||
|     assert!(jwt_signer.await.is_ok()); |  | ||||||
| } |  | ||||||
| @ -1,5 +0,0 @@ | |||||||
| //! **jwt** module aims to read `.pem` files and sign/validate the token.
 |  | ||||||
| 
 |  | ||||||
| mod jwt; |  | ||||||
| 
 |  | ||||||
| pub use jwt::JWTSigner; |  | ||||||
							
								
								
									
										58
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								src/main.rs
									
									
									
									
									
								
							| @ -1,7 +1,4 @@ | |||||||
| mod config; | mod http; | ||||||
| mod jwt; |  | ||||||
| mod message; |  | ||||||
| mod router; |  | ||||||
| mod stores; | mod stores; | ||||||
| 
 | 
 | ||||||
| use clap::Parser; | use clap::Parser; | ||||||
| @ -9,11 +6,9 @@ use configparser::ini::Ini; | |||||||
| use tokio::{ | use tokio::{ | ||||||
|     io::{AsyncReadExt, AsyncWriteExt}, |     io::{AsyncReadExt, AsyncWriteExt}, | ||||||
|     net::{TcpListener, TcpStream}, |     net::{TcpListener, TcpStream}, | ||||||
|     time::{timeout, Duration}, |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use crate::router::ROUTER; | use http::{Config, ROUTER}; | ||||||
| use config::Config; |  | ||||||
| 
 | 
 | ||||||
| #[derive(Parser)] | #[derive(Parser)] | ||||||
| #[clap(author, version, about, long_about = None)] | #[clap(author, version, about, long_about = None)] | ||||||
| @ -24,14 +19,13 @@ struct Cli { | |||||||
| 
 | 
 | ||||||
| #[tokio::main] | #[tokio::main] | ||||||
| async fn main() { | async fn main() { | ||||||
|     simple_logger::init_with_level(log::Level::Info).unwrap(); |  | ||||||
|     let args = Cli::parse(); |     let args = Cli::parse(); | ||||||
| 
 | 
 | ||||||
|     let mut config = Ini::new(); |     let mut config = Ini::new(); | ||||||
|     match config.load(args.config) { |     match config.load(args.config) { | ||||||
|         Ok(c) => c, |         Ok(c) => c, | ||||||
|         Err(e) => { |         Err(e) => { | ||||||
|             log::error!("error while loading the config file details={}", e); |             eprintln!("error while loading the config file, err={}", e); | ||||||
|             std::process::exit(1); |             std::process::exit(1); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| @ -40,61 +34,37 @@ async fn main() { | |||||||
|     let listener = { |     let listener = { | ||||||
|         match TcpListener::bind(&server_url).await { |         match TcpListener::bind(&server_url).await { | ||||||
|             Ok(t) => { |             Ok(t) => { | ||||||
|                 log::info!("server is listening on '{}'", server_url); |                 println!("server is listening on '{}'", server_url); | ||||||
|                 t |                 t | ||||||
|             } |             } | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 log::error!("while initializing tcp listener details={}", e); |                 eprintln!("error occurred while initializing tcp listener err={}", e); | ||||||
|                 std::process::exit(1); |                 std::process::exit(1); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     let router_config: Config = { |     let router_config: Config = if let Ok(c) = Config::try_from(config) { | ||||||
|         match Config::try_from(config) { |         c | ||||||
|             Ok(c) => c, |     } else { | ||||||
|             Err(e) => { |  | ||||||
|                 log::error!("unable to load the configuration details={}", e); |  | ||||||
|         std::process::exit(1); |         std::process::exit(1); | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     loop { |     loop { | ||||||
|         let (stream, addr) = listener.accept().await.unwrap(); |         let (stream, _) = listener.accept().await.unwrap(); | ||||||
|         let conf = router_config.clone(); |         handle_connection(stream, router_config.clone()).await; | ||||||
|         tokio::spawn(handle_connection(stream, addr.to_string(), conf.clone())); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// 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, config: Config) { | ||||||
|     log::info!("client connected: {}", addr); |  | ||||||
| 
 |  | ||||||
|     let mut message = vec![]; |  | ||||||
|     let mut buffer: [u8; 1024] = [0; 1024]; |     let mut buffer: [u8; 1024] = [0; 1024]; | ||||||
|  |     let n = stream.read(&mut buffer).await.unwrap(); | ||||||
| 
 | 
 | ||||||
|     let duration = Duration::from_millis(5); |     let request_string = std::str::from_utf8(&buffer[0..n]).unwrap(); | ||||||
| 
 |  | ||||||
|     // loop until the message is read
 |  | ||||||
|     // the stream can be fragmented so, using a timeout (5ms should be far enough) for the future for completion
 |  | ||||||
|     // after the timeout, the message is "considered" as entirely read
 |  | ||||||
|     loop { |  | ||||||
|         match timeout(duration, stream.read(&mut buffer)).await { |  | ||||||
|             Ok(v) => { |  | ||||||
|                 let n = v.unwrap(); |  | ||||||
|                 message.extend_from_slice(&buffer[0..n]); |  | ||||||
|             } |  | ||||||
|             Err(_e) => break, |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let request_string = std::str::from_utf8(&message).unwrap(); |  | ||||||
|     let response = ROUTER.route(request_string, 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(); | ||||||
|     stream.flush().await.unwrap(); |     stream.flush().await.unwrap(); | ||||||
| 
 |  | ||||||
|     log::info!("connection closed: {}", addr); |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -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,162 +0,0 @@ | |||||||
| use http::{HTTPRequest, HTTPResponse, JSONMessage}; |  | ||||||
| use json::JsonValue; |  | ||||||
| 
 |  | ||||||
| use crate::config::Config; |  | ||||||
| use crate::jwt::JWTSigner; |  | ||||||
| use crate::message::{JWTMessage, ValidationMessage}; |  | ||||||
| use crate::stores::{Credentials, FileStore, Store}; |  | ||||||
| 
 |  | ||||||
| // TODO: must be mapped with corresponding handler
 |  | ||||||
| const GET_ROUTE: &'static str = "/get/"; |  | ||||||
| const VALIDATE_ROUTE: &'static str = "/validate/"; |  | ||||||
| const PUBKEY_ROUTE: &'static str = "/pubkey/"; |  | ||||||
| 
 |  | ||||||
| async fn handle_get(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { |  | ||||||
|     if method.trim().to_lowercase() != "post" { |  | ||||||
|         return HTTPResponse::as_400(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let mut store = FileStore::new(config.filestore_path.clone()); |  | ||||||
| 
 |  | ||||||
|     match request.get_body() { |  | ||||||
|         Some(d) => { |  | ||||||
|             let credentials = Credentials::from(d); |  | ||||||
|             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(); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             let jwt_signer = { |  | ||||||
|                 match JWTSigner::new(config).await { |  | ||||||
|                     Ok(s) => s, |  | ||||||
|                     Err(e) => { |  | ||||||
|                         let message = JSONMessage::error(&e); |  | ||||||
|                         return HTTPResponse::as_500(message); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             }; |  | ||||||
| 
 |  | ||||||
|             match jwt_signer.sign(credentials) { |  | ||||||
|                 Ok(t) => send_token(&t), |  | ||||||
|                 Err(e) => { |  | ||||||
|                     let message = JSONMessage::error(&e); |  | ||||||
|                     return HTTPResponse::as_500(message); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         None => HTTPResponse::as_400(), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// handle_validate validates the token by checking:
 |  | ||||||
| /// * expiration time
 |  | ||||||
| /// * signature
 |  | ||||||
| async fn handle_validate(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { |  | ||||||
|     if request.get_method().trim().to_lowercase() != method { |  | ||||||
|         return HTTPResponse::as_400(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let token = { |  | ||||||
|         match request.get_body_value("token") { |  | ||||||
|             Some(t) => t, |  | ||||||
|             None => { |  | ||||||
|                 let mut message = ValidationMessage::default(); |  | ||||||
|                 message.set_reason("no token provided in the request body"); |  | ||||||
| 
 |  | ||||||
|                 let json = json::parse(&serde_json::to_string(&message).unwrap()).unwrap(); |  | ||||||
|                 return HTTPResponse::as_200(Some(json)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     let jwt_signer = { |  | ||||||
|         match JWTSigner::new(config).await { |  | ||||||
|             Ok(s) => s, |  | ||||||
|             Err(e) => { |  | ||||||
|                 let message = JSONMessage::error(&e); |  | ||||||
|                 let json = message.try_into().unwrap(); |  | ||||||
|                 return HTTPResponse::as_500(Some(json)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     let mut message = ValidationMessage::default(); |  | ||||||
|     match jwt_signer.validate(&token) { |  | ||||||
|         Ok(()) => { |  | ||||||
|             message.set_valid(true); |  | ||||||
|         } |  | ||||||
|         Err(e) => { |  | ||||||
|             message.set_reason(&e); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let json: JsonValue = json::parse(&serde_json::to_string(&message).unwrap()).unwrap(); |  | ||||||
|     HTTPResponse::as_200(Some(json)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// handle_public_key returns the JWT public key in base64 encoded
 |  | ||||||
| async fn handle_public_key(request: HTTPRequest<'_>, config: Config, method: &str) -> HTTPResponse { |  | ||||||
|     if request.get_method().trim().to_lowercase() != method { |  | ||||||
|         return HTTPResponse::as_400(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let jwt_signer = { |  | ||||||
|         match JWTSigner::new(config).await { |  | ||||||
|             Ok(s) => s, |  | ||||||
|             Err(e) => { |  | ||||||
|                 let message = JSONMessage::error(&e); |  | ||||||
|                 let json = message.try_into().unwrap(); |  | ||||||
|                 return HTTPResponse::as_500(Some(json)); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     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())) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| pub struct Router; |  | ||||||
| 
 |  | ||||||
| impl Router { |  | ||||||
|     /// route routes the request to the corresponding handler
 |  | ||||||
|     pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse { |  | ||||||
|         let request = HTTPRequest::from(request_str); |  | ||||||
|         match request.get_target() { |  | ||||||
|             GET_ROUTE => handle_get(request, config, "post").await, |  | ||||||
|             VALIDATE_ROUTE => handle_validate(request, config, "post").await, |  | ||||||
|             PUBKEY_ROUTE => handle_public_key(request, config, "get").await, |  | ||||||
|             _ => HTTPResponse::as_404(), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// send_token generates an HTTPResponse with the new token
 |  | ||||||
| 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 {}; |  | ||||||
| 
 |  | ||||||
| #[tokio::test] |  | ||||||
| async fn test_route() { |  | ||||||
|     use http::HTTPStatusCode; |  | ||||||
| 
 |  | ||||||
|     let router: &Router = &ROUTER; |  | ||||||
|     let config: Config = Config::default(); |  | ||||||
|     let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; |  | ||||||
| 
 |  | ||||||
|     let response: HTTPResponse = router.route(request_str, config).await; |  | ||||||
|     assert_eq!(HTTPStatusCode::Http400, response.get_status_code()); |  | ||||||
| } |  | ||||||
| @ -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![]; | ||||||
| @ -41,18 +41,21 @@ impl FileStore { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Err(e) => { |             Err(e) => { | ||||||
|                 log::error!("while reading store file: {}, details={:?}", self.path, e); |                 eprintln!( | ||||||
|  |                     "error occurred while reading store file: {}, err={:?}", | ||||||
|  |                     self.path, e | ||||||
|  |                 ); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         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, username: String, password: String) -> bool { | ||||||
|         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.username == username && x.password == password) | ||||||
|             .collect(); |             .collect(); | ||||||
|         if credentials.len() == 1 { |         if credentials.len() == 1 { | ||||||
|             return true; |             return true; | ||||||
| @ -63,15 +66,21 @@ impl FileStore { | |||||||
| 
 | 
 | ||||||
| #[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) -> bool { | ||||||
|         // 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); |             eprintln!("{} path referencing file store does not exist", self.path); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let credentials = Credentials::from(data); | ||||||
|  |         if credentials.is_empty() { | ||||||
|  |             eprintln!("unable to parse the credentials correctly from the incoming request"); | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         self.parse_contents().await; |         self.parse_contents().await; | ||||||
|         self.auth(credentials.get_email(), credentials.get_password()) |         self.auth(credentials.username, credentials.password) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -80,14 +89,11 @@ 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#"{"username": "toto", "password": "tata"}"#).unwrap(); | ||||||
|     let credentials = Credentials::from(&data); |     assert_eq!(store.is_auth(&data).await, true); | ||||||
|     assert_eq!(credentials.get_email(), "toto@toto.fr"); |  | ||||||
| 
 |  | ||||||
|     let is_auth = store.is_auth(&credentials).await; |  | ||||||
|     assert_eq!(true, is_auth); |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,10 +1,11 @@ | |||||||
| //! **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; | ||||||
| 
 | 
 | ||||||
| pub use file::FileStore; | pub use file::FileStore; | ||||||
| pub use store::{Credentials, Store}; | pub use store::Store; | ||||||
|  | |||||||
| @ -1,55 +1,57 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use json::JsonValue; | use json; | ||||||
| use serde::Deserialize; | use json::object::Object; | ||||||
| 
 | 
 | ||||||
| #[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) -> bool; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Default, Debug, Deserialize)] | /// extracts `String` json value from a key
 | ||||||
|  | fn extract_json_value(data: &Object, key: &str) -> String { | ||||||
|  |     if let Some(u) = data.get(key) { | ||||||
|  |         match u.as_str() { | ||||||
|  |             Some(s) => return s.to_string(), | ||||||
|  |             None => return "".to_string(), | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     "".to_string() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Default, Debug)] | ||||||
| pub struct Credentials { | pub struct Credentials { | ||||||
|     email: String, |     pub username: 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(username: String, password: String) -> Self { | ||||||
|         Credentials { email, password } |         Credentials { username, 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.username == "" || 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.username = extract_json_value(&d, "username"); | ||||||
|             Err(e) => { |                 credentials.password = extract_json_value(&d, "password"); | ||||||
|                 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] = [ | ||||||
| @ -58,7 +60,7 @@ fn test_credentials() { | |||||||
|             is_empty: true |             is_empty: true | ||||||
|         }, |         }, | ||||||
|         Expect { |         Expect { | ||||||
|             data: json::parse(r#"{"email":"toto@toto.fr","password": "tata"}"#).unwrap(), |             data: json::parse(r#"{"username":"toto","password": "tata"}"#).unwrap(), | ||||||
|             is_empty: false |             is_empty: false | ||||||
|         } |         } | ||||||
|     ]; |     ]; | ||||||
|  | |||||||
| @ -6,41 +6,20 @@ | |||||||
| # | # | ||||||
| ####################################### | ####################################### | ||||||
| 
 | 
 | ||||||
| URL=${SIMPLE_AUTH_URL} | URL="https://dev.thegux.fr" | ||||||
| if [ -z ${URL} ] |  | ||||||
| then |  | ||||||
| 	echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:5555" |  | ||||||
| 	URL="http://localhost:5555" |  | ||||||
| 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,18 +30,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 |  | ||||||
| 	fi |  | ||||||
| done |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| for i in {0..10} |  | ||||||
| do |  | ||||||
| 	http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/pubkey/) |  | ||||||
| 	if [ $http_response != "200" ] |  | ||||||
| 	then |  | ||||||
| 		echo "bad http status code : ${http_response}, expect 200" |  | ||||||
| 		exit 1 | 		exit 1 | ||||||
| 	fi | 	fi | ||||||
| done | done | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| # this a test password storage with password in clear | # this a test password storage with password in clear | ||||||
| # need to be updated in the future to encrypt or hash the password | # need to be updated in the future to encrypt or hash the password | ||||||
| # <email>:<password> | # <username>:<password> | ||||||
| toto@toto.fr:tata | toto:tata | ||||||
|  | |||||||
| @ -1,10 +1,8 @@ | |||||||
| attrs==22.1.0 | attrs==22.1.0 | ||||||
| black==22.8.0 | black==22.8.0 | ||||||
| certifi==2022.9.14 | certifi==2022.9.14 | ||||||
| cffi==1.15.1 |  | ||||||
| charset-normalizer==2.1.1 | charset-normalizer==2.1.1 | ||||||
| click==8.1.3 | click==8.1.3 | ||||||
| cryptography==38.0.1 |  | ||||||
| idna==3.4 | idna==3.4 | ||||||
| iniconfig==1.1.1 | iniconfig==1.1.1 | ||||||
| mypy-extensions==0.4.3 | mypy-extensions==0.4.3 | ||||||
| @ -13,10 +11,7 @@ pathspec==0.10.1 | |||||||
| platformdirs==2.5.2 | platformdirs==2.5.2 | ||||||
| pluggy==1.0.0 | pluggy==1.0.0 | ||||||
| py==1.11.0 | py==1.11.0 | ||||||
| pycparser==2.21 |  | ||||||
| PyJWT==2.5.0 |  | ||||||
| pyparsing==3.0.9 | pyparsing==3.0.9 | ||||||
| requests==2.28.1 | requests==2.28.1 | ||||||
| tomli==2.0.1 | tomli==2.0.1 | ||||||
| types-cryptography==3.3.23 |  | ||||||
| urllib3==1.26.12 | urllib3==1.26.12 | ||||||
|  | |||||||
| @ -1,75 +1,41 @@ | |||||||
| import base64 |  | ||||||
| import jwt | import jwt | ||||||
| import os |  | ||||||
| import requests | import requests | ||||||
| 
 | 
 | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  | 
 | ||||||
| from unittest import TestCase | from unittest import TestCase | ||||||
| 
 | 
 | ||||||
| URL = os.getenv("SIMPLE_AUTH_URL", "http://127.0.0.1:5555") | URL = "https://dev.thegux.fr" | ||||||
| PUB_KEY_PATH = os.getenv("SIMPLE_AUTH_PUB_KEY", "") |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestResponse(TestCase): | class TestResponse(TestCase): | ||||||
|     def setUp(self): |     def test_get_target(self): | ||||||
|         with open(PUB_KEY_PATH, "r") as f: |  | ||||||
|             self.pub_key = f.read() |  | ||||||
| 
 |  | ||||||
|     def test_get_target(self, pubkey=None): |  | ||||||
|         resp = requests.post( |         resp = requests.post( | ||||||
|             URL + "/get/", json={"email": "toto@toto.fr", "password": "tata"} |             URL + "/get/", json={"username": "toto", "password": "tata"} | ||||||
|         ) |         ) | ||||||
|         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, options={"verify_signature": False}) | ||||||
|             token, |  | ||||||
|             pubkey or self.pub_key, |  | ||||||
|             algorithms=["RS384"], |  | ||||||
|             options={ |  | ||||||
|                 "verify_signature": True, |  | ||||||
|                 "verify_claims": True, |  | ||||||
|                 "verify_iss": True, |  | ||||||
|             }, |  | ||||||
|         ) |  | ||||||
|         self.assertEqual("thegux.fr", jwt_decoded["iss"]) |         self.assertEqual("thegux.fr", jwt_decoded["iss"]) | ||||||
|         self.assertEqual("toto@toto.fr", jwt_decoded["email"]) |  | ||||||
| 
 | 
 | ||||||
|         jwt_exp = datetime.fromtimestamp(jwt_decoded["exp"]) |         jwt_exp = datetime.fromtimestamp(jwt_decoded["exp"]) | ||||||
|         jwt_iat = datetime.fromtimestamp(jwt_decoded["iat"]) |         jwt_iat = datetime.fromtimestamp(jwt_decoded["iat"]) | ||||||
|         date_exp = datetime.strptime(str(jwt_exp - jwt_iat), "%H:%M:%S") |         date_exp = datetime.strptime(str(jwt_exp - jwt_iat), "%H:%M:%S") | ||||||
|         self.assertEqual(2, date_exp.hour) |         self.assertEqual(2, date_exp.hour) | ||||||
|         return token |  | ||||||
| 
 |  | ||||||
|     def test_validate_target_no_token(self): |  | ||||||
|         resp = requests.post( |  | ||||||
|             URL + "/validate/", json={"username": "toto@toto.fr", "password": "tata"} |  | ||||||
|         ) |  | ||||||
|         self.assertEqual(resp.status_code, 200, "bad status code returned") |  | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |  | ||||||
|         self.assertEqual(resp.json()["valid"], False, "bad status returned") |  | ||||||
|         self.assertEqual(resp.json()["reason"], "no token provided in the request body") |  | ||||||
| 
 |  | ||||||
|     def test_validate_target_empty_token(self): |  | ||||||
|         resp = requests.post(URL + "/validate/", json={"tutu": "tutu", "token": ""}) |  | ||||||
|         self.assertEqual(resp.status_code, 200, "bad status code returned") |  | ||||||
|         self.assertIsNotNone(resp.json(), "response data can't be empty") |  | ||||||
|         self.assertEqual(resp.json()["valid"], False, "bad status returned") |  | ||||||
|         self.assertEqual( |  | ||||||
|             resp.json()["reason"], |  | ||||||
|             "token validation failed details=JWT compact encoding error", |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|     def test_validate_target(self): |     def test_validate_target(self): | ||||||
|         token = self.test_get_target() |         resp = requests.post( | ||||||
| 
 |             URL + "/validate/", json={"username": "toto", "password": "tata"} | ||||||
|         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()["token"], "header.payload.signature", "bad status returned" | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     # TODO: must be updated after implementing `/refresh/` url handler |     # TODO: must be updated after implmenting `/refresh/` url handler | ||||||
|     def test_refresh_target(self): |     def test_refresh_target(self): | ||||||
|         resp = requests.post( |         resp = requests.post( | ||||||
|             URL + "/refresh/", json={"username": "toto", "password": "tata"} |             URL + "/refresh/", json={"username": "toto", "password": "tata"} | ||||||
| @ -78,7 +44,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,17 +54,19 @@ 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", | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     def test_bad_credentials(self): |     def test_bad_credentials(self): | ||||||
|         resp = requests.post(URL + "/get/", json={"email": "tutu", "password": "titi"}) |         resp = requests.post( | ||||||
|         self.assertEqual(resp.status_code, 403, "bad status code returned") |             URL + "/get/", json={"username": "tutu", "password": "titi"} | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(resp.status_code, 403, "bas status code returned") | ||||||
|         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,29 +78,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"], | ||||||
|             "url not found", |             "the url requested does not exist", | ||||||
|             "invalid error message returned", |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     def test_get_pubkey(self): |  | ||||||
|         resp = requests.get(URL + "/pubkey/") |  | ||||||
|         self.assertEqual(resp.status_code, 200, "bad status code returned") |  | ||||||
|         self.assertIsNotNone(resp.json(), "response data must not be empty") |  | ||||||
|         self.assertIsNotNone(resp.json()["pubkey"], "invalid error message returned") |  | ||||||
| 
 |  | ||||||
|         b64_pubkey = base64.b64decode(resp.json()["pubkey"]) |  | ||||||
|         self.assertIsNotNone(b64_pubkey, "public key b64 decoded can't be empty") |  | ||||||
|         b64_pubkey_decoded = b64_pubkey.decode() |  | ||||||
|         self.assertIn("-BEGIN PUBLIC KEY-", b64_pubkey_decoded) |  | ||||||
| 
 |  | ||||||
|         self.test_get_target(b64_pubkey_decoded) |  | ||||||
| 
 |  | ||||||
|     def test_get_pubkey_bad_method(self): |  | ||||||
|         resp = requests.post(URL + "/pubkey/", json={"tutu": "toto"}) |  | ||||||
|         self.assertEqual(resp.status_code, 400, "bad status code returned") |  | ||||||
|         self.assertIsNotNone(resp.json(), "response data must not be empty") |  | ||||||
|         self.assertEqual( |  | ||||||
|             resp.json()["error"], |  | ||||||
|             "bad request", |  | ||||||
|             "invalid error message returned", |             "invalid error message returned", | ||||||
|         ) |         ) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user