diff --git a/Cargo.lock b/Cargo.lock index e0b0061..ccb0e3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cfg-if" version = "1.0.1" @@ -144,6 +150,29 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "cssparser" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctrlc" version = "3.4.7" @@ -177,12 +206,72 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "getopts" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +dependencies = [ + "unicode-width", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -201,6 +290,18 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + [[package]] name = "httparse" version = "1.10.1" @@ -246,12 +347,55 @@ dependencies = [ "serde", ] +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.30.1" @@ -305,12 +449,76 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -329,6 +537,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "redox_syscall" version = "0.5.13" @@ -344,6 +567,40 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scraper" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e65d9d888567588db4c12da1087598d0f6f8b346cc2c5abc91f05fc2dffe2" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "selectors" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "serde" version = "1.0.219" @@ -373,18 +630,64 @@ dependencies = [ "serde", ] +[[package]] +name = "servo_arc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -402,6 +705,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "thiserror" version = "2.0.12" @@ -506,6 +820,8 @@ dependencies = [ "dashmap", "httparse", "parking_lot", + "paste", + "scraper", "serde", "shlex", "thiserror", @@ -518,6 +834,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index f8e9573..d352e7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ ctrlc = "3" dashmap = "6" httparse = "1" parking_lot = { version = "0", features = ["arc_lock", "serde"] } +paste = "1" +scraper = "0" serde = { version = "1", features = ["derive"] } shlex = "1" thiserror = "2" diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs index c88421e..92693b4 100644 --- a/src/ipc/mod.rs +++ b/src/ipc/mod.rs @@ -49,6 +49,7 @@ pub fn start_client(config: config::Schema) -> anyhow::Result<()> { Ok(()) } +#[allow(clippy::too_many_lines)] pub fn handle_daemon_client(mut stream: UnixStream, config: &Arc) { let Ok(stream2) = stream.try_clone() else { return; @@ -65,9 +66,15 @@ pub fn handle_daemon_client(mut stream: UnixStream, config: &Arc "help" => { _ = writeln!( stream, - "available commands:\n help\n configcount\n confdump\n hosts ..." + "available commands (in no particular order):\n help\n configcount\n userdump \n csrfdump\n confdump\n hosts ..." ); } + "userdump" => { + _ = writeln!(stream, "{:#?}", crate::listener::AUTH_MAP); + }, + "csrfdump" => { + _ = writeln!(stream, "{:#?}", crate::listener::CSRF_MAP); + }, "configcount" => { _ = writeln!( stream, diff --git a/src/listener/mod.rs b/src/listener/mod.rs new file mode 100644 index 0000000..f06de4d --- /dev/null +++ b/src/listener/mod.rs @@ -0,0 +1,502 @@ +use core::str; +use std::{ + io::{self, Read, Write}, + net::{Shutdown, TcpListener, TcpStream}, + str::Utf8Error, + sync::Arc, + thread, +}; + +use dashmap::{DashMap, mapref::one::Ref}; +use httparse::{Request, Response}; +use thiserror::Error; + +use crate::{config, constants, utils::headers::HeadersExt}; + +pub fn start_listener_loop(config_arc: &Arc, listener: &TcpListener) -> ! { + 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), + #[error("failed to parse response head: {0}")] + ParseResponseHead(httparse::Error), + #[error("attempted to parse an unfinished response")] + UnfinishedResponse, + #[error("error with text encoding: {0}")] + TextEncodingError(Utf8Error), + #[error("error building html csrf finder selector: {0}")] + SelectorBuildError(String), + #[error("couldn't find csrf token in response")] + CsrfNotFound, + #[error("invalid header {0:?}")] + InvalidHeader(&'static str), +} + +// 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)] +#[derive(Debug)] +enum ProxyAction<'a> { + // combining like this eases a lot the implementation + CaptureBodyAndHijack(String), + // ( + // fn(&httparse::Request, &[u8]) -> Result<(), ClientError>, + // fn(&httparse::Request, &mut TcpStream) -> Result<(), ClientError>, + // ), + // ), + CaptureResponse(String), + //fn(&httparse::Request, &[u8]) -> Result<(), ClientError>), + ContWithMaybeAuth(Option>), +} + +impl ProxyAction<'_> { + // username - password + #[allow(dead_code)] + type CaptureBodyConclussion = (String, String); + + pub fn capture_body( + _req: &httparse::Request, + _id: &str, + _body: &[u8], + ) -> Result { + todo!(); + } + + pub fn hijack( + _req: &httparse::Request, + _body_conclussion: Self::CaptureBodyConclussion, + _stream: &mut TcpStream, + ) -> Result<(), ClientError> { + todo!(); + } + + pub fn capture_response( + _req: &httparse::Request, + _res: &httparse::Response, + id: String, + csrf_keyname: &str, + body: &[u8], + ) -> Result<(), ClientError> { + // Find csrf token and add it to list of things + let document = scraper::Html::parse_document( + str::from_utf8(body).map_err(ClientError::TextEncodingError)?, + ); + let selector = scraper::Selector::parse(format!("input[name=\"{csrf_keyname}\"]").as_str()) + .map_err(|err| ClientError::SelectorBuildError(format!("{err}")))?; + let csrf = document + .select(&selector) + .next() + .and_then(|elem| elem.attr("value")) + .ok_or(ClientError::CsrfNotFound)?; + + println!("inserting {id} - {csrf}"); + CSRF_MAP.insert(id, csrf.to_string()); + Ok(()) + } +} + +#[derive(Debug)] +pub 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>)>, +) -> ProxyAction<'a> { + let trimedpath = req + .path + .and_then(|path| path.split_once('?').map(|spl| spl.0).or(Some(path))); + // FIXME: auth might not be set up until gitea replies and sets an ID + match (trimedpath, req.method, id_auth) { + (Some("/user/login"), Some("GET"), Some((id, None))) => { + // We gotta get the gitea response to find the csrf token and save that + ProxyAction::CaptureResponse(id) + } + (Some("/user/login"), Some("POST"), Some((id, None))) => { + // Here we gotta get the post body, check csrf, and check passwd + // Then get their account data, add it to their ID and redirect back to home + ProxyAction::CaptureBodyAndHijack(id) + } + (_, _, id_auth) => ProxyAction::ContWithMaybeAuth(id_auth.and_then(|id_auth| id_auth.1)), + } +} + +// TODO: might wanna save this one between runtimes +pub static AUTH_MAP: std::sync::LazyLock> = + std::sync::LazyLock::new(DashMap::new); +// TODO: time expire old keys? +pub static CSRF_MAP: std::sync::LazyLock> = + std::sync::LazyLock::new(DashMap::new); + +#[allow(clippy::too_many_lines)] +fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(), ClientError> { + use ClientError as E; + use ProxyAction as A; + + http_bodies::parse_http!(client_req, client, Request, let (req, _, body_over)); + + let theres_body = req.headers.expect_body(); + + let host_header = req.headers.header("host").ok_or(E::HostHeaderNotFound)?; + let cookies = req.headers.header("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); + if let A::CaptureBodyAndHijack(id) = action { + let mut body = body_over.to_vec(); + if theres_body { + // not sure if this will append or overwrite, wanna append + io::copy(client, &mut body).map_err(|err| E::BufferError("body", err))?; + } + let c = A::capture_body(&req, id.as_ref(), &body[..])?; + A::hijack(&req, c, client)?; + } else { + let mut stream = TcpStream::connect(addr).map_err(E::BackendConnectFail)?; + // stream.write_all(body_head).map_err(E::ExchangeIoError)?; + stream + .write_all( + format!( + "{} {} HTTP/1.1\r\n", + req.method.unwrap_or("GET"), + req.path.unwrap_or("/") + ) + .as_bytes(), + ) + .map_err(E::ExchangeIoError)?; + req.headers + .iter() + .filter(|header| !header.name.eq_ignore_ascii_case("accept-encoding")) + .map(|header| { + stream.write_all( + &{ + let mut all = Vec::new(); + all.extend_from_slice(header.name.as_bytes()); + all.extend_from_slice(b": "); + all.extend_from_slice(header.value); + all.extend_from_slice(b"\r\n"); + all + }[..], + ) + }) + .try_fold((), |(), header| header) + .map_err(E::ExchangeIoError)?; + if let A::ContWithMaybeAuth(Some(ref auth)) = action { + auth.write_as_headers(&mut stream)?; + } + stream + .write_all(constants::HTTP_HEADERS_NEWLINE) + .map_err(E::ExchangeIoError)?; + + // send our overhead + stream.write_all(body_over).map_err(E::ExchangeIoError)?; + if theres_body { + io::copy(client, &mut stream).map_err(E::ExchangeIoError)?; + } + if let A::CaptureResponse(id) = action { + http_bodies::parse_http!(stream_res, stream, Response, let (resp, _, res_body_over)); + + let buf = http_bodies::capture_body( + resp.headers, + res_body_over, + &mut stream, + "response buffer", + )? + .unwrap_or_else(Vec::new); + A::capture_response(&req, &resp, id, csrf_keyname.as_ref(), &buf[..])?; + + client + .write_all( + format!( + "HTTP/1.1 {} {}\r\n", + resp.code.unwrap_or(200), + resp.reason.unwrap_or("OK") + ) + .as_bytes(), + ) + .map_err(E::ExchangeIoError)?; + resp.headers + .iter() + .filter(|header| !header.name.eq_ignore_ascii_case("transfer-encoding")) + .map(|header| { + client.write_all( + &{ + let mut all = Vec::new(); + all.extend_from_slice(header.name.as_bytes()); + all.extend_from_slice(b": "); + all.extend_from_slice(header.value); + all.extend_from_slice(b"\r\n"); + all + }[..], + ) + }) + .try_fold((), |(), header| header) + .map_err(E::ExchangeIoError)?; + + // FIXME: :grimacing: (append to headers or smth) + client + .write_all(format!("Content-Length: {}\r\n\r\n", buf.len()).as_bytes()) + .map_err(E::ExchangeIoError)?; + client + .write_all(constants::HTTP_HEADERS_NEWLINE) + .map_err(E::ExchangeIoError)?; + + client.write_all(&buf[..]).map_err(E::ExchangeIoError)?; + } else { + io::copy(&mut stream, client).map_err(E::ExchangeIoError)?; + } + } + + Ok(()) +} + +mod http_bodies { + use crate::{listener::ClientError, utils::headers::HeadersExt}; + + // pub trait HttpMessage<'h, 'b> { + // fn new(headers: &'h mut [httparse::Header<'b>]) -> Self; + // fn parse(&mut self, buf: &'b [u8]) -> httparse::Result; + // } + + // impl<'h, 'b> HttpMessage<'h, 'b> for httparse::Request<'h, 'b> { + // fn new(headers: &'h mut [httparse::Header<'b>]) -> Self { + // httparse::Request::new(headers) + // } + + // fn parse(&mut self, buf: &'b [u8]) -> httparse::Result { + // self.parse(buf) + // } + // } + + // impl<'h, 'b> HttpMessage<'h, 'b> for httparse::Response<'h, 'b> { + // fn new(headers: &'h mut [httparse::Header<'b>]) -> Self { + // httparse::Response::new(headers) + // } + + // fn parse(&mut self, buf: &'b [u8]) -> httparse::Result { + // self.parse(buf) + // } + // } + + pub struct ChunckedTransferEncoding<'a, R: Read> { + rem: Vec, + reader: &'a mut R, + } + // This was a read impl, then tried to go into an interator and ended up being... this + impl ChunckedTransferEncoding<'_, R> { + fn read(&mut self, mut cb: impl FnMut(&[u8])) -> io::Result<()> { + loop { + let Some(busize_after_pos) = + self.rem.windows(b"\r\n".len()).position(|w| w == b"\r\n") + else { + let mut tmp = [0u8; 4096]; + let n = self.reader.read(&mut tmp)?; + self.rem.extend_from_slice(&tmp[..n]); + continue; + }; + + let chunk_len = self.rem[0..busize_after_pos] + .iter() + .try_fold(0usize, |mut acc, i| { + acc <<= 4; + + #[allow(clippy::manual_is_ascii_check)] + { + acc += (if (b'0'..=b'9').contains(i) { + i - b'0' + } else if (b'A'..=b'F').contains(i) { + i - b'A' + } else if (b'a'..=b'f').contains(i) { + i - b'f' + } else { + return None; + }) as usize; + } + Some(acc) + }) + .ok_or(io::Error::new( + io::ErrorKind::InvalidData, + "invalid chunk size head contents", + ))?; + + if chunk_len == 0 { + return Ok(()); + } + + while self.rem.len() < busize_after_pos + chunk_len + (b"\r\n".len() * 2) { + let mut tmp = [0u8; 4096]; + let n = self.reader.read(&mut tmp)?; + self.rem.extend_from_slice(&tmp[..n]); + } + cb(&self.rem[(busize_after_pos + b"\r\n".len()) + ..(busize_after_pos + b"\r\n".len() + chunk_len)]); + self.rem = self.rem + [(busize_after_pos + b"\r\n".len() + chunk_len + b"\r\n".len())..] + .to_vec(); + } + } + } + + pub fn capture_body( + headers: &[httparse::Header<'_>], + body_over: &[u8], + reader: &mut impl Read, + debug_bufname: &'static str, + ) -> Result>, ClientError> { + Ok(if let Some(tenc) = headers.header("transfer-encoding") { + if tenc != "chunked" { + return Err(ClientError::InvalidHeader("transfer-encoding")); + } + + let mut buf = Vec::new(); + (ChunckedTransferEncoding { + rem: body_over.to_vec(), + reader, + }) + .read(|chunk| { + buf.extend_from_slice(chunk); + }) + .map_err(|err| ClientError::BufferError(debug_bufname, err))?; + Some(buf) + } else if let Some(clen) = headers.header("content-length") { + let n: usize = clen + .parse() + .map_err(|_| ClientError::InvalidHeader("content-length"))?; + let mut buf = Vec::with_capacity(n); + buf.extend_from_slice(body_over); + reader + .read_exact(&mut buf[body_over.len()..]) + .map_err(|err| ClientError::BufferError(debug_bufname, err))?; + Some(buf) + } else { + None + }) + } + + // I hate it too + macro_rules! parse_http { + ($trash_state:tt, $stream:expr, $msg_type:ty, $ret:stmt) => { + $crate::listener::http_bodies::parse_http!($trash_state, $stream, $msg_type, 16, {1024 * 8}, $ret); + }; + ($trash_state:tt, $stream:expr, $msg_type:ty, $headers_len:expr, $buf_size:expr, $ret:stmt) => { + paste::paste! { + let mut [<$trash_state _buf>] = [0u8; $buf_size]; + let mut [<$trash_state _pos>] = 0; + let mut [<$trash_state _headers>]; + + $ret = loop { + [<$trash_state _headers>] = [httparse::EMPTY_HEADER; $headers_len]; + let mut res = <$msg_type>::new(&mut [<$trash_state _headers>]); + + let n = $stream + .read(&mut [<$trash_state _buf>][[<$trash_state _pos>]..]) + .map_err(ClientError::HeadReadError)?; + [<$trash_state _pos>] += n; + + if let Ok(httparse::Status::Complete(n)) = res.parse(&[<$trash_state _buf>][0..[<$trash_state _pos>]]) { + break (res, &[<$trash_state _buf>][0..(n - $crate::constants::HTTP_HEADERS_NEWLINE.len())], &[<$trash_state _buf>][n..[<$trash_state _pos>]]); + } + }; + } + }; + } + use std::io::{self, Read}; + + pub(crate) use parse_http; +} diff --git a/src/main.rs b/src/main.rs index 6bd5849..c4ac8e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ //! # Tuxcord Reverse Proxy Header Authenthication +#![allow(incomplete_features)] #![feature( anonymous_lifetime_in_impl_trait, iterator_try_collect, + inherent_associated_types, + stmt_expr_attributes, + read_buf, rwlock_downgrade, try_blocks )] @@ -9,24 +13,20 @@ use std::{ fs::File, - io::{self, Read, Write}, - net::{self, Shutdown, TcpListener, TcpStream}, + io::{Read, Write}, + net::TcpListener, 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 listener; pub mod utils; use args::Args; @@ -66,210 +66,5 @@ fn main() -> anyhow::Result<()> { #[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(()) + listener::start_listener_loop(&config_arc, &listener); } diff --git a/src/utils.rs b/src/utils.rs index ce0a1ed..81ca3ff 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,10 @@ pub mod headers { - use std::borrow::Cow; + use std::borrow::{Borrow, Cow}; pub trait HeadersExt<'a> { + fn expect_body(&self) -> bool { + self.has_any(["conten-length", "transfer-encoding"]) + } fn has_any(&self, headers: impl IntoIterator) -> bool { headers.into_iter().any(|h| self.has(h)) } @@ -9,16 +12,19 @@ pub mod headers { headers.into_iter().all(|h| self.has(h)) } fn has(&self, header: &str) -> bool; - fn get(&self, header: &str) -> Option>; + fn header(&self, header: &str) -> Option>; } - impl<'a> HeadersExt<'a> for &mut [httparse::Header<'a>] { + impl<'a, T: Borrow<[httparse::Header<'a>]>> HeadersExt<'a> for T { fn has(&self, header: &str) -> bool { - self.iter().any(|h| h.name.eq_ignore_ascii_case(header)) + self.borrow() + .iter() + .any(|h| h.name.eq_ignore_ascii_case(header)) } - fn get(&self, header: &str) -> Option> { - self.iter() + fn header(&self, header: &str) -> Option> { + self.borrow() + .iter() .find(|h| h.name.eq_ignore_ascii_case(header)) .map(|h| String::from_utf8_lossy(h.value)) }