From 384e8685012e150c0cd21387cdd386a58d9bc0c4 Mon Sep 17 00:00:00 2001 From: landrigun Date: Mon, 3 Oct 2022 15:09:44 +0000 Subject: [PATCH] #8 return a 403 if credentials are invalid + adjust corresponding test --- src/handlers/request.rs | 4 ++ src/handlers/response.rs | 87 +++++++++++++++++++++++++---------- src/main.rs | 2 +- src/stores/file.rs | 2 +- tests/bash/curling.bash | 6 +-- tests/python/test_requests.py | 22 +++++++++ 6 files changed, 94 insertions(+), 29 deletions(-) diff --git a/src/handlers/request.rs b/src/handlers/request.rs index 4911bd4..ef1b4fe 100644 --- a/src/handlers/request.rs +++ b/src/handlers/request.rs @@ -156,6 +156,10 @@ impl HTTPBody { fn new(data: json::JsonValue) -> HTTPBody { HTTPBody { data } } + + pub fn get_data(&self) -> &json::JsonValue { + &self.data + } } impl TryFrom for HTTPBody { diff --git a/src/handlers/response.rs b/src/handlers/response.rs index 23de852..a7d14ae 100644 --- a/src/handlers/response.rs +++ b/src/handlers/response.rs @@ -3,13 +3,17 @@ //! message specs. see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages //! NOTE: only few parts of the specification has been implemented -use json; - use crate::handlers::request::{HTTPRequest, HTTPVersion}; +use async_trait::async_trait; +use json; +// add the Store trait to be used by `FileStore` +use crate::stores::FileStore; +use crate::stores::Store; enum HTTPStatusCode { Http200, Http400, + Http403, Http404, Http500, } @@ -20,6 +24,7 @@ impl Into for HTTPStatusCode { 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(), } } @@ -47,6 +52,12 @@ impl Into for HTTPStatusLine { } } +impl HTTPStatusLine { + fn set_status_code(&mut self, code: HTTPStatusCode) { + self.status_code = code; + } +} + pub struct HTTPResponse { status_line: HTTPStatusLine, body: json::JsonValue, @@ -61,28 +72,6 @@ impl Default for HTTPResponse { } } -impl From for HTTPResponse { - fn from(request: HTTPRequest) -> Self { - let mut response = HTTPResponse::default(); - if !request.is_valid() { - return response; - } - - // TODO: impl a valid credentials in `Store` - - 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 - } -} - impl Into for HTTPResponse { fn into(self) -> String { // move `self.body` into a new var @@ -98,3 +87,53 @@ impl Into for HTTPResponse { ) } } + +impl HTTPResponse { + // `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; + } + + // 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 + } + + /// as_403 generates a 403 response with a correct error message + 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 + } +} diff --git a/src/main.rs b/src/main.rs index 83c2b0d..f753f09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ async fn handle_connection(mut stream: TcpStream) { let request_string = std::str::from_utf8(&buffer[0..n]).unwrap(); let request = handle_request(request_string); - let response = HTTPResponse::from(request); + let response = HTTPResponse::from(request).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 73dcf36..0a56da4 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -15,7 +15,7 @@ pub struct FileStore { } impl FileStore { - fn new(path: String) -> Self { + pub fn new(path: String) -> Self { FileStore { path, credentials: vec![], diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index 3021842..d144993 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -11,15 +11,15 @@ URL="https://dev.thegux.fr" for i in {0..10} do http_response=$(curl -s -o response.txt -w "%{http_code}" ${URL}/get/ -d '{"username":"toto", "password":"tutu"}') - if [ $http_response != "200" ] + if [ $http_response != "403" ] then echo "bad http status code : ${http_response}, expect 200" exit 1 fi - if [ "$(cat response.txt | jq -r '.token')" != "header.payload.signature" ] + if [ "$(cat response.txt | jq -r '.error')" != "invalid credentials" ] then - echo "bad data returned, expect : ok" + echo "bad data returned, expect : invalid credentials" exit 1 fi done diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index 95ec7a7..d17f41e 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -36,6 +36,28 @@ class TestResponse(TestCase): resp.json()["token"], "header.payload.signature", "bad status returned" ) + def test_no_credentials(self): + resp = requests.post(URL + "/get/") + self.assertEqual(resp.status_code, 403, "bad status code returned") + self.assertIsNotNone(resp.json(), "response data must not be empty") + self.assertEqual( + resp.json()["error"], + "invalid credentials", + "invalid error message returned", + ) + + def test_bad_credentials(self): + resp = requests.post( + 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.assertEqual( + resp.json()["error"], + "invalid credentials", + "invalid error message returned", + ) + def test_bad_target(self): resp = requests.post( URL + "/token/", json={"username": "toto", "password": "tata"}