diff options
-rw-r--r-- | Cargo.lock | 254 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/config.rs | 11 | ||||
-rw-r--r-- | src/filters/auth/basic.rs (renamed from src/filters/auth.rs) | 11 | ||||
-rw-r--r-- | src/filters/auth/cookie.html | 35 | ||||
-rw-r--r-- | src/filters/auth/cookie.rs | 181 | ||||
-rw-r--r-- | src/filters/auth/mod.rs | 90 | ||||
-rw-r--r-- | src/filters/file.rs | 62 | ||||
-rw-r--r-- | src/filters/files.rs | 2 | ||||
-rw-r--r-- | src/filters/mod.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 3 |
11 files changed, 644 insertions, 16 deletions
@@ -18,6 +18,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.6", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -81,6 +117,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] +name = "argon2" +version = "0.6.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23c799111f751be3d73409b0b9e4160b0c69389216a66c463797eee5b243f7ef" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -141,6 +189,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] name = "bindgen" version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -176,6 +230,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] +name = "blake2" +version = "0.11.0-pre.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb6b33ba68af672bcef0f6d1cceeeaf36e4143cd1456cafafda5d7f12d91f14" +dependencies = [ + "digest 0.11.0-pre.8", +] + +[[package]] name = "block-buffer" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -185,6 +248,15 @@ dependencies = [ ] [[package]] +name = "block-buffer" +version = "0.11.0-pre.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded684142010808eb980d9974ef794da2bcf97d13396143b1515e9f0fb4a10e" +dependencies = [ + "crypto-common 0.2.0-pre.5", +] + +[[package]] name = "bytes" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -216,6 +288,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.6", + "inout", +] + +[[package]] name = "clang-sys" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -243,9 +325,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -257,17 +339,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] [[package]] +name = "crypto-common" +version = "0.2.0-pre.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7aa2ec04f5120b830272a481e8d9d8ba4dda140d2cda59b0f1110d5eb93c38e" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] name = "digest" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.3", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.0-pre.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065d93ead7c220b85d5b4be4795d8398eac4ff68b5ee63895de0a3c1fb6edf25" +dependencies = [ + "block-buffer 0.11.0-pre.5", + "crypto-common 0.2.0-pre.5", + "subtle", ] [[package]] @@ -459,7 +571,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" name = "gnix" version = "1.0.0" dependencies = [ + "aes-gcm-siv", "anyhow", + "argon2", "base64 0.22.0", "bytes", "env_logger", @@ -478,6 +592,7 @@ dependencies = [ "mime_guess", "percent-encoding", "pin-project", + "rand", "rustls", "rustls-pemfile", "serde", @@ -620,6 +735,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] +name = "hybrid-array" +version = "0.2.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53668f5da5a41d9eaf4bf7064be46d1ebe6a4e1ceed817f387587b18f2b51047" +dependencies = [ + "typenum", +] + +[[package]] name = "hyper" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -687,6 +811,15 @@ dependencies = [ ] [[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] name = "itertools" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -735,7 +868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -876,6 +1009,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -899,6 +1038,17 @@ dependencies = [ ] [[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -943,6 +1093,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] name = "prettyplease" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -971,6 +1139,46 @@ dependencies = [ ] [[package]] +name = "rand" +version = "0.9.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31e63ea85be51c423e52ba8f2e68a3efd53eed30203ee029dd09947333693e" +dependencies = [ + "rand_chacha", + "rand_core 0.9.0-alpha.1", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78674ef918c19451dbd250f8201f8619b494f64c9aa6f3adb28fd8a0f1f6da46" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0-alpha.1", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_core" +version = "0.9.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc89dffba8377c5ec847d12bb41492bda235dba31a25e8b695cd0fe6589eb8c9" +dependencies = [ + "getrandom", + "zerocopy", +] + +[[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1132,7 +1340,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.6", ] [[package]] @@ -1301,9 +1509,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" @@ -1321,6 +1529,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.6", + "subtle", +] + +[[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1511,6 +1729,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] +name = "zerocopy" +version = "0.8.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db678a6ee512bd06adf35c35be471cae2f9c82a5aed2b5d15e03628c98bddd57" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201585ea96d37ee69f2ac769925ca57160cef31acb137c16f38b02b76f4c1e62" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -43,3 +43,8 @@ mime_guess = "2.0.4" bytes = "1.6.0" anyhow = "1.0.82" thiserror = "1.0.59" + +# Crypto for authentificating clients +aes-gcm-siv = "0.11.1" +argon2 = "0.6.0-pre.0" +rand = "0.9.0-alpha.1" diff --git a/src/config.rs b/src/config.rs index dfc4e73..a474e1d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,9 +2,10 @@ use crate::{ filters::{Node, NodeKind}, State, }; -use anyhow::{anyhow, Context}; +use anyhow::Context; use inotify::{EventMask, Inotify, WatchMask}; use log::{error, info}; +use rand::random; use serde::{ de::{value, Error, SeqAccess, Visitor}, Deserialize, Deserializer, Serialize, @@ -27,11 +28,16 @@ pub struct Config { pub watch_config: bool, pub http: Option<HttpConfig>, pub https: Option<HttpsConfig>, + #[serde(default = "random_bytes")] + pub private_key: [u8; 32], #[serde(default)] pub limits: Limits, pub handler: DynNode, } +fn random_bytes() -> [u8; 32] { + [(); 32].map(|_| random()) +} pub fn return_true() -> bool { true } @@ -155,7 +161,8 @@ impl<'de> Deserialize<'de> for DynNode { .ok_or(serde::de::Error::unknown_variant(s, &[]))? .instanciate(tv.value) .map_err(|e| { - serde::de::Error::custom(e.context(anyhow!("instanciating modules {s:?}"))) + let x = format!("instanciating modules {s:?}: {e:?}"); + serde::de::Error::custom(e.context(x)) })?; Ok(Self(inst)) diff --git a/src/filters/auth.rs b/src/filters/auth/basic.rs index 7d5b03e..a7a74c8 100644 --- a/src/filters/auth.rs +++ b/src/filters/auth/basic.rs @@ -1,5 +1,8 @@ -use super::{Node, NodeKind, NodeRequest, NodeResponse}; -use crate::{config::DynNode, error::ServiceError}; +use crate::{ + config::DynNode, + error::ServiceError, + filters::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse}, +}; use base64::Engine; use futures::Future; use http_body_util::{combinators::BoxBody, BodyExt}; @@ -17,7 +20,7 @@ impl NodeKind for HttpBasicAuthKind { fn name(&self) -> &'static str { "http_basic_auth" } - fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn super::Node>> { + fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> { Ok(Arc::new(serde_yaml::from_value::<HttpBasicAuth>(config)?)) } } @@ -32,7 +35,7 @@ pub struct HttpBasicAuth { impl Node for HttpBasicAuth { fn handle<'a>( &'a self, - context: &'a mut super::NodeContext, + context: &'a mut NodeContext, request: NodeRequest, ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> { Box::pin(async move { diff --git a/src/filters/auth/cookie.html b/src/filters/auth/cookie.html new file mode 100644 index 0000000..40bb157 --- /dev/null +++ b/src/filters/auth/cookie.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Document</title> + <style> + body { + background-color: grey; + } + form { + margin: auto; + width: 200px; + padding: 20px; + border: 2px solid black; + background-color: white; + } + input[type="text"], + input[type="password"] { + box-sizing: border-box; + width: 100%; + margin-bottom: 1em; + } + </style> + </head> + <body> + <form action="/_gnix_login" method="post"> + <label for="username">Username: </label><br /> + <input type="text" name="username" id="username" /><br /> + <label for="password">Password: </label><br /> + <input type="password" name="password" id="password" /><br /> + <input type="submit" value="Login" /> + </form> + </body> +</html> diff --git a/src/filters/auth/cookie.rs b/src/filters/auth/cookie.rs new file mode 100644 index 0000000..c1847ce --- /dev/null +++ b/src/filters/auth/cookie.rs @@ -0,0 +1,181 @@ +use crate::{ + config::{return_true, DynNode}, + error::ServiceError, + filters::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse}, +}; +use aes_gcm_siv::{ + aead::{Aead, Payload}, + Nonce, +}; +use base64::Engine; +use futures::Future; +use headers::{Cookie, HeaderMapExt}; +use http_body_util::{combinators::BoxBody, BodyExt}; +use hyper::{ + header::{HeaderValue, LOCATION, REFERER, SET_COOKIE}, + Method, Response, StatusCode, +}; +use log::debug; +use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC}; +use rand::random; +use serde::Deserialize; +use serde_yaml::Value; +use std::fmt::Write; +use std::{pin::Pin, sync::Arc, time::SystemTime}; + +use super::Credentials; + +pub struct CookieAuthKind; +impl NodeKind for CookieAuthKind { + fn name(&self) -> &'static str { + "cookie_auth" + } + fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> { + Ok(Arc::new(serde_yaml::from_value::<CookieAuth>(config)?)) + } +} + +#[derive(Deserialize)] +pub struct CookieAuth { + users: Credentials, + expire: Option<u64>, + #[serde(default = "return_true")] + secure: bool, + next: DynNode, + fail: DynNode, +} + +impl Node for CookieAuth { + fn handle<'a>( + &'a self, + context: &'a mut NodeContext, + request: NodeRequest, + ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> { + Box::pin(async move { + if request.method() == Method::POST { + let referrer = request.headers().get(REFERER).cloned(); + let d = request + .into_body() + .collect() + .await + .map_err(|_| todo!()) + .unwrap(); + let d = String::from_utf8(d.to_bytes().to_vec()).unwrap(); + + let mut username = "user"; + let mut password = ""; + for kv in d.split("&") { + let (key, value) = kv.split_once("=").ok_or(ServiceError::BadAuth)?; + match key { + "username" => username = value, + "password" => password = value, + _ => (), + } + } + let mut r = Response::new(BoxBody::<_, ServiceError>::new( + String::new().clone().map_err(|_| unreachable!()), + )); + *r.status_mut() = StatusCode::FOUND; + debug!("login attempt for {username:?}"); + if self.users.authentificate(username, password) { + debug!("login success"); + let nonce = [(); 12].map(|_| random::<u8>()); + let plaintext = unix_seconds().to_le_bytes(); + let mut ciphertext = context + .state + .crypto_key + .encrypt( + Nonce::from_slice(&nonce), + Payload { + msg: &plaintext, + aad: username.as_bytes(), + }, + ) + .unwrap(); + + ciphertext.extend(nonce); + let auth = base64::engine::general_purpose::URL_SAFE.encode(ciphertext); + + let mut cookie_opts = String::new(); + if let Some(e) = self.expire { + write!(cookie_opts, "; Expire={e}").unwrap(); + } + if self.secure { + write!(cookie_opts, "; Secure").unwrap(); + } + + r.headers_mut().append( + SET_COOKIE, + HeaderValue::from_str(&format!( + "gnix_username={}{}", + percent_encode(username.as_bytes(), NON_ALPHANUMERIC), + cookie_opts + )) + .unwrap(), + ); + r.headers_mut().append( + SET_COOKIE, + HeaderValue::from_str(&format!("gnix_auth={}{}", auth, cookie_opts)) + .unwrap(), + ); + } else { + debug!("login fail"); + } + r.headers_mut() + .append(LOCATION, referrer.unwrap_or(HeaderValue::from_static("/"))); + + Ok(r) + } else { + if let Some(cookie) = request.headers().typed_get::<Cookie>() { + if let Some(auth) = cookie.get("gnix_auth") { + let username = + percent_decode_str(cookie.get("gnix_username").unwrap_or("user")) + .decode_utf8()?; + + let auth = base64::engine::general_purpose::URL_SAFE.decode(auth)?; + if auth.len() < 12 { + return Err(ServiceError::BadAuth); + } + let (msg, nonce) = auth.split_at(auth.len() - 12); + let plaintext = context.state.crypto_key.decrypt( + Nonce::from_slice(nonce), + Payload { + msg, + aad: username.as_bytes(), + }, + ); + if let Ok(plaintext) = plaintext { + let created = u64::from_le_bytes(plaintext[0..8].try_into().unwrap()); + + if self + .expire + .map(|e| created + e > unix_seconds()) + .unwrap_or(true) + { + debug!("valid auth for {username:?}"); + return self.next.handle(context, request).await; + } else { + debug!("auth expired"); + } + } else { + debug!("aead invalid"); + } + } else { + debug!("no auth cookie"); + } + } + debug!("unauthorized"); + let mut r = self.fail.handle(context, request).await?; + *r.status_mut() = StatusCode::UNAUTHORIZED; + Ok(r) + } + }) + } +} + +fn unix_seconds() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() +} diff --git a/src/filters/auth/mod.rs b/src/filters/auth/mod.rs new file mode 100644 index 0000000..d6e1a35 --- /dev/null +++ b/src/filters/auth/mod.rs @@ -0,0 +1,90 @@ +use argon2::PasswordVerifier; +use argon2::{ + password_hash::{Encoding, PasswordHashString}, + Algorithm, Argon2, Params, PasswordHash, Version, +}; +use serde::de::MapAccess; +use serde::{ + de::{value, Error, Visitor}, + Deserialize, +}; +use std::{collections::HashMap, fmt, fs::read_to_string}; + +pub mod basic; +pub mod cookie; + +struct Credentials { + wrong_user: PasswordHashString, + hashes: HashMap<String, PasswordHashString>, +} + +impl Credentials { + fn get(&self, usernamme: &str) -> &PasswordHashString { + self.hashes.get(usernamme).unwrap_or(&self.wrong_user) + } + pub fn authentificate(&self, usernamme: &str, password: &str) -> bool { + let algo = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default()); + let hash = self.get(usernamme); + match hash.algorithm().as_str() { + "argon2id" => algo + .verify_password( + password.as_bytes(), + &PasswordHash::parse(hash.as_str(), hash.encoding()).unwrap(), + ) + .is_ok(), + "never" => false, + _ => false, + } + } +} + +impl<'de> Deserialize<'de> for Credentials { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct StringOrMap; + impl<'de> Visitor<'de> for StringOrMap { + type Value = HashMap<String, String>; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("credentials map or file path") + } + fn visit_str<E>(self, val: &str) -> Result<Self::Value, E> + where + E: Error, + { + let path = String::deserialize(value::StrDeserializer::new(val))?; + let c = serde_yaml::from_str(&read_to_string(path).map_err(|io| { + serde::de::Error::custom(format!("cannot read creds file: {io:?}")) + })?) + .map_err(|e| serde::de::Error::custom(format!("cannot parse creds file: {e:?}")))?; + Ok(c) + } + fn visit_map<A>(self, val: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'de>, + { + Ok(HashMap::deserialize(value::MapAccessDeserializer::new( + val, + ))?) + } + } + let k = deserializer.deserialize_any(StringOrMap)?; + Ok(Credentials { + wrong_user: PasswordHashString::parse("$never", Encoding::B64).unwrap(), + hashes: k + .into_iter() + .map(|(k, v)| { + let hash = PasswordHash::parse(&v, Encoding::B64) + .map_err(|e| { + serde::de::Error::custom(format!( + "phc string for user {k:?} is invalid: {e:?}" + )) + })? + .serialize(); + Ok((k, hash)) + }) + .try_collect()?, + }) + } +} diff --git a/src/filters/file.rs b/src/filters/file.rs new file mode 100644 index 0000000..53c27f4 --- /dev/null +++ b/src/filters/file.rs @@ -0,0 +1,62 @@ +use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse}; +use crate::error::ServiceError; +use futures::Future; +use http_body_util::{combinators::BoxBody, BodyExt}; +use hyper::{ + header::{HeaderValue, CONTENT_TYPE}, + Response, +}; +use serde::Deserialize; +use serde_yaml::Value; +use std::{fs::read_to_string, path::PathBuf, pin::Pin, sync::Arc}; + +pub struct FileKind; + +#[derive(Debug, Deserialize)] +struct FileConfig { + path: Option<PathBuf>, + content: Option<String>, + r#type: Option<String>, +} + +#[derive(Debug, Deserialize)] +struct File { + content: String, + r#type: String, +} + +impl NodeKind for FileKind { + fn name(&self) -> &'static str { + "file" + } + fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> { + let conf = serde_yaml::from_value::<FileConfig>(config)?; + Ok(Arc::new(File { + content: conf + .content + .or(conf + .path + .map(|p| Ok::<_, ServiceError>(read_to_string(p)?)) + .transpose()?) + .unwrap_or_default(), + r#type: conf.r#type.unwrap_or("text/html".to_string()), // TODO infer mime from ext + })) + } +} + +impl Node for File { + fn handle<'a>( + &'a self, + _context: &'a mut NodeContext, + _request: NodeRequest, + ) -> Pin<Box<dyn Future<Output = Result<NodeResponse, ServiceError>> + Send + Sync + 'a>> { + Box::pin(async move { + let mut r = Response::new(BoxBody::<_, ServiceError>::new( + self.content.clone().map_err(|_| unreachable!()), + )); + r.headers_mut() + .insert(CONTENT_TYPE, HeaderValue::from_str(&self.r#type).unwrap()); + Ok(r) + }) + } +} diff --git a/src/filters/files.rs b/src/filters/files.rs index fc3d63b..4fdd5cd 100644 --- a/src/filters/files.rs +++ b/src/filters/files.rs @@ -36,6 +36,7 @@ use tokio::{ use tokio_util::io::poll_read_buf; pub struct FilesKind; +pub struct FileKind; #[derive(Debug, Deserialize)] struct Files { @@ -49,7 +50,6 @@ struct Files { #[serde(default)] cache: CacheMode, } - #[derive(Debug, Default, Deserialize)] #[serde(rename_all = "snake_case")] enum CacheMode { diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 10520a3..2bee8e3 100644 --- a/src/filters/mod.rs +++ b/src/filters/mod.rs @@ -1,9 +1,10 @@ use crate::error::ServiceError; use crate::State; use accesslog::AccessLogKind; -use auth::HttpBasicAuthKind; +use auth::{basic::HttpBasicAuthKind, cookie::CookieAuthKind}; use bytes::Bytes; use error::ErrorKind; +use file::FileKind; use files::FilesKind; use futures::Future; use hosts::HostsKind; @@ -16,6 +17,7 @@ use std::{net::SocketAddr, pin::Pin, sync::Arc}; pub mod accesslog; pub mod auth; pub mod error; +pub mod file; pub mod files; pub mod hosts; pub mod proxy; @@ -25,9 +27,11 @@ pub type NodeResponse = Response<BoxBody<Bytes, ServiceError>>; pub static MODULES: &'static [&'static dyn NodeKind] = &[ &HttpBasicAuthKind, + &CookieAuthKind, &ProxyKind, &HostsKind, &FilesKind, + &FileKind, &AccessLogKind, &ErrorKind, ]; diff --git a/src/main.rs b/src/main.rs index 7c74f70..5a88ad1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ pub mod helper; #[cfg(feature = "mond")] pub mod reporting; +use aes_gcm_siv::{aead::generic_array::GenericArray, Aes256GcmSiv, KeyInit}; use anyhow::{anyhow, Context, Result}; use config::{setup_file_watch, Config, NODE_KINDS}; use error::ServiceError; @@ -46,6 +47,7 @@ use tokio::{ use tokio_rustls::TlsAcceptor; pub struct State { + pub crypto_key: Aes256GcmSiv, pub config: RwLock<Arc<Config>>, pub access_logs: RwLock<HashMap<String, BufWriter<File>>>, pub l_incoming: Semaphore, @@ -81,6 +83,7 @@ async fn main() -> anyhow::Result<()> { } }; let state = Arc::new(State { + crypto_key: aes_gcm_siv::Aes256GcmSiv::new(GenericArray::from_slice(&config.private_key)), l_incoming: Semaphore::new(config.limits.max_incoming_connections), l_outgoing: Semaphore::new(config.limits.max_outgoing_connections), #[cfg(feature = "mond")] |