diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs deleted file mode 100644 index c6e09ea..0000000 --- a/src/handlers/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! handlers module includes tools to parse an HTTP request and build and HTTP response - -pub mod request; -pub mod response; - -pub use request::handle_request; -pub use response::HTTPResponse; diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..f9dbf7c --- /dev/null +++ b/src/http/mod.rs @@ -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::ROUTER; diff --git a/src/handlers/request.rs b/src/http/request.rs similarity index 83% rename from src/handlers/request.rs rename to src/http/request.rs index 2cec0a5..113459f 100644 --- a/src/handlers/request.rs +++ b/src/http/request.rs @@ -13,15 +13,11 @@ type RequestParts = (String, VecDeque, String); const HTTP_REQUEST_SEPARATOR: &'static str = "\r\n"; const NULL_CHAR: &'static str = "\0"; -// TODO: put this const in a conf file ? -const HTTP_METHODS: [&'static str; 1] = ["POST"]; -const HTTP_TARGETS: [&'static str; 3] = ["/validate/", "/get/", "/refresh/"]; - lazy_static! { static ref HTTP_VERSION_REGEX: Regex = Regex::new("^HTTP/(1.1|1.0|2)$").unwrap(); } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub enum HTTPVersion { Http1_0, Http1_1, @@ -54,9 +50,9 @@ impl From<&String> for HTTPVersion { #[derive(Debug)] pub struct HTTPStartLine { - pub method: String, - pub target: String, - pub version: HTTPVersion, + method: String, + target: String, + version: HTTPVersion, } impl HTTPStartLine { @@ -81,13 +77,6 @@ impl HTTPStartLine { let target = parts[1].to_string(); let version = parts[2].to_string(); - if !Self::check_method(&method) { - return Err("method validation failed, bad method"); - } - - if !Self::check_target(&target) { - return Err("target validation failed, unvalid target"); - } if !Self::check_version(&version) { return Err("http version validation failed, unknown version"); } @@ -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 { HTTP_VERSION_REGEX.is_match(version) } @@ -126,12 +95,17 @@ impl HTTPStartLine { 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, } @@ -184,11 +158,6 @@ pub struct HTTPRequest { } impl HTTPRequest { - // associated function to build a new HTTPRequest - fn new(start_line: HTTPStartLine, body: Option) -> Self { - HTTPRequest { start_line, body } - } - /// split correctly the incoming request in order to get : /// * start_line /// * headers @@ -263,12 +232,8 @@ impl From<&str> for HTTPRequest { } } -pub fn handle_request(request: &str) -> HTTPRequest { - HTTPRequest::from(request) -} - #[test] -fn test_handle_request() { +fn test_request() { struct Expect { start_line: String, body: Option, @@ -303,9 +268,9 @@ fn test_handle_request() { ( "GET / HTTP/1.1\r\n\r\n".to_string(), Expect { - start_line: " UNKNOWN".to_string(), + start_line: "GET / HTTP/1.1".to_string(), body: None, - is_valid: false, + is_valid: true, }, ), // 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(), Expect { - start_line: " UNKNOWN".to_string(), + start_line: "fjlqskjd /oks?id=65 HTTP/2".to_string(), 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)); - } -} diff --git a/src/handlers/response.rs b/src/http/response.rs similarity index 58% rename from src/handlers/response.rs rename to src/http/response.rs index 6142725..0f0b436 100644 --- a/src/handlers/response.rs +++ b/src/http/response.rs @@ -1,21 +1,16 @@ //! response handles the incoming request parsed `HTTPRequest` -//! it will check if the `HTTPRequest` is valid and build an HTTPResponse corresponding to the HTTP -//! message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages +//! 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::handlers::request::{HTTPRequest, HTTPVersion}; -use async_trait::async_trait; +use crate::http::request::HTTPVersion; use json; -// add the Store trait to be used by `FileStore` -use crate::stores::FileStore; -use crate::stores::Store; -enum HTTPStatusCode { +#[derive(Debug, PartialEq, Clone)] +pub enum HTTPStatusCode { Http200, Http400, Http403, Http404, - Http500, } impl Into for HTTPStatusCode { @@ -25,7 +20,6 @@ impl Into for HTTPStatusCode { Self::Http400 => "400".to_string(), Self::Http404 => "404".to_string(), Self::Http403 => "403".to_string(), - Self::Http500 => "500".to_string(), } } } @@ -53,15 +47,19 @@ impl Into for HTTPStatusLine { } impl HTTPStatusLine { - fn set_status_code(&mut self, code: HTTPStatusCode) { + pub fn set_status_code(&mut self, code: HTTPStatusCode) { self.status_code = 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 { - status_line: HTTPStatusLine, + pub status_line: HTTPStatusLine, body: json::JsonValue, } @@ -91,44 +89,17 @@ impl Into for HTTPResponse { } impl HTTPResponse { - /// creates a response from the incoming `Request` - /// `From` could be used instead of forcing it like this - /// 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; - } + pub fn as_404() -> Self { + let mut response = Self::default(); - // empty body -> invalid request (credentials needed) - if let None = request.body { - return Self::as_403(); - } - - // 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 + .status_line + .set_status_code(HTTPStatusCode::Http404); + response.body = json::parse(r#"{"error": "the url requested does not exist"}"#).unwrap(); response } - /// generates a 403 response with a correct error message pub fn as_403() -> Self { let mut response = HTTPResponse { status_line: HTTPStatusLine::default(), @@ -139,4 +110,24 @@ impl HTTPResponse { .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() -> 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 + } } diff --git a/src/http/router.rs b/src/http/router.rs new file mode 100644 index 0000000..50a61ca --- /dev/null +++ b/src/http/router.rs @@ -0,0 +1,86 @@ +//! 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 crate::stores::FileStore; +use crate::stores::Store; +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; + +type FuturePinned = Pin>>; +type Handler = fn(HTTPRequest) -> FuturePinned; + +fn handle_get(request: HTTPRequest) -> FuturePinned { + Box::pin(async move { + // 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 { + Some(ref b) => { + let is_auth = store.is_auth(&b.get_data()).await; + if !is_auth { + return HTTPResponse::as_403(); + } + HTTPResponse::as_200() + } + None => HTTPResponse::as_400(), + } + }) +} + +/// validates the token by checking: +/// * expiration time +fn handle_validate(request: HTTPRequest) -> FuturePinned { + Box::pin(async move { + match &request.body { + Some(ref _b) => { + // TODO: impl the JWT validation + HTTPResponse::as_200() + } + 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` + // 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) -> 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 request_str = "POST /get/ HTTP/1.1\r\n\r\n"; + + let response: HTTPResponse = router.route(request_str).await; + assert_eq!( + HTTPStatusCode::Http400, + response.status_line.get_status_code() + ); +} diff --git a/src/main.rs b/src/main.rs index 93b29cc..5b58bda 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -mod handlers; +mod http; mod stores; use tokio::{ @@ -6,7 +6,7 @@ use tokio::{ net::{TcpListener, TcpStream}, }; -use handlers::{handle_request, HTTPResponse}; +use http::ROUTER; 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 request_string = std::str::from_utf8(&buffer[0..n]).unwrap(); - let request = handle_request(request_string); - - let response = HTTPResponse::from(request).await; + let response = ROUTER.route(request_string).await; let response_str: String = response.into(); stream.write(response_str.as_bytes()).await.unwrap(); diff --git a/src/stores/file.rs b/src/stores/file.rs index 4aef2de..fda7f36 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -1,11 +1,7 @@ use async_trait::async_trait; use json; -use json::object::Object; use std::path::Path; -use tokio::fs::File; -use tokio::io::AsyncReadExt; // for read_to_end() - use super::store::{Credentials, Store}; /// references a credentials store file @@ -83,8 +79,7 @@ impl Store for FileStore { return false; } - let contents = self.parse_contents().await; - + self.parse_contents().await; self.auth(credentials.username, credentials.password) } } diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index d144993..5642cca 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -28,7 +28,7 @@ done for i in {0..10} do http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/ge/ -d '{"username":"toto", "password":"tutu"}') - if [ $http_response != "400" ] + if [ $http_response != "404" ] then echo "bad http status code : ${http_response}, expect 400" exit 1 diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index d17f41e..20e8409 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -26,23 +26,26 @@ class TestResponse(TestCase): resp.json()["token"], "header.payload.signature", "bad status returned" ) + # TODO: must be updated after implmenting `/refresh/` url handler def test_refresh_target(self): resp = requests.post( URL + "/refresh/", json={"username": "toto", "password": "tata"} ) - self.assertEqual(resp.status_code, 200, "bad status code returned") + self.assertEqual(resp.status_code, 404, "bad status code returned") self.assertIsNotNone(resp.json(), "response data can't be empty") self.assertEqual( - resp.json()["token"], "header.payload.signature", "bad status returned" + resp.json()["error"], + "the url requested does not exist", + "bad status returned", ) def test_no_credentials(self): resp = requests.post(URL + "/get/") - self.assertEqual(resp.status_code, 403, "bad status code returned") + 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"], - "invalid credentials", + "the incoming request is not valid", "invalid error message returned", ) @@ -62,10 +65,10 @@ class TestResponse(TestCase): resp = requests.post( URL + "/token/", json={"username": "toto", "password": "tata"} ) - self.assertEqual(resp.status_code, 400, "bad status code returned") + self.assertEqual(resp.status_code, 404, "bad status code returned") self.assertIsNotNone(resp.json(), "response data must not be empty") self.assertEqual( resp.json()["error"], - "the incoming request is not valid", + "the url requested does not exist", "invalid error message returned", )