From abc76a8c1f19e7efe2172f8808c171cb9d2e69ad Mon Sep 17 00:00:00 2001 From: Jose Quintana Date: Wed, 20 Oct 2021 01:10:24 +0200 Subject: [PATCH] feat: basic http authentication support resolves #53 --- Cargo.lock | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + src/basic_auth.rs | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 4 ++++ src/handler.rs | 48 ++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 1 + src/server.rs | 4 ++++ 7 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 src/basic_auth.rs diff --git a/Cargo.lock b/Cargo.lock index 141e0d1..90dd536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] +name = "bcrypt" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f691e63585950d8c1c43644d11bab9073e40f5060dd2822734ae7c3dc69a3a80" +dependencies = [ + "base64", + "blowfish", + "getrandom", +] + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -80,6 +91,17 @@ dependencies = [ ] [[package]] +name = "blowfish" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3ff3fc1de48c1ac2e3341c4df38b0d1bfb8fdf04632a187c8b75aaa319a7ab" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug", +] + +[[package]] name = "brotli" version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -107,6 +129,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -137,6 +165,15 @@ dependencies = [ ] [[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] name = "clap" version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -266,6 +303,17 @@ dependencies = [ ] [[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] name = "h2" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -839,6 +887,7 @@ version = "2.0.3" dependencies = [ "anyhow", "async-compression", + "bcrypt", "bytes", "ctrlc", "futures-util", @@ -916,12 +965,11 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "wasi", "winapi", ] @@ -1128,9 +1176,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" diff --git a/Cargo.toml b/Cargo.toml index 9ef739e..83a591c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ humansize = "1.1" time = "0.1" listenfd = "0.3" ctrlc = { version = "3.1", features = ["termination"] } +bcrypt = "0.10" [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator] version = "0.3" diff --git a/src/basic_auth.rs b/src/basic_auth.rs new file mode 100644 index 0000000..34c13b6 --- /dev/null +++ b/src/basic_auth.rs @@ -0,0 +1,101 @@ +use bcrypt::verify as bcrypt_verify; +use headers::{authorization::Basic, Authorization, HeaderMapExt}; +use hyper::StatusCode; + +/// Check for a `Basic` HTTP Authorization Schema of an incoming request +/// and uses `bcrypt` for password hashing verification. +pub fn check_request( + headers: &http::HeaderMap, + userid: &str, + password: &str, +) -> Result<(), StatusCode> { + if let Some(ref credentials) = headers.typed_get::>() { + if credentials.0.username() == userid { + return match bcrypt_verify(credentials.0.password(), password) { + Ok(valid) => { + if valid { + Ok(()) + } else { + Err(StatusCode::UNAUTHORIZED) + } + } + Err(err) => { + tracing::error!("bcrypt password verification error: {:?}", err); + Err(StatusCode::UNAUTHORIZED) + } + }; + } + } + + Err(StatusCode::UNAUTHORIZED) +} + +#[cfg(test)] +mod tests { + use super::check_request; + use headers::HeaderMap; + + #[test] + fn test_valid_auth() { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "Basic anE6anE=".parse().unwrap()); + assert!(check_request( + &headers, + "jq", + "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q" + ) + .is_ok()); + } + + #[test] + fn test_invalid_auth_header() { + let headers = HeaderMap::new(); + assert!(check_request(&headers, "jq", "").is_err()); + } + + #[test] + fn test_invalid_auth_pairs() { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "Basic anE6anE=".parse().unwrap()); + assert!(check_request(&headers, "xyz", "").is_err()); + } + + #[test] + fn test_invalid_auth() { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "Basic anE6anE=".parse().unwrap()); + assert!(check_request( + &headers, + "abc", + "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q" + ) + .is_err()); + assert!(check_request(&headers, "jq", "password").is_err()); + assert!(check_request(&headers, "", "password").is_err()); + assert!(check_request(&headers, "jq", "").is_err()); + } + + #[test] + fn test_invalid_auth_encoding() { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "Basic xyz".parse().unwrap()); + assert!(check_request( + &headers, + "jq", + "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q" + ) + .is_err()); + } + + #[test] + fn test_invalid_auth_encoding2() { + let mut headers = HeaderMap::new(); + headers.insert("Authorization", "abcd".parse().unwrap()); + assert!(check_request( + &headers, + "jq", + "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q" + ) + .is_err()); + } +} diff --git a/src/config.rs b/src/config.rs index 0f18c08..f29acbe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -141,4 +141,8 @@ pub struct Config { )] /// Enable cache control headers for incoming requests based on a set of file types. The file type list can be found on `src/control_headers.rs` file. pub cache_control_headers: bool, + + /// It provides The "Basic" HTTP Authentication scheme using credentials as "user-id:password" pairs. Password must be encoded using the "BCrypt" password-hashing function. + #[structopt(long, default_value = "", env = "SERVER_BASIC_AUTH")] + pub basic_auth: String, } diff --git a/src/handler.rs b/src/handler.rs index 4abb266..db67075 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,8 +1,9 @@ -use http::StatusCode; -use hyper::{Body, Request, Response}; +use hyper::{header::WWW_AUTHENTICATE, Body, Request, Response, StatusCode}; use std::{future::Future, path::PathBuf, sync::Arc}; -use crate::{compression, control_headers, cors, error_page, security_headers, static_files}; +use crate::{ + basic_auth, compression, control_headers, cors, error_page, security_headers, static_files, +}; use crate::{Error, Result}; /// It defines options for a request handler. @@ -15,6 +16,7 @@ pub struct RequestHandlerOpts { pub cache_control_headers: bool, pub page404: Arc, pub page50x: Arc, + pub basic_auth: Arc, } /// It defines the main request handler used by the Hyper service request. @@ -40,11 +42,11 @@ impl RequestHandler { if self.opts.cors.is_some() { let cors = self.opts.cors.as_ref().unwrap(); match cors.check_request(method, headers) { - Ok(r) => { - tracing::debug!("cors ok: {:?}", r); + Ok(res) => { + tracing::debug!("cors ok: {:?}", res); } - Err(e) => { - tracing::debug!("cors error kind: {:?}", e); + Err(err) => { + tracing::error!("cors error kind: {:?}", err); return error_page::error_response( method, &StatusCode::FORBIDDEN, @@ -55,6 +57,36 @@ impl RequestHandler { }; } + // `Basic` HTTP Authorization Schema + if !self.opts.basic_auth.is_empty() { + if let Some((user_id, password)) = self.opts.basic_auth.split_once(':') { + if let Err(err) = basic_auth::check_request(headers, user_id, password) { + tracing::warn!("basic authentication failed {:?}", err); + let mut resp = error_page::error_response( + method, + &StatusCode::UNAUTHORIZED, + self.opts.page404.as_ref(), + self.opts.page50x.as_ref(), + )?; + resp.headers_mut().insert( + WWW_AUTHENTICATE, + "Basic realm=\"Static Web Server\", charset=\"UTF-8\"" + .parse() + .unwrap(), + ); + return Ok(resp); + } + } else { + tracing::error!("invalid basic authentication `user_id:password` pairs"); + return error_page::error_response( + method, + &StatusCode::INTERNAL_SERVER_ERROR, + self.opts.page404.as_ref(), + self.opts.page50x.as_ref(), + ); + } + } + // Static files match static_files::handle(method, headers, root_dir, uri_path, dir_listing).await { Ok(mut resp) => { @@ -63,7 +95,7 @@ impl RequestHandler { resp = match compression::auto(method, headers, resp) { Ok(res) => res, Err(err) => { - tracing::debug!("error during body compression: {:?}", err); + tracing::error!("error during body compression: {:?}", err); return error_page::error_response( method, &StatusCode::INTERNAL_SERVER_ERROR, diff --git a/src/lib.rs b/src/lib.rs index aa05b3f..2f28353 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ #[macro_use] extern crate anyhow; +pub mod basic_auth; pub mod compression; pub mod config; pub mod control_headers; diff --git a/src/server.rs b/src/server.rs index c3247df..addf960 100644 --- a/src/server.rs +++ b/src/server.rs @@ -109,6 +109,9 @@ impl Server { // CORS option let cors = cors::new(opts.cors_allow_origins.trim().to_owned()); + // `Basic` HTTP Authorization Schema option + let basic_auth = Arc::from(opts.basic_auth.trim()); + // Create a service router for Hyper let router_service = RouterService::new(RequestHandler { opts: RequestHandlerOpts { @@ -120,6 +123,7 @@ impl Server { cache_control_headers, page404, page50x, + basic_auth, }, }); -- libgit2 1.7.2