From d5e65e61269c4ab033c4c4b84163fa3fca93d15f Mon Sep 17 00:00:00 2001 From: SIMULATAN Date: Wed, 4 Jan 2023 20:00:05 +0100 Subject: [PATCH] v3.0.0 - switch to the hyper framework - add customizable listen address and port - add description to Cargo.toml This change allows a throughput of up to 700 Thousand Requests per Second! --- Cargo.lock | 386 +++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 8 +- README.md | 31 +++- src/main.rs | 98 +++++++------ src/signals.rs | 14 -- 5 files changed, 462 insertions(+), 75 deletions(-) delete mode 100644 src/signals.rs diff --git a/Cargo.lock b/Cargo.lock index 1d7912b..bf609ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,34 +2,398 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "error_server" -version = "2.0.0" +version = "3.0.0" +dependencies = [ + "http-body-util", + "hyper", + "tokio", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ - "signal-hook", + "futures-core", ] +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951dfc2e32ac02d67c90c0d65bd27009a635dc9b381a2cc7d284ab01e3a0150d" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92445bc9cc14bfa0a3ce56817dc3b5bcc227a168781a356b702410789cec0d10" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "1.0.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "289cfdbf735dea222b0ec6a10224b4d9552c7662bb451d4589cbfda3d407d1a3" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", + "tracing", + "want", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + [[package]] name = "libc" -version = "0.2.138" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] [[package]] -name = "signal-hook" -version = "0.3.14" +name = "socket2" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", - "signal-hook-registry", + "winapi", ] [[package]] -name = "signal-hook-registry" -version = "1.4.0" +name = "syn" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a54aca0c15d014013256222ba0ebed095673f89345dd79119d912eb561b7a8" +dependencies = [ + "autocfg", "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", ] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 9f4d1fa..2db0675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,15 @@ [package] name = "error_server" -version = "2.0.0" +description = "🚀 a simple HTTP server that returns error codes with their respective messages and debug information" +version = "3.0.0" edition = "2021" repository = "https://github.com/SIMULATAN/ErrorServer" +license = "MIT" [dependencies] -signal-hook = "0.3.14" +hyper = { version = "1.0.0-rc.2", features = ["http1", "server"], default-features = false } +tokio = { version = "1", features = ["rt-multi-thread", "net", "macros"], default-features = false } +http-body-util = "0.1.0-rc.2" [profile.release] opt-level = 3 diff --git a/README.md b/README.md index cbdbf3f..01f9b3d 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,39 @@ - dark mode per default - colors follow the dracula theme specification - display debug information - - the whole request is printed in the debug section + - the request headers are printed in the debug section - hidden behind a dropdown to prevent accidentally leaking sensitive information -- perfect for traefik (config coming soon!) +- perfect for traefik (examples coming soon!) - written in rust :crab: :rocket: -- low-memory footprint - - ~ 1 MB while idling, benchmarks will follow +- blazingly fast + - over 700k requests per second on my machine (i7-9700k) +- low memory footprint + - ~ 3.5 MB while idling, benchmarks will follow - small binary size - - ~ 400 KB + - ~ 650 KB - low-dependency - - apart from the rust standard library, only one dependencies is used (`signal-hook`) + - only hyper and tokio are used to provide a fast & solid base - easy to use - just run the binary and you're good to go +- customizable + - the HTML file served can be replaced with your own - docker support - built & published for AMD64 and ARM64 +## Usage +### Docker +```bash +# caution: pin the tag for production use! Semver is supported. +docker run -p 7878:7878 ghcr.io/SIMULATAN/errorserver:latest +``` + +### Binary +```bash +# download the latest release from the releases page +# or build it yourself +# caution: the "error.html" file must be in the same directory as the binary +./error_server +``` + ## Contributions ... are welcome! If you have any ideas, feel free to open an issue or a pull request. I'd also appreciate feedback on the code, as I'm still learning rust (this is literally my first project using it :sweat_smile:). diff --git a/src/main.rs b/src/main.rs index e617612..09aba5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,55 +1,69 @@ -use std::{fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}}; +use std::{env, fs}; +use std::future::Future; +use std::pin::Pin; -use crate::http_codes::get_code; +use http_body_util::Full; +use hyper::{Request, Response}; +use hyper::body::{Bytes, Incoming}; +use hyper::server::conn::http1; +use hyper::service::Service; +use tokio::net::TcpListener; mod http_codes; -mod signals; -fn main() { - signals::setup(); - let listener = TcpListener::bind("0.0.0.0:7878").unwrap(); - println!("Listening on :7878"); - let file_contents: String = fs::read_to_string("error.html").unwrap(); +#[tokio::main] +async fn main() -> Result<(), Box> { + let listen_addr = env::var("LISTEN_ADDR").ok().unwrap_or_else(|| String::from("0.0.0.0")); + let port = env::var("PORT") + .ok() + .and_then(|p| p.parse::().ok()) + .unwrap_or_else(|| 7878); - for stream in listener.incoming() { - let stream = stream.unwrap(); + println!("Trying to bind to {listen_addr}:{port}..."); + let listener = TcpListener::bind(format!("{listen_addr}:{port}")).await?; + println!("Listening on {listen_addr}:{port}"); - let _ = std::panic::catch_unwind(|| handle_connection(stream, &file_contents)); - } -} - -// path: /{code}.html -fn handle_connection(mut stream: TcpStream, file_contents: &String) { - let buf_reader = BufReader::new(&mut stream); - - let http_request: Vec<_> = buf_reader - .lines() - .map(|result| result.unwrap()) - .take_while(|line| !line.is_empty()) - .collect(); - - let empty_string = String::from(""); + let errorpage_template: &'static str = Box::leak(fs::read_to_string("error.html").unwrap().into_boxed_str()); - let request_line = http_request.get(0).unwrap_or(&empty_string); - let (method, path) = parse_request_line(request_line); + loop { + let (stream, _) = listener.accept().await?; - let status_code = path.map(|p| p.parse::().unwrap_or(404)).unwrap_or(404); - let status_code_message = get_code(status_code); + tokio::task::spawn(async move { + http1::Builder::new() + .serve_connection(stream, Svc { errorpage_template }) + .await + }); + } +} - let content = file_contents - .replace("{error}", &status_code.to_string()) - .replace("{message}", status_code_message.unwrap_or("Unknown error occurred")) - .replace("{debug}", &*(http_request.join("\r\n") + "\r\n-- Method: " + &*method.unwrap_or_else(|| "GET".to_string()))); - let length = content.len(); +struct Svc { + errorpage_template: &'static str, +} - let response = format!("HTTP/1.1 {status_code} {}\r\nContent-Type: text/html\r\nContent-Length: {length}\r\n\r\n{content}", status_code_message.unwrap_or("NOT FOUND")); +impl Service> for Svc { + type Response = Response>; + type Error = hyper::Error; + type Future = Pin> + Send>>; - stream.write_all(response.as_bytes()).unwrap(); -} + fn call(&mut self, request: Request) -> Self::Future { + let mut status_code = request.uri().path().replace("/", "").parse::().unwrap_or(404); + if status_code < 100 || status_code > 999 { + status_code = 404; + } -fn parse_request_line(request_line: &str) -> (Option, Option) { - let mut parts = request_line.split_whitespace(); - let method = parts.next().map(|x| x.to_string()); - let path = parts.next().map(|x| x.replace("/", "")); - (method, path) + let response = Ok(Response::builder() + .status(status_code) + .body(Full::new(Bytes::from(self.errorpage_template + .replace("{error}", status_code.to_string().as_str()) + .replace("{message}", http_codes::get_code(status_code).unwrap_or("Unknown error occurred")) + .replace("{debug}", request.headers() + .iter() + .map(|(k, v)| format!("{}: {}\r\n", k, v.to_str().unwrap())) + .collect::() + .as_str())) + )) + .unwrap() + ); + Box::pin(async { response }) + } } diff --git a/src/signals.rs b/src/signals.rs deleted file mode 100644 index 3ab0f73..0000000 --- a/src/signals.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::thread; -use signal_hook::consts::{SIGINT, SIGTERM}; -use signal_hook::iterator::Signals; - -pub fn setup() { - let mut signals = Signals::new(&[SIGINT, SIGTERM]).unwrap(); - - thread::spawn(move || { - for sig in signals.forever() { - println!("Received signal {:?}", sig); - std::process::exit(0); - } - }); -}