From 53b5c7a65f49b8cb4df9b1d7dc13167eec40c2f2 Mon Sep 17 00:00:00 2001 From: landrigun Date: Fri, 7 Oct 2022 15:57:51 +0000 Subject: [PATCH] feat: #12 impl router for get target + clean the code --- src/http/mod.rs | 2 +- src/http/request.rs | 79 ++++++++------------------------------------ src/http/response.rs | 75 +++++++++++++++++------------------------ src/http/router.rs | 66 ++++++++++++++++++++++++++---------- src/main.rs | 6 ++-- src/stores/file.rs | 7 +--- 6 files changed, 98 insertions(+), 137 deletions(-) diff --git a/src/http/mod.rs b/src/http/mod.rs index fa33d9c..f9dbf7c 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -4,6 +4,6 @@ pub mod request; pub mod response; pub mod router; -pub use request::{handle_request, HTTPRequest}; +pub use request::HTTPRequest; pub use response::{HTTPResponse, HTTPStatusCode}; pub use router::ROUTER; diff --git a/src/http/request.rs b/src/http/request.rs index 2cec0a5..113459f 100644 --- a/src/http/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/http/response.rs b/src/http/response.rs index 944026c..166fc80 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -3,20 +3,15 @@ //! 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::{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; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum HTTPStatusCode { Http200, Http400, Http403, Http404, - Http500, } impl Into for HTTPStatusCode { @@ -26,7 +21,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(), } } } @@ -54,12 +48,12 @@ 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 + pub fn get_status_code(&self) -> HTTPStatusCode { + self.status_code.clone() } } @@ -96,44 +90,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(), @@ -144,4 +111,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 index 7fcc37b..f6191bb 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -1,34 +1,66 @@ //! 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; -// TODO: this must be set in a config file (type might be changed) -const HTTP_TARGETS: &[&'static str; 3] = &["/validate/", "/get/", "/refresh/"]; +type FuturePinned = Pin>>; +type Handler = fn(HTTPRequest) -> FuturePinned; -pub struct Router<'a> { - routes: &'a [&'static str], -} - -// assuming a static lifetime -impl Router<'_> { - pub fn route(&self, request_str: &str) -> HTTPResponse { - HTTPResponse::default() +async fn handle_get(request: HTTPRequest) -> HTTPResponse { + // 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(), } } -pub const ROUTER: Router = Router { - routes: HTTP_TARGETS, -}; +fn handle_get_pinned(request: HTTPRequest) -> FuturePinned { + Box::pin(handle_get(request)) +} -#[test] -fn test_route() { +lazy_static! { + 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 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!( - &HTTPStatusCode::Http400, + HTTPStatusCode::Http400, response.status_line.get_status_code() ); } diff --git a/src/main.rs b/src/main.rs index 7595846..5b58bda 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use tokio::{ net::{TcpListener, TcpStream}, }; -use http::{handle_request, HTTPResponse, ROUTER}; +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) } }