From eda7f79fb0d11427b1f8954a3c155232fb67f0b8 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 14 Apr 2025 17:14:22 +0100 Subject: [PATCH] you ever get bored and just write a raw TCP HTTP server --- Cargo.lock | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/http.rs | 247 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 85 +++++++++++++++++- src/main.rs | 33 ++++--- 5 files changed, 605 insertions(+), 14 deletions(-) create mode 100644 src/http.rs diff --git a/Cargo.lock b/Cargo.lock index f36b30c..d914905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,16 +2,125 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "mcstatusface" version = "0.1.0" dependencies = [ + "chrono", "serde", "serde_json", ] @@ -22,6 +131,21 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "proc-macro2" version = "1.0.94" @@ -40,6 +164,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.20" @@ -78,6 +208,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "syn" version = "2.0.100" @@ -94,3 +230,120 @@ name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index 5b293e4..52324b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = "0.4.40" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..bdedce5 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,247 @@ +use std::{collections::HashMap, io::{BufRead, BufReader, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}, thread, time::Duration}; + +use chrono::Local; + +use crate::ThreadPool; + +pub enum StatusCode { + OK, + NotFound, + // InternalServerError, + // ImATeapot, +} + +pub struct Request<'a> { + stream: &'a TcpStream, + path: &'a str, + method: &'a str, + version: &'a str, + headers: HashMap<&'a str, &'a str>, + body: Option, +} + +impl<'a> Request<'a> { + pub fn new(stream: &'a TcpStream, lines: &'a Vec) -> Result> { + let request_line = lines[0].as_str(); + let request_line_split: Vec<&str> = request_line.split(" ").collect(); + let method = request_line_split[0]; + let path = request_line_split[1]; + let version = request_line_split[2]; + + let mut headers: HashMap<&'a str, &'a str> = HashMap::new(); + if lines.len() > 1 { + let mut i: usize = 1; + loop { + if i >= lines.len() { break; } + let line = &lines[i]; + if line.len() == 0 || !line.contains(":") { break; } + let (name, value) = line.split_once(":").unwrap(); + headers.insert(name, value.trim()); + i += 1; + } + } + + let mut body: Option = None; + if lines.len() > headers.len() + 2 && + (method == "POST" || method == "PUT") && + lines[headers.len() + 1] == "\r\n" + { + body = Some(lines[headers.len() + 2..].join("\n")); + } + + Ok(Request { + stream, + path, + method, + version, + headers, + body, + }) + } + + pub fn address(&self) -> Result { + self.stream.peer_addr() + } + pub fn path(&self) -> &'a str { + self.path + } + pub fn method(&self) -> &'a str { + self.method + } + pub fn version(&self) -> &'a str { + self.version + } + pub fn body(&self) -> &Option { + &self.body + } + pub fn headers(&self) -> &HashMap<&'a str, &'a str> { + &self.headers + } +} + +pub struct Response<'a> { + stream: &'a TcpStream, + status: StatusCode, + headers: HashMap<&'a str, String>, + body: Option, +} + +impl<'a> Response<'a> { + pub fn new(stream: &'a TcpStream) -> Response<'a> { + Response { + stream, + status: StatusCode::OK, + headers: HashMap::from([ + ("Server", "mcstatusface".to_string()), + ("Content-Type", "text/plain".to_string()), + ]), + body: None, + } + } + + pub fn status(&mut self, status: StatusCode) { + self.status = status; + } + pub fn headers(&self) -> &HashMap<&'a str, String> { + &self.headers + } + pub fn set_header(&mut self, name: &'a str, value: String) { + self.headers.insert(name, value); + } + pub fn body(&mut self, body: String) { + self.body = Some(body); + } + + pub fn send(&mut self) -> Result { + let mut len: usize = 0; + let code = match self.status { + StatusCode::OK => 200, + StatusCode::NotFound => 404, + // StatusCode::ImATeapot => 418, + // StatusCode::InternalServerError => 500, + }; + let reason = match self.status { + StatusCode::OK => "OK", + StatusCode::NotFound => "Not Found", + // StatusCode::ImATeapot => "I'm a teapot", + // StatusCode::InternalServerError => "Internal Server Error", + }; + len += self.stream.write(format!("HTTP/1.1 {} {}\r\n", code, reason).as_bytes()).unwrap(); + + let mut content_length: usize = 0; + if self.body.is_some() { + content_length = self.body.as_ref().unwrap().len(); + } + self.set_header("Content-Length", content_length.to_string()); + for (name, value) in &self.headers { + len += self.stream.write(format!("{name}: {value}\r\n").as_bytes()).unwrap() + } + + if self.body.is_some() { + len += self.stream.write("\r\n".as_bytes()).unwrap(); + len += self.stream.write(self.body.as_ref().unwrap().as_bytes()).unwrap(); + } + Ok(len) + } +} + +pub struct HttpServer { + address: String, + port: u16, + max_connections: usize, +} + +impl HttpServer { + pub fn new(address: &str) -> HttpServer { + HttpServer { + address: address.to_string(), + port: 8080, + max_connections: 16, + } + + } + + pub fn start(&self) -> Result<()> { + let pool = ThreadPool::new(self.max_connections); + let listener = TcpListener::bind(format!("{}:{}", self.address, self.port)).expect("Failed to bind to port"); + + println!("Now listening on {}:{}", self.address, self.port); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + pool.execute(move || { + HttpServer::handle_client(&stream); + }); + } + Err(e) => { + eprintln!("Failed to handle incoming connection: {e}"); + } + } + } + + Ok(()) + } + + fn handle_client(stream: &TcpStream) { + let buf_reader = BufReader::new(stream); + let http_request: Vec = buf_reader + .lines() + .map(|result| result.unwrap()) + .take_while(|line| !line.is_empty()) + .collect(); + + let request = Request::new(stream, &http_request); + if request.is_err() { + eprintln!("Failed to process request: {}", request.err().unwrap()); + return; + } + let request = request.unwrap(); + + let response = Response::new(stream); + + let start_date = Local::now(); + match HttpServer::handle_request(&request, response) { + Ok(_) => { + let end_date = Local::now(); + println!( + "[{}] {} {} {} - {}ms - {}", + start_date.format("%Y-%m-%d %H:%M:%S"), + request.method(), + request.path(), + request.version(), + (end_date - start_date).num_milliseconds(), + request.address().unwrap().ip(), + ); + } + Err(e) => { + eprintln!("Failed to handle request: {e}") + } + } + } + + fn handle_request(request: &Request, mut response: Response) -> Result { + response.status(StatusCode::OK); + response.set_header("x-powered-by", "GIRL FUEL".to_string()); + + if request.method != "GET" { + response.status(StatusCode::NotFound); + return response.send() + } + + response.status(StatusCode::OK); + response.set_header("Content-Type", "text/html".to_string()); + response.body(r#" + + + + hello world!~ + + +

it works!!

+ + +"#.to_string()); + response.send() + } +} diff --git a/src/lib.rs b/src/lib.rs index e45878d..8a34bdd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,87 @@ +use std::{sync::{mpsc, Arc, Mutex}, thread}; + pub mod leb128; pub mod status; - +pub mod http; pub use status::MinecraftStatus; + +pub struct ThreadPool { + workers: Vec, + sender: Option>, +} + +type Job = Box; + +impl ThreadPool { + // Create a new ThreadPool with `size` available threads. + // + // # Panics + // + // `new` will panic if `size` is zero. + pub fn new(size: usize) -> ThreadPool { + assert!(size > 0); + + let (sender, receiver) = mpsc::channel(); + + let receiver = Arc::new(Mutex::new(receiver)); + + let mut workers = Vec::with_capacity(size); + + for id in 0..size { + workers.push(ThreadWorker::new(id, Arc::clone(&receiver))); + } + + ThreadPool { + workers, + sender: Some(sender) + } + } + + pub fn execute(&self, f: F) + where + F: FnOnce() + Send + 'static + { + let job = Box::new(f); + + self.sender.as_ref().unwrap().send(job).unwrap(); + } +} + +impl Drop for ThreadPool { + fn drop(&mut self) { + drop(self.sender.take()); + + for worker in &mut self.workers.drain(..) { + println!("Shutting down worker {}", worker.id); + + worker.thread.join().unwrap(); + } + } +} + +struct ThreadWorker { + id: usize, + thread: thread::JoinHandle<()>, +} + +impl ThreadWorker { + fn new(id: usize, receiver: Arc>>) -> ThreadWorker { + let thread = thread::spawn(move || loop { + let msg = receiver.lock().unwrap().recv(); + + match msg { + Ok(job) => { + // println!("Job received by worker {id}"); + job(); + } + Err(_) => { + // println!("Worker {id} disconnected. Shutting down..."); + break; + } + } + }); + + ThreadWorker { id, thread } + } +} + diff --git a/src/main.rs b/src/main.rs index ec0cabc..a5c7c1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,28 +2,35 @@ use std::io::{Result}; use std::net::{ToSocketAddrs}; use std::env; -use mcstatusface::MinecraftStatus; +use mcstatusface::http::HttpServer; +use mcstatusface::{MinecraftStatus}; fn main() -> Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { - eprintln!("Usage: {} ", args[0]); + eprintln!("Usage: {} ", args[0]); std::process::exit(1); } - let mut address = String::from(args[1].as_str()); - if !address.contains(":") { - address += ":25565"; + if args[1] != "serve" { + let mut address = String::from(args[1].as_str()); + if !address.contains(":") { + address += ":25565"; + } + let mut addrs_iter = args[1].to_socket_addrs().unwrap(); + let address = addrs_iter.next().unwrap(); + + let status = MinecraftStatus::fetch(address).unwrap(); + + println!("\nVersion: {} ({})", status.version.name, status.version.protocol); + println!("Players: {}/{}", status.players.online, status.players.max); + println!("MOTD:"); + println!("{}", status.parse_description()); + + return Ok(()); } - let mut addrs_iter = args[1].to_socket_addrs().unwrap(); - let address = addrs_iter.next().unwrap(); - let status = MinecraftStatus::fetch(address).unwrap(); - - println!("\nVersion: {} ({})", status.version.name, status.version.protocol); - println!("Players: {}/{}", status.players.online, status.players.max); - println!("MOTD:"); - println!("{}", status.parse_description()); + HttpServer::new("0.0.0.0").start().unwrap(); Ok(()) }