feat: add way more proxy handling

This commit is contained in:
2025-07-02 05:55:05 +02:00
parent 716707f1bc
commit 2ab9c5d134
4 changed files with 262 additions and 15 deletions

View File

@@ -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

View File

@@ -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<AuthData>, (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<fn(&httparse::Request, &[u8]) -> Result<(), ClientError>>,
auth: Option<Ref<'a, String, AuthData>>,
}
struct AuthData {
pub username: String,
pub email: String,
pub fullname: Option<String>,
}
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<Ref<'a, String, AuthData>>)>,
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<String, AuthData> = DashMap::new();
static ref CSRF_MAP: DashMap<String, AuthData> = 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(())
}