diff --git a/Cargo.lock b/Cargo.lock index e01f82b..4ad8d44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,22 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "ctrlc" version = "3.4.7" @@ -138,12 +154,41 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.4" @@ -169,7 +214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.4", ] [[package]] @@ -178,6 +223,18 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.174" @@ -213,6 +270,18 @@ dependencies = [ "libc", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.1" @@ -242,6 +311,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -353,6 +428,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "toml" version = "0.8.23" @@ -401,8 +507,11 @@ dependencies = [ "anstyle", "anyhow", "clap", + "cookie", "ctrlc", + "dashmap", "httparse", + "lazy_static", "parking_lot", "serde", "shlex", @@ -422,6 +531,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index c03d6e3..850e3ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,11 @@ edition = "2024" anstyle = "1" anyhow = "1" clap = { version = "4", features = ["derive", "string"] } +cookie = "0" ctrlc = "3" +dashmap = "6" httparse = "1" +lazy_static = "1" parking_lot = { version = "0", features = ["arc_lock", "serde"] } serde = { version = "1", features = ["derive"] } shlex = "1" diff --git a/src/constants.rs b/src/constants.rs index ef9d11a..8f3dba6 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1 +1,3 @@ pub const DEFAULT_CONFIG: &[u8] = include_bytes!("../config.default.toml"); +pub const DOUBLE_CRLF: &[u8] = b"\r\n\r\n"; +pub const HTTP_HEADERS_NEWLINE: &[u8] = b"\r\n"; // I'm not so sure about this diff --git a/src/main.rs b/src/main.rs index 233126b..9fe8744 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,13 +10,15 @@ use std::{ fs::File, io::{self, Read, Write}, - net::{Shutdown, TcpListener, TcpStream}, + net::{self, Shutdown, TcpListener, TcpStream}, sync::Arc, thread, }; use anyhow::Context; use clap::Parser; +use dashmap::{DashMap, mapref::one::Ref}; +use lazy_static::lazy_static; use thiserror::Error; use crate::utils::headers::HeadersExt; @@ -97,10 +99,95 @@ pub enum ClientError { BackendConnectFail(io::Error), #[error("io error exchanging client and backend: {0}")] ExchangeIoError(io::Error), + #[error("io error capturing {0} buffer: {0}")] + BufferError(&'static str, io::Error), + #[error("io error writing auth headers: {0}")] + AuthHeadersIoError(io::Error), +} + +// fn(req, (adrr, etc)) -> Action +// Action { +// CaptureBody? +// CaptureResponse? +// } +// +// cases: +// * GET login -> get response to access csrf token +// * POST login -> get body to handle authentication +// +// auth headers are just added depending on the ID cookie soo, keep a hashmap of IDs and user +// data?? +// +// check authethication first of everything as it changes decissions on eveything and give it to fn +// too +// fn(req, Option, (adrr, etc)) -> Action +// +// and also add an action to... hook in the response without even letting backend know (redirect +// ppl out and such), also in no case we need multiple actions. NOPE, POST login should capture +// body and hijack the response, I'll also just make this flexible too. + +#[allow(clippy::type_complexity)] +struct ProxyAction<'a> { + // combining like this eases a lot the implementation + capture_body_and_hijack: Option<( + fn(&httparse::Request, &[u8]) -> Result<(), ClientError>, + fn(&httparse::Request, &mut TcpStream) -> Result<(), ClientError>, + )>, + capture_response: Option Result<(), ClientError>>, + auth: Option>, +} + +struct AuthData { + pub username: String, + pub email: String, + pub fullname: Option, +} +impl AuthData { + pub fn write_as_headers(&self, into: &mut impl Write) -> Result<(), ClientError> { + use constants::HTTP_HEADERS_NEWLINE as NL; + let r: Result<_, io::Error> = try { + into.write_all(b"X-WEBAUTH-USER: ")?; + into.write_all(self.username.as_bytes())?; + into.write_all(NL)?; + into.write_all(b"X-WEBAUTH-EMAIL: ")?; + into.write_all(self.email.as_bytes())?; + into.write_all(NL)?; + if let Some(fullname) = self.fullname.as_ref() { + into.write_all(b"X-WEBAUTH-FULLNAME: ")?; + into.write_all(fullname.as_bytes())?; + into.write_all(NL)?; + }; + }; + r.map_err(ClientError::AuthHeadersIoError) + } +} + +fn what_to_do<'a>( + req: &httparse::Request, + id_auth: Option<(String, Option>)>, + backend: (net::SocketAddr, String, String), +) -> ProxyAction<'a> { + let is_auth = id_auth.as_ref().is_some_and(|id_auth| id_auth.1.is_some()); + match (req.path, req.method, is_auth) { + (Some("/user/login"), Some("GET"), false) => todo!(), + (Some("/user/login"), Some("POST"), false) => todo!(), + _ => ProxyAction { + capture_body_and_hijack: None, + capture_response: None, + auth: id_auth.and_then(|id_auth| id_auth.1), + }, + } +} + +lazy_static! { + // TODO: might wanna save this one between runtimes + static ref AUTH_MAP: DashMap = DashMap::new(); + static ref CSRF_MAP: DashMap = DashMap::new(); } fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(), ClientError> { use ClientError as E; + use constants::DOUBLE_CRLF; let mut header_buf = [0u8; 1024 * 8]; let mut read_pos = 0usize; @@ -116,34 +203,74 @@ fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(), read_pos += n; if let Ok(httparse::Status::Complete(n)) = req.parse(&header_buf[0..read_pos]) { - break (n - b"\r\n\r\n".len(), req); + break (n - DOUBLE_CRLF.len(), req); } }; let theres_body = req.headers.has_any(["conten-length", "transfer-encoding"]); let host_header = req.headers.get("host").ok_or(E::HostHeaderNotFound)?; + let cookies = req.headers.get("cookie"); - // Now find that header and pas everything + // Now find that header and pass everything let read_hosts = config.hosts.read(); - let (addr, _, _) = read_hosts + let (addr, id_keyname, csrf_keyname) = read_hosts .get(host_header.as_ref()) - .ok_or_else(|| E::HostNotRegistered(host_header.to_string()))?; - - let mut stream = TcpStream::connect(addr).map_err(E::BackendConnectFail)?; + .ok_or_else(|| E::HostNotRegistered(host_header.to_string()))? + .clone(); drop(read_hosts); - let r: io::Result<()> = try { - stream.write_all(&header_buf[0..pos])?; - // here we sent all original headers, append our own (TODO) + + // Try to find auth data + let id_auth = if let Some(cookies) = cookies { + if let Some(id_ck) = cookie::Cookie::split_parse(cookies) + .filter_map(|ck| ck.ok()) + .find(|ck| ck.name() == id_keyname) + { + let id = id_ck.value().to_string(); + let val = AUTH_MAP.get(&id); + Some((id, val)) + } else { + None + } + } else { + None + }; + + let action = what_to_do(&req, id_auth, (addr, id_keyname, csrf_keyname)); + if let Some((cb1, cb2)) = action.capture_body_and_hijack { + let mut body = header_buf[pos..read_pos].to_vec(); + if theres_body { + io::copy(client, &mut body).map_err(|err| E::BufferError("body", err))?; + } + cb1(&req, &body[..])?; + cb2(&req, client)?; + } else { + let mut stream = TcpStream::connect(addr).map_err(E::BackendConnectFail)?; + stream + .write_all(&header_buf[0..pos]) + .map_err(E::ExchangeIoError)?; + if let Some(auth) = action.auth { + auth.write_as_headers(&mut stream)?; + }; // send our overhead - stream.write_all(&header_buf[pos..read_pos])?; + stream + .write_all(&header_buf[pos..read_pos]) + .map_err(E::ExchangeIoError)?; if theres_body { - io::copy(client, &mut stream)?; + io::copy(client, &mut stream).map_err(E::ExchangeIoError)?; + } + if let Some(cb) = action.capture_response { + let mut buf = Vec::new(); + stream + .read_to_end(&mut buf) + .map_err(|err| E::BufferError("response", err))?; + cb(&req, &buf[..])?; + client.write_all(&buf[..]).map_err(E::ExchangeIoError)?; + } else { + io::copy(&mut stream, client).map_err(E::ExchangeIoError)?; } - io::copy(&mut stream, client)?; }; - r.map_err(E::ExchangeIoError)?; Ok(()) }