From 74e8d58b5c088be2c7e94122286b6b69bd35b0a1 Mon Sep 17 00:00:00 2001 From: landrigun Date: Fri, 14 Oct 2022 15:29:44 +0000 Subject: [PATCH 1/9] fix(tests): set URL default value if no one set --- tests/bash/curling.bash | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index 0b2c7b6..4c9813d 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -6,7 +6,8 @@ # ####################################### -if [ -z ${SIMPLE_AUTH_URL} ] +URL=${SIMPLE_AUTH_URL} +if [ -z ${URL} ] then echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:9001" URL="http://localhost:9001" From 45c9112af24ac865ce2107a256065c47f885423e Mon Sep 17 00:00:00 2001 From: landrigun Date: Mon, 7 Nov 2022 10:28:58 +0000 Subject: [PATCH 2/9] improv: add a logger (#18) + log client IP (#19) --- Cargo.lock | 135 +++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 2 + src/http/request.rs | 22 +++++--- src/http/router.rs | 6 +- src/main.rs | 26 +++++---- 5 files changed, 163 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9a5d07..07f2c40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + [[package]] name = "concurrent-queue" version = "1.2.4" @@ -781,7 +792,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -842,6 +853,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.15.0" @@ -902,7 +922,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -1214,10 +1234,25 @@ dependencies = [ "json", "jwt-simple", "lazy_static", + "log", "regex", + "simple_logger", "tokio", ] +[[package]] +name = "simple_logger" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e190a521c2044948158666916d9e872cbb9984f755e9bb3b5b75a836205affcd" +dependencies = [ + "atty", + "colored", + "log", + "time", + "windows-sys 0.42.0", +] + [[package]] name = "slab" version = "0.4.7" @@ -1327,6 +1362,35 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa", + "libc", + "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + [[package]] name = "tokio" version = "1.21.2" @@ -1520,43 +1584,100 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "zeroize" version = "1.5.7" diff --git a/Cargo.toml b/Cargo.toml index 4121298..0514ec3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ regex = "1" tokio = { version = "1.21.1", features = ["full"] } async-trait = "0.1.57" jwt-simple = "0.11.1" +simple_logger = "4.0.0" +log = "0.4.17" # useful for tests (embedded files should be delete in release ?) #rust-embed="6.4.1" diff --git a/src/http/request.rs b/src/http/request.rs index 2d1bc4c..087c3e2 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -143,10 +143,7 @@ impl TryFrom for HTTPBody { 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 - )), + Err(e) => Err(format!("during request body parsing details={}", e)), } } } @@ -156,6 +153,8 @@ impl TryFrom for HTTPBody { pub struct HTTPRequest { pub start_line: HTTPStartLine, pub body: Option, + // includes the client IP + port (should be in the headers) + pub addr: String, } impl HTTPRequest { @@ -190,19 +189,19 @@ impl HTTPRequest { 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), + Err(e) => log::error!("while parsing start_line details={}", 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), + Err(e) => log::error!("{}", e), } return Ok(request); } Err(e) => { - return Err(format!("error occurred getting request parts err={}", e)); + return Err(e); } } } @@ -222,6 +221,10 @@ impl HTTPRequest { pub fn is_valid(&self) -> bool { return self.start_line.is_valid(); } + + pub fn set_addr(&mut self, addr: String) { + self.addr = addr; + } } impl Default for HTTPRequest { @@ -229,6 +232,7 @@ impl Default for HTTPRequest { HTTPRequest { start_line: HTTPStartLine::default(), body: None, + addr: "".to_string(), } } } @@ -237,8 +241,8 @@ impl From<&str> for HTTPRequest { fn from(request: &str) -> Self { match Self::parse(request) { Ok(v) => v, - Err(v) => { - eprintln!("{}", format!("[ERR]: {v}")); + Err(e) => { + log::error!("{}", e); return HTTPRequest::default(); } } diff --git a/src/http/router.rs b/src/http/router.rs index d1f60ca..36a9f68 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -90,8 +90,10 @@ async fn handle_validate(request: HTTPRequest, config: Config) -> HTTPResponse { pub struct Router; impl Router { - pub async fn route(&self, request_str: &str, config: Config) -> HTTPResponse { - let request = HTTPRequest::from(request_str); + pub async fn route(&self, request_str: &str, addr: String, config: Config) -> HTTPResponse { + let mut request = HTTPRequest::from(request_str); + request.set_addr(addr); + let target = request.start_line.get_target(); match target.as_str() { diff --git a/src/main.rs b/src/main.rs index 87eb6cd..d16c45a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,13 +24,14 @@ struct Cli { #[tokio::main] async fn main() { + simple_logger::init_with_level(log::Level::Info).unwrap(); let args = Cli::parse(); let mut config = Ini::new(); match config.load(args.config) { Ok(c) => c, Err(e) => { - eprintln!("error while loading the config file, err={}", e); + log::error!("error while loading the config file details={}", e); std::process::exit(1); } }; @@ -39,11 +40,11 @@ async fn main() { let listener = { match TcpListener::bind(&server_url).await { Ok(t) => { - println!("server is listening on '{}'", server_url); + log::info!("server is listening on '{}'", server_url); t } Err(e) => { - eprintln!("error occurred while initializing tcp listener err={}", e); + log::error!("while initializing tcp listener details={}", e); std::process::exit(1); } } @@ -52,28 +53,31 @@ async fn main() { let router_config: Config = { match Config::try_from(config) { Ok(c) => c, - Err(_e) => { + Err(e) => { + log::error!("unable to load the configuration details={}", e); std::process::exit(1); } } }; loop { - let (stream, _) = listener.accept().await.unwrap(); + let (stream, addr) = listener.accept().await.unwrap(); let conf = router_config.clone(); - tokio::spawn(handle_connection(stream, conf.clone())); + tokio::spawn(handle_connection(stream, addr.to_string(), conf.clone())); } } /// parses the incoming request (partial spec implementation) and build an HTTP response -async fn handle_connection(mut stream: TcpStream, config: Config) { +async fn handle_connection(mut stream: TcpStream, addr: String, config: Config) { + log::info!("client connected: {}", addr); + let mut message = vec![]; let mut buffer: [u8; 1024] = [0; 1024]; - let duration = Duration::from_micros(500); + let duration = Duration::from_millis(5); // loop until the message is read - // the stream can be fragmented so, using a timeout (500um should be enough) for the future for completion + // 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 { @@ -86,9 +90,11 @@ async fn handle_connection(mut stream: TcpStream, config: Config) { } let request_string = std::str::from_utf8(&message).unwrap(); - let response = ROUTER.route(request_string, config).await; + let response = ROUTER.route(request_string, addr.clone(), config).await; let response_str: String = response.into(); stream.write(response_str.as_bytes()).await.unwrap(); stream.flush().await.unwrap(); + + log::info!("connection closed: {}", addr); } From 17a098ef89a720087b4d7bcbb9788c84c2e3b4aa Mon Sep 17 00:00:00 2001 From: landrigun Date: Mon, 7 Nov 2022 10:47:45 +0000 Subject: [PATCH 3/9] improv: replace eprintln with logger (#18) + fix mod documentation --- src/config/config.rs | 15 +++++++++------ src/config/mod.rs | 1 + src/http/router.rs | 2 +- src/jwt/jwt.rs | 14 ++++++-------- src/jwt/mod.rs | 1 + src/stores/file.rs | 9 +++------ src/utils/mod.rs | 1 + 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index 1f77698..d70a3d5 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -34,7 +34,10 @@ impl TryFrom for Config { match u64::from_str(&exp_time) { Ok(v) => v, Err(e) => { - eprintln!("unable to convert JWT expiration time into u64 err={}", e); + log::error!( + "unable to convert JWT expiration time into u64 details={}", + e + ); 0 } } @@ -59,27 +62,27 @@ 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"); + log::error!("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"); + log::error!("invalid config parameter: JWT issuer is empty"); return false; } if self.jwt_pub_key == "" { - eprintln!("invalid config parameter: JWT public key file path is empty"); + log::error!("invalid config parameter: JWT public key file path is empty"); return false; } if self.jwt_priv_key == "" { - eprintln!("invalid config parameter: JWT private key file path is empty"); + log::error!("invalid config parameter: JWT private key file path is empty"); return false; } if self.filestore_path == "" { - eprintln!("invalid config parameter: filestore path is empty"); + log::error!("invalid config parameter: filestore path is empty"); return false; } diff --git a/src/config/mod.rs b/src/config/mod.rs index 43fe76c..b82b271 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,3 +1,4 @@ +//! provides `Config` struct to load and validate `.ini` file mod config; pub use config::Config; diff --git a/src/http/router.rs b/src/http/router.rs index 36a9f68..1c41b40 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -115,7 +115,7 @@ async fn test_route() { 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; + let response: HTTPResponse = router.route(request_str, "".to_string(), config).await; assert_eq!( HTTPStatusCode::Http400, response.status_line.get_status_code() diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs index df9f932..ddcba72 100644 --- a/src/jwt/jwt.rs +++ b/src/jwt/jwt.rs @@ -1,5 +1,3 @@ -//! simple module to read `.pem` files and sign the token - use crate::config::Config; use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::*; @@ -28,7 +26,7 @@ impl JWTSigner { jwt_signer.private_key = c; } Err(e) => { - return Err(format!("unable to read the private key err={}", e)); + return Err(format!("unable to read the private key details={}", e)); } } @@ -37,7 +35,7 @@ impl JWTSigner { jwt_signer.public_key = c; } Err(e) => { - return Err(format!("unable to read the public key err={}", e)); + return Err(format!("unable to read the public key details={}", e)); } } @@ -60,7 +58,7 @@ impl JWTSigner { match RS384KeyPair::from_pem(&self.private_key) { Ok(k) => k, Err(e) => { - return Err(format!("unable to load the private key err={}", e)); + return Err(format!("unable to load the private key details={}", e)); } } }; @@ -70,7 +68,7 @@ impl JWTSigner { match jwt_key.sign(claims) { Ok(token) => Ok(token), Err(e) => { - return Err(format!("unable to sign the token err={}", e)); + return Err(format!("unable to sign the token details={}", e)); } } } @@ -82,12 +80,12 @@ impl JWTSigner { if let Err(e) = key.verify_token::(token, Some(verification_options)) { - return Err(format!("token validation failed err={}", e)); + return Err(format!("token validation failed details={}", e)); } Ok(()) } Err(e) => Err(format!( - "token validation failed can't read the public key err={}", + "token validation failed, can't read the public key details={}", e )), } diff --git a/src/jwt/mod.rs b/src/jwt/mod.rs index 051f27f..9b43d55 100644 --- a/src/jwt/mod.rs +++ b/src/jwt/mod.rs @@ -1,3 +1,4 @@ +//! simple module to read `.pem` files and sign the token mod jwt; pub use jwt::JWTSigner; diff --git a/src/stores/file.rs b/src/stores/file.rs index fda7f36..d5ab489 100644 --- a/src/stores/file.rs +++ b/src/stores/file.rs @@ -41,10 +41,7 @@ impl FileStore { } } Err(e) => { - eprintln!( - "error occurred while reading store file: {}, err={:?}", - self.path, e - ); + log::error!("while reading store file: {}, details={:?}", self.path, e); } } self.credentials = credentials; @@ -69,13 +66,13 @@ impl Store for FileStore { async fn is_auth(&mut self, data: &json::JsonValue) -> bool { // ensure that the store file already exists even after its instanciation if !Path::new(&self.path).is_file() { - eprintln!("{} path referencing file store does not exist", self.path); + log::error!("{} 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"); + log::error!("unable to parse the credentials correctly from the incoming request"); return false; } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index de6e774..4815b85 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ +//! includes utility function, that's all ! mod utils; pub use utils::extract_json_value; From 5a638fd35473134c1a74b1c85c7a456ee4c3bd60 Mon Sep 17 00:00:00 2001 From: landrigun Date: Mon, 7 Nov 2022 10:50:18 +0000 Subject: [PATCH 4/9] improv: fix doc in config.rs (#18) --- src/config/config.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/config/config.rs b/src/config/config.rs index d70a3d5..819f24f 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -1,5 +1,3 @@ -//! config module implements all the utilities to properly create and validate a router config - use configparser::ini::Ini; use std::str::FromStr; From b1cb4dec23a9579d83380a7e8c192b9ec4bdbf1a Mon Sep 17 00:00:00 2001 From: landrigun Date: Sat, 19 Nov 2022 14:16:44 +0000 Subject: [PATCH 5/9] fix(tests): error message + tests server url --- tests/python/test_requests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index ba45ce5..bc28b5a 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -5,7 +5,7 @@ import requests from datetime import datetime from unittest import TestCase -URL = os.getenv("SIMPLE_AUTH_URL", "http://127.0.0.1:9001") +URL = os.getenv("SIMPLE_AUTH_URL", "http://127.0.0.1:5555") PUB_KEY_PATH = os.getenv("SIMPLE_AUTH_PUB_KEY", "") @@ -56,7 +56,7 @@ class TestResponse(TestCase): self.assertEqual(resp.json()["valid"], "false", "bad status returned") self.assertEqual( resp.json()["reason"], - "token validation failed err=JWT compact encoding error", + "token validation failed details=JWT compact encoding error", ) def test_validate_target(self): From 91e80cfbf4c4b431f2425ee58f1e792515fe9d84 Mon Sep 17 00:00:00 2001 From: landrigun Date: Sat, 19 Nov 2022 14:48:22 +0000 Subject: [PATCH 6/9] feat(jwt): #16 add a route to get the public key --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/http/router.rs | 25 +++++++++++++++++++++++++ src/jwt/jwt.rs | 4 ++++ 4 files changed, 37 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 07f2c40..79283d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64ct" version = "1.5.2" @@ -1229,6 +1235,7 @@ version = "0.2.0" dependencies = [ "async-std", "async-trait", + "base64", "clap", "configparser", "json", diff --git a/Cargo.toml b/Cargo.toml index 0514ec3..bc542d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ async-trait = "0.1.57" jwt-simple = "0.11.1" simple_logger = "4.0.0" log = "0.4.17" +base64 = "0.13.1" # useful for tests (embedded files should be delete in release ?) #rust-embed="6.4.1" diff --git a/src/http/router.rs b/src/http/router.rs index 1c41b40..57b0070 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -1,6 +1,7 @@ //! router aims to handle correctly the request corresponding to the target //! it implements all the logic to build an `HTTPResponse` +use base64; use json; use super::{HTTPMessage, HTTPRequest, HTTPResponse}; @@ -11,6 +12,7 @@ use crate::stores::{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) -> HTTPResponse { let mut store = FileStore::new(config.filestore_path.clone()); @@ -87,6 +89,28 @@ async fn handle_validate(request: HTTPRequest, config: Config) -> HTTPResponse { HTTPResponse::as_200(Some(json)) } +/// returns the JWT public key in base64 encoded +async fn handle_public_key(_request: HTTPRequest, config: Config) -> HTTPResponse { + let jwt_signer = { + match JWTSigner::new(config).await { + Ok(s) => s, + Err(e) => { + let message = HTTPMessage::error(&e); + let json = message.try_into().unwrap(); + return HTTPResponse::as_500(Some(json)); + } + } + }; + + let public_key = jwt_signer.get_public_key(); + + let mut message = HTTPMessage::default(); + message.put("pubkey", &base64::encode(public_key)); + + let json = message.try_into().unwrap(); + HTTPResponse::as_200(Some(json)) +} + pub struct Router; impl Router { @@ -99,6 +123,7 @@ impl Router { match target.as_str() { GET_ROUTE => handle_get(request, config).await, VALIDATE_ROUTE => handle_validate(request, config).await, + PUBKEY_ROUTE => handle_public_key(request, config).await, _ => HTTPResponse::as_404(), } } diff --git a/src/jwt/jwt.rs b/src/jwt/jwt.rs index ddcba72..8772517 100644 --- a/src/jwt/jwt.rs +++ b/src/jwt/jwt.rs @@ -90,6 +90,10 @@ impl JWTSigner { )), } } + + pub fn get_public_key(&self) -> String { + self.public_key.clone() + } } #[tokio::test] From 1d2924b7efbb3bdf9e6e227dd6e0d5938956e7ee Mon Sep 17 00:00:00 2001 From: landrigun Date: Sat, 19 Nov 2022 14:52:59 +0000 Subject: [PATCH 7/9] feat(log): display warning in log if body parsing failed --- src/http/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http/request.rs b/src/http/request.rs index 087c3e2..1dccfb5 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -195,7 +195,7 @@ impl HTTPRequest { let body = HTTPBody::try_from(rp.2); match body { Ok(v) => request.body = Some(v), - Err(e) => log::error!("{}", e), + Err(e) => log::warn!("{}", e), } return Ok(request); From 8d3651d6fc388b48456228462ccab6b6c345153a Mon Sep 17 00:00:00 2001 From: landrigun Date: Sat, 19 Nov 2022 15:53:31 +0000 Subject: [PATCH 8/9] add http method validation on each route + add pubkey tests --- README.md | 6 ++++++ src/http/request.rs | 4 ++++ src/http/router.rs | 25 +++++++++++++++++++------ tests/bash/curling.bash | 15 +++++++++++++-- tests/python/test_requests.py | 21 +++++++++++++++++++++ 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b8bc60f..e58d5c2 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,19 @@ expiration_time = 2 # in hours ```bash ./simple-auth +# get a JWT curl http://:/get/ -d '{"username":"", "password":""}' # should returned {"token":"
.."} +# validate a JWT curl http://:/validate/ -d '{"token":"
.."}' # should returned (if valid) {"valid":"true"} + +# get the public key for local validation +curl http://:/pubkey/ +{"pubkey":""} ``` ## Test diff --git a/src/http/request.rs b/src/http/request.rs index 1dccfb5..c4c7bc2 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -217,6 +217,10 @@ impl HTTPRequest { } } + pub fn get_method(&self) -> String { + self.start_line.method.clone() + } + #[allow(dead_code)] pub fn is_valid(&self) -> bool { return self.start_line.is_valid(); diff --git a/src/http/router.rs b/src/http/router.rs index 57b0070..f12b677 100644 --- a/src/http/router.rs +++ b/src/http/router.rs @@ -14,7 +14,11 @@ 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) -> HTTPResponse { +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.body { Some(ref b) => { @@ -48,7 +52,11 @@ async fn handle_get(request: HTTPRequest, config: Config) -> HTTPResponse { /// validates the token by checking: /// * expiration time /// * signature -async fn handle_validate(request: HTTPRequest, config: Config) -> HTTPResponse { +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, @@ -90,7 +98,11 @@ async fn handle_validate(request: HTTPRequest, config: Config) -> HTTPResponse { } /// returns the JWT public key in base64 encoded -async fn handle_public_key(_request: HTTPRequest, config: Config) -> HTTPResponse { +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, @@ -114,6 +126,7 @@ async fn handle_public_key(_request: HTTPRequest, config: Config) -> HTTPRespons pub struct Router; impl Router { + /// routes the request to the corresponding handling method pub async fn route(&self, request_str: &str, addr: String, config: Config) -> HTTPResponse { let mut request = HTTPRequest::from(request_str); request.set_addr(addr); @@ -121,9 +134,9 @@ impl Router { let target = request.start_line.get_target(); match target.as_str() { - GET_ROUTE => handle_get(request, config).await, - VALIDATE_ROUTE => handle_validate(request, config).await, - PUBKEY_ROUTE => handle_public_key(request, config).await, + 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(), } } diff --git a/tests/bash/curling.bash b/tests/bash/curling.bash index 4c9813d..5bd0be3 100755 --- a/tests/bash/curling.bash +++ b/tests/bash/curling.bash @@ -9,8 +9,8 @@ URL=${SIMPLE_AUTH_URL} if [ -z ${URL} ] then - echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:9001" - URL="http://localhost:9001" + echo "[WARN]: SIMPLE_AUTH_URL is empty, set to http://localhost:5555" + URL="http://localhost:5555" fi for i in {0..10} @@ -39,3 +39,14 @@ do 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 400" + exit 1 + fi +done diff --git a/tests/python/test_requests.py b/tests/python/test_requests.py index bc28b5a..9247873 100644 --- a/tests/python/test_requests.py +++ b/tests/python/test_requests.py @@ -1,3 +1,4 @@ +import base64 import jwt import os import requests @@ -113,3 +114,23 @@ class TestResponse(TestCase): "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") + self.assertIn("-BEGIN PUBLIC KEY-", b64_pubkey.decode()) + + 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"], + "the incoming request is not valid", + "invalid error message returned", + ) From b933853a13b7bbec7b6e481d5173d73c9a89130a Mon Sep 17 00:00:00 2001 From: landrigun Date: Mon, 21 Nov 2022 08:19:40 +0000 Subject: [PATCH 9/9] release: version number bumped --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bc542d0..e10da92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "simple-auth" -version = "0.2.0" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html