From 8737c29932b6fd54467ffb7f1af2d3454fa8c06f Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:27:51 +0200 Subject: [PATCH 01/12] dont be more restrictive than a test --- .github/workflows/build_bins.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build_bins.yml b/.github/workflows/build_bins.yml index d526fc3..c68aa5e 100644 --- a/.github/workflows/build_bins.yml +++ b/.github/workflows/build_bins.yml @@ -13,7 +13,6 @@ env: CARGO_NET_RETRY: 10 CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - RUSTFLAGS: -D warnings RUSTUP_MAX_RETRIES: 10 defaults: From f02b94045c93082227e2d69ce305cabbb08e81df Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:11:57 +0100 Subject: [PATCH 02/12] always allow content len and type --- src/dispatch/proxy/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/proxy/mod.rs b/src/dispatch/proxy/mod.rs index 02c221d..3d5ecab 100644 --- a/src/dispatch/proxy/mod.rs +++ b/src/dispatch/proxy/mod.rs @@ -145,7 +145,7 @@ fn filter_header(headers: &mut HeaderMap, filter: &Option>) { let mut last = true; headers.extend(old.into_iter().filter(|(h, _v)| { if let Some(h) = h { - last = allowed.iter().any(|a| a.0 == *h); + last = [header::CONTENT_LENGTH, header::CONTENT_TYPE].iter().any(|a| a == h) || allowed.iter().any(|a| a.0 == *h); } last })); From 0c8adf3ea61aeb5903356c164a5f3c54c319d067 Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:12:32 +0100 Subject: [PATCH 03/12] dont reimplement rustls_pemfile function --- src/transport/tls/rustls/mod.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/transport/tls/rustls/mod.rs b/src/transport/tls/rustls/mod.rs index 06bfc46..62cdc43 100644 --- a/src/transport/tls/rustls/mod.rs +++ b/src/transport/tls/rustls/mod.rs @@ -183,21 +183,7 @@ fn load_private_key(filename: &Path) -> Result { let keyfile = fs::File::open(filename)?; let mut reader = io::BufReader::new(keyfile); - loop { - match rustls_pemfile::read_one(&mut reader).map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidData, - "cannot parse private key .pem file", - ) - })? { - Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(PrivateKey::Pkcs1(key)), - Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(PrivateKey::Pkcs8(key)), - Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(PrivateKey::Sec1(key)), - None => break, - _ => {} - } - } - Err(io::Error::new( + rustls_pemfile::private_key(&mut reader)?.ok_or(io::Error::new( io::ErrorKind::InvalidData, "expected a single private key", )) From ea82b7a496e05fb1ef323966e1ac49757a686e8e Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:13:21 +0100 Subject: [PATCH 04/12] reason about h1 pooling --- src/dispatch/proxy/client.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/dispatch/proxy/client.rs b/src/dispatch/proxy/client.rs index 1880dce..4679aa3 100644 --- a/src/dispatch/proxy/client.rs +++ b/src/dispatch/proxy/client.rs @@ -29,15 +29,17 @@ impl super::Proxy { if let Some(pool) = client.h1.write().await.as_mut() { let stat = pool.status(); match if stat.size == stat.max_size { - pool.get().await + pool.get().await //can't timeout } else { - pool.try_get() + pool.try_get() //get,forget permit } { Ok(mut h1) => { match h1.ready().await { Err(_e) => { //only error here is is_closed let _ = Object::take(h1); + //try again without connecting + continue; } Ok(()) => { //persist connection, if its not an upgrade @@ -50,12 +52,14 @@ impl super::Proxy { } let res = h1.send_request(req).await; //is_ready / is_closed - tokio::spawn(delay_drop(h1)); + if !h1.is_ready() { + tokio::spawn(delay_drop(h1)); + } return res.map_err(IoError::other); } } } - Err(PoolError::Timeout) => {} //connect a new IO + Err(PoolError::Timeout) => {} //semaphore:NoPermits && size < max_size -> connect a new IO Err(PoolError::Closed) => unreachable!("pool should not close"), Err(PoolError::NoRuntimeSpecified) => unreachable!("pool not using timeout"), } From c9f2fe922cf4941c8013a6b80830a324b173f5cc Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Tue, 7 Oct 2025 09:45:02 +0200 Subject: [PATCH 05/12] proxy: only log some headers --- src/dispatch/proxy/mod.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/dispatch/proxy/mod.rs b/src/dispatch/proxy/mod.rs index 3d5ecab..e0833f7 100644 --- a/src/dispatch/proxy/mod.rs +++ b/src/dispatch/proxy/mod.rs @@ -378,7 +378,18 @@ pub async fn forward( return Ok(resp); } }; - debug!("response: {:?} {:?}", resp.status(), resp.headers()); + if log::log_enabled!(log::Level::Debug) { + let mut resp_gist = String::with_capacity(128); + for h in [header::DATE, header::CONTENT_LENGTH, header::CONTENT_TYPE, header::LOCATION] { + if let Some(v) = resp.headers().get(&h) { + resp_gist.push(' '); + resp_gist.push_str(h.as_str()); + resp_gist.push_str(": "); + resp_gist.push_str(v.to_str().unwrap_or_default()); + } + } + debug!("response: {:?}{}", resp.status(), resp_gist); + } let resp_upgrade = if resp.status() == StatusCode::SWITCHING_PROTOCOLS && resp.version() == Version::HTTP_11 { From 18fd5eb9ac372b2d8a4824e519ee02b497908394 Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Tue, 7 Oct 2025 10:01:57 +0200 Subject: [PATCH 06/12] dep update --- Cargo.toml | 28 ++++++++++++++-------------- src/auth/digest.rs | 4 ++-- src/dispatch/dav/propfind.rs | 11 ++++------- src/main.rs | 21 ++++++++++----------- 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d71a21b..2ac5b72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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.5", 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 = "0.9" # config files in toml serde = { version = "1.0", features = ["derive"] } #main: @@ -34,24 +34,24 @@ 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.12", 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 @@ -64,9 +64,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"] diff --git a/src/auth/digest.rs b/src/auth/digest.rs index 57befe5..7a23523 100644 --- a/src/auth/digest.rs +++ b/src/auth/digest.rs @@ -6,7 +6,7 @@ use lazy_static::lazy_static; use log::{info, trace}; use md5::Context; use rand::rngs::OsRng; -use rand::RngCore; +use rand::TryRngCore; use std::io::{Error as IoError, ErrorKind}; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; @@ -18,7 +18,7 @@ use log::{log_enabled, Level::Trace}; lazy_static! { 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()); diff --git a/src/dispatch/dav/propfind.rs b/src/dispatch/dav/propfind.rs index a9a6b98..b7e82cb 100644 --- a/src/dispatch/dav/propfind.rs +++ b/src/dispatch/dav/propfind.rs @@ -94,13 +94,10 @@ async fn get_props_wanted(body: IncomingBody) -> Result, IoError> .map_err(|se| IoError::new(ErrorKind::InvalidData, se))? .reader(); - let xml = EventReader::new_with_config( - read, - ParserConfig { - trim_whitespace: true, - ..Default::default() - }, - ); + let xml = ParserConfig::new() + .trim_whitespace(true) + .allow_multiple_root_elements(false) + .create_reader(read); let mut props = Vec::new(); parse_propfind(xml, |prop| { props.push(prop); diff --git a/src/main.rs b/src/main.rs index ec2a416..2efa8fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -255,7 +255,9 @@ pub(crate) mod tests { l: TcpListener, a: SocketAddr, w: WwwRoot, - #[cfg(any(feature = "tlsrust", feature = "tlsnative"))] tls: Option, + #[cfg(any(feature = "tlsrust", feature = "tlsnative"))] tls: Option< + transport::tls::ParsedTLSConfig, + >, ) -> JoinHandle>> { let mut listening_ifs = HashMap::new(); let mut cfg = HostCfg::new(l.into_std().unwrap()); @@ -321,11 +323,11 @@ pub(crate) mod tests { transport::tls::ParsedTLSConfig, ) { use crate::dispatch::test::TempFile; - use rand::{rngs::OsRng, RngCore}; + use rand::{rngs::OsRng, TryRngCore}; use rustls_pemfile::{read_one, Item}; use tokio_rustls::rustls::{ClientConfig, RootCertStore}; - let tls_inst = OsRng.next_u32(); + let tls_inst = OsRng.try_next_u32().unwrap(); let key_file = TempFile::create( &format!("edkey{}.pem", tls_inst), crate::transport::tls::test::ED_KEY, @@ -416,24 +418,21 @@ pub(crate) mod tests { connector.connect(dnsname, stream).await.unwrap() }; - let req = b"\0\0\x15\x01\x05\0\0\0\x01\x82D\x83`lGA\x8c\x9d)\xacK\xccz\x07T\xcb\x9e\xc9\xbf\x87"; + let req = + b"\0\0\x15\x01\x05\0\0\0\x01\x82D\x83`lGA\x8c\x9d)\xacK\xccz\x07T\xcb\x9e\xc9\xbf\x87"; /*headers = [ (':method', 'GET'), (':path', '/a/b'), (':authority', SERVER_NAME), (':scheme', 'https'), ]*/ - let (end_stream, header) = h2_client( - &mut stream, - req, - None, - ).await.unwrap(); + let (end_stream, header) = h2_client(&mut stream, req, None).await.unwrap(); assert!(end_stream); assert_eq!(header[0], 0x88); } /// connect to a H2 server, send PREFACE, empty Options and a `req`. Ack settings. Wait for the return headers. /// Return if END_STREAM was set and the last header payload - pub(crate) async fn h2_client( + pub(crate) async fn h2_client( stream: &mut RW, req: &[u8], assert_option: Option<&[u8]>, @@ -466,7 +465,7 @@ pub(crate) mod tests { stream.write_all(req).await?; ack = true; } - if buf[3] == 1 && buf[4]&4 == 4 { + if buf[3] == 1 && buf[4] & 4 == 4 { //Header + END_STREAM + END_HEADERS println!("headers done"); return Ok((buf[4] == 5, buf[9..].to_vec())); From dcdf791db9aa75bdf58b7fcbe659996ae058d75e Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:24:28 +0100 Subject: [PATCH 07/12] logging --- src/dispatch/mod.rs | 4 ++-- src/dispatch/proxy/client.rs | 21 +++++++++++++++++---- src/main.rs | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/dispatch/mod.rs b/src/dispatch/mod.rs index 2733e04..2b75d30 100644 --- a/src/dispatch/mod.rs +++ b/src/dispatch/mod.rs @@ -269,7 +269,7 @@ async fn handle_vhost( Err(IoError::new( ErrorKind::PermissionDenied, - "not a mount path", + format!("not a mount path: {:?}", req.path()), )) } @@ -331,7 +331,7 @@ pub(crate) async fn handle_request( dispatch_to_vhost(req, cfg, remote_addr) .await .or_else(|err| { - error!("{}", err); + error!("handle_request {}", err); if let Some(cause) = err.get_ref() { let mut e: &dyn Error = cause; loop { diff --git a/src/dispatch/proxy/client.rs b/src/dispatch/proxy/client.rs index 4679aa3..48d74e1 100644 --- a/src/dispatch/proxy/client.rs +++ b/src/dispatch/proxy/client.rs @@ -26,7 +26,7 @@ impl super::Proxy { ) -> Result, IoError> { let client = self.client.as_ref().unwrap(); loop { - if let Some(pool) = client.h1.write().await.as_mut() { + if let Some(pool) = client.h1.read().await.as_ref() { let stat = pool.status(); match if stat.size == stat.max_size { pool.get().await //can't timeout @@ -38,6 +38,7 @@ impl super::Proxy { Err(_e) => { //only error here is is_closed let _ = Object::take(h1); + log::trace!("con dead"); //try again without connecting continue; } @@ -53,13 +54,14 @@ impl super::Proxy { let res = h1.send_request(req).await; //is_ready / is_closed if !h1.is_ready() { + log::trace!("w8 4 con"); tokio::spawn(delay_drop(h1)); } return res.map_err(IoError::other); } } } - Err(PoolError::Timeout) => {} //semaphore:NoPermits && size < max_size -> connect a new IO + Err(PoolError::Timeout) => {log::trace!("pool could use another con");} //semaphore:NoPermits && size < max_size -> connect a new IO Err(PoolError::Closed) => unreachable!("pool should not close"), Err(PoolError::NoRuntimeSpecified) => unreachable!("pool not using timeout"), } @@ -82,6 +84,7 @@ impl super::Proxy { async fn delay_drop(mut h1: Object>) { if let Err(_e) = h1.ready().await { //only error here is is_closed + log::trace!("con done"); let _ = Object::take(h1); } } @@ -103,8 +106,18 @@ impl Client { } async fn add_to_h1_pool(&self, s: http1::SendRequest, max_size: usize) { let mut lock = self.h1.write().await; - let pool = lock.get_or_insert(Pool::new(max_size)); - pool.try_add(s).expect("pool should never close"); + let pool = lock.get_or_insert(Pool::new(max_size)); //semaphore:0permits, size_semaphore:max + //pool.try_add(s).expect("pool should never close"); + match pool.try_add(s) { + Ok(()) => {}, + Err((s, PoolError::Timeout)) => { + log::warn!("pool add {:?}", pool.status()); + pool.add(s).await.expect("wtf"); //FIXME size max + } + Err((_, PoolError::Closed)) => unreachable!("pool should never close"), + Err((_, PoolError::NoRuntimeSpecified)) => unreachable!("pool not using timeout"), + } + } async fn connect(&self, cfg: &super::Proxy) -> Result<(), IoError> { let addr = match &cfg.forward.addr { diff --git a/src/main.rs b/src/main.rs index 2efa8fc..c7851d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,7 +75,7 @@ async fn prepare_hyper_servers( //this was an ACME challenge. Don't print an error continue; } - error!("{:?}", e); + error!("TLS Handshake {:?}", e);//TODO Os { code: 113, kind: HostUnreachable, message: "No route to host" } continue; } }; From a303b755b056d7f37261a9535e3023921bdd3eb1 Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:36:35 +0100 Subject: [PATCH 08/12] Exn --- Cargo.toml | 1 + src/auth/digest.rs | 35 +++++---- src/auth/mod.rs | 9 +-- src/body.rs | 57 ++++++++++++--- src/config.rs | 38 +++++----- src/dispatch/dav/mod.rs | 117 +++++++++++++++++------------- src/dispatch/dav/propfind.rs | 31 ++++---- src/dispatch/fcgi/mod.rs | 78 ++++++++++---------- src/dispatch/fcgi/test.rs | 18 ++--- src/dispatch/mod.rs | 66 ++++------------- src/dispatch/proxy/cfg.rs | 27 +++++++ src/dispatch/proxy/client.rs | 9 +-- src/dispatch/proxy/mod.rs | 133 ++++++++++++++++++++++++++--------- src/dispatch/proxy/test.rs | 2 + src/dispatch/staticf.rs | 38 +++++----- src/dispatch/test.rs | 24 +++---- src/dispatch/webpath.rs | 36 +++++----- src/dispatch/websocket.rs | 14 ++-- src/main.rs | 9 +-- src/transport/mod.rs | 8 +-- src/transport/tls/mod.rs | 43 +++++++---- 21 files changed, 467 insertions(+), 326 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ac5b72..d84e757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ 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] diff --git a/src/auth/digest.rs b/src/auth/digest.rs index 7a23523..2cb354c 100644 --- a/src/auth/digest.rs +++ b/src/auth/digest.rs @@ -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::TryRngCore; -use std::io::{Error as IoError, ErrorKind}; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::fs::File; @@ -94,11 +94,7 @@ fn validate_nonce(nonce: &[u8]) -> Result { Err(()) } -pub async fn check_digest( - auth_file: &Path, - req: &Req, - realm: &str, -) -> Result, IoError> { +pub async fn check_digest(auth_file: &Path, req: &Req, realm: &str) -> AuthResult { match req .headers() .get(header::AUTHORIZATION) @@ -131,18 +127,29 @@ pub async fn check_digest( 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 @@ -233,7 +240,7 @@ pub async fn check_digest( } } //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")) } } } @@ -368,13 +375,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() { diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 963e372..d36af5c 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,15 +1,12 @@ -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; +pub type AuthResult = StatusResult>; mod digest; -pub async fn check_is_authorized( - auth: &Authenticatoin, - req: &Req, -) -> Result, IoError> { +pub async fn check_is_authorized(auth: &Authenticatoin, req: &Req) -> AuthResult { match auth { Authenticatoin::Digest { userfile, realm } => { digest::check_digest(userfile, req, realm).await diff --git a/src/body.rs b/src/body.rs index c35f031..b978c48 100644 --- a/src/body.rs +++ b/src/body.rs @@ -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"))] @@ -9,8 +10,47 @@ use std::io::Error as IoError; use std::pin::Pin; use std::task::{Context, Poll}; +pub type StatusResult = exn::Result; pub type FRWSResp = hyper::Response; -pub type FRWSResult = Result, IoError>; +pub type FRWSResult = StatusResult; + +#[derive(Debug)] +pub struct FRWSErr { + pub reason: String, + pub code: hyper::StatusCode, +} +impl FRWSErr { + pub fn new>(code: hyper::StatusCode, reason: S) -> FRWSErr { + FRWSErr { + reason: reason.into(), + code, + } + } + #[track_caller] + pub fn from_io(io: std::io::Error, reason: &str) -> Exn { + 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 @@ -219,7 +259,7 @@ pin_project! { impl + Unpin> futures_util::Future for BufferBody { - type Output = Result, IoError>; + type Output = StatusResult>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut this = self.project(); @@ -232,10 +272,10 @@ impl + 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", - ))); + )))); } } } @@ -248,9 +288,10 @@ impl + 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, }; } diff --git a/src/config.rs b/src/config.rs index 741dafc..3d373ef 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,7 +10,7 @@ use crate::dispatch::test::UnitTestUseCase; use crate::dispatch::websocket::Websocket; #[cfg(any(feature = "tlsrust", feature = "tlsnative"))] use crate::transport::tls::{ParsedTLSConfig, TLSBuilderTrait, TlsUserConfig}; -use anyhow::{Context, Result}; +use exn::ResultExt as _; use hyper::header::HeaderName; use hyper::http::HeaderValue; use hyper::StatusCode; @@ -354,8 +354,7 @@ pub struct Configuration { pub hosts: HashMap, } -#[derive(Debug)] -struct CFGError { +pub struct CFGError { errors: Vec, } impl CFGError { @@ -368,6 +367,9 @@ impl CFGError { fn has_errors(&self) -> bool { !self.errors.is_empty() } + fn one(s: String) -> CFGError { + CFGError { errors: vec![s] } + } } impl fmt::Display for CFGError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -377,10 +379,15 @@ impl fmt::Display for CFGError { Ok(()) } } +impl fmt::Debug for CFGError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self, f) + } +} impl std::error::Error for CFGError {} /// load and verify configuration options -pub fn load_config() -> anyhow::Result { +pub fn load_config() -> exn::Result { #[cfg(not(test))] let path = { let mut path = None; @@ -393,23 +400,18 @@ pub fn load_config() -> anyhow::Result { } match path { Some(p) => p, - None => { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "No config file found!", - ) - .into()) - } + None => return Err(CFGError::one("No config file found!".into()).into()), } }; #[cfg(test)] let path = "./test_cfg.toml"; // read the whole file - let buffer = - read_to_string(&path).with_context(|| format!("Failed to open config from {:?}", path))?; + let buffer = read_to_string(&path) + .or_raise(|| CFGError::one(format!("Failed to open config from {:?}", path)))?; - let mut cfg = toml::from_str::(&buffer)?; + let mut cfg = toml::from_str::(&buffer) + .or_raise(|| CFGError::one("failed to parse config".into()))?; if log::log_enabled!(log::Level::Info) { for (host_name, vhost) in cfg.hosts.iter_mut() { info!("host: {} @ {}", host_name, vhost.ip); @@ -451,7 +453,9 @@ impl fmt::Debug for HostCfg { /// - binds on the `SocketAddr`s, /// - executes and connects to FCGI servers /// - setup TLS config -pub async fn group_config(cfg: &mut Configuration) -> anyhow::Result> { +pub async fn group_config( + cfg: &mut Configuration, +) -> exn::Result, CFGError> { let mut listening_ifs = HashMap::new(); let mut errors = CFGError::new(); #[cfg(unix)] @@ -492,11 +496,11 @@ pub async fn group_config(cfg: &mut Configuration) -> anyhow::Result DavMethod::MOVE, "MKCOL" => DavMethod::MKCOL, "PROPPATCH" => { - return Err(IoError::new( - ErrorKind::PermissionDenied, + bail!(FRWSErr::new( + StatusCode::FORBIDDEN, "properties are read only", )) } @@ -84,7 +85,7 @@ pub async fn do_dav( | DavMethod::MKCOL ) { - return Err(IoError::new(ErrorKind::PermissionDenied, "read only mount")); + bail!(FRWSErr::new(StatusCode::FORBIDDEN, "read only mount")); } let full_path = req.path().prefix_with(&config.dav); @@ -93,8 +94,8 @@ pub async fn do_dav( //Path will be created -> check dad: match full_path.parent() { None => { - return Err(IoError::new( - ErrorKind::Other, //should not be reachable + bail!(FRWSErr::new( + StatusCode::INTERNAL_SERVER_ERROR, //should not be reachable "No Parent", )); } @@ -105,25 +106,25 @@ pub async fn do_dav( *res.status_mut() = StatusCode::CONFLICT; return Ok(res); } - Err(e) => return Err(e), + Err(e) => bail!(FRWSErr::from_io(e, "unable to canonicalize")), }, } } else { - full_path.canonicalize()? + full_path + .canonicalize() + .map_err(|e| FRWSErr::from_io(e, "unable to canonicalize"))? }; //check if the canonicalized version is still inside of the (abs) root path if !fp.starts_with(&config.dav) { - return Err(IoError::new( - ErrorKind::PermissionDenied, + bail!(FRWSErr::new( + StatusCode::FORBIDDEN, "Symlinks are not allowed", )); } } match m { - DavMethod::PROPFIND => { - propfind::handle_propfind(req, full_path, &config.dav).await - } + DavMethod::PROPFIND => propfind::handle_propfind(req, full_path, &config.dav).await, DavMethod::GET => handle_get(req, &full_path).await, DavMethod::PUT => handle_put(req, &full_path, config.dont_overwrite).await, DavMethod::MKCOL => handle_mkdir(&full_path).await, @@ -148,17 +149,24 @@ pub async fn do_dav( .await } DavMethod::DELETE if !config.dont_overwrite => handle_delete(&full_path).await, - DavMethod::DELETE => Err(IoError::new( - ErrorKind::PermissionDenied, + DavMethod::DELETE => bail!(FRWSErr::new( + StatusCode::FORBIDDEN, "dont_overwrite forbids delete", )), } } async fn list_dir(full_path: &Path, url_path: String) -> FRWSResult { - let mut dir = tokio::fs::read_dir(full_path).await?; + let mut dir = tokio::fs::read_dir(full_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to read_dir"))?; let mut buf = BytesMut::new().writer(); - buf.write_all(b"")?; - while let Some(f) = dir.next_entry().await? { + let xml_err = || FRWSErr::new(StatusCode::INTERNAL_SERVER_ERROR, "xml writer failed"); + buf.write_all(b"").or_raise(xml_err)?; + while let Some(f) = dir + .next_entry() + .await + .map_err(|e| FRWSErr::from_io(e, "unable to get next entry"))? + { let path = f.path(); let meta = match f.metadata().await { Ok(meta) => meta, @@ -177,7 +185,8 @@ async fn list_dir(full_path: &Path, url_path: String) -> FRWSResult { f.file_name().to_string_lossy() ) .as_bytes(), - )?; + ) + .or_raise(xml_err)?; } else { buf.write_all( format!( @@ -187,10 +196,11 @@ async fn list_dir(full_path: &Path, url_path: String) -> FRWSResult { meta.len() ) .as_bytes(), - )?; + ) + .or_raise(xml_err)?; } } - buf.write_all(b"")?; + buf.write_all(b"").or_raise(xml_err)?; let res = Response::builder() .header(header::CONTENT_TYPE, &b"text/html; charset=UTF-8"[..]) .body(buf.into()) @@ -198,10 +208,7 @@ async fn list_dir(full_path: &Path, url_path: String) -> FRWSResult { Ok(res) } -async fn handle_get( - req: Req, - full_path: &Path, -) -> FRWSResult { +async fn handle_get(req: Req, full_path: &Path) -> FRWSResult { //we could serve dir listings as well. with a litte webdav client :-D let (_, file_lookup) = resolve_path(full_path, false, &None).await?; @@ -224,26 +231,29 @@ async fn handle_get( async fn handle_delete(full_path: &Path) -> FRWSResult { let res = Response::new(BoxBody::empty()); - let meta = metadata(full_path).await?; + let meta = metadata(full_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to open"))?; if meta.is_dir() { - remove_dir_all(full_path).await?; + remove_dir_all(full_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to remove_dir_all"))?; } else { - remove_file(full_path).await?; + remove_file(full_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to remove_file"))?; } log::info!("Deleted {:?}", full_path); //HTTP NO_CONTENT ? Ok(res) } -async fn parent_exists(path: &Path) -> Result { +async fn parent_exists(path: &Path) -> StatusResult { match path.parent() { - None => Err(IoError::new( - std::io::ErrorKind::PermissionDenied, - "No parent", - )), + None => bail!(FRWSErr::new(StatusCode::FORBIDDEN, "No parent",)), Some(p) => match metadata(p).await { Ok(m) => Ok(m.is_dir()), Err(ref e) if e.kind() == ErrorKind::NotFound => Ok(false), - Err(e) => Err(e), + Err(e) => bail!(FRWSErr::from_io(e, "could not open")), }, } } @@ -259,7 +269,9 @@ async fn handle_mkdir(full_path: &Path) -> FRWSResult { *res.status_mut() = StatusCode::CONFLICT; return Ok(res); } - create_dir(full_path).await?; + create_dir(full_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to create dir"))?; *res.status_mut() = StatusCode::CREATED; log::info!("Created {:?}", full_path); /* @@ -307,8 +319,8 @@ impl AsyncRead for BodyW { async fn handle_put(req: Req, full_path: &Path, dont_overwrite: bool) -> FRWSResult { // Check if file exists before proceeding if dont_overwrite && metadata(full_path).await.is_ok() { - return Err(IoError::new( - ErrorKind::AlreadyExists, + bail!(FRWSErr::new( + StatusCode::FORBIDDEN, "file overwriting is disabled", )); } @@ -319,10 +331,14 @@ async fn handle_put(req: Req, full_path: &Path, dont_overwrite: bo return Ok(res); } log::info!("about to store {:?}", full_path); - let mut f = File::create(full_path).await?; + let mut f = File::create(full_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to create"))?; let (_, s) = req.into_parts(); let mut b = BodyW { s, b: None }; - redirect(&mut b, &mut f).await?; + redirect(&mut b, &mut f) + .await + .or_raise(|| FRWSErr::new(StatusCode::INTERNAL_SERVER_ERROR, "writing failed"))?; let res = Response::new(BoxBody::empty()); //MAY 405 if file is a folder Ok(res) @@ -332,21 +348,18 @@ fn get_dst( header: &HeaderMap, root: &AbsPathBuf, web_mount: &str, -) -> Result { +) -> StatusResult { // Get the destination let dst = header .get("Destination") .map(|hv| hv.as_bytes()) .and_then(|s| hyper::Uri::try_from(s).ok()) - .ok_or_else(|| IoError::new(ErrorKind::InvalidData, "no valid destination path"))?; + .ok_or_raise(|| FRWSErr::new(StatusCode::BAD_REQUEST, "no valid destination path"))?; let request_path = super::WebPath::try_from(&dst)?; - let path = request_path.strip_prefix(Path::new(web_mount)).map_err(|_| { - IoError::new( - ErrorKind::PermissionDenied, - "destination path outside of mount", - ) - })?; + let path = request_path + .strip_prefix(Path::new(web_mount)) + .map_err(|_| FRWSErr::new(StatusCode::FORBIDDEN, "destination path outside of mount"))?; let req_path = path.prefix_with(root); Ok(req_path) } @@ -373,7 +386,9 @@ async fn handle_copy( *res.status_mut() = StatusCode::PRECONDITION_FAILED; return Ok(res); } - copy(src_path, dst_path).await?; + copy(src_path, dst_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to copy"))?; //resulted in the creation of a new resource. *res.status_mut() = StatusCode::CREATED; /* @@ -426,7 +441,9 @@ async fn handle_move( *res.status_mut() = StatusCode::PRECONDITION_FAILED; return Ok(res); } - rename(src_path, dst_path).await?; + rename(src_path, dst_path) + .await + .map_err(|e| FRWSErr::from_io(e, "unable to rename"))?; //a new URL mapping was created at the destination. *res.status_mut() = StatusCode::CREATED; /* diff --git a/src/dispatch/dav/propfind.rs b/src/dispatch/dav/propfind.rs index b7e82cb..ba93a22 100644 --- a/src/dispatch/dav/propfind.rs +++ b/src/dispatch/dav/propfind.rs @@ -1,5 +1,6 @@ use bytes::{Buf, BufMut, BytesMut}; use chrono::{DateTime, Utc}; +use exn::ResultExt; use hyper::{body::Body as _, Response, StatusCode}; use std::{ fs::Metadata, @@ -16,7 +17,10 @@ use xml::{ EmitterConfig, ParserConfig, }; -use crate::config::AbsPathBuf; +use crate::{ + body::{FRWSErr, StatusResult}, + config::AbsPathBuf, +}; use crate::{ body::{FRWSResult, IncomingBody, IncomingBodyTrait}, dispatch::Req, @@ -41,7 +45,9 @@ pub async fn handle_propfind( //log::debug!("Propfind {:?} {:?}", path, props); - let meta = metadata(&path).await?; + let meta = metadata(&path) + .await + .map_err(|io| FRWSErr::from_io(io, "could not get metadata from file"))?; let mut buf = BytesMut::new().writer(); let mut xmlwriter = EventWriter::new_with_config( @@ -51,28 +57,29 @@ pub async fn handle_propfind( ..Default::default() }, ); + let xml_err = || FRWSErr::new(StatusCode::INTERNAL_SERVER_ERROR, "xml writer failed"); xmlwriter .write(XmlWEvent::StartDocument { version: XmlVersion::Version10, encoding: Some("utf-8"), standalone: None, }) - .map_err(|se| IoError::new(ErrorKind::Other, se))?; + .or_raise(xml_err)?; xmlwriter .write(XmlWEvent::start_element("D:multistatus").ns("D", "DAV:")) - .map_err(|se| IoError::new(ErrorKind::Other, se))?; + .or_raise(xml_err)?; handle_propfind_path(&mut xmlwriter, &path, root, &web_mount, &meta, &props) - .map_err(|se| IoError::new(ErrorKind::Other, se))?; + .or_raise(xml_err)?; if meta.is_dir() && depth > 0 { handle_propfind_path_recursive(&path, root, &web_mount, depth, &mut xmlwriter, &props) .await - .map_err(|se| IoError::new(ErrorKind::Other, se))?; + .or_raise(xml_err)?; } xmlwriter .write(XmlWEvent::end_element()) - .map_err(|se| IoError::new(ErrorKind::Other, se))?; + .or_raise(xml_err)?; let mut res = Response::new(buf.into()); @@ -80,7 +87,7 @@ pub async fn handle_propfind( Ok(res) } -async fn get_props_wanted(body: IncomingBody) -> Result, IoError> { +async fn get_props_wanted(body: IncomingBody) -> StatusResult> { if let Some(0) = body.size_hint().exact() { //Windows explorer does not send a body Ok(vec![ @@ -88,11 +95,7 @@ async fn get_props_wanted(body: IncomingBody) -> Result, IoError> OwnedName::qualified("getcontentlength", "DAV", Option::::None), ]) } else { - let read = body - .buffer(9 * 1024) - .await - .map_err(|se| IoError::new(ErrorKind::InvalidData, se))? - .reader(); + let read = body.buffer(9 * 1024).await?.reader(); let xml = ParserConfig::new() .trim_whitespace(true) @@ -102,7 +105,7 @@ async fn get_props_wanted(body: IncomingBody) -> Result, IoError> parse_propfind(xml, |prop| { props.push(prop); }) - .map_err(|_| IoError::new(ErrorKind::InvalidData, "xml parse error"))?; + .or_raise(|| FRWSErr::new(StatusCode::BAD_REQUEST, "xml parse error"))?; Ok(props) } } diff --git a/src/dispatch/fcgi/mod.rs b/src/dispatch/fcgi/mod.rs index 5ea64f7..2d1f825 100644 --- a/src/dispatch/fcgi/mod.rs +++ b/src/dispatch/fcgi/mod.rs @@ -4,6 +4,8 @@ pub(crate) mod test; pub use cfg::*; use bytes::{Bytes, BytesMut}; +use exn::{bail, Exn, Frame}; +use hyper::StatusCode; use hyper::{body::Body as _, Request, Response}; use log::{debug, error, trace}; use std::collections::HashMap; @@ -15,6 +17,7 @@ use std::{ }; use tokio::time::timeout; +use crate::body::{FRWSErr, StatusResult}; use crate::{ body::{BoxBody, BufferedBody, FRWSResult, IncomingBody, IncomingBodyTrait as _}, config::StaticFiles, @@ -46,19 +49,13 @@ pub async fn fcgi_call( app } else { error!("FCGI app not set"); - return Err(IoError::new( - ErrorKind::NotConnected, + bail!(FRWSErr::new( + StatusCode::INTERNAL_SERVER_ERROR, "FCGI app not available", )); }; - let params = create_params( - fcgi_cfg, - &req, - fs_full_path, - path_info_offset, - remote_addr, - ); + let params = create_params(fcgi_cfg, &req, fs_full_path, path_info_offset, remote_addr); trace!("to FCGI: {:?}", ¶ms); let (req, body) = req.into_parts(); @@ -75,17 +72,7 @@ pub async fn fcgi_call( //...but no len indicator if let Some(max_size) = fcgi_cfg.buffer_request { //read everything to memory - match body.buffer(max_size).await { - Ok(b) => b, - Err(e) if e.kind() == ErrorKind::PermissionDenied => { - //body is more than max_size -> abort - return Ok(Response::builder() - .status(hyper::StatusCode::PAYLOAD_TOO_LARGE) - .body(BoxBody::empty()) - .expect("unable to build response")); - } - Err(e) => return Err(e), - } + body.buffer(max_size).await? } else { //reject it return Ok(Response::builder() @@ -104,14 +91,15 @@ pub async fn fcgi_call( 0 => app.forward(req, params).await, secs => match timeout(Duration::from_secs(secs), app.forward(req, params)).await { Err(_) => { - return Err(IoError::new( - ErrorKind::TimedOut, + bail!(FRWSErr::new( + StatusCode::GATEWAY_TIMEOUT, "FCGI app did not respond", )) } Ok(resp) => resp, }, - }?; + } + .map_err(|io| FRWSErr::from_io(io, "could not open file"))?; debug!("FCGI response: {:?} {:?}", resp.status(), resp.headers()); //return types: resp.headers_mut().remove("Status"); @@ -176,10 +164,11 @@ fn create_params( if add_index { let index_file_name = full_path.file_name().and_then(|o| o.to_str()); if let Some(f) = index_file_name { - abs_name = req - .path() - .prefixed_as_abs_url_path(req.mount(), f.len()+1, false); - if !abs_name.ends_with('/') { // same as !req.path().is_empty() + abs_name = + req.path() + .prefixed_as_abs_url_path(req.mount(), f.len() + 1, false); + if !abs_name.ends_with('/') { + // same as !req.path().is_empty() abs_name.push('/'); } abs_name.push_str(f); @@ -213,9 +202,7 @@ fn create_params( Bytes::from(abs_web_mount), ); //... so everything inside it is PATH_INFO - let abs_path = req - .path() - .prefixed_as_abs_url_path("", 0, false); + let abs_path = req.path().prefixed_as_abs_url_path("", 0, false); params.insert( // opt CGI/1.1 4.1.5 Bytes::from(PATH_INFO), @@ -262,7 +249,9 @@ fn create_params( ); if fcgi_cfg.set_request_uri { let q = req.query(); - let mut r_uri = req.path().prefixed_as_abs_url_path(req.mount(), q.map_or(0, |q| q.len() + 1), false); + let mut r_uri = + req.path() + .prefixed_as_abs_url_path(req.mount(), q.map_or(0, |q| q.len() + 1), false); if let Some(q) = q { r_uri.push('?'); @@ -283,10 +272,7 @@ fn create_params( path_to_bytes(full_path) } else { // I am guessing here - Bytes::from( - req.path() - .prefixed_as_abs_url_path("", 0, false), - ) + Bytes::from(req.path().prefixed_as_abs_url_path("", 0, false)) }, ); } @@ -311,17 +297,30 @@ fn path_to_bytes>(path: P) -> Bytes { // but end up with u16 BytesMut::from(path.as_ref().to_string_lossy().to_string().as_bytes()).freeze() } + +fn find_error( + exn: &Exn, +) -> Option<&T> { + fn walk(frame: &Frame) -> Option<&T> { + if let Some(e) = frame.error().downcast_ref::() { + return Some(e); + } + frame.children().iter().find_map(walk) + } + walk(exn.frame()) +} + /// just like `staticf::resolve_path` but if a NotADirectory error would occur, it tries to split the request into file and PATH_INFO pub async fn resolve_path<'a>( full_path: PathBuf, is_dir_request: bool, sf: &StaticFiles, req: &'a Req, -) -> Result<(PathBuf, staticf::ResolveResult, Option), IoError> { +) -> StatusResult<(PathBuf, staticf::ResolveResult, Option)> { match staticf::resolve_path(&full_path, is_dir_request, &sf.index).await { Ok((p, r)) => Ok((p, r, None)), Err(err) => { - if error_indicates_path_info(&err) { + if find_error(&err).is_some_and(error_indicates_path_info) { debug!("{:?} might have a PATH_INFO", &full_path); /* pop the last path component until we hit a file @@ -347,7 +346,10 @@ pub async fn resolve_path<'a>( if error_indicates_path_info(&e) { //keep going up } else { - return Err(e); + bail!(FRWSErr::new( + StatusCode::NOT_FOUND, + "error while trying to split path_info" + )); } } } diff --git a/src/dispatch/fcgi/test.rs b/src/dispatch/fcgi/test.rs index e09550d..b7e4ad9 100644 --- a/src/dispatch/fcgi/test.rs +++ b/src/dispatch/fcgi/test.rs @@ -269,13 +269,7 @@ fn params_flup_example() { .insert(hyper::http::uri::Authority::from_static("localhost")); let req = Req::test_on_mount(req); - let params = create_params( - &fcgi_cfg, - &req, - None, - None, - "[::1]:1337".parse().unwrap(), - ); + let params = create_params(&fcgi_cfg, &req, None, None, "[::1]:1337".parse().unwrap()); assert_eq!( params.get(&Bytes::from(GATEWAY_INTERFACE)), @@ -385,7 +379,7 @@ async fn dont_resolve_file() { .unwrap(); let res = handle_wwwroot(req, mount).await; let res = res.unwrap_err(); - assert_eq!(res.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!(res.code, StatusCode::BAD_REQUEST); } /*#[tokio::test] async fn body_no_len() { @@ -743,7 +737,7 @@ async fn test_resolve_path() { assert_eq!(req.path(), "a/b/c/d"); let e = resolve_path(full_path, false, &sf, &req).await.unwrap_err(); - assert_eq!(e.kind(), ErrorKind::NotFound); + assert_eq!(e.code, StatusCode::NOT_FOUND); } #[test] /// https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/ @@ -792,7 +786,7 @@ fn params_nginx_example() { params.get(&Bytes::from(PATH_INFO)), Some(&"/foo/bar.php".into()) ); -/* - 'DOCUMENT_URI' => '/test.php/foo/bar.php', -*/ + /* + 'DOCUMENT_URI' => '/test.php/foo/bar.php', + */ } diff --git a/src/dispatch/mod.rs b/src/dispatch/mod.rs index 2b75d30..6f9de91 100644 --- a/src/dispatch/mod.rs +++ b/src/dispatch/mod.rs @@ -12,11 +12,12 @@ mod upgrades; mod webpath; #[cfg(feature = "websocket")] pub mod websocket; +use exn::{bail, ResultExt as _}; use hyper::body::Body as HttpBody; use hyper::header::HeaderValue; pub use webpath::{Req, WebPath}; -use crate::body::{BoxBody, FRWSResp, FRWSResult, IncomingBody}; +use crate::body::{BoxBody, FRWSErr, FRWSResp, FRWSResult, IncomingBody}; use crate::config::{self, Utf8PathBuf}; use hyper::http::uri::Authority; use hyper::{ @@ -25,8 +26,6 @@ use hyper::{ use log::{debug, error, info, trace}; use staticf::ResolveResult; use std::collections::HashMap; -use std::error::Error; -use std::io::{Error as IoError, ErrorKind}; use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; @@ -151,7 +150,7 @@ async fn handle_wwwroot( &r_uri[1..] }, ) - .map_err(|e| IoError::new(ErrorKind::InvalidData, e))? + .or_raise(|| FRWSErr::new(StatusCode::BAD_REQUEST, "bad header value"))? } else { redir.redirect.0.clone() }; @@ -177,7 +176,7 @@ async fn handle_wwwroot( } match static_files { Some(sf) => sf, - None => return Err(IoError::new(ErrorKind::PermissionDenied, "no dir to serve")), + None => bail!(FRWSErr::new(StatusCode::FORBIDDEN, "no dir to serve")), } } #[cfg(feature = "websocket")] @@ -208,10 +207,12 @@ async fn handle_wwwroot( if !sf.follow_symlinks { //check if the canonicalized version is still inside of the (abs) root path - let fp = full_path.canonicalize()?; + let fp = full_path + .canonicalize() + .map_err(|io| FRWSErr::from_io(io, "could not canonicalize"))?; if !fp.starts_with(&sf.dir) { - return Err(IoError::new( - ErrorKind::PermissionDenied, + bail!(FRWSErr::new( + StatusCode::FORBIDDEN, "Symlinks are not allowed", )); } @@ -235,10 +236,7 @@ async fn handle_wwwroot( if ext_in_list(&sf.serve, &full_path) { staticf::return_file(req, file, metadata, mime).await } else { - Err(IoError::new( - ErrorKind::PermissionDenied, - "bad file extension", - )) + bail!(FRWSErr::new(StatusCode::FORBIDDEN, "bad file extension",)) } } } @@ -267,8 +265,8 @@ async fn handle_vhost( } } - Err(IoError::new( - ErrorKind::PermissionDenied, + bail!(FRWSErr::new( + StatusCode::FORBIDDEN, format!("not a mount path: {:?}", req.path()), )) } @@ -307,7 +305,7 @@ async fn dispatch_to_vhost( if let Some(hcfg) = &cfg.default_host { return handle_vhost(req, hcfg, remote_addr).await; } - Err(IoError::new(ErrorKind::PermissionDenied, "no vHost found")) + bail!(FRWSErr::new(StatusCode::FORBIDDEN, "no vHost found")) } /// new request on a `SocketAddr`. @@ -332,42 +330,6 @@ pub(crate) async fn handle_request( .await .or_else(|err| { error!("handle_request {}", err); - if let Some(cause) = err.get_ref() { - let mut e: &dyn Error = cause; - loop { - error!("{}", e); - e = match e.source() { - Some(e) => { - error!("caused by:"); - e - } - None => break, - } - } - } - match err.kind() { - ErrorKind::NotFound => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(BoxBody::empty()), - ErrorKind::PermissionDenied => Response::builder() - .status(StatusCode::FORBIDDEN) - .body(BoxBody::empty()), - ErrorKind::InvalidInput | ErrorKind::InvalidData => Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(BoxBody::empty()), - ErrorKind::BrokenPipe - | ErrorKind::UnexpectedEof - | ErrorKind::ConnectionAborted - | ErrorKind::ConnectionRefused - | ErrorKind::ConnectionReset => Response::builder() - .status(StatusCode::BAD_GATEWAY) - .body(BoxBody::empty()), - ErrorKind::TimedOut => Response::builder() - .status(StatusCode::GATEWAY_TIMEOUT) - .body(BoxBody::empty()), - _ => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(BoxBody::empty()), - } + Response::builder().status(err.code).body(BoxBody::empty()) }) } diff --git a/src/dispatch/proxy/cfg.rs b/src/dispatch/proxy/cfg.rs index b6a0ab6..f326112 100644 --- a/src/dispatch/proxy/cfg.rs +++ b/src/dispatch/proxy/cfg.rs @@ -40,6 +40,7 @@ pub struct Proxy { #[serde(default = "pool_size")] pub(super) h1_pool_size: usize, #[cfg(any(feature = "tlsrust", feature = "tlsnative"))] + /// cert to expect from the upstream server pub(super) tls_root: Option, //timeout: u8, //max_req_body_size: u32, @@ -48,6 +49,10 @@ pub struct Proxy { //header_policy: u8, pub(super) filter_req_header: Option>, pub(super) filter_resp_header: Option>, + ///path adjustment in localtion headers + pub(super) location_jail: RewriteUrls, + ///path adjustment in the body + pub(super) rewrite_urls: RewriteUrls, #[serde(skip)] pub(super) client: Option, } @@ -58,6 +63,28 @@ fn pool_size() -> usize { 10 } +#[derive(Deserialize, Debug, Default)] +#[serde(try_from = "String")] +pub enum RewriteUrls { + #[default] + DontRewrite, + ForceWebmount, + /// remove String and the force webmount + StripPath(String), + //advanced option: regex? +} +impl TryFrom for RewriteUrls { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + match value.as_str() { + "on" | "yes" | "true" => Ok(RewriteUrls::ForceWebmount), + path if path.starts_with('/') => Ok(RewriteUrls::StripPath(value)), + _ => anyhow::bail!("true or absolute uri"), + } + } +} + #[derive(Deserialize, Debug)] #[serde(try_from = "String")] pub struct ProxyAdress { diff --git a/src/dispatch/proxy/client.rs b/src/dispatch/proxy/client.rs index 48d74e1..37708e8 100644 --- a/src/dispatch/proxy/client.rs +++ b/src/dispatch/proxy/client.rs @@ -61,7 +61,9 @@ impl super::Proxy { } } } - Err(PoolError::Timeout) => {log::trace!("pool could use another con");} //semaphore:NoPermits && size < max_size -> connect a new IO + Err(PoolError::Timeout) => { + log::trace!("pool could use another con"); + } //semaphore:NoPermits && size < max_size -> connect a new IO Err(PoolError::Closed) => unreachable!("pool should not close"), Err(PoolError::NoRuntimeSpecified) => unreachable!("pool not using timeout"), } @@ -107,9 +109,9 @@ impl Client { async fn add_to_h1_pool(&self, s: http1::SendRequest, max_size: usize) { let mut lock = self.h1.write().await; let pool = lock.get_or_insert(Pool::new(max_size)); //semaphore:0permits, size_semaphore:max - //pool.try_add(s).expect("pool should never close"); + //pool.try_add(s).expect("pool should never close"); match pool.try_add(s) { - Ok(()) => {}, + Ok(()) => {} Err((s, PoolError::Timeout)) => { log::warn!("pool add {:?}", pool.status()); pool.add(s).await.expect("wtf"); //FIXME size max @@ -117,7 +119,6 @@ impl Client { Err((_, PoolError::Closed)) => unreachable!("pool should never close"), Err((_, PoolError::NoRuntimeSpecified)) => unreachable!("pool not using timeout"), } - } async fn connect(&self, cfg: &super::Proxy) -> Result<(), IoError> { let addr = match &cfg.forward.addr { diff --git a/src/dispatch/proxy/mod.rs b/src/dispatch/proxy/mod.rs index e0833f7..48bb8c3 100644 --- a/src/dispatch/proxy/mod.rs +++ b/src/dispatch/proxy/mod.rs @@ -1,11 +1,12 @@ use crate::{ - body::{BoxBody, FRWSResult, IncomingBody}, + body::{BoxBody, FRWSErr, FRWSResult, IncomingBody}, config::HeaderNameCfg, - dispatch::{upgrades::MyUpgraded, webpath::Req}, + dispatch::{proxy::cfg::RewriteUrls, upgrades::MyUpgraded, webpath::Req}, }; -use bytes::{Bytes, BytesMut}; +use bytes::{BufMut, Bytes, BytesMut}; +use exn::{bail, ResultExt}; use hyper::{ - header::{self, HeaderName, HeaderValue}, + header::{self, HeaderName, HeaderValue, InvalidHeaderValue}, http::uri, upgrade::OnUpgrade, HeaderMap, Response, StatusCode, Uri, Version, @@ -145,7 +146,10 @@ fn filter_header(headers: &mut HeaderMap, filter: &Option>) { let mut last = true; headers.extend(old.into_iter().filter(|(h, _v)| { if let Some(h) = h { - last = [header::CONTENT_LENGTH, header::CONTENT_TYPE].iter().any(|a| a == h) || allowed.iter().any(|a| a.0 == *h); + last = [header::CONTENT_LENGTH, header::CONTENT_TYPE] + .iter() + .any(|a| a == h) + || allowed.iter().any(|a| a.0 == *h); } last })); @@ -170,6 +174,17 @@ pub async fn forward( new_path.push_str(q); } + // we need the mount for rewrite shenanigans + let mount = if let RewriteUrls::DontRewrite = config.location_jail { + if let RewriteUrls::DontRewrite = config.rewrite_urls { + None + } else { + Some(req.mount().to_string()) + } + } else { + Some(req.mount().to_string()) + }; + let client_vers = req.version(); let (mut parts, body) = req.into_parts(); @@ -188,7 +203,8 @@ pub async fn forward( let request_upgrade = if client_vers == Version::HTTP_11 { get_upgrade_type(&mut parts.headers).and_then(|u| check_upgrade(u, config)) } else if client_vers == Version::HTTP_2 && parts.method == hyper::Method::CONNECT { - parts.extensions + parts + .extensions .get::() .and_then(|p| HeaderValue::from_bytes(p.as_ref()).ok()) .and_then(|up| check_upgrade(up, config)) @@ -204,7 +220,8 @@ pub async fn forward( remove_connection_headers(client_vers, &mut parts.headers); remove_hop_by_hop_headers(&mut parts.headers); if contains_te_trailers_value { - parts.headers + parts + .headers .insert(header::TE, HeaderValue::from_static(TRAILERS)); } filter_header(&mut parts.headers, &config.filter_req_header); @@ -242,8 +259,11 @@ pub async fn forward( buf.extend_from_slice(host.as_str().as_bytes()); } //;proto=http;by=203.0.113.43 - parts.headers - .insert(header::FORWARDED, into_header_value(buf.freeze())?); + parts.headers.insert( + header::FORWARDED, + into_header_value(buf.freeze()) + .or_raise(|| FRWSErr::new(StatusCode::BAD_REQUEST, "invalid header"))?, + ); } ForwardedHeader::Remove => { parts.headers.remove(header::FORWARDED); @@ -271,7 +291,10 @@ pub async fn forward( addr.extend_from_slice(b", "); } addr.extend_from_slice(client_ip_str.as_bytes()); - entry.insert(into_header_value(addr.freeze())?); + entry.insert( + into_header_value(addr.freeze()) + .or_raise(|| FRWSErr::new(StatusCode::BAD_REQUEST, "invalid header"))?, + ); } } } else { @@ -285,8 +308,11 @@ pub async fn forward( buf.extend_from_slice(b", "); } buf.extend_from_slice(format!("{:?} {}", client_vers, &host).as_bytes()); - parts.headers - .insert(header::VIA, into_header_value(buf.freeze())?); + parts.headers.insert( + header::VIA, + into_header_value(buf.freeze()) + .or_raise(|| FRWSErr::new(StatusCode::BAD_REQUEST, "invalid header"))?, + ); } else { parts.headers.remove(header::VIA); } @@ -326,9 +352,11 @@ pub async fn forward( }; // turn upgrade into extended connect if needed (set :protocol) if let (Some(_), Some(val)) = (request_upgraded.as_ref(), request_upgrade.as_ref()) { - parts.extensions.insert(hyper::ext::Protocol::from( - val.to_str().map_err(|_| ErrorKind::InvalidData)?, - )); + parts + .extensions + .insert(hyper::ext::Protocol::from(val.to_str().or_raise(|| { + FRWSErr::new(StatusCode::BAD_REQUEST, "bad upgrade") + })?)); parts.method = hyper::Method::CONNECT; } @@ -345,11 +373,13 @@ pub async fn forward( trace!("Forward h1 to {}", &new_uri); parts.uri = new_uri; //new host header - parts.headers + parts + .headers .insert(header::HOST, config.forward.host.clone()); // insert upgrad header if needed if let (Some(_), Some(val)) = (request_upgraded.as_ref(), request_upgrade.as_ref()) { - parts.headers + parts + .headers .insert(header::CONNECTION, HeaderValue::from_static("UPGRADE")); parts.headers.insert(header::UPGRADE, val.clone()); if val == "websocket" && client_vers == Version::HTTP_2 { @@ -380,7 +410,12 @@ pub async fn forward( }; if log::log_enabled!(log::Level::Debug) { let mut resp_gist = String::with_capacity(128); - for h in [header::DATE, header::CONTENT_LENGTH, header::CONTENT_TYPE, header::LOCATION] { + for h in [ + header::DATE, + header::CONTENT_LENGTH, + header::CONTENT_TYPE, + header::LOCATION, + ] { if let Some(v) = resp.headers().get(&h) { resp_gist.push(' '); resp_gist.push_str(h.as_str()); @@ -403,6 +438,39 @@ pub async fn forward( remove_hop_by_hop_headers(resp.headers_mut()); filter_header(resp.headers_mut(), &config.filter_resp_header); + match config.location_jail { + cfg::RewriteUrls::DontRewrite => {} + cfg::RewriteUrls::ForceWebmount | cfg::RewriteUrls::StripPath(_) => { + if let Some(location_header) = resp.headers_mut().get_mut(header::LOCATION) { + let prefix = if let cfg::RewriteUrls::StripPath(p) = &config.location_jail { + //replace a specific prefix .. + p + } else { + //.. or a absolute URI without authority + "/" + }; + if let Some(url) = location_header.as_bytes().strip_prefix(prefix.as_bytes()) { + let mp = mount.expect("mount has a value"); + let mp = mp.as_bytes(); + let mut new_uri = Vec::with_capacity(mp.len() + 2 + url.len()); + new_uri.push(b'/'); + new_uri.put_slice(mp); + new_uri.push(b'/'); + new_uri.put_slice(url); + *location_header = new_uri.try_into().unwrap(); //can't happen was a HV before + } + } + } + } + //href= src= + // script ? + // css ? + match config.rewrite_urls { + cfg::RewriteUrls::DontRewrite => {} + cfg::RewriteUrls::ForceWebmount => todo!(), + cfg::RewriteUrls::StripPath(_) => todo!(), + } + if let Some(host) = &config.add_via_header_to_client { //https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via let mut buf = BytesMut::with_capacity(512); @@ -411,17 +479,23 @@ pub async fn forward( buf.extend_from_slice(b", "); } buf.extend_from_slice(format!("{:?} {}", resp.version(), &host).as_bytes()); - resp.headers_mut() - .insert(header::VIA, into_header_value(buf.freeze())?); + resp.headers_mut().insert( + header::VIA, + into_header_value(buf.freeze()) + .or_raise(|| FRWSErr::new(StatusCode::BAD_GATEWAY, "invalid header"))?, + ); } - if resp_upgrade.is_some() { - if request_upgrade != resp_upgrade { + if let Some(resp_upgrade_hdr) = resp_upgrade { + if request_upgrade + .as_ref() + .is_none_or(|u| u != resp_upgrade_hdr) + { error!( "client upgrade {:?} != server upgrade {:?}", - request_upgrade, resp_upgrade + request_upgrade, resp_upgrade_hdr ); - return Err(ErrorKind::InvalidInput.into()); + bail!(FRWSErr::new(StatusCode::BAD_REQUEST, "upgrade missmatch")); } if let Some(request_upgraded) = request_upgraded { let response_upgraded = resp @@ -447,7 +521,7 @@ pub async fn forward( { let h = resp.headers_mut(); h.insert(header::CONNECTION, HeaderValue::from_static("UPGRADE")); - h.insert(header::UPGRADE, resp_upgrade.unwrap()); + h.insert(header::UPGRADE, resp_upgrade_hdr); if let Some(ws_key) = ws_key { //ACCEPT header if upstream is h2 let mut s = sha1::Sha1::new(); @@ -485,11 +559,6 @@ pub async fn forward( Ok(resp.map(BoxBody::Proxy)) } #[inline] -fn into_header_value(src: Bytes) -> Result { - HeaderValue::from_maybe_shared(src.clone()).map_err(|_e| { - IoError::new( - ErrorKind::InvalidData, - format!("Invalid Header Value for {:?}", &src), - ) - }) +fn into_header_value(src: Bytes) -> Result { + HeaderValue::from_maybe_shared(src.clone()) } diff --git a/src/dispatch/proxy/test.rs b/src/dispatch/proxy/test.rs index 45f358c..3e4a2ed 100644 --- a/src/dispatch/proxy/test.rs +++ b/src/dispatch/proxy/test.rs @@ -26,6 +26,8 @@ fn create_conf(f: impl FnOnce(&mut Proxy)) -> Proxy { filter_resp_header: None, h1_pool_size: 2, tls_root: None, + location_jail: cfg::RewriteUrls::DontRewrite, + rewrite_urls: cfg::RewriteUrls::DontRewrite, }; f(&mut p); p diff --git a/src/dispatch/staticf.rs b/src/dispatch/staticf.rs index b89791a..202e88c 100644 --- a/src/dispatch/staticf.rs +++ b/src/dispatch/staticf.rs @@ -1,10 +1,11 @@ -use hyper::{Method, Response}; +use exn::{bail, Exn, ResultExt as _}; +use hyper::{Method, Response, StatusCode}; use hyper_staticfile::util::FileResponseBuilder; use hyper_staticfile::ResolvedFile; use log::debug; use mime_guess::{Mime, MimeGuess}; use std::fs::{Metadata, OpenOptions as StdOpenOptions}; -use std::io::{Error as IoError, ErrorKind as IoErrorKind}; +use std::io::Error as IoError; use std::path::Path; use std::path::PathBuf; use tokio::fs::{File, OpenOptions}; @@ -12,7 +13,7 @@ use tokio::fs::{File, OpenOptions}; #[cfg(windows)] use std::os::windows::fs::OpenOptionsExt; -use crate::body::{BoxBody, FRWSResp, FRWSResult, IncomingBody}; +use crate::body::{BoxBody, FRWSErr, FRWSResp, FRWSResult, IncomingBody, StatusResult}; use crate::config::Utf8PathBuf; use super::webpath::Req; @@ -111,14 +112,16 @@ pub async fn resolve_path( full_path: &Path, is_dir_request: bool, index_files: &Option>, -) -> Result<(PathBuf, ResolveResult), IoError> { - let (file, metadata) = open_with_metadata(&full_path).await?; +) -> StatusResult<(PathBuf, ResolveResult)> { + let (file, metadata) = open_with_metadata(&full_path) + .await + .map_err(|io| FRWSErr::from_io(io, "could not open file"))?; debug!("have {:?}", metadata); // The resolved `full_path` doesn't contain the trailing slash anymore, so we may // have opened a file for a directory request, which we treat as 'not found'. if is_dir_request && !metadata.is_dir() { - return Err(IoError::new(IoErrorKind::NotFound, "")); + bail!(FRWSErr::new(StatusCode::NOT_FOUND, "file is opened as dir")); } // We may have opened a directory for a file request, in which case we redirect. @@ -141,7 +144,7 @@ pub async fn resolve_path( if let Ok((file, metadata)) = open_with_metadata(&full_path_index).await { // The directory index cannot itself be a directory. if metadata.is_dir() { - return Err(IoError::new(IoErrorKind::NotFound, "")); + bail!(FRWSErr::new(StatusCode::NOT_FOUND, "index is dir")); } // Serve this file. @@ -150,10 +153,7 @@ pub async fn resolve_path( } } } - Err(IoError::new( - IoErrorKind::PermissionDenied, - "dir w/o index file", - )) + bail!(FRWSErr::new(StatusCode::FORBIDDEN, "dir w/o index file",)) } #[cfg(test)] @@ -169,7 +169,7 @@ mod tests { webpath::Req, }, }; - use hyper::{header, Request}; + use hyper::{header, Request, StatusCode}; use std::path::Path; //use crate::dispatch::test:: @@ -248,8 +248,8 @@ mod tests { let req = Request::get("/mount/").body(TestBody::empty()).unwrap(); let res = handle_wwwroot(req, sf).await; let res = res.unwrap_err(); - assert_eq!(res.kind(), std::io::ErrorKind::PermissionDenied); - assert_eq!(res.into_inner().unwrap().to_string(), "dir w/o index file"); + assert_eq!(res.code, StatusCode::FORBIDDEN); + assert_eq!(res.reason, "dir w/o index file"); } #[tokio::test] async fn allowlist_blocks() { @@ -267,8 +267,8 @@ mod tests { .unwrap(); let res = handle_wwwroot(req, sf).await; let res = res.unwrap_err(); - assert_eq!(res.kind(), std::io::ErrorKind::PermissionDenied); - assert_eq!(res.into_inner().unwrap().to_string(), "bad file extension"); + assert_eq!(res.code, StatusCode::FORBIDDEN); + assert_eq!(res.reason, "bad file extension"); } #[tokio::test] async fn allowlist_allows() { @@ -335,7 +335,7 @@ mod tests { let res = handle_wwwroot(req, sf).await; let res = res.unwrap_err(); - assert_eq!(res.kind(), std::io::ErrorKind::NotFound); + assert_eq!(res.code, StatusCode::NOT_FOUND); } #[tokio::test] @@ -395,7 +395,7 @@ mod tests { .body(TestBody::empty()) .unwrap(); let res = handle_wwwroot(req, sf).await.unwrap_err(); - assert_eq!(res.kind(), std::io::ErrorKind::PermissionDenied); + assert_eq!(res.code, StatusCode::FORBIDDEN); let sf = StaticFiles { dir: AbsPathBuf::from(mount), @@ -441,7 +441,7 @@ mod tests { }; let req = Request::get("/mount/lnk").body(TestBody::empty()).unwrap(); let res = handle_wwwroot(req, sf).await.unwrap_err(); - assert_eq!(res.kind(), std::io::ErrorKind::PermissionDenied); + assert_eq!(res.code, StatusCode::FORBIDDEN); let sf = StaticFiles { dir: AbsPathBuf::from(mount), diff --git a/src/dispatch/test.rs b/src/dispatch/test.rs index d590a48..e782445 100644 --- a/src/dispatch/test.rs +++ b/src/dispatch/test.rs @@ -90,8 +90,8 @@ mod mount { let cfg = config::VHost::new(sa); let res = handle_vhost(req, &cfg, sa).await; - let res: IoError = res.unwrap_err(); - assert_eq!(res.into_inner().unwrap().to_string(), "not a mount path"); + let res = res.unwrap_err(); + assert_eq!(res.reason, "not a mount path"); } #[tokio::test] async fn full_folder_names_as_mounts() { @@ -160,8 +160,8 @@ mod mount { ); let res = handle_vhost(req, &cfg, sa).await; let res = res.unwrap_err(); - assert_eq!(res.kind(), ErrorKind::PermissionDenied); - assert_eq!(res.into_inner().unwrap().to_string(), "not a mount path"); + assert_eq!(res.code, StatusCode::FORBIDDEN); + assert_eq!(res.reason, "not a mount path"); } #[tokio::test] async fn path_trav_outside_webroot() { @@ -175,8 +175,8 @@ mod mount { ); let res = handle_vhost(req, &cfg, sa).await; let res = res.unwrap_err(); - assert_eq!(res.kind(), ErrorKind::PermissionDenied); - assert_eq!(res.into_inner().unwrap().to_string(), "path traversal"); + assert_eq!(res.code, StatusCode::FORBIDDEN); + assert_eq!(res.reason, "path traversal"); } #[tokio::test] async fn rustsec_2022_0072_part1() { @@ -348,8 +348,8 @@ mod vhost { let cfg = make_host_cfg(None, Some(("1".to_string(), config::VHost::new(sa)))); let res = dispatch_to_vhost(req, cfg, sa).await; - let res: IoError = res.unwrap_err(); - assert_eq!(res.into_inner().unwrap().to_string(), "no vHost found"); + let res = res.unwrap_err(); + assert_eq!(res.reason, "no vHost found"); } #[tokio::test] async fn specific_vhost() { @@ -362,8 +362,8 @@ mod vhost { let cfg = make_host_cfg(None, Some(("1".to_string(), config::VHost::new(sa)))); let res = dispatch_to_vhost(req, cfg, sa).await; - let res: IoError = res.unwrap_err(); - assert_eq!(res.into_inner().unwrap().to_string(), "not a mount path"); + let res = res.unwrap_err(); + assert_eq!(res.reason, "not a mount path"); } #[tokio::test] async fn default_vhost() { @@ -373,8 +373,8 @@ mod vhost { let cfg = make_host_cfg(Some(config::VHost::new(sa)), None); let res = dispatch_to_vhost(req, cfg, sa).await; - let res: IoError = res.unwrap_err(); - assert_eq!(res.into_inner().unwrap().to_string(), "not a mount path"); + let res = res.unwrap_err(); + assert_eq!(res.reason, "not a mount path"); } } diff --git a/src/dispatch/webpath.rs b/src/dispatch/webpath.rs index a1ad7a1..ff07d92 100644 --- a/src/dispatch/webpath.rs +++ b/src/dispatch/webpath.rs @@ -1,16 +1,21 @@ +use exn::{bail, Exn}; use hyper::Uri; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; use std::io::{Error as IoError, ErrorKind}; use std::path::{Path, PathBuf, MAIN_SEPARATOR}; +use crate::body::FRWSErr; use crate::config::AbsPathBuf; impl<'a> TryFrom<&'a Uri> for WebPath<'a> { - type Error = IoError; + type Error = Exn; fn try_from(uri: &'a Uri) -> Result { if !uri.path().starts_with('/') { - return Err(IoError::new(ErrorKind::InvalidData, "path does not start with /")); + bail!(FRWSErr::new( + hyper::StatusCode::BAD_REQUEST, + "path does not start with /" + )); } let path = percent_encoding::percent_decode_str(&uri.path()[1..]).decode_utf8_lossy(); @@ -19,7 +24,7 @@ impl<'a> TryFrom<&'a Uri> for WebPath<'a> { #[cfg(windows)] if path.contains('\\') { - return Err(IoError::new(ErrorKind::InvalidData, "win dir sep")); + bail!(FRWSErr::new(hyper::StatusCode::BAD_REQUEST, "win dir sep")); } let mut parts = Vec::new(); @@ -41,7 +46,7 @@ impl<'a> TryFrom<&'a Uri> for WebPath<'a> { } ".." => { if parts.pop().is_none() { - return Err(IoError::new(ErrorKind::PermissionDenied, "path traversal")); + bail!(FRWSErr::new(hyper::StatusCode::FORBIDDEN, "path traversal")); } skip += 3; } @@ -127,12 +132,7 @@ impl<'a> WebPath<'a> { Ok(WebPath(Cow::from(&self.0[offset..]))) } /// Create "/{pre}/{WebPath}" and leave extra_cap of free space at the end - pub fn prefixed_as_abs_url_path( - &self, - pre: &str, - extra_cap: usize, - encode: bool, - ) -> String { + pub fn prefixed_as_abs_url_path(&self, pre: &str, extra_cap: usize, encode: bool) -> String { //https://docs.rs/hyper-staticfile/latest/src/hyper_staticfile/response_builder.rs.html#75-123 let s = self.0.as_ref(); let capa = pre.len() + s.len() + extra_cap + 2; @@ -179,7 +179,7 @@ pub struct Req { body: B, } impl Req { - pub fn from_req(req: hyper::Request) -> Result, IoError> { + pub fn from_req(req: hyper::Request) -> Result, Exn> { let (parts, body) = req.into_parts(); let req_path = WebPath::try_from(&parts.uri)?; let path = match req_path.0 { @@ -247,7 +247,7 @@ impl Req { WebPathSelfRef::Borrowed(b) => &self.parts.uri.path()[*b..*b + self.prefix_len], }; if m.ends_with('/') { - return &m[..self.prefix_len-1]; + return &m[..self.prefix_len - 1]; } m } @@ -308,8 +308,8 @@ mod tests { assert_eq!( WebPath::try_from(&"/../../".parse().unwrap()) .unwrap_err() - .kind(), - ErrorKind::PermissionDenied + .code, + hyper::StatusCode::FORBIDDEN ); assert_eq!( WebPath::try_from(&"/a/c:/b".parse().unwrap()).unwrap().0, @@ -391,8 +391,8 @@ mod tests { assert_eq!( WebPath::try_from(&"//./j/../a/.././../".parse().unwrap()) .unwrap_err() - .kind(), - ErrorKind::PermissionDenied + .code, + hyper::StatusCode::FORBIDDEN ); assert_eq!( WebPath::try_from(&"//./j/../a/./k".parse().unwrap()) @@ -476,9 +476,7 @@ mod tests { #[test] fn misc() { assert!(matches!( - WebPath::try_from(&"/test/c/".parse().unwrap()) - .unwrap() - .0, + WebPath::try_from(&"/test/c/".parse().unwrap()).unwrap().0, Cow::Borrowed("test/c/") )); } diff --git a/src/dispatch/websocket.rs b/src/dispatch/websocket.rs index 5caadbb..8621428 100644 --- a/src/dispatch/websocket.rs +++ b/src/dispatch/websocket.rs @@ -1,7 +1,8 @@ use bytes::BytesMut; +use exn::bail; use hyper::{header, HeaderMap, Response, StatusCode}; use log::error; -use std::io::{Error as IoError, ErrorKind}; +use std::io::Error as IoError; use std::net::SocketAddr; use tokio_util::codec::{Decoder, Framed}; use websocket_codec::{ClientRequest, Message, MessageCodec, Opcode}; @@ -11,6 +12,7 @@ use futures_util::{SinkExt, StreamExt}; use serde::Deserialize; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use crate::body::FRWSErr; use crate::{ body::{BoxBody, FRWSResult, IncomingBody}, dispatch::{upgrades::MyUpgraded, webpath::Req}, @@ -59,7 +61,7 @@ pub async fn upgrade( }) { req.ws_accept() } else { - return Err(IoError::new(ErrorKind::InvalidData, "wrong WS update")); + bail!(FRWSErr::new(StatusCode::BAD_REQUEST, "wrong WS update")); }; let headers = res.headers_mut(); @@ -92,7 +94,7 @@ pub async fn upgrade( } } if !check_h2_ws(&parts) { - return Err(IoError::new(ErrorKind::InvalidData, "wrong WS update")); + bail!(FRWSErr::new(StatusCode::BAD_REQUEST, "wrong WS update")); } *res.status_mut() = StatusCode::OK; } @@ -452,8 +454,10 @@ mod tests { let (end_stream, header) = crate::tests::h2_client( &mut stream, req, - Some(b"\0\x08\0\0\0\x01"),//SETTINGS_ENABLE_CONNECT_PROTOCOL (8) = 1 - ).await.unwrap(); + Some(b"\0\x08\0\0\0\x01"), //SETTINGS_ENABLE_CONNECT_PROTOCOL (8) = 1 + ) + .await + .unwrap(); assert!(!end_stream); assert_eq!(header[0], 0x88); //DATA diff --git a/src/main.rs b/src/main.rs index c7851d1..0e4112c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -66,16 +66,13 @@ async fn prepare_hyper_servers( Ok(s) => s, Err(e) => { #[cfg(feature = "tlsrust_acme")] - if e.get_ref() - .and_then(|b| { - b.downcast_ref::().map(|_| ()) - }) - .is_some() + if let Some(transport::tls::TlsErr::ACMEdone) = + e.frame().error().downcast_ref::() { //this was an ACME challenge. Don't print an error continue; } - error!("TLS Handshake {:?}", e);//TODO Os { code: 113, kind: HostUnreachable, message: "No route to host" } + error!("TLS Handshake {:?}", e); //FIXME Os { code: 113, kind: HostUnreachable, message: "No route to host" } continue; } }; diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 27fee76..45ddc44 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -30,8 +30,8 @@ use std::time::Duration; use hyper::Version; use tokio::net::TcpListener; -pub use tokio::net::TcpStream as PlainStream; use log::{debug, error, trace}; +pub use tokio::net::TcpStream as PlainStream; use tokio::io::{AsyncRead, AsyncWrite}; @@ -84,7 +84,7 @@ impl PlainIncoming { self.sleep_on_errors = val; } - pub(crate) async fn accept(&self) -> io::Result<(PlainStream, SocketAddr)> { + pub(crate) async fn accept(&self) -> exn::Result<(PlainStream, SocketAddr), io::Error> { loop { match self.listener.accept().await { Ok((socket, remote)) => { @@ -107,7 +107,7 @@ impl PlainIncoming { // Sleep 1s. tokio::time::sleep(Duration::from_secs(1)).await; } else { - return Err(e); + return Err(exn::Exn::new(e)); } } } @@ -148,4 +148,4 @@ impl Connection for PlainStream { fn proto(&self) -> Version { Version::HTTP_11 } -} \ No newline at end of file +} diff --git a/src/transport/tls/mod.rs b/src/transport/tls/mod.rs index c1ea33e..c2f54bc 100644 --- a/src/transport/tls/mod.rs +++ b/src/transport/tls/mod.rs @@ -7,8 +7,8 @@ pub use self::rustls::{ParsedTLSConfig, TlsUserConfig}; use self::rustls::{TLSConfig, UnderlyingAccept, UnderlyingTLSStream}; use super::{Connection, PlainIncoming, PlainStream}; +use exn::ResultExt; use hyper::Version; -use std::io; use std::sync::Arc; use tokio::io::AsyncWriteExt; @@ -63,31 +63,46 @@ impl TlsAcceptor { incoming, } } - pub(crate) async fn accept(&self) -> io::Result<(TlsStream, SocketAddr)> { - let (stream, remote) = self.incoming.accept().await?; - let mut stream = ParsedTLSConfig::get_accept_feature(self, stream).await?; + pub(crate) async fn accept(&self) -> exn::Result<(TlsStream, SocketAddr), TlsErr> { + let (stream, remote) = self + .incoming + .accept() + .await + .or_raise(|| TlsErr::IoDuringAccept)?; + let mut stream = ParsedTLSConfig::get_accept_feature(self, stream) + .await + .or_raise(|| TlsErr::IoDuringHandshake)?; + log::trace!( + "Alpn offered by client: {:?}", + stream.get_ref().1.alpn_protocol() + ); #[cfg(feature = "tlsrust_acme")] if stream.get_ref().1.alpn_protocol() == Some(ACME_TLS_ALPN_NAME) { log::debug!("completed acme-tls/1 handshake"); - stream.shutdown().await?; - return Err(io::Error::other(ACMEdone())); + stream + .shutdown() + .await + .or_raise(|| TlsErr::IoDuringShutdown)?; + return Err(exn::Exn::new(TlsErr::ACMEdone)); } Ok((stream, remote)) } } -pub struct ACMEdone(); -impl std::error::Error for ACMEdone {} -impl std::fmt::Display for ACMEdone { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("ACMEdone").finish() - } +#[derive(Debug)] +pub enum TlsErr { + ///not actually an error + ACMEdone, + IoDuringAccept, + IoDuringHandshake, + IoDuringShutdown, } -impl std::fmt::Debug for ACMEdone { +impl std::error::Error for TlsErr {} +impl std::fmt::Display for TlsErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("ACMEdone").finish() + std::fmt::Debug::fmt(&self, f) } } #[cfg(test)] From d36a2e462fa76ba3105d01d716362afcd656804b Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:51:53 +0100 Subject: [PATCH 09/12] fix tests --- src/dispatch/proxy/cfg.rs | 2 ++ src/dispatch/proxy/test.rs | 5 +++-- src/dispatch/test.rs | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/dispatch/proxy/cfg.rs b/src/dispatch/proxy/cfg.rs index f326112..94dd72e 100644 --- a/src/dispatch/proxy/cfg.rs +++ b/src/dispatch/proxy/cfg.rs @@ -50,8 +50,10 @@ pub struct Proxy { pub(super) filter_req_header: Option>, pub(super) filter_resp_header: Option>, ///path adjustment in localtion headers + #[serde(default)] pub(super) location_jail: RewriteUrls, ///path adjustment in the body + #[serde(default)] pub(super) rewrite_urls: RewriteUrls, #[serde(skip)] pub(super) client: Option, diff --git a/src/dispatch/proxy/test.rs b/src/dispatch/proxy/test.rs index 3e4a2ed..fc9e4ed 100644 --- a/src/dispatch/proxy/test.rs +++ b/src/dispatch/proxy/test.rs @@ -35,11 +35,12 @@ fn create_conf(f: impl FnOnce(&mut Proxy)) -> Proxy { #[test] fn basic_config() { - if let Ok(UseCase::Proxy(p)) = toml::from_str( + let r = toml::from_str( r#" forward = "http://remote/path" "#, - ) { + ); + if let UseCase::Proxy(p) = r.unwrap() { assert_eq!(p.forward.host, "remote"); assert_eq!(p.forward.scheme, uri::Scheme::HTTP); assert_eq!(p.forward.path, Utf8PathBuf::from("/path")); diff --git a/src/dispatch/test.rs b/src/dispatch/test.rs index e782445..a81ac54 100644 --- a/src/dispatch/test.rs +++ b/src/dispatch/test.rs @@ -91,7 +91,7 @@ mod mount { let cfg = config::VHost::new(sa); let res = handle_vhost(req, &cfg, sa).await; let res = res.unwrap_err(); - assert_eq!(res.reason, "not a mount path"); + assert_eq!(res.reason, "not a mount path: WebPath(\"\")"); } #[tokio::test] async fn full_folder_names_as_mounts() { @@ -161,7 +161,7 @@ mod mount { let res = handle_vhost(req, &cfg, sa).await; let res = res.unwrap_err(); assert_eq!(res.code, StatusCode::FORBIDDEN); - assert_eq!(res.reason, "not a mount path"); + assert_eq!(res.reason, "not a mount path: WebPath(\"b\")"); } #[tokio::test] async fn path_trav_outside_webroot() { @@ -363,7 +363,7 @@ mod vhost { let res = dispatch_to_vhost(req, cfg, sa).await; let res = res.unwrap_err(); - assert_eq!(res.reason, "not a mount path"); + assert_eq!(res.reason, "not a mount path: WebPath(\"\")"); } #[tokio::test] async fn default_vhost() { @@ -374,7 +374,7 @@ mod vhost { let res = dispatch_to_vhost(req, cfg, sa).await; let res = res.unwrap_err(); - assert_eq!(res.reason, "not a mount path"); + assert_eq!(res.reason, "not a mount path: WebPath(\"\")"); } } From 595710a1075d183ccbae23a94a151f86f3eb15cb Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:08:56 +0100 Subject: [PATCH 10/12] fix win --- Cargo.toml | 4 ++-- src/dispatch/fcgi/mod.rs | 2 +- src/dispatch/proxy/mod.rs | 2 +- src/dispatch/staticf.rs | 2 +- src/dispatch/webpath.rs | 5 ++--- src/transport/tls/rustls/mod.rs | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d84e757..6671be0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ log = "0.4" log4rs = { version = "1", default-features = false, features = ["all_components", "config_parsing"] } #config: -toml = "0.9" # config files in toml +toml = "1.0" # config files in toml serde = { version = "1.0", features = ["derive"] } #main: @@ -51,7 +51,7 @@ tokio-util = { version = "0.7", features=["codec"], optional = true } xml-rs = { version = "1.0", optional = true } chrono = { version = "0.4", optional = true } #proxy -deadpool = {version="0.12", 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 diff --git a/src/dispatch/fcgi/mod.rs b/src/dispatch/fcgi/mod.rs index 2d1f825..f9ef11c 100644 --- a/src/dispatch/fcgi/mod.rs +++ b/src/dispatch/fcgi/mod.rs @@ -12,7 +12,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::time::Duration; use std::{ - io::{Error as IoError, ErrorKind}, + io::Error as IoError, net::SocketAddr, }; use tokio::time::timeout; diff --git a/src/dispatch/proxy/mod.rs b/src/dispatch/proxy/mod.rs index 48bb8c3..414a760 100644 --- a/src/dispatch/proxy/mod.rs +++ b/src/dispatch/proxy/mod.rs @@ -12,7 +12,7 @@ use hyper::{ HeaderMap, Response, StatusCode, Uri, Version, }; use log::{debug, error, trace}; -use std::io::{Error as IoError, ErrorKind}; +use std::io::ErrorKind; use std::net::SocketAddr; use tokio::io::copy_bidirectional; diff --git a/src/dispatch/staticf.rs b/src/dispatch/staticf.rs index 202e88c..a83c905 100644 --- a/src/dispatch/staticf.rs +++ b/src/dispatch/staticf.rs @@ -1,4 +1,4 @@ -use exn::{bail, Exn, ResultExt as _}; +use exn::bail; use hyper::{Method, Response, StatusCode}; use hyper_staticfile::util::FileResponseBuilder; use hyper_staticfile::ResolvedFile; diff --git a/src/dispatch/webpath.rs b/src/dispatch/webpath.rs index ff07d92..02f12d5 100644 --- a/src/dispatch/webpath.rs +++ b/src/dispatch/webpath.rs @@ -2,7 +2,6 @@ use exn::{bail, Exn}; use hyper::Uri; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; -use std::io::{Error as IoError, ErrorKind}; use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use crate::body::FRWSErr; @@ -407,8 +406,8 @@ mod tests { assert_eq!( WebPath::try_from(&"/a\\..\\..\\..\\..\\..\\".parse().unwrap()) .unwrap_err() - .kind(), - ErrorKind::InvalidData + .code, + hyper::StatusCode::BAD_REQUEST ); } #[test] diff --git a/src/transport/tls/rustls/mod.rs b/src/transport/tls/rustls/mod.rs index 62cdc43..e37e380 100644 --- a/src/transport/tls/rustls/mod.rs +++ b/src/transport/tls/rustls/mod.rs @@ -179,7 +179,7 @@ fn load_certs(filename: &Path) -> Result>, io::Error> { } // Load private key from file. -fn load_private_key(filename: &Path) -> Result { +fn load_private_key(filename: &Path) -> Result, io::Error> { let keyfile = fs::File::open(filename)?; let mut reader = io::BufReader::new(keyfile); From 16ccfbe403456269a890a2229806d063fa50d1be Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:02:40 +0100 Subject: [PATCH 11/12] improved error logging --- src/dispatch/mod.rs | 6 +++++- src/main.rs | 30 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/dispatch/mod.rs b/src/dispatch/mod.rs index 6f9de91..4c36cd0 100644 --- a/src/dispatch/mod.rs +++ b/src/dispatch/mod.rs @@ -329,7 +329,11 @@ pub(crate) async fn handle_request( dispatch_to_vhost(req, cfg, remote_addr) .await .or_else(|err| { - error!("handle_request {}", err); + if log::log_enabled!(log::Level::Debug) { + error!("handle_request {:?}", err); + }else{ + error!("{}", err); + } Response::builder().status(err.code).body(BoxBody::empty()) }) } diff --git a/src/main.rs b/src/main.rs index 0e4112c..bbb51c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,8 +13,9 @@ use hyper::{body::Incoming, Request}; use hyper_util::rt::{TokioExecutor, TokioIo}; use log::{debug, error, info, trace}; use std::collections::HashMap; +use std::convert::Infallible; use std::error::Error; -use std::io::{Error as IoError, ErrorKind}; +use std::io::Error as IoError; use std::net::SocketAddr; use std::sync::Arc; use tokio::signal; @@ -40,13 +41,13 @@ use transport::PlainIncoming; /// If its config has TLS wrap the `PlainIncoming` into an `TlsAcceptor` async fn prepare_hyper_servers( mut listening_ifs: HashMap, -) -> Result>>>, Box> { +) -> Result>>, IoError> { let mut handles = vec![]; for (addr, mut cfg) in listening_ifs.drain() { let l = match cfg.listener.take() { Some(l) => l, None => { - return Err(Box::new(IoError::new(ErrorKind::Other, "could not listen"))); + return Err(IoError::other("could not listen")); } }; let server = match PlainIncoming::from_std(l) { @@ -100,7 +101,7 @@ async fn prepare_hyper_servers( } _ => unreachable!("neither h1 nor h2"), } { - error!("{} -> {}: {}", remote_addr, addr, err); + print_hyper_error(remote_addr, addr, err); } }); } @@ -113,7 +114,7 @@ async fn prepare_hyper_servers( } Err(err) => { error!("{}: {}", addr, err); - return Err(Box::new(err)); + return Err(err); } }; handles.push(server); @@ -121,12 +122,25 @@ async fn prepare_hyper_servers( Ok(handles) } +fn print_hyper_error(rem: SocketAddr, here: SocketAddr, err: hyper::Error) { + if !log::log_enabled!(log::Level::Debug) { + error!("{} -> {}: {}", rem, here, err); + return; + } + error!("{} -> {}: {:?}", rem, here, err); + let mut s = err.source(); + while let Some(e) = s { + error!(" | {:?}", e); + s = e.source(); + } +} + #[inline] async fn run_http11_server( incoming: PlainIncoming, addr: SocketAddr, hcfg: Arc, -) -> Result<(), hyper::Error> { +) -> Result<(), Infallible> { let builder = hyper::server::conn::http1::Builder::new(); loop { let (stream, remote_addr) = match incoming.accept().await { @@ -151,7 +165,7 @@ async fn run_http11_server( .with_upgrades() .await { - error!("{} -> {}: {}", remote_addr, addr, err); + print_hyper_error(remote_addr, addr, err); } }); } @@ -255,7 +269,7 @@ pub(crate) mod tests { #[cfg(any(feature = "tlsrust", feature = "tlsnative"))] tls: Option< transport::tls::ParsedTLSConfig, >, - ) -> JoinHandle>> { + ) -> JoinHandle> { let mut listening_ifs = HashMap::new(); let mut cfg = HostCfg::new(l.into_std().unwrap()); From a93d0a1fe1c31c9126b67f0359a0b2ffd8fd1190 Mon Sep 17 00:00:00 2001 From: User65k <15049544+User65k@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:17:50 +0100 Subject: [PATCH 12/12] return type cleanup --- Cargo.toml | 2 +- src/auth/digest.rs | 15 ++++++++------- src/auth/mod.rs | 5 +++++ src/dispatch/mod.rs | 5 +++-- src/main.rs | 14 +++++--------- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6671be0..3f4fc67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" keywords = ["webserver", "fcgi"] [dependencies] -hyper = { version = "1.5", default-features = false, features = ["http1", "http2", "server"]} # HTTP +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" diff --git a/src/auth/digest.rs b/src/auth/digest.rs index 2cb354c..cf77930 100644 --- a/src/auth/digest.rs +++ b/src/auth/digest.rs @@ -17,6 +17,7 @@ use bytes::Bytes; use log::{log_enabled, Level::Trace}; lazy_static! { + /// hash of `random U64 | ProcId` static ref NONCESTARTHASH: Context = { let rnd = OsRng.try_next_u64().unwrap(); @@ -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() } @@ -83,7 +84,7 @@ fn validate_nonce(nonce: &[u8]) -> Result { //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 ? @@ -169,7 +170,7 @@ pub async fn check_digest(auth_file: &Path, req: &Req, realm: &str) 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()) { @@ -185,7 +186,7 @@ pub async fn check_digest(auth_file: &Path, req: &Req, realm: &str) if let Some(cnonce) = user_vals.get(b"cnonce".as_ref()) { c.consume(cnonce); } - format!("{:x}", c.compute()) + format!("{:x}", c.finalize()) }; } } @@ -208,7 +209,7 @@ pub async fn check_digest(auth_file: &Path, req: &Req, realm: &str) c.consume(qop); c.consume(b":"); c.consume(&*ha2); - format!("{:x}", c.compute()) + format!("{:x}", c.finalize()) }); } } @@ -221,7 +222,7 @@ pub async fn check_digest(auth_file: &Path, req: &Req, realm: &str) 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 { @@ -392,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 diff --git a/src/auth/mod.rs b/src/auth/mod.rs index d36af5c..4513f4f 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -2,6 +2,11 @@ use crate::body::{FRWSResp, IncomingBody, StatusResult}; use crate::config::Authenticatoin; use crate::dispatch::Req; use std::collections::HashMap; + +/// 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>; mod digest; diff --git a/src/dispatch/mod.rs b/src/dispatch/mod.rs index 4c36cd0..fc98652 100644 --- a/src/dispatch/mod.rs +++ b/src/dispatch/mod.rs @@ -314,11 +314,12 @@ pub(crate) async fn handle_request( req: Request, cfg: Arc, remote_addr: SocketAddr, -) -> Result { +) -> Result { info!("{} {} {}", remote_addr, req.method(), req.uri()); #[cfg(test)] let req = { + //turn the `Incomming` into a `IncomingBody` aka `TestBody` let (parts, body) = req.into_parts(); Request::from_parts( parts, @@ -334,6 +335,6 @@ pub(crate) async fn handle_request( }else{ error!("{}", err); } - Response::builder().status(err.code).body(BoxBody::empty()) + Ok(Response::builder().status(err.code).body(BoxBody::empty()).expect("only status cant fail")) }) } diff --git a/src/main.rs b/src/main.rs index bbb51c5..2aced84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,6 @@ use hyper::{body::Incoming, Request}; use hyper_util::rt::{TokioExecutor, TokioIo}; use log::{debug, error, info, trace}; use std::collections::HashMap; -use std::convert::Infallible; use std::error::Error; use std::io::Error as IoError; use std::net::SocketAddr; @@ -41,7 +40,7 @@ use transport::PlainIncoming; /// If its config has TLS wrap the `PlainIncoming` into an `TlsAcceptor` async fn prepare_hyper_servers( mut listening_ifs: HashMap, -) -> Result>>, IoError> { +) -> Result>, IoError> { let mut handles = vec![]; for (addr, mut cfg) in listening_ifs.drain() { let l = match cfg.listener.take() { @@ -107,10 +106,7 @@ async fn prepare_hyper_servers( } }); } - tokio::spawn(async move { - run_http11_server(incoming, addr, hcfg).await?; - Ok(()) - }) + tokio::spawn(run_http11_server(incoming, addr, hcfg)) } Err(err) => { error!("{}: {}", addr, err); @@ -130,7 +126,7 @@ fn print_hyper_error(rem: SocketAddr, here: SocketAddr, err: hyper::Error) { error!("{} -> {}: {:?}", rem, here, err); let mut s = err.source(); while let Some(e) = s { - error!(" | {:?}", e); + error!(" |-> {:?}", e); s = e.source(); } } @@ -140,7 +136,7 @@ async fn run_http11_server( incoming: PlainIncoming, addr: SocketAddr, hcfg: Arc, -) -> Result<(), Infallible> { +) { let builder = hyper::server::conn::http1::Builder::new(); loop { let (stream, remote_addr) = match incoming.accept().await { @@ -269,7 +265,7 @@ pub(crate) mod tests { #[cfg(any(feature = "tlsrust", feature = "tlsnative"))] tls: Option< transport::tls::ParsedTLSConfig, >, - ) -> JoinHandle> { + ) -> JoinHandle<()> { let mut listening_ifs = HashMap::new(); let mut cfg = HostCfg::new(l.into_std().unwrap());