index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2021-10-21 5:52:43.0 +00:00:00
committer GitHub <noreply@github.com> 2021-10-21 5:52:43.0 +00:00:00
commit
a3f4258e9a8b0a2b19bbf50786aab29ae9813c7b [patch]
tree
e8fff487908fe029ecf7d2b7662e3cdfd314b5b2
parent
5528bcb9ad2d611e8517b85762722880a90ad613
parent
f89c5c9158dad55d5a0507f17d5ada548865f36c
download
a3f4258e9a8b0a2b19bbf50786aab29ae9813c7b.tar.gz

Merge pull request #55 from joseluisq/feature/basic_http_authentication

feat: basic http authentication support

Diff

 Cargo.lock        |  58 +++++++++++++++++++++++++++++---
 Cargo.toml        |   1 +-
 README.md         |   8 +++-
 src/basic_auth.rs | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 src/config.rs     |   4 ++-
 src/handler.rs    |  48 +++++++++++++++++++++-----
 src/lib.rs        |   1 +-
 src/server.rs     |   4 ++-
 8 files changed, 210 insertions(+), 15 deletions(-)

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/README.md b/README.md
index 0075aea..2a8b5a3 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 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::<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 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<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 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,
            },
        });