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(-)
@@ -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"
@@ -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"
@@ -102,13 +102,14 @@ Server can be configured either via environment variables or their equivalent co
### 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
@@ -0,0 +1,101 @@
use bcrypt::verify as bcrypt_verify;
use headers::{authorization::Basic, Authorization, HeaderMapExt};
use hyper::StatusCode;
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());
}
}
@@ -141,4 +141,8 @@ pub struct Config {
)]
pub cache_control_headers: bool,
#[structopt(long, default_value = "", env = "SERVER_BASIC_AUTH")]
pub basic_auth: String,
}
@@ -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};
@@ -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>,
}
@@ -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 {
};
}
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(),
);
}
}
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,
@@ -4,6 +4,7 @@
#[macro_use]
extern crate anyhow;
pub mod basic_auth;
pub mod compression;
pub mod config;
pub mod control_headers;
@@ -109,6 +109,9 @@ impl Server {
let cors = cors::new(opts.cors_allow_origins.trim().to_owned());
let basic_auth = Arc::from(opts.basic_auth.trim());
let router_service = RouterService::new(RequestHandler {
opts: RequestHandlerOpts {
@@ -120,6 +123,7 @@ impl Server {
cache_control_headers,
page404,
page50x,
basic_auth,
},
});