//! # Tuxcord Reverse Proxy Header Authenthication #![feature( anonymous_lifetime_in_impl_trait, iterator_try_collect, rwlock_downgrade, try_blocks )] #![allow(clippy::missing_errors_doc)] use std::{ fs::File, io::{self, Read, Write}, net::{self, Shutdown, TcpListener, TcpStream}, sync::Arc, thread, }; use anyhow::Context; use clap::Parser; use dashmap::{DashMap, mapref::one::Ref}; use thiserror::Error; use crate::utils::headers::HeadersExt; pub mod args; pub mod config; pub mod constants; #[cfg(feature = "ipc")] pub mod ipc; pub mod utils; use args::Args; fn main() -> anyhow::Result<()> { let args = Args::parse(); if !args.config.exists() { println!( "{:?} doesn't exist, creating a default config", &args.config ); File::create(args.config) .context("failure creating the config file")? .write_all(constants::DEFAULT_CONFIG) .context("failure writing the contents to the config file")?; return Ok(()); } let mut config_file = File::open(&args.config).context("failure opening the config file")?; let mut config = String::new(); config_file .read_to_string(&mut config) .context("failure reading the config file")?; let config: config::Schema = toml::from_str(&config).context("invalid config file")?; #[cfg(feature = "ipc")] if let Some(args::Commands::Ipc) = args.command { ipc::start_client(config)?; return Ok(()); } println!("config: {config:#?}"); let listener = TcpListener::bind(config.listen_at).context("failure tcp listening")?; let config_arc = Arc::new(config); // will also serve as a counter #[cfg(feature = "ipc")] ipc::handle_daemon(config_arc.clone()); for stream in listener.incoming() { match stream { Ok(mut client) => { let config_arc = config_arc.clone(); thread::spawn(|| { if let Err(err) = handle_client(&mut client, config_arc.as_ref()) { eprintln!("err: invalid req head ({err}), closing..."); _ = client.shutdown(Shutdown::Both); } drop(client); drop(config_arc); }); } Err(err) => eprintln!("error with an incoming listener {err:#?}"), } } unreachable!("listener had to be killed unexpectedly"); } #[derive(Error, Debug)] pub enum ClientError { #[error("error reading request head: {0}")] HeadReadError(io::Error), #[error("failed to find \"host\" header")] HostHeaderNotFound, #[error("requested host {0:?}, but it's not registered")] HostNotRegistered(String), #[error("failed to connect to backend: {0}")] 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 authentication first of everything as it changes decisions on everything 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_authd = id_auth.as_ref().is_some_and(|id_auth| id_auth.1.is_some()); match (req.path, req.method, is_authd) { (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), }, } } // TODO: might wanna save this one between runtimes static AUTH_MAP: std::sync::LazyLock> = std::sync::LazyLock::new(DashMap::new); static _CSRF_MAP: std::sync::LazyLock> = std::sync::LazyLock::new(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; let mut headers; let (pos, req) = loop { headers = [httparse::EMPTY_HEADER; 16]; let mut req = httparse::Request::new(&mut headers); let n = client .read(&mut header_buf[read_pos..]) .map_err(E::HeadReadError)?; read_pos += n; if let Ok(httparse::Status::Complete(n)) = req.parse(&header_buf[0..read_pos]) { 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 pass everything let read_hosts = config.hosts.read(); let (addr, id_keyname, csrf_keyname) = read_hosts .get(host_header.as_ref()) .ok_or_else(|| E::HostNotRegistered(host_header.to_string()))? .clone(); drop(read_hosts); // 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(Result::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]) .map_err(E::ExchangeIoError)?; if theres_body { 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)?; } } Ok(()) }