Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/build_bins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ env:
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
RUSTFLAGS: -D warnings
RUSTUP_MAX_RETRIES: 10

defaults:
Expand Down
29 changes: 15 additions & 14 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ readme = "README.md"
keywords = ["webserver", "fcgi"]

[dependencies]
hyper = { version = "1.2", default-features = false, features = ["http1", "http2", "server"]} # HTTP
hyper-util = {version = "0.1.3", features = ["tokio"]}
hyper = { version = "1.8", default-features = false, features = ["http1", "http2", "server"]} # HTTP
hyper-util = {version = "0.1", features = ["tokio"]}
pin-project-lite = "0.2"
bytes = "1"
log = "0.4"
log4rs = { version = "1", default-features = false, features = ["all_components", "config_parsing"] }

#config:
toml = "0.8" # config files in toml
toml = "1.0" # config files in toml
serde = { version = "1.0", features = ["derive"] }

#main:
Expand All @@ -34,28 +34,29 @@ async-fcgi = {version = "0.5", features = ["app_start"], optional = true}
#rproxy
#hyper-reverse-proxy = "0.4"
#https:
tokio-rustls = { version = "0.25", optional = true }
async-acme = { version="0.5", optional = true }
rustls-pemfile = { version = "2.1", optional = true}
tokio-rustls = { version = "0.26", optional = true, default-features = false, features = ["tls12", "logging"]}
async-acme = { version="0.6", optional = true, features = ["use_tokio"]}
rustls-pemfile = { version = "2.2", optional = true}

#async_compression # in no BREACH / TIME cases?

md5 = "0.7"
rand = "0.8"
lazy_static = "1.4"
md5 = "0.8"
rand = "0.9"
lazy_static = "1.5"

#websocket
websocket-codec = { version = "0.5", optional = true }
tokio-util = { version = "0.7", features=["codec"], optional = true }
#webdav
xml-rs = { version = "0.8", optional = true }
xml-rs = { version = "1.0", optional = true }
chrono = { version = "0.4", optional = true }
#proxy
deadpool = {version="0.11", features=["unmanaged"], default-features = false, optional = true }
deadpool = {version="0.13", features=["unmanaged"], default-features = false, optional = true }
sha1 = {version="0.6", optional = true } # same as websocket-codec
base64 = {version="0.13", optional = true } # same as websocket-codec

anyhow = "1.0"
exn = "0.3.0"
async-stream-connection = {version="^1.0.1", features=["serde"], optional = true}

[target.'cfg(unix)'.dependencies]
Expand All @@ -64,9 +65,9 @@ libsystemd = "0.7"
libc = "0.2"

[features]
default = ["tlsrust", "tlsrust_acme", "fcgi", "webdav", "proxy"]
tlsrust = ["tokio-rustls", "rustls-pemfile"]
tlsrust_acme = ["async-acme/hyper_rustls"]
default = ["tlsrust", "tlsrust_acme", "fcgi", "webdav", "proxy", "websocket"]
tlsrust = ["tokio-rustls/ring", "rustls-pemfile"]
tlsrust_acme = ["async-acme/rustls_ring"]
logrot = ["log4rs/background_rotation"]
fcgi = ["async-fcgi", "async-stream-connection"]
websocket = ["websocket-codec", "tokio-util", "futures-util/sink", "async-stream-connection"]
Expand Down
54 changes: 31 additions & 23 deletions src/auth/digest.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use crate::auth::{get_map_from_header, strip_prefix};
use crate::body::{BoxBody, FRWSResp};
use crate::auth::{get_map_from_header, strip_prefix, AuthResult};
use crate::body::{BoxBody, FRWSErr, FRWSResp};
use crate::dispatch::Req;
use exn::{bail, ResultExt};
use hyper::{body::Body, header, Response, StatusCode};
use lazy_static::lazy_static;
use log::{info, trace};
use md5::Context;
use rand::rngs::OsRng;
use rand::RngCore;
use std::io::{Error as IoError, ErrorKind};
use rand::TryRngCore;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::fs::File;
Expand All @@ -17,8 +17,9 @@ use bytes::Bytes;
use log::{log_enabled, Level::Trace};

lazy_static! {
/// hash of `random U64 | ProcId`
static ref NONCESTARTHASH: Context = {
let rnd = OsRng.next_u64();
let rnd = OsRng.try_next_u64().unwrap();

let mut h = Context::new();
h.consume(rnd.to_be_bytes());
Expand Down Expand Up @@ -59,7 +60,7 @@ fn create_nonce() -> String {
let mut h = NONCESTARTHASH.clone();
h.consume(secs.to_be_bytes());

let n = format!("{:08x}{:032x}", secs, h.compute());
let n = format!("{:08x}{:032x}", secs, h.finalize());
n[..34].to_string()
}

Expand All @@ -83,7 +84,7 @@ fn validate_nonce(nonce: &[u8]) -> Result<bool, ()> {
//check hash
let mut h = NONCESTARTHASH.clone();
h.consume(secs_nonce.to_be_bytes());
let h = format!("{:x}", h.compute());
let h = format!("{:x}", h.finalize());
if h[..26] == n[8..34] {
return Ok(dur < 300); // from the last 5min
//Authentication-Info ?
Expand All @@ -94,11 +95,7 @@ fn validate_nonce(nonce: &[u8]) -> Result<bool, ()> {
Err(())
}

pub async fn check_digest<B: Body>(
auth_file: &Path,
req: &Req<B>,
realm: &str,
) -> Result<Option<FRWSResp>, IoError> {
pub async fn check_digest<B: Body>(auth_file: &Path, req: &Req<B>, realm: &str) -> AuthResult {
match req
.headers()
.get(header::AUTHORIZATION)
Expand Down Expand Up @@ -131,18 +128,29 @@ pub async fn check_digest<B: Body>(
Ok(true) => {} // good
Ok(false) => return Ok(Some(create_resp_needs_auth(realm, true))), // old
Err(()) => {
return Err(IoError::new(ErrorKind::PermissionDenied, "Invalid Nonce"))
bail!(FRWSErr::new(StatusCode::FORBIDDEN, "Invalid Nonce"))
} // strange
}

let file = File::open(auth_file).await?;
let file = File::open(auth_file).await.or_raise(|| {
FRWSErr::new(
StatusCode::INTERNAL_SERVER_ERROR,
"cant read user auth file",
)
})?;
let mut file = BufReader::new(file);

//read HA1 from file
//HA1 = make_md5(username+":"+realm+":"+password)
let mut ha1 = loop {
let mut buf = String::new();
if file.read_line(&mut buf).await? < 1 {
if file.read_line(&mut buf).await.or_raise(|| {
FRWSErr::new(
StatusCode::INTERNAL_SERVER_ERROR,
"cant read user auth file",
)
})? < 1
{
//user not found
info!("user not found");
//don't return to avaid timing attacks
Expand All @@ -162,7 +170,7 @@ pub async fn check_digest<B: Body>(
if let Some(uri) = user_vals.get(b"uri".as_ref()) {
ha2.consume(uri);
}
let ha2 = format!("{:x}", ha2.compute());
let ha2 = format!("{:x}", ha2.finalize());

let mut correct_response = None;
if let Some(qop) = user_vals.get(b"qop".as_ref()) {
Expand All @@ -178,7 +186,7 @@ pub async fn check_digest<B: Body>(
if let Some(cnonce) = user_vals.get(b"cnonce".as_ref()) {
c.consume(cnonce);
}
format!("{:x}", c.compute())
format!("{:x}", c.finalize())
};
}
}
Expand All @@ -201,7 +209,7 @@ pub async fn check_digest<B: Body>(
c.consume(qop);
c.consume(b":");
c.consume(&*ha2);
format!("{:x}", c.compute())
format!("{:x}", c.finalize())
});
}
}
Expand All @@ -214,7 +222,7 @@ pub async fn check_digest<B: Body>(
c.consume(nonce);
c.consume(b":");
c.consume(&*ha2);
format!("{:x}", c.compute())
format!("{:x}", c.finalize())
}
};
return if correct_response.as_bytes() == *user_response {
Expand All @@ -233,7 +241,7 @@ pub async fn check_digest<B: Body>(
}
}
//there is an auth header, but its garbage - at least to us
Err(IoError::new(ErrorKind::InvalidData, "auth failed"))
bail!(FRWSErr::new(StatusCode::BAD_REQUEST, "auth failed"))
}
}
}
Expand Down Expand Up @@ -368,13 +376,13 @@ mod tests {
let e = check_digest(&path, &h, &String::from("abc"))
.await
.unwrap_err();
assert_eq!(e.kind(), ErrorKind::InvalidData);
assert_eq!(e.code, StatusCode::BAD_REQUEST);

let h = create_req(Some("Digest ===,,"));
let e = check_digest(&path, &h, &String::from("abc"))
.await
.unwrap_err();
assert_eq!(e.kind(), ErrorKind::InvalidData);
assert_eq!(e.code, StatusCode::BAD_REQUEST);
}
#[test]
fn nonce() {
Expand All @@ -385,7 +393,7 @@ mod tests {
let mut h = NONCESTARTHASH.clone();
h.consume(secs.to_be_bytes());

let n = format!("{:08x}{:032x}", secs, h.compute());
let n = format!("{:08x}{:032x}", secs, h.finalize());
let n = n[..34].as_bytes();
assert!(!validate_nonce(n).unwrap());
//garbage not
Expand Down
14 changes: 8 additions & 6 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
use crate::body::{FRWSResp, IncomingBody};
use crate::body::{FRWSResp, IncomingBody, StatusResult};
use crate::config::Authenticatoin;
use crate::dispatch::Req;
use std::collections::HashMap;
use std::io::Error as IoError;

/// can be:
/// - an Error (with HTTP status)
/// - `Ok(None)` aka Auth is ok, proceed
/// - `Ok(HTTPResponse)` aka do stuff in oder to auth
pub type AuthResult = StatusResult<Option<FRWSResp>>;

mod digest;

pub async fn check_is_authorized(
auth: &Authenticatoin,
req: &Req<IncomingBody>,
) -> Result<Option<FRWSResp>, IoError> {
pub async fn check_is_authorized(auth: &Authenticatoin, req: &Req<IncomingBody>) -> AuthResult {
match auth {
Authenticatoin::Digest { userfile, realm } => {
digest::check_digest(userfile, req, realm).await
Expand Down
57 changes: 49 additions & 8 deletions src/body.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[cfg(any(feature = "fcgi", feature = "webdav"))]
use bytes::Buf;
use bytes::Bytes;
use exn::Exn;
use hyper::body::{Body as HttpBody, Frame, Incoming, SizeHint};
use pin_project_lite::pin_project;
#[cfg(any(feature = "fcgi", feature = "webdav"))]
Expand All @@ -9,8 +10,47 @@ use std::io::Error as IoError;
use std::pin::Pin;
use std::task::{Context, Poll};

pub type StatusResult<T> = exn::Result<T, FRWSErr>;
pub type FRWSResp = hyper::Response<BoxBody>;
pub type FRWSResult = Result<hyper::Response<BoxBody>, IoError>;
pub type FRWSResult = StatusResult<FRWSResp>;

#[derive(Debug)]
pub struct FRWSErr {
pub reason: String,
pub code: hyper::StatusCode,
}
impl FRWSErr {
pub fn new<S: Into<String>>(code: hyper::StatusCode, reason: S) -> FRWSErr {
FRWSErr {
reason: reason.into(),
code,
}
}
#[track_caller]
pub fn from_io(io: std::io::Error, reason: &str) -> Exn<FRWSErr> {
let code = match io.kind() {
std::io::ErrorKind::NotFound => hyper::StatusCode::NOT_FOUND,
std::io::ErrorKind::PermissionDenied => hyper::StatusCode::FORBIDDEN,
std::io::ErrorKind::InvalidInput | std::io::ErrorKind::InvalidData => {
hyper::StatusCode::BAD_REQUEST
}
std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::UnexpectedEof
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::ConnectionReset => hyper::StatusCode::BAD_GATEWAY,
std::io::ErrorKind::TimedOut => hyper::StatusCode::GATEWAY_TIMEOUT,
_ => hyper::StatusCode::INTERNAL_SERVER_ERROR,
};
Exn::new(io).raise(FRWSErr::new(code, reason))
}
}
impl core::fmt::Display for FRWSErr {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} error: {}", self.code, self.reason)
}
}
impl core::error::Error for FRWSErr {}

#[cfg(any(feature = "fcgi", feature = "webdav"))]
/// Request Body
Expand Down Expand Up @@ -219,7 +259,7 @@ pin_project! {
impl<B: HttpBody<Data = Bytes, Error = hyper::Error> + Unpin> futures_util::Future
for BufferBody<B>
{
type Output = Result<BufferedBody<B, Bytes>, IoError>;
type Output = StatusResult<BufferedBody<B, Bytes>>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.project();
Expand All @@ -232,10 +272,10 @@ impl<B: HttpBody<Data = Bytes, Error = hyper::Error> + Unpin> futures_util::Futu
this.buf.as_mut().unwrap().push_back(chunk);

if this.len > this.max_size {
return Poll::Ready(Err(IoError::new(
std::io::ErrorKind::PermissionDenied,
return Poll::Ready(Err(Exn::new(FRWSErr::new(
hyper::StatusCode::PAYLOAD_TOO_LARGE,
"body too big",
)));
))));
}
}
}
Expand All @@ -248,9 +288,10 @@ impl<B: HttpBody<Data = Bytes, Error = hyper::Error> + Unpin> futures_util::Futu
len: *this.len,
}))
}
Poll::Ready(Some(Err(e))) => {
Poll::Ready(Err(IoError::new(std::io::ErrorKind::Other, e.to_string())))
}
Poll::Ready(Some(Err(e))) => Poll::Ready(Err(Exn::new(FRWSErr::new(
hyper::StatusCode::UNPROCESSABLE_ENTITY,
e.to_string(),
)))),
Poll::Pending => Poll::Pending,
};
}
Expand Down
Loading
Loading