summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-05-29 23:44:14 +0200
committermetamuffin <metamuffin@disroot.org>2024-05-29 23:44:14 +0200
commit29c48afafb4a6a0a0636774f9b56423881fb1703 (patch)
treefa610555a33c25a1aaeb98242099c2010ac243b0
parent886a18e0c67624d0882f04c7f6659bcfee6b4d8d (diff)
downloadgnix-29c48afafb4a6a0a0636774f9b56423881fb1703.tar
gnix-29c48afafb4a6a0a0636774f9b56423881fb1703.tar.bz2
gnix-29c48afafb4a6a0a0636774f9b56423881fb1703.tar.zst
implement cookie base auth.
-rw-r--r--Cargo.lock254
-rw-r--r--Cargo.toml5
-rw-r--r--src/config.rs11
-rw-r--r--src/filters/auth/basic.rs (renamed from src/filters/auth.rs)11
-rw-r--r--src/filters/auth/cookie.html35
-rw-r--r--src/filters/auth/cookie.rs181
-rw-r--r--src/filters/auth/mod.rs90
-rw-r--r--src/filters/file.rs62
-rw-r--r--src/filters/files.rs2
-rw-r--r--src/filters/mod.rs6
-rw-r--r--src/main.rs3
11 files changed, 644 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b18a3f6..ee124b5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index a1501f1..060f95e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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")]