Repository: static-web-server/static-web-server (language: rust)

Issue:
Basic auth



Discussion / hints:
Not bad idea at least only for HTTP/2+TLS.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 141e0d1f..90dd536c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -64,6 +64,17 @@ version = "0.13.0"
 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"
@@ -79,6 +90,17 @@ dependencies = [
  "generic-array",
 ]
 
+[[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"
@@ -106,6 +128,12 @@ version = "3.8.0"
 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"
@@ -136,6 +164,15 @@ dependencies = [
  "winapi",
 ]
 
+[[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"
@@ -265,6 +302,17 @@ dependencies = [
  "version_check",
 ]
 
+[[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"
@@ -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 9ef739ed..83a591cf 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/README.md b/README.md
index 0075aea9..2a8b5a3c 100644
--- a/README.md
+++ b/README.md
@@ -102,13 +102,14 @@ Server can be configured either via environment variables or their equivalent co
 | `SERVER_DIRECTORY_LISTING`  | Enable directory listing for all requests ending with the slash character (‘/’) | Default `false` (disabled) |
 | `SERVER_SECURITY_HEADERS` | Enable security headers by default when HTTP/2 feature is activated. Headers included: `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` (2 years max-age), `X-Frame-Options: DENY`, `X-XSS-Protection: 1; mode=block` and `Content-Security-Policy: frame-ancestors 'self'` | Default `false` (disabled) |
 | `SERVER_CACHE_CONTROL_HEADERS` | 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`](./src/control_headers.rs) file. | Default `true` (enabled) |
+| `SERVER_BASIC_AUTH` | It provides [The "Basic" HTTP Authentication Scheme](https://datatracker.ietf.org/doc/html/rfc7617) using credentials as `user-id:password` pairs, encoded using `Base64`. Password must be encoded using the [BCrypt](https://en.wikipedia.org/wiki/Bcrypt) password-hashing function. | Default empty (disabled) |
 
 ### Command-line arguments
 
 CLI arguments listed with `static-web-server -h`.
 
 ```
-static-web-server 2.0.2
+static-web-server 2.0.3
 Jose Quintana <https://git.io/joseluisq>
 A blazing fast and asynchronous web server for static files-serving.
 
@@ -120,6 +121,9 @@ FLAGS:
     -V, --version    Prints version information
 
 OPTIONS:
+        --basic-auth <basic-auth>
+            It provides The "Basic" HTTP Authentication scheme using credentials as "user-id:password" pairs. Password
+            must be encoded using the "BCrypt" password-hashing function [env: SERVER_BASIC_AUTH=]  [default: ]
     -e, --cache-control-headers <cache-control-headers>
             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 [env: SERVER_CACHE_CONTROL_HEADERS=]  [default: true]
@@ -243,7 +247,7 @@ networks:
 
 Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in current work by you, as defined in the Apache-2.0 license, shall be dual licensed as described below, without any additional terms or conditions.
 
-Feel free to send some [Pull request](https://github.com/joseluisq/static-web-server/pulls) or [issue](https://github.com/joseluisq/static-web-server/issues).
+Feel free to send some [Pull request](https://github.com/joseluisq/static-web-server/pulls) or file an [issue](https://github.com/joseluisq/static-web-server/issues).
 
 ## License
 
diff --git a/src/basic_auth.rs b/src/basic_auth.rs
new file mode 100644
index 00000000..34c13b6c
--- /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::<Authorization<Basic>>() {
+        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 0f18c088..f29acbed 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 4abb266c..db670752 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<str>,
     pub page50x: Arc<str>,
+    pub basic_auth: Arc<str>,
 }
 
 /// 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 aa05b3f1..2f283531 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 c3247df2..addf9605 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,
             },
         });
 


Test patch: