summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock129
-rw-r--r--Cargo.toml7
-rw-r--r--README.txt13
-rw-r--r--rustfmt.toml1
-rw-r--r--src/argparse.rs82
-rw-r--r--src/main.rs222
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(&param)?.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
+}