chore: great error handling

This commit is contained in:
javalsai 2025-07-02 04:08:41 +02:00
parent db5247c713
commit 716707f1bc
Signed by: javalsai
SSH Key Fingerprint: SHA256:3G83yKhBUWVABVX/vPWH88xnK4+ptMtHkZGCRXD4Mk8
5 changed files with 106 additions and 60 deletions

28
Cargo.lock generated
View File

@ -52,6 +52,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@ -327,6 +333,26 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.23" version = "0.8.23"
@ -373,12 +399,14 @@ name = "trpha"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anyhow",
"clap", "clap",
"ctrlc", "ctrlc",
"httparse", "httparse",
"parking_lot", "parking_lot",
"serde", "serde",
"shlex", "shlex",
"thiserror",
"toml", "toml",
] ]

View File

@ -5,12 +5,14 @@ edition = "2024"
[dependencies] [dependencies]
anstyle = "1" anstyle = "1"
anyhow = "1"
clap = { version = "4", features = ["derive", "string"] } clap = { version = "4", features = ["derive", "string"] }
ctrlc = "3" ctrlc = "3"
httparse = "1" httparse = "1"
parking_lot = { version = "0", features = ["arc_lock", "serde"] } parking_lot = { version = "0", features = ["arc_lock", "serde"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
shlex = "1" shlex = "1"
thiserror = "2"
toml = "0" toml = "0"
[features] [features]

1
src/constants.rs Normal file
View File

@ -0,0 +1 @@
pub const DEFAULT_CONFIG: &[u8] = include_bytes!("../config.default.toml");

View File

@ -8,15 +8,17 @@ use std::{
thread::{self, JoinHandle}, thread::{self, JoinHandle},
}; };
use anyhow::Context;
use parking_lot::RwLockWriteGuard; use parking_lot::RwLockWriteGuard;
use crate::config; use crate::config;
pub fn start_client(config: config::Schema) { // TODO: make more anyhow
let mut stream = UnixStream::connect(config.ipc.expect("no ipc socket specified")) pub fn start_client(config: config::Schema) -> anyhow::Result<()> {
.expect("failed to connect to unix socket"); let mut stream = UnixStream::connect(config.ipc.context("no ipc socket specified")?)
.context("failed to connect to unix socket")?;
let stream2 = stream.try_clone().expect("failed to clone stream"); let stream2 = stream.try_clone().context("failed to clone stream")?;
let mut reader = BufReader::new(stream2); let mut reader = BufReader::new(stream2);
let stdin = io::stdin(); let stdin = io::stdin();
@ -32,7 +34,7 @@ pub fn start_client(config: config::Schema) {
for line in (&mut reader).lines() { for line in (&mut reader).lines() {
let Ok(line) = line else { let Ok(line) = line else {
return; return Ok(());
}; };
if line == "EOF" { if line == "EOF" {
break; break;
@ -43,9 +45,11 @@ pub fn start_client(config: config::Schema) {
print!("> "); print!("> ");
_ = stdout().flush(); _ = stdout().flush();
} }
Ok(())
} }
pub fn handle_daemon_client(mut stream: UnixStream, config: Arc<config::Schema>) { pub fn handle_daemon_client(mut stream: UnixStream, config: &Arc<config::Schema>) {
let Ok(stream2) = stream.try_clone() else { let Ok(stream2) = stream.try_clone() else {
return; return;
}; };
@ -68,13 +72,11 @@ pub fn handle_daemon_client(mut stream: UnixStream, config: Arc<config::Schema>)
_ = writeln!( _ = writeln!(
stream, stream,
"weak {}, strong {}", "weak {}, strong {}",
Arc::weak_count(&config), Arc::weak_count(config),
Arc::strong_count(&config) Arc::strong_count(config)
); );
} }
"confdump" => { "confdump" => _ = writeln!(stream, "{:#?}", config.as_ref()),
_ = writeln!(stream, "{:#?}", config.as_ref());
}
"hosts" => { "hosts" => {
if let [arg0, args @ ..] = args { if let [arg0, args @ ..] = args {
match arg0.as_str() { match arg0.as_str() {
@ -107,13 +109,13 @@ pub fn handle_daemon_client(mut stream: UnixStream, config: Arc<config::Schema>)
"err: parsing value as socket addres {err:?}" "err: parsing value as socket addres {err:?}"
); );
} }
}; }
} else { } else {
_ = writeln!( _ = writeln!(
stream, stream,
"err: value wasn't 3 comma separated values" "err: value wasn't 3 comma separated values"
); );
}; }
} else { } else {
_ = writeln!( _ = writeln!(
stream, stream,
@ -136,43 +138,40 @@ pub fn handle_daemon_client(mut stream: UnixStream, config: Arc<config::Schema>)
} }
} else { } else {
_ = writeln!(stream, "err: not enough arguments"); _ = writeln!(stream, "err: not enough arguments");
}; }
} }
_ => { _ => {
_ = writeln!(stream, "command {arg0:?} doesn't exist, type \"help\""); _ = writeln!(stream, "command {arg0:?} doesn't exist, type \"help\"");
} }
}; }
}; }
_ = stream.write_all(b"EOF\n"); // BEL char _ = stream.write_all(b"EOF\n"); // BEL char
} }
} }
pub fn handle_daemon(config: Arc<config::Schema>) -> Option<JoinHandle<()>> { pub fn handle_daemon(config: Arc<config::Schema>) -> Option<anyhow::Result<JoinHandle<()>>> {
if let Some(ipc_path) = config.ipc.as_ref() { config.ipc.clone().map(|ipc_path| {
println!("starting ipc daemon at {ipc_path:#?}"); println!("starting ipc daemon at {ipc_path:#?}");
let listener = UnixListener::bind(ipc_path.clone()).expect("failed to bind to ipc socket"); let listener = UnixListener::bind(&ipc_path).context("failed to bind to ipc socket")?;
let ipc_path_clone = ipc_path.clone();
ctrlc::try_set_handler(move || { ctrlc::try_set_handler(move || {
_ = fs::remove_file(&ipc_path_clone); _ = fs::remove_file(&ipc_path);
process::exit(2); process::exit(2);
}) })
.expect("failed to set exit handler"); .context("failed to set exit handler")?;
println!("ipc daemon started"); println!("ipc daemon started");
Some(thread::spawn(move || { Ok(thread::spawn(move || {
for stream in listener.incoming() { for stream in listener.incoming() {
match stream { match stream {
Ok(stream) => { Ok(stream) => {
handle_daemon_client(stream, config.clone()); handle_daemon_client(stream, &config);
} }
Err(err) => eprintln!("ipc daemon error: {err:#?}"), Err(err) => eprintln!("ipc daemon error: {err:#?}"),
} }
} }
})) }))
} else { })
None
}
} }

View File

@ -1,10 +1,11 @@
//! # Tuxcord Reverse Proxy Header Authenthication //! # Tuxcord Reverse Proxy Header Authenthication
#![feature( #![feature(
rwlock_downgrade, anonymous_lifetime_in_impl_trait,
try_blocks,
iterator_try_collect, iterator_try_collect,
anonymous_lifetime_in_impl_trait rwlock_downgrade,
try_blocks
)] )]
#![allow(clippy::missing_errors_doc)]
use std::{ use std::{
fs::File, fs::File,
@ -14,20 +15,22 @@ use std::{
thread, thread,
}; };
use anyhow::Context;
use clap::Parser; use clap::Parser;
use thiserror::Error;
use crate::utils::headers::HeadersExt; use crate::utils::headers::HeadersExt;
pub mod args; pub mod args;
pub mod config; pub mod config;
pub mod constants;
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
pub mod ipc; pub mod ipc;
pub mod utils; pub mod utils;
use args::Args; use args::Args;
const DEFAULT_CONFIG: &[u8] = include_bytes!("../config.default.toml"); fn main() -> anyhow::Result<()> {
fn main() {
let args = Args::parse(); let args = Args::parse();
if !args.config.exists() { if !args.config.exists() {
@ -36,27 +39,27 @@ fn main() {
&args.config &args.config
); );
File::create(args.config) File::create(args.config)
.expect("failure creating the config file") .context("failure creating the config file")?
.write_all(DEFAULT_CONFIG) .write_all(constants::DEFAULT_CONFIG)
.expect("failure writting the contents to the config file"); .context("failure writting the contents to the config file")?;
return; return Ok(());
} }
let mut config_file = File::open(&args.config).expect("failure opening the config file"); let mut config_file = File::open(&args.config).context("failure opening the config file")?;
let mut config = String::new(); let mut config = String::new();
config_file config_file
.read_to_string(&mut config) .read_to_string(&mut config)
.expect("failure reading the config file"); .context("failure reading the config file")?;
let config: config::Schema = toml::from_str(&config).expect("invalid config file"); let config: config::Schema = toml::from_str(&config).context("invalid config file")?;
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
if let Some(args::Commands::Ipc) = args.command { if let Some(args::Commands::Ipc) = args.command {
ipc::start_client(config); ipc::start_client(config)?;
return; return Ok(());
} }
println!("config: {config:#?}"); println!("config: {config:#?}");
let listener = TcpListener::bind(config.listen_at).expect("failure tcp listening"); let listener = TcpListener::bind(config.listen_at).context("failure tcp listening")?;
let config_arc = Arc::new(config); // will also serve as a counter let config_arc = Arc::new(config); // will also serve as a counter
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
@ -68,7 +71,7 @@ fn main() {
let config_arc = config_arc.clone(); let config_arc = config_arc.clone();
thread::spawn(|| { thread::spawn(|| {
if let Err(err) = handle_client(&mut client, config_arc.as_ref()) { if let Err(err) = handle_client(&mut client, config_arc.as_ref()) {
eprintln!("err: invalid req head ({err:?}), closing..."); eprintln!("err: invalid req head ({err}), closing...");
_ = client.shutdown(Shutdown::Both); _ = client.shutdown(Shutdown::Both);
} }
drop(client); drop(client);
@ -82,7 +85,23 @@ fn main() {
unreachable!("listener had to be killed unexpectedly"); unreachable!("listener had to be killed unexpectedly");
} }
fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(), &'static str> { #[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),
}
fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(), ClientError> {
use ClientError as E;
let mut header_buf = [0u8; 1024 * 8]; let mut header_buf = [0u8; 1024 * 8];
let mut read_pos = 0usize; let mut read_pos = 0usize;
@ -91,9 +110,9 @@ fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(),
headers = [httparse::EMPTY_HEADER; 16]; headers = [httparse::EMPTY_HEADER; 16];
let mut req = httparse::Request::new(&mut headers); let mut req = httparse::Request::new(&mut headers);
let Ok(n) = client.read(&mut header_buf[read_pos..]) else { let n = client
return Err("error reading stream"); .read(&mut header_buf[read_pos..])
}; .map_err(E::HeadReadError)?;
read_pos += n; read_pos += n;
if let Ok(httparse::Status::Complete(n)) = req.parse(&header_buf[0..read_pos]) { if let Ok(httparse::Status::Complete(n)) = req.parse(&header_buf[0..read_pos]) {
@ -103,31 +122,28 @@ fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(),
let theres_body = req.headers.has_any(["conten-length", "transfer-encoding"]); let theres_body = req.headers.has_any(["conten-length", "transfer-encoding"]);
let Some(host_header) = req.headers.get("host") else { let host_header = req.headers.get("host").ok_or(E::HostHeaderNotFound)?;
return Err("failed to find \"host\" header");
};
// Now find that header and pas everything // Now find that header and pas everything
let read_hosts = config.hosts.read(); let read_hosts = config.hosts.read();
let Some((addr, _, _)) = read_hosts.get(host_header.as_ref()) else { let (addr, _, _) = read_hosts
return Err("host not in hashmap"); .get(host_header.as_ref())
}; .ok_or_else(|| E::HostNotRegistered(host_header.to_string()))?;
let Ok(mut stream) = TcpStream::connect(addr) else { let mut stream = TcpStream::connect(addr).map_err(E::BackendConnectFail)?;
return Err("failed to connect to the hashmap address");
};
drop(read_hosts); drop(read_hosts);
let Ok(_): io::Result<()> = (try { let r: io::Result<()> = try {
stream.write_all(&header_buf[0..pos])?; stream.write_all(&header_buf[0..pos])?;
// here we sent all original headers, append our own (TODO) // here we sent all original headers, append our own (TODO)
stream.write_all(&header_buf[pos..read_pos])?; // send our overhead
// send our overhead
stream.write_all(&header_buf[pos..read_pos])?;
if theres_body { if theres_body {
io::copy(client, &mut stream)?; io::copy(client, &mut stream)?;
} }
io::copy(&mut stream, client)?; io::copy(&mut stream, client)?;
}) else {
return Err("io error exchanging head and/or body");
}; };
r.map_err(E::ExchangeIoError)?;
Ok(()) Ok(())
} }