summaryrefslogtreecommitdiff
path: root/src
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 /src
parent886a18e0c67624d0882f04c7f6659bcfee6b4d8d (diff)
downloadgnix-29c48afafb4a6a0a0636774f9b56423881fb1703.tar
gnix-29c48afafb4a6a0a0636774f9b56423881fb1703.tar.bz2
gnix-29c48afafb4a6a0a0636774f9b56423881fb1703.tar.zst
implement cookie base auth.
Diffstat (limited to 'src')
-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
9 files changed, 393 insertions, 8 deletions
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")]