feat: get to a normal reverse proxy
This commit is contained in:
61
src/args/mod.rs
Normal file
61
src/args/mod.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
/// Tuxcord Reverse Proxy Header Authenthication
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
#[command(styles = get_clap_styles())]
|
||||
pub struct Args {
|
||||
/// Path to the config file
|
||||
#[arg(short, default_value = "./config.toml")]
|
||||
pub config: PathBuf,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Connects to the selected IPC
|
||||
#[cfg(feature = "ipc")]
|
||||
Ipc,
|
||||
}
|
||||
|
||||
fn get_clap_styles() -> clap::builder::Styles {
|
||||
clap::builder::Styles::styled()
|
||||
.usage(
|
||||
anstyle::Style::new()
|
||||
.bold()
|
||||
.underline()
|
||||
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))),
|
||||
)
|
||||
.header(
|
||||
anstyle::Style::new()
|
||||
.bold()
|
||||
.underline()
|
||||
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))),
|
||||
)
|
||||
.literal(
|
||||
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))),
|
||||
)
|
||||
.invalid(
|
||||
anstyle::Style::new()
|
||||
.bold()
|
||||
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
|
||||
)
|
||||
.error(
|
||||
anstyle::Style::new()
|
||||
.bold()
|
||||
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
|
||||
)
|
||||
.valid(
|
||||
anstyle::Style::new()
|
||||
.bold()
|
||||
.underline()
|
||||
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))),
|
||||
)
|
||||
.placeholder(
|
||||
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))),
|
||||
)
|
||||
}
|
11
src/config/mod.rs
Normal file
11
src/config/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::RwLock};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Schema {
|
||||
pub listen_at: SocketAddr,
|
||||
#[cfg(feature = "ipc")]
|
||||
pub ipc: Option<PathBuf>,
|
||||
pub hosts: RwLock<HashMap<String, SocketAddr>>,
|
||||
}
|
186
src/ipc/mod.rs
Normal file
186
src/ipc/mod.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use core::net;
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, BufRead, BufReader, Write, stdout},
|
||||
os::unix::net::{UnixListener, UnixStream},
|
||||
process,
|
||||
sync::{Arc, RwLockWriteGuard},
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
|
||||
use crate::config;
|
||||
|
||||
pub fn start_client(config: config::Schema) {
|
||||
let mut stream = UnixStream::connect(config.ipc.expect("no ipc socket specified"))
|
||||
.expect("failed to connect to unix socket");
|
||||
|
||||
let stream2 = stream.try_clone().expect("failed to clone stream");
|
||||
let mut reader = BufReader::new(stream2);
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
||||
print!("> ");
|
||||
_ = stdout().flush();
|
||||
for line in stdin.lines() {
|
||||
let Ok(line) = line else {
|
||||
break;
|
||||
};
|
||||
_ = stream.write_all(line.as_bytes());
|
||||
_ = stream.write_all(b"\n");
|
||||
|
||||
for line in (&mut reader).lines() {
|
||||
let Ok(line) = line else {
|
||||
return;
|
||||
};
|
||||
if line == "EOF" {
|
||||
break;
|
||||
}
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
print!("> ");
|
||||
_ = stdout().flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_daemon_client(mut stream: UnixStream, config: Arc<config::Schema>) {
|
||||
let Ok(stream2) = stream.try_clone() else {
|
||||
return;
|
||||
};
|
||||
for line in BufReader::new(stream2).lines() {
|
||||
let Ok(line) = line else {
|
||||
return;
|
||||
};
|
||||
let args = shlex::Shlex::new(line.trim()).collect::<Vec<_>>();
|
||||
|
||||
// i hate it too
|
||||
if let [arg0, args @ ..] = &args[..] {
|
||||
match arg0.as_str() {
|
||||
"help" => {
|
||||
_ = writeln!(
|
||||
stream,
|
||||
"avaliable commands:\n help\n configcount\n confdump\n hosts <list|create|delete> ..."
|
||||
);
|
||||
}
|
||||
"configcount" => {
|
||||
_ = writeln!(
|
||||
stream,
|
||||
"weak {}, strong {}",
|
||||
Arc::weak_count(&config),
|
||||
Arc::strong_count(&config)
|
||||
);
|
||||
}
|
||||
"confdump" => {
|
||||
_ = writeln!(stream, "{:#?}", config.as_ref());
|
||||
}
|
||||
"hosts" => {
|
||||
if let [arg0, args @ ..] = args {
|
||||
match arg0.as_str() {
|
||||
"list" => {
|
||||
_ = writeln!(stream, "aquiring read lock");
|
||||
let rlock = config.hosts.read();
|
||||
match rlock {
|
||||
Ok(lock) => {
|
||||
_ = writeln!(stream, "{lock:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
_ = writeln!(stream, "err with read lock: {err:?}");
|
||||
}
|
||||
};
|
||||
}
|
||||
"create" => {
|
||||
if let [name, value] = args {
|
||||
match value.parse::<net::SocketAddr>() {
|
||||
Ok(addr) => {
|
||||
_ = writeln!(stream, "aquiring write lock");
|
||||
let wlock = config.hosts.write();
|
||||
match wlock {
|
||||
Ok(mut lock) => {
|
||||
lock.insert(name.to_string(), addr);
|
||||
let lock = RwLockWriteGuard::downgrade(lock);
|
||||
_ = writeln!(stream, "{lock:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
_ = writeln!(
|
||||
stream,
|
||||
"err with write lock: {err:?}"
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(err) => {
|
||||
_ = writeln!(
|
||||
stream,
|
||||
"err: parsing value as socket addres {err:?}"
|
||||
);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
_ = writeln!(
|
||||
stream,
|
||||
"invalid arg count, expected name and value"
|
||||
);
|
||||
}
|
||||
}
|
||||
"delete" => {
|
||||
if let [name] = args {
|
||||
_ = writeln!(stream, "aquiring write lock");
|
||||
let wlock = config.hosts.write();
|
||||
match wlock {
|
||||
Ok(mut lock) => {
|
||||
lock.remove(name);
|
||||
let lock = RwLockWriteGuard::downgrade(lock);
|
||||
_ = writeln!(stream, "{lock:#?}");
|
||||
}
|
||||
Err(err) => {
|
||||
_ = writeln!(stream, "err with write lock: {err:?}");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
_ = writeln!(stream, "invalid arg count, expected a name");
|
||||
}
|
||||
}
|
||||
_ => _ = writeln!(stream, "err: invalid argument {arg0:?}"),
|
||||
}
|
||||
} else {
|
||||
_ = writeln!(stream, "err: not enough arguments");
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
_ = writeln!(stream, "command {arg0:?} doesn't exist, type \"help\"");
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
_ = stream.write_all(b"EOF\n"); // BEL char
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_daemon(config: Arc<config::Schema>) -> Option<JoinHandle<()>> {
|
||||
if let Some(ipc_path) = config.ipc.as_ref() {
|
||||
println!("starting ipc daemon at {ipc_path:#?}");
|
||||
|
||||
let listener = UnixListener::bind(ipc_path.clone()).expect("failed to bind to ipc socket");
|
||||
let ipc_path_clone = ipc_path.clone();
|
||||
ctrlc::try_set_handler(move || {
|
||||
_ = fs::remove_file(&ipc_path_clone);
|
||||
process::exit(2);
|
||||
})
|
||||
.expect("failed to set exit handler");
|
||||
|
||||
println!("ipc daemon started");
|
||||
|
||||
Some(thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => {
|
||||
handle_daemon_client(stream, config.clone());
|
||||
}
|
||||
Err(err) => eprintln!("ipc daemon error: {err:#?}"),
|
||||
}
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
146
src/main.rs
146
src/main.rs
@@ -1,5 +1,149 @@
|
||||
//! # Tuxcord Reverse Proxy Header Authenthication
|
||||
#![feature(rwlock_downgrade, try_blocks)]
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Read, Write},
|
||||
net::{Shutdown, TcpListener, TcpStream},
|
||||
sync::Arc,
|
||||
thread,
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
#[cfg(feature = "ipc")]
|
||||
pub mod ipc;
|
||||
|
||||
use args::Args;
|
||||
|
||||
const DEFAULT_CONFIG: &[u8] = include_bytes!("../config.default.toml");
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
let args = Args::parse();
|
||||
|
||||
if !args.config.exists() {
|
||||
println!(
|
||||
"{:?} doesn't exist, creating a default config",
|
||||
&args.config
|
||||
);
|
||||
File::create(args.config)
|
||||
.expect("failure creating the config file")
|
||||
.write_all(DEFAULT_CONFIG)
|
||||
.expect("failure writting the contents to the config file");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut config_file = File::open(&args.config).expect("failure opening the config file");
|
||||
let mut config = String::new();
|
||||
config_file
|
||||
.read_to_string(&mut config)
|
||||
.expect("failure reading the config file");
|
||||
let config: config::Schema = toml::from_str(&config).expect("invalid config file");
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
if let Some(args::Commands::Ipc) = args.command {
|
||||
ipc::start_client(config);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("config: {config:#?}");
|
||||
let listener = TcpListener::bind(config.listen_at).expect("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");
|
||||
}
|
||||
|
||||
fn handle_client(client: &mut TcpStream, config: &config::Schema) -> Result<(), &'static str> {
|
||||
let mut header_buf = [0u8; 1024 * 8];
|
||||
let mut read_pos = 0usize;
|
||||
|
||||
let pos = loop {
|
||||
let Ok(n) = client.read(&mut header_buf[read_pos..]) else {
|
||||
return Err("error reading stream");
|
||||
};
|
||||
read_pos += n;
|
||||
|
||||
let pos = header_buf
|
||||
.windows(b"\r\n\r\n".len())
|
||||
.position(|w| w == b"\r\n\r\n");
|
||||
if let Some(pos) = pos {
|
||||
break pos;
|
||||
};
|
||||
};
|
||||
|
||||
let mut headers = [httparse::EMPTY_HEADER; 16];
|
||||
let mut req = httparse::Request::new(&mut headers);
|
||||
let Ok(httparse::Status::Complete(_)) =
|
||||
httparse::Request::parse(&mut req, &header_buf[0..(pos + b"\r\n\r\n".len())])
|
||||
else {
|
||||
return Err("parsing request was not complete");
|
||||
};
|
||||
|
||||
let theres_body = req
|
||||
.headers
|
||||
.iter()
|
||||
.any(|header| header.name.to_lowercase() == "content-length")
|
||||
|| req
|
||||
.headers
|
||||
.iter()
|
||||
.any(|header| header.name.to_lowercase() == "transfer-encoding");
|
||||
|
||||
let Some(header) = req
|
||||
.headers
|
||||
.iter()
|
||||
.find(|header| header.name.to_lowercase() == "host")
|
||||
else {
|
||||
return Err("failed to find \"host\" header");
|
||||
};
|
||||
|
||||
let Ok(host_header) = String::from_utf8(header.value.to_vec()) else {
|
||||
return Err("\"host\" header is not valid UTF-8");
|
||||
};
|
||||
|
||||
// Now find that header and pas everything
|
||||
let Ok(read_hosts) = config.hosts.read() else {
|
||||
return Err("poisoned RwLock");
|
||||
};
|
||||
let Some(addr) = read_hosts.get(&host_header) else {
|
||||
return Err("host not in hashmap");
|
||||
};
|
||||
|
||||
let Ok(mut stream) = TcpStream::connect(addr) else {
|
||||
return Err("failed to connect to the hashmap address");
|
||||
};
|
||||
drop(read_hosts);
|
||||
let Ok(_): io::Result<()> = (try {
|
||||
stream.write_all(&header_buf[0..pos])?;
|
||||
// here we sent all original headers, append our own (TODO)
|
||||
stream.write_all(&header_buf[pos..read_pos])?; // send our overhead
|
||||
if theres_body {
|
||||
io::copy(client, &mut stream)?;
|
||||
}
|
||||
io::copy(&mut stream, client)?;
|
||||
}) else {
|
||||
return Err("io error exchanging head and/or body");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Reference in New Issue
Block a user