diff options
author | pommicket <pommicket@gmail.com> | 2025-08-29 04:25:57 -0400 |
---|---|---|
committer | pommicket <pommicket@gmail.com> | 2025-08-29 04:25:57 -0400 |
commit | af16284921b65f92601279001531862d1c80cee7 (patch) | |
tree | ffabe5598ab96839de4ff44c53fa2ef44f4db062 |
Initial commit (v. 0.1.0)v0.1.0
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 129 | ||||
-rw-r--r-- | Cargo.toml | 7 | ||||
-rw-r--r-- | README.txt | 13 | ||||
-rw-r--r-- | rustfmt.toml | 1 | ||||
-rw-r--r-- | src/argparse.rs | 82 | ||||
-rw-r--r-- | src/main.rs | 222 |
7 files changed, 455 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cc31115 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,129 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ctrlc" +version = "3.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" +dependencies = [ + "nix", + "windows-sys", +] + +[[package]] +name = "ipaddrsrv" +version = "0.1.0" +dependencies = [ + "ctrlc", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d6aef46 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ipaddrsrv" +version = "0.1.0" +edition = "2024" + +[dependencies] +ctrlc = { version = "3.4.7", features = ["termination"] } diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..949d9b3 --- /dev/null +++ b/README.txt @@ -0,0 +1,13 @@ +Very simple server for obtaining IP address. + +This program runs a TCP server which responds to every HTTP request +with the IP address of the sender. It doesn't even check for valid HTTP +and ignores all headers. + +OPTIONS + --help - Display this help text and exit + --addr <ADDR> - Set address to bind server on (default: 0.0.0.0) + --port <PORT> - Set port to host server on (default: 80) + --max-connections <NUMBER> - Set maximum number of simultaneous connections (default: 32) + --timeout <NUMBER> - Set timeout for requests, in seconds (default: 15) + --version - Display version number and exit diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/src/argparse.rs b/src/argparse.rs new file mode 100644 index 0000000..8fd6ae5 --- /dev/null +++ b/src/argparse.rs @@ -0,0 +1,82 @@ +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use std::hash::Hash; + +#[derive(Debug)] +pub struct Args<Flag, Param> { + flags: HashSet<Flag>, + params: HashMap<Param, String>, + lone_args: Vec<String>, +} + +impl<Flag: Hash + Eq, Param: Hash + Eq> Args<Flag, Param> { + pub fn is_set(&self, flag: Flag) -> bool { + self.flags.contains(&flag) + } + pub fn get(&self, param: Param) -> Option<&str> { + Some(self.params.get(¶m)?.as_ref()) + } + pub fn lone_args(&self) -> impl '_ + Iterator<Item = &str> { + self.lone_args.iter().map(|x| x.as_ref()) + } + pub fn lone_args_count(&self) -> usize { + self.lone_args.len() + } +} + +pub fn parse_args<Flag: Copy + Hash + Eq + Debug, Param: Copy + Hash + Eq + Debug>( + flag_names: &HashMap<&str, Flag>, + param_names: &HashMap<&str, Param>, +) -> Result<Args<Flag, Param>, String> { + let mut arg_iter = std::env::args_os(); + arg_iter.next(); // program name + let mut args = Args { + flags: HashSet::new(), + params: HashMap::new(), + lone_args: vec![], + }; + let mut double_dash = false; + let mut param: Option<(String, Param)> = None; + assert!(flag_names.keys().all(|x| x.starts_with('-'))); + assert!(param_names.keys().all(|x| x.starts_with('-'))); + for arg in arg_iter { + let arg = arg + .into_string() + .map_err(|arg| format!("Argument includes bad UTF-8: {arg:?}"))?; + if let Some((_name, p)) = param.as_ref() { + args.params.insert(*p, arg); + param = None; + continue; + } + if double_dash { + args.lone_args.push(arg); + continue; + } + if arg == "--" { + double_dash = true; + continue; + } + if let Some(flag) = flag_names.get(arg.as_str()) { + args.flags.insert(*flag); + continue; + } + if let Some(p) = param_names.get(arg.as_str()) { + param = Some((arg, *p)); + continue; + } + if let Some((p, value)) = arg.split_once('=') + && let Some(p) = param_names.get(p) + { + args.params.insert(*p, value.into()); + continue; + } + if arg.starts_with('-') { + return Err(format!("Unrecognized option: {arg}")); + } + args.lone_args.push(arg); + } + if let Some((name, _p)) = param { + return Err(format!("No argument provided to {name}")); + } + Ok(args) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ffa1892 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,222 @@ +pub mod argparse; + +use std::collections::HashMap; +use std::io::{self, prelude::*}; +use std::net::{IpAddr, TcpListener, TcpStream}; +use std::process::ExitCode; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; +use std::thread::sleep; +use std::time::Duration; + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum Flag { + Help, + Version, +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum Param { + Port, + Address, + Timeout, + MaxConnections, +} + +fn print_version() { + println!( + "{} v. {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + ); +} + +fn print_doc() { + print_version(); + println!("{}", include_str!("../README.txt")); +} + +#[derive(Debug)] +struct ConnectionSettings { + timeout: u32, + buffer: [u8; 1024], +} + +const SLEEP_MS_PER_ROUND: u32 = 50; + +impl ConnectionSettings { + /// handle this connection as much as possible *without blocking* + #[must_use] + fn handle_connection(&mut self, addr: IpAddr, conn: &mut Connection) -> bool { + let buffer = &mut self.buffer; + conn.age_ms += SLEEP_MS_PER_ROUND; + if conn.age_ms > self.timeout * 1000 { + return false; + } + if conn.state < 4 { + let n = match conn.stream.read(buffer) { + Ok(n) => n, + Err(e) if e.kind() == io::ErrorKind::WouldBlock => 0, + Err(e) => { + eprintln!("WARNING: error reading from connection: {e}"); + return false; + } + }; + for &c in &buffer[..n] { + if c != b'\r' && c != b'\n' { + continue; + } + let expected = if conn.state % 2 == 0 { b'\r' } else { b'\n' }; + if c == expected { + conn.state += 1; + if conn.state == 4 { + break; + } + } + } + } + if conn.state == 4 { + // 90B is more than long enough for our response + write!( + &mut buffer[..90], + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n{addr}\n\0" + ) + .expect("?? writing address to buffer failed??"); + let length = buffer[..90] + .iter() + .position(|&c| c == 0) + .expect("?? no null byte even though we just wrote one??"); + match conn + .stream + .write(&buffer[usize::from(conn.written)..length]) + { + Ok(n) => { + conn.written += n as u8; + if conn.written >= length as u8 { + // hooray! done sending response + return false; + } + } + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + // keep connection around + } + Err(e) => { + eprintln!("WARNING: error writing to connection: {e}"); + return false; + } + } + } + true + } +} + +#[derive(Debug)] +struct Connection { + stream: TcpStream, + /// rough time connection has been around in milliseconds + age_ms: u32, + /// number of characters out of \r\n\r\n (end of HTTP headers) that we have + state: u8, + /// number of characters of address which have been written + written: u8, +} + +fn try_main() -> Result<(), Box<dyn std::error::Error>> { + let args = argparse::parse_args( + &HashMap::from_iter([("--help", Flag::Help), ("--version", Flag::Version)]), + &HashMap::from([ + ("--port", Param::Port), + ("--address", Param::Address), + ("--timeout", Param::Timeout), + ("--max-connections", Param::MaxConnections), + ]), + )?; + if args.is_set(Flag::Help) { + print_doc(); + return Ok(()); + } + if args.is_set(Flag::Version) { + print_version(); + return Ok(()); + } + let addr = args.get(Param::Address).unwrap_or("0.0.0.0"); + let port = args.get(Param::Port).unwrap_or("80"); + let port = port + .parse::<u16>() + .map_err(|_| format!("Invalid port: {port}"))?; + let timeout = args.get(Param::Timeout).unwrap_or("15"); + let timeout = timeout + .parse::<u32>() + .map_err(|_| format!("Invalid timeout: {timeout}"))?; + let max_connections = args.get(Param::MaxConnections).unwrap_or("32"); + let max_connections = max_connections + .parse::<u32>() + .map_err(|_| format!("Invalid max connections: {max_connections}"))?; + + let listener = TcpListener::bind((addr, port)) + .map_err(|e| format!("Couldn't bind on {addr}:{port}: {e}"))?; + + let interrupted = Arc::new(AtomicBool::new(false)); + let handler_interrupted = interrupted.clone(); + if let Err(e) = ctrlc::set_handler(move || { + handler_interrupted.store(true, Ordering::Relaxed); + }) { + eprintln!("Warning: Couldn't set SIGINT/TERM handler: {e}"); + } + listener + .set_nonblocking(true) + .map_err(|e| format!("Couldn't set socket to non-blocking: {e}"))?; + + let was_interrupted = || interrupted.load(Ordering::Relaxed); + let mut connections: HashMap<IpAddr, Connection> = HashMap::new(); + let mut settings = ConnectionSettings { + timeout, + buffer: [0; 1024], + }; + + 'outer: while !was_interrupted() { + sleep(Duration::from_millis(SLEEP_MS_PER_ROUND.into())); // don't busy loop + while (connections.len() as u32) < max_connections { + match listener.accept() { + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + if was_interrupted() { + break 'outer; + } + break; + } + Err(e) => eprintln!("Warning: accept() failed: {e}"), + Ok((stream, source_addr)) => { + if let Err(e) = stream.set_nonblocking(true) { + eprintln!( + "WARNING: dropping connection because set_nonblocking failed: {e}" + ); + continue; + } + // if there was another connection from this address, it gets + // unceremoniously dropped. too bad. + connections.insert( + source_addr.ip(), + Connection { + stream, + age_ms: 0, + written: 0, + state: Default::default(), + }, + ); + } + } + } + connections.retain(|addr, conn| settings.handle_connection(*addr, conn)); + } + Ok(()) +} + +fn main() -> ExitCode { + if let Err(e) = try_main() { + eprintln!("Error: {e}"); + return ExitCode::FAILURE; + } + ExitCode::SUCCESS +} |