feat: #12 impl router for get target + clean the code
This commit is contained in:
		
							parent
							
								
									df38268566
								
							
						
					
					
						commit
						53b5c7a65f
					
				| @ -4,6 +4,6 @@ pub mod request; | |||||||
| pub mod response; | pub mod response; | ||||||
| pub mod router; | pub mod router; | ||||||
| 
 | 
 | ||||||
| pub use request::{handle_request, HTTPRequest}; | pub use request::HTTPRequest; | ||||||
| pub use response::{HTTPResponse, HTTPStatusCode}; | pub use response::{HTTPResponse, HTTPStatusCode}; | ||||||
| pub use router::ROUTER; | pub use router::ROUTER; | ||||||
|  | |||||||
| @ -13,15 +13,11 @@ type RequestParts = (String, VecDeque<String>, String); | |||||||
| const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; | const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; | ||||||
| const NULL_CHAR: &'static str = "\0"; | const NULL_CHAR: &'static str = "\0"; | ||||||
| 
 | 
 | ||||||
| // TODO: put this const in a conf file ?
 |  | ||||||
| const HTTP_METHODS: [&'static str; 1] = ["POST"]; |  | ||||||
| const HTTP_TARGETS: [&'static str; 3] = ["/validate/", "/get/", "/refresh/"]; |  | ||||||
| 
 |  | ||||||
| lazy_static! { | lazy_static! { | ||||||
|     static ref HTTP_VERSION_REGEX: Regex = Regex::new("^HTTP/(1.1|1.0|2)$").unwrap(); |     static ref HTTP_VERSION_REGEX: Regex = Regex::new("^HTTP/(1.1|1.0|2)$").unwrap(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug, Copy, Clone)] | ||||||
| pub enum HTTPVersion { | pub enum HTTPVersion { | ||||||
|     Http1_0, |     Http1_0, | ||||||
|     Http1_1, |     Http1_1, | ||||||
| @ -54,9 +50,9 @@ impl From<&String> for HTTPVersion { | |||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct HTTPStartLine { | pub struct HTTPStartLine { | ||||||
|     pub method: String, |     method: String, | ||||||
|     pub target: String, |     target: String, | ||||||
|     pub version: HTTPVersion, |     version: HTTPVersion, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl HTTPStartLine { | impl HTTPStartLine { | ||||||
| @ -81,13 +77,6 @@ impl HTTPStartLine { | |||||||
|         let target = parts[1].to_string(); |         let target = parts[1].to_string(); | ||||||
|         let version = parts[2].to_string(); |         let version = parts[2].to_string(); | ||||||
| 
 | 
 | ||||||
|         if !Self::check_method(&method) { |  | ||||||
|             return Err("method validation failed, bad method"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if !Self::check_target(&target) { |  | ||||||
|             return Err("target validation failed, unvalid target"); |  | ||||||
|         } |  | ||||||
|         if !Self::check_version(&version) { |         if !Self::check_version(&version) { | ||||||
|             return Err("http version validation failed, unknown version"); |             return Err("http version validation failed, unknown version"); | ||||||
|         } |         } | ||||||
| @ -99,26 +88,6 @@ impl HTTPStartLine { | |||||||
|         )) |         )) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// checks if the start_line method is in a predefined HTTP method list
 |  | ||||||
|     fn check_method(method: &String) -> bool { |  | ||||||
|         for m in HTTP_METHODS.iter() { |  | ||||||
|             if m.to_string() == *method { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// checks if the start_line target is in a predefined HTTP target whitelist
 |  | ||||||
|     fn check_target(target: &String) -> bool { |  | ||||||
|         for t in HTTP_TARGETS.iter() { |  | ||||||
|             if t.to_string() == *target { |  | ||||||
|                 return true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn check_version(version: &String) -> bool { |     fn check_version(version: &String) -> bool { | ||||||
|         HTTP_VERSION_REGEX.is_match(version) |         HTTP_VERSION_REGEX.is_match(version) | ||||||
|     } |     } | ||||||
| @ -126,12 +95,17 @@ impl HTTPStartLine { | |||||||
|     pub fn is_valid(&self) -> bool { |     pub fn is_valid(&self) -> bool { | ||||||
|         return self.method != "" && self.target != ""; |         return self.method != "" && self.target != ""; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     pub fn get_target(&self) -> String { | ||||||
|  |         self.target.clone() | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Default for HTTPStartLine { | impl Default for HTTPStartLine { | ||||||
|     fn default() -> Self { |     fn default() -> Self { | ||||||
|         HTTPStartLine { |         HTTPStartLine { | ||||||
|             method: "".to_string(), |             method: "".to_string(), | ||||||
|  | 
 | ||||||
|             target: "".to_string(), |             target: "".to_string(), | ||||||
|             version: HTTPVersion::Unknown, |             version: HTTPVersion::Unknown, | ||||||
|         } |         } | ||||||
| @ -184,11 +158,6 @@ pub struct HTTPRequest { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl HTTPRequest { | impl HTTPRequest { | ||||||
|     // associated function to build a new HTTPRequest
 |  | ||||||
|     fn new(start_line: HTTPStartLine, body: Option<HTTPBody>) -> Self { |  | ||||||
|         HTTPRequest { start_line, body } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// split correctly the incoming request in order to get :
 |     /// split correctly the incoming request in order to get :
 | ||||||
|     /// * start_line
 |     /// * start_line
 | ||||||
|     /// * headers
 |     /// * headers
 | ||||||
| @ -263,12 +232,8 @@ impl From<&str> for HTTPRequest { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub fn handle_request(request: &str) -> HTTPRequest { |  | ||||||
|     HTTPRequest::from(request) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[test] | #[test] | ||||||
| fn test_handle_request() { | fn test_request() { | ||||||
|     struct Expect { |     struct Expect { | ||||||
|         start_line: String, |         start_line: String, | ||||||
|         body: Option<String>, |         body: Option<String>, | ||||||
| @ -303,9 +268,9 @@ fn test_handle_request() { | |||||||
|         ( |         ( | ||||||
|             "GET / HTTP/1.1\r\n\r\n".to_string(), |             "GET / HTTP/1.1\r\n\r\n".to_string(), | ||||||
|             Expect { |             Expect { | ||||||
|                 start_line: "  UNKNOWN".to_string(), |                 start_line: "GET / HTTP/1.1".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: false, |                 is_valid: true, | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|         // intentionally add HTTP with no version number
 |         // intentionally add HTTP with no version number
 | ||||||
| @ -336,9 +301,9 @@ fn test_handle_request() { | |||||||
|         ( |         ( | ||||||
|             "fjlqskjd /oks?id=65 HTTP/2\r\n\r\n".to_string(), |             "fjlqskjd /oks?id=65 HTTP/2\r\n\r\n".to_string(), | ||||||
|             Expect { |             Expect { | ||||||
|                 start_line: "  UNKNOWN".to_string(), |                 start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(), | ||||||
|                 body: None, |                 body: None, | ||||||
|                 is_valid: false, |                 is_valid: true, | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
| @ -402,19 +367,3 @@ fn test_http_body() { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| #[test] |  | ||||||
| fn test_http_method() { |  | ||||||
|     let test_cases: Vec<(String, bool)> = vec![ |  | ||||||
|         ("POST".to_string(), true), |  | ||||||
|         ("POST     ".to_string(), false), |  | ||||||
|         ("GET".to_string(), false), |  | ||||||
|         ("get".to_string(), false), |  | ||||||
|         ("qsdqsfqsf/".to_string(), false), |  | ||||||
|         ("OPTIONS".to_string(), false), |  | ||||||
|     ]; |  | ||||||
| 
 |  | ||||||
|     for (method, is_valid) in test_cases { |  | ||||||
|         assert_eq!(is_valid, HTTPStartLine::check_method(&method)); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -3,20 +3,15 @@ | |||||||
| //! message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
 | //! message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
 | ||||||
| //! NOTE: only few parts of the specification has been implemented
 | //! NOTE: only few parts of the specification has been implemented
 | ||||||
| 
 | 
 | ||||||
| use crate::http::request::{HTTPRequest, HTTPVersion}; | use crate::http::request::HTTPVersion; | ||||||
| use async_trait::async_trait; |  | ||||||
| use json; | use json; | ||||||
| // add the Store trait to be used by `FileStore`
 |  | ||||||
| use crate::stores::FileStore; |  | ||||||
| use crate::stores::Store; |  | ||||||
| 
 | 
 | ||||||
| #[derive(Debug, PartialEq)] | #[derive(Debug, PartialEq, Clone)] | ||||||
| pub enum HTTPStatusCode { | pub enum HTTPStatusCode { | ||||||
|     Http200, |     Http200, | ||||||
|     Http400, |     Http400, | ||||||
|     Http403, |     Http403, | ||||||
|     Http404, |     Http404, | ||||||
|     Http500, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl Into<String> for HTTPStatusCode { | impl Into<String> for HTTPStatusCode { | ||||||
| @ -26,7 +21,6 @@ impl Into<String> for HTTPStatusCode { | |||||||
|             Self::Http400 => "400".to_string(), |             Self::Http400 => "400".to_string(), | ||||||
|             Self::Http404 => "404".to_string(), |             Self::Http404 => "404".to_string(), | ||||||
|             Self::Http403 => "403".to_string(), |             Self::Http403 => "403".to_string(), | ||||||
|             Self::Http500 => "500".to_string(), |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -54,12 +48,12 @@ impl Into<String> for HTTPStatusLine { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl HTTPStatusLine { | impl HTTPStatusLine { | ||||||
|     fn set_status_code(&mut self, code: HTTPStatusCode) { |     pub fn set_status_code(&mut self, code: HTTPStatusCode) { | ||||||
|         self.status_code = code; |         self.status_code = code; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn get_status_code(&self) -> &HTTPStatusCode { |     pub fn get_status_code(&self) -> HTTPStatusCode { | ||||||
|         &self.status_code |         self.status_code.clone() | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -96,44 +90,17 @@ impl Into<String> for HTTPResponse { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl HTTPResponse { | impl HTTPResponse { | ||||||
|     /// creates a response from the incoming `Request`
 |     pub fn as_404() -> Self { | ||||||
|     /// `From<T>` could be used instead of forcing it like this
 |         let mut response = Self::default(); | ||||||
|     /// it fails using `async_trait` attributes (only custom traits work ?)
 |  | ||||||
|     pub async fn from(request: HTTPRequest) -> Self { |  | ||||||
|         let mut response = HTTPResponse::default(); |  | ||||||
|         if !request.is_valid() { |  | ||||||
|             return response; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         // empty body -> invalid request (credentials needed)
 |         response | ||||||
|         if let None = request.body { |             .status_line | ||||||
|             return Self::as_403(); |             .set_status_code(HTTPStatusCode::Http404); | ||||||
|         } |         response.body = json::parse(r#"{"error": "the url requested does not exist"}"#).unwrap(); | ||||||
| 
 |  | ||||||
|         // TODO: path to `store.txt` must not be hardcoded, should be in a config file and load at
 |  | ||||||
|         // runtime
 |  | ||||||
|         let mut store = FileStore::new("tests/data/store.txt".to_string()); |  | ||||||
|         let body = request.body.unwrap(); |  | ||||||
|         let is_auth = store.is_auth(&body.get_data()).await; |  | ||||||
| 
 |  | ||||||
|         if !is_auth { |  | ||||||
|             return Self::as_403(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // TODO: must be a valid JWT (to implement)
 |  | ||||||
|         let body = json::parse( |  | ||||||
|             r#"{"token": "header.payload.signature", "refresh": "header.payload.signature"}"#, |  | ||||||
|         ) |  | ||||||
|         .unwrap(); |  | ||||||
| 
 |  | ||||||
|         response.status_line.version = request.start_line.version; |  | ||||||
|         response.status_line.status_code = HTTPStatusCode::Http200; |  | ||||||
|         response.body = body; |  | ||||||
| 
 | 
 | ||||||
|         response |         response | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// generates a 403 response with a correct error message
 |  | ||||||
|     pub fn as_403() -> Self { |     pub fn as_403() -> Self { | ||||||
|         let mut response = HTTPResponse { |         let mut response = HTTPResponse { | ||||||
|             status_line: HTTPStatusLine::default(), |             status_line: HTTPStatusLine::default(), | ||||||
| @ -144,4 +111,24 @@ impl HTTPResponse { | |||||||
|             .set_status_code(HTTPStatusCode::Http403); |             .set_status_code(HTTPStatusCode::Http403); | ||||||
|         response |         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() -> Self { | ||||||
|  |         let mut response = Self::default(); | ||||||
|  | 
 | ||||||
|  |         response | ||||||
|  |             .status_line | ||||||
|  |             .set_status_code(HTTPStatusCode::Http200); | ||||||
|  |         response.body = json::parse( | ||||||
|  |             r#"{"token": "header.payload.signature", "refresh": "header.payload.signature"}"#, | ||||||
|  |         ) | ||||||
|  |         .unwrap(); | ||||||
|  | 
 | ||||||
|  |         response | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,34 +1,66 @@ | |||||||
| //! router aims to handle correctly the request corresponding to the target
 | //! router aims to handle correctly the request corresponding to the target
 | ||||||
|  | //! it implements all the logic to build an `HTTPResponse`
 | ||||||
| 
 | 
 | ||||||
| use super::{HTTPRequest, HTTPResponse, HTTPStatusCode}; | use super::{HTTPRequest, HTTPResponse, HTTPStatusCode}; | ||||||
|  | use crate::stores::FileStore; | ||||||
|  | use crate::stores::Store; | ||||||
| use lazy_static::lazy_static; | use lazy_static::lazy_static; | ||||||
|  | use std::collections::HashMap; | ||||||
|  | use std::future::Future; | ||||||
|  | use std::pin::Pin; | ||||||
| 
 | 
 | ||||||
| // TODO: this must be set in a config file (type might be changed)
 | type FuturePinned<HTTPResponse> = Pin<Box<dyn Future<Output = HTTPResponse>>>; | ||||||
| const HTTP_TARGETS: &[&'static str; 3] = &["/validate/", "/get/", "/refresh/"]; | type Handler = fn(HTTPRequest) -> FuturePinned<HTTPResponse>; | ||||||
| 
 | 
 | ||||||
| pub struct Router<'a> { | async fn handle_get(request: HTTPRequest) -> HTTPResponse { | ||||||
|     routes: &'a [&'static str], |     // TODO: path to `store.txt` must not be hardcoded, should be in a config file and load at runtime
 | ||||||
| } |     let mut store = FileStore::new("tests/data/store.txt".to_string()); | ||||||
| 
 |     match request.body { | ||||||
| // assuming a static lifetime
 |         Some(ref b) => { | ||||||
| impl Router<'_> { |             let is_auth = store.is_auth(&b.get_data()).await; | ||||||
|     pub fn route(&self, request_str: &str) -> HTTPResponse { |             if !is_auth { | ||||||
|         HTTPResponse::default() |                 return HTTPResponse::as_403(); | ||||||
|  |             } | ||||||
|  |             HTTPResponse::as_200() | ||||||
|  |         } | ||||||
|  |         None => HTTPResponse::as_400(), | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub const ROUTER: Router = Router { | fn handle_get_pinned(request: HTTPRequest) -> FuturePinned<HTTPResponse> { | ||||||
|     routes: HTTP_TARGETS, |     Box::pin(handle_get(request)) | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| #[test] | lazy_static! { | ||||||
| fn test_route() { |     static ref HTTP_METHODS: HashMap<&'static str, Handler> = | ||||||
|  |         HashMap::from([("/get/", handle_get_pinned as Handler),]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct Router; | ||||||
|  | 
 | ||||||
|  | impl Router { | ||||||
|  |     pub async fn route(&self, request_str: &str) -> 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).await, | ||||||
|  |             None => HTTPResponse::as_404(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // this MUST be used like a Singleton
 | ||||||
|  | pub const ROUTER: Router = Router {}; | ||||||
|  | 
 | ||||||
|  | #[tokio::test] | ||||||
|  | async fn test_route() { | ||||||
|     let router: &Router = &ROUTER; |     let router: &Router = &ROUTER; | ||||||
|     let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; |     let request_str = "POST /get/ HTTP/1.1\r\n\r\n"; | ||||||
| 
 | 
 | ||||||
|     let response: HTTPResponse = router.route(request_str); |     let response: HTTPResponse = router.route(request_str).await; | ||||||
|     assert_eq!( |     assert_eq!( | ||||||
|         &HTTPStatusCode::Http400, |         HTTPStatusCode::Http400, | ||||||
|         response.status_line.get_status_code() |         response.status_line.get_status_code() | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ use tokio::{ | |||||||
|     net::{TcpListener, TcpStream}, |     net::{TcpListener, TcpStream}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use http::{handle_request, HTTPResponse, ROUTER}; | use http::ROUTER; | ||||||
| 
 | 
 | ||||||
| const SERVER_URL: &str = "127.0.0.1:9000"; | const SERVER_URL: &str = "127.0.0.1:9000"; | ||||||
| 
 | 
 | ||||||
| @ -27,9 +27,7 @@ async fn handle_connection(mut stream: TcpStream) { | |||||||
|     let n = stream.read(&mut buffer).await.unwrap(); |     let n = stream.read(&mut buffer).await.unwrap(); | ||||||
| 
 | 
 | ||||||
|     let request_string = std::str::from_utf8(&buffer[0..n]).unwrap(); |     let request_string = std::str::from_utf8(&buffer[0..n]).unwrap(); | ||||||
|     let request = handle_request(request_string); |     let response = ROUTER.route(request_string).await; | ||||||
| 
 |  | ||||||
|     let response = HTTPResponse::from(request).await; |  | ||||||
|     let response_str: String = response.into(); |     let response_str: String = response.into(); | ||||||
| 
 | 
 | ||||||
|     stream.write(response_str.as_bytes()).await.unwrap(); |     stream.write(response_str.as_bytes()).await.unwrap(); | ||||||
|  | |||||||
| @ -1,11 +1,7 @@ | |||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
| use json; | use json; | ||||||
| use json::object::Object; |  | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
| 
 | 
 | ||||||
| use tokio::fs::File; |  | ||||||
| use tokio::io::AsyncReadExt; // for read_to_end()
 |  | ||||||
| 
 |  | ||||||
| use super::store::{Credentials, Store}; | use super::store::{Credentials, Store}; | ||||||
| 
 | 
 | ||||||
| /// references a credentials store file
 | /// references a credentials store file
 | ||||||
| @ -83,8 +79,7 @@ impl Store for FileStore { | |||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         let contents = self.parse_contents().await; |         self.parse_contents().await; | ||||||
| 
 |  | ||||||
|         self.auth(credentials.username, credentials.password) |         self.auth(credentials.username, credentials.password) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user