aboutsummaryrefslogtreecommitdiff
path: root/src/filters/auth
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-05-30 00:09:11 +0200
committermetamuffin <metamuffin@disroot.org>2024-05-30 00:09:11 +0200
commit532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a (patch)
treec4422c4d54e01f63bae391cd95788cad74f59fbb /src/filters/auth
parent8b39940a58c28bc1bbe291eb5229e9ce1444e33c (diff)
downloadgnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar
gnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar.bz2
gnix-532cc431d1c5ca1ffcf429a4ccb94edc7848fe7a.tar.zst
rename filters dir
Diffstat (limited to 'src/filters/auth')
-rw-r--r--src/filters/auth/basic.rs68
-rw-r--r--src/filters/auth/cookie.rs182
-rw-r--r--src/filters/auth/login.html35
-rw-r--r--src/filters/auth/mod.rs90
4 files changed, 0 insertions, 375 deletions
diff --git a/src/filters/auth/basic.rs b/src/filters/auth/basic.rs
deleted file mode 100644
index a7a74c8..0000000
--- a/src/filters/auth/basic.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-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};
-use hyper::{
- header::{HeaderValue, AUTHORIZATION, WWW_AUTHENTICATE},
- Response, StatusCode,
-};
-use log::debug;
-use serde::Deserialize;
-use serde_yaml::Value;
-use std::{collections::HashSet, pin::Pin, sync::Arc};
-
-pub struct HttpBasicAuthKind;
-impl NodeKind for HttpBasicAuthKind {
- fn name(&self) -> &'static str {
- "http_basic_auth"
- }
- fn instanciate(&self, config: Value) -> anyhow::Result<Arc<dyn Node>> {
- Ok(Arc::new(serde_yaml::from_value::<HttpBasicAuth>(config)?))
- }
-}
-
-#[derive(Deserialize)]
-pub struct HttpBasicAuth {
- realm: String,
- valid: HashSet<String>,
- next: DynNode,
-}
-
-impl Node for HttpBasicAuth {
- 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 let Some(auth) = request.headers().get(AUTHORIZATION) {
- let k = auth
- .as_bytes()
- .strip_prefix(b"Basic ")
- .ok_or(ServiceError::BadAuth)?;
- let k = base64::engine::general_purpose::STANDARD.decode(k)?;
- let k = String::from_utf8(k)?;
- if self.valid.contains(&k) {
- debug!("valid auth");
- return self.next.handle(context, request).await;
- } else {
- debug!("invalid auth");
- }
- }
- debug!("unauthorized; sending auth challenge");
- let mut r = Response::new(BoxBody::<_, ServiceError>::new(
- String::new().map_err(|_| unreachable!()),
- ));
- *r.status_mut() = StatusCode::UNAUTHORIZED;
- r.headers_mut().insert(
- WWW_AUTHENTICATE,
- HeaderValue::from_str(&format!("Basic realm=\"{}\"", self.realm)).unwrap(),
- );
- Ok(r)
- })
- }
-}
diff --git a/src/filters/auth/cookie.rs b/src/filters/auth/cookie.rs
deleted file mode 100644
index 620911d..0000000
--- a/src/filters/auth/cookie.rs
+++ /dev/null
@@ -1,182 +0,0 @@
-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 && request.uri().path() == "/_gnix_login" {
- 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();
-
- // TODO proper parser
- 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/login.html b/src/filters/auth/login.html
deleted file mode 100644
index c7782bd..0000000
--- a/src/filters/auth/login.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Gnix Login</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/mod.rs b/src/filters/auth/mod.rs
deleted file mode 100644
index d6e1a35..0000000
--- a/src/filters/auth/mod.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-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()?,
- })
- }
-}