From bac32045724f6e9dedd7383fa627d3eeaa35e3b1 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 14 Apr 2025 23:53:03 +0100 Subject: [PATCH 01/11] improve runtime docs --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 4 ++-- src/main.rs | 12 ++++++++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a04376..7166ce7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "mcstatusface" -version = "1.0.0" +version = "1.0.1" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index 70db94f..a4a24c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ authors = ["ari melody "] repository = "https://git.arimelody.me/ari/mcstatusface" license = "MIT" keywords = ["minecraft", "server", "query", "web"] -version = "1.0.0" +version = "1.0.1" edition = "2024" [dependencies] diff --git a/README.md b/README.md index 6ab3e2f..3708042 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ convenient format! ## Usage -McStatusFace can be run as a web server with `./mcstatusface serve`. This will +McStatusFace can be run as a web server with `mcstatusface serve`. This will provide server information in JSON format to requests on `GET /?s=`. (e.g. `curl -sS "127.0.0.1:8080?s=127.0.0.1:25565" | jq .`) -Alternatively, you can simply run `./mcstatusface `, and the +Alternatively, you can simply run `mcstatusface `, and the tool will provide server details in plain-text format. diff --git a/src/main.rs b/src/main.rs index 645fec8..8548f9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,16 @@ use mcstatusface::{MinecraftStatus}; fn main() -> Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { - eprintln!("Usage: {} [serve] ", args[0]); - std::process::exit(1); + println!( + r#"Crafty McStatusFace, v{} - made with <3 by ari melody + + Host a web API: +$ mcstatusface serve [address[:port]] + + Query a server: +$ mcstatusface "#, +env!("CARGO_PKG_VERSION")); + std::process::exit(0); } if args[1] != "serve" { From 6bd7379df3447cc533f5c6fec69d2b443368cb94 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 17:57:51 +0100 Subject: [PATCH 02/11] browser-friendly frontend :3 --- README.md | 2 + public/style/index.css | 58 +++++++++++++ src/http.rs | 12 ++- src/main.rs | 191 ++++++++++++++++++++++++++++++++--------- views/index.html | 42 +++++++++ 5 files changed, 259 insertions(+), 46 deletions(-) create mode 100644 public/style/index.css create mode 100644 views/index.html diff --git a/README.md b/README.md index 3708042..9745dbb 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ A light application that serves Minecraft server query information in a convenient format! +For more information, see https://minecraft.wiki/w/Query + ## Usage McStatusFace can be run as a web server with `mcstatusface serve`. This will diff --git a/public/style/index.css b/public/style/index.css new file mode 100644 index 0000000..9aac8cf --- /dev/null +++ b/public/style/index.css @@ -0,0 +1,58 @@ +:root { + --accent: #7ca82f; +} + +body { + font-size: 16px; + font-family: 'Inter', sans-serif; +} + +a { + color: var(--accent); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +form { + width: fit-content; + padding: .5em; + border: 1px solid black; + border-radius: 4px; +} + +form input[type="text"] { + width: fit-content; + min-width: 16em; +} + +form button { + margin-top: .5em; + padding: .2em .3em; + font-family: inherit; + font-size: inherit; + border: 1px solid black; + border-radius: 4px; + color: white; + background-color: var(--accent); +} + +pre code { + padding: .5em; + border: 1px solid black; + border-radius: 4px; + font-size: .8em; + color: #e0e0e0; + background-color: #303030; +} + +pre code#motd { + display: block; + width: fit-content; + min-width: 440px; +} + +footer { + font-family: monospace; +} diff --git a/src/http.rs b/src/http.rs index 18b406d..bc2d69e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::{BufRead, BufReader, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}}; +use std::{collections::HashMap, io::{BufRead, BufReader, Error, ErrorKind, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}}; use chrono::Local; @@ -51,13 +51,17 @@ 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(); + if request_line_split.len() < 3 { + return Err(Error::new(ErrorKind::Other, "invalid request start-line")); + } let method = request_line_split[0]; - let path = request_line_split[1]; + let mut path = request_line_split[1]; let version = request_line_split[2]; let mut query: HashMap<&'a str, &'a str> = HashMap::new(); - match path.split_once("?") { - Some((_, query_string)) => { + match request_line_split[1].split_once("?") { + Some((path_without_query, query_string)) => { + path = path_without_query; let query_splits: Vec<&'a str> = query_string.split("&").collect(); for pair in query_splits { match pair.split_once("=") { diff --git a/src/main.rs b/src/main.rs index 8548f9b..c4ab06d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,18 @@ use std::io::{Result}; use std::net::{ToSocketAddrs}; -use std::env; +use std::{env, fs}; use mcstatusface::http::{HttpServer, StatusCode}; use mcstatusface::{MinecraftStatus}; +#[derive(serde::Serialize)] +struct MinecraftStatusResponse<'a> { + version: &'a String, + players: u32, + max_players: u32, + motd: String, +} + fn main() -> Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { @@ -41,6 +49,7 @@ env!("CARGO_PKG_VERSION")); HttpServer::new(address, 64).start(|request, mut response| { response.status(StatusCode::OK); + response.set_header("Content-Type", "text/plain".to_string()); response.set_header("x-powered-by", "GIRL FUEL".to_string()); if request.method() != "GET" { @@ -48,51 +57,149 @@ env!("CARGO_PKG_VERSION")); return response.send() } - if !request.query().contains_key("s") { - response.status(StatusCode::BadRequest); - // TODO: nice index landing page for browsers - response.body("?s=\n".to_string()); - return response.send() - } - - let mut address = request.query().get("s").unwrap().to_string(); - if !address.contains(":") { address.push_str(":25565"); } - let mut addrs_iter = address.to_socket_addrs().unwrap(); - let address = addrs_iter.next().unwrap(); - - let status = MinecraftStatus::fetch(address).unwrap(); - - #[derive(serde::Serialize)] - struct MinecraftStatusResponse<'a> { - version: &'a String, - players: u32, - max_players: u32, - motd: String, - } - - let minecraft_status = MinecraftStatusResponse{ - version: &status.version.name, - players: status.players.online, - max_players: status.players.max, - motd: status.parse_description(), - }; - - match serde_json::to_string(&minecraft_status) { - Ok(json) => { - response.status(StatusCode::OK); - response.set_header("Content-Type", "application/json".to_string()); - response.body(json); - } - Err(e) => { - eprintln!("Request to {address} failed: {e}"); - response.status(StatusCode::InternalServerError); - response.set_header("Content-Type", "text/plain".to_string()); - response.body("Unable to reach the requested server.\n".to_string()); + if request.path() == "/style/index.css" { + match fs::read_to_string("./public/style/index.css") { + Ok(content) => { + response.set_header("Content-Type", "text/css".to_string()); + response.status(StatusCode::OK); + response.body(content.to_string()); + return response.send(); + } + Err(err) => { + eprint!("failed to load index.css: {}\n", err.to_string()); + response.status(StatusCode::InternalServerError); + response.body("Internal Server Error\n".to_string()); + return response.send(); + } } } - response.send() + if request.path() == "/" { + if request.headers().get("Accept").is_some_and( + |accept| accept.contains("text/html") + ) { + // HTML response + match fs::read_to_string("./views/index.html") { + Ok(mut content) => { + response.set_header("Content-Type", "text/html".to_string()); + response.status(StatusCode::OK); + let query_response: String; + match request.query().get("s") { + Some(query_address) => { + let mut address = query_address.to_string(); + if !address.contains(":") { address.push_str(":25565"); } + match address.to_socket_addrs() { + Err(_) => { + response.set_header("Content-Type", "text/html".to_string()); + response.status(StatusCode::BadRequest); + response.body("Server address is invalid or unreachable.\n".to_string()); + return response.send(); + } + Ok(mut addrs_iter) => { + let address = addrs_iter.next().unwrap(); + + let status = MinecraftStatus::fetch(address).unwrap(); + + let minecraft_status = MinecraftStatusResponse{ + version: &status.version.name, + players: status.players.online, + max_players: status.players.max, + motd: status.parse_description(), + }; + + query_response = format!( + "
+

Server Details

+

+ Version: {}
+ Players: {}/{}
+ MOTD: +

+
{}
", + sanitize_html(minecraft_status.version).to_string(), + minecraft_status.players, + minecraft_status.max_players, + sanitize_html(&minecraft_status.motd).to_string(), + ); + } + } + } + None => { + query_response = String::from(""); + } + } + + content = content + .replace("{{response}}", &query_response) + .replace("{{host}}", match request.headers().get("Host") { + Some(host) => { host } + None => { "mcq.bliss.town" } + }); + response.body(content.to_string()); + return response.send(); + } + Err(err) => { + eprint!("failed to load index.html: {}\n", err.to_string()); + response.status(StatusCode::InternalServerError); + response.body("Internal Server Error\n".to_string()); + return response.send(); + } + } + } + + // JSON response + match request.query().get("s") { + None => { + response.status(StatusCode::BadRequest); + response.body("?s=\n".to_string()); + return response.send(); + } + Some(query_address) => { + let mut address = query_address.to_string(); + if !address.contains(":") { address.push_str(":25565"); } + let mut addrs_iter = address.to_socket_addrs().unwrap(); + let address = addrs_iter.next().unwrap(); + + let status = MinecraftStatus::fetch(address).unwrap(); + + let minecraft_status = MinecraftStatusResponse{ + version: &status.version.name, + players: status.players.online, + max_players: status.players.max, + motd: status.parse_description(), + }; + + match serde_json::to_string(&minecraft_status) { + Ok(json) => { + response.status(StatusCode::OK); + response.set_header("Content-Type", "application/json".to_string()); + response.body(json + "\n"); + } + Err(e) => { + eprintln!("Request to {address} failed: {e}"); + response.status(StatusCode::InternalServerError); + response.body("Unable to reach the requested server.\n".to_string()); + } + } + + return response.send() + } + } + } + + response.status(StatusCode::NotFound); + response.body("Not Found".to_string()); + return response.send(); }).unwrap(); Ok(()) } + +fn sanitize_html(input: &String) -> String { + input + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") +} diff --git a/views/index.html b/views/index.html new file mode 100644 index 0000000..8168602 --- /dev/null +++ b/views/index.html @@ -0,0 +1,42 @@ + + + + + + Minecraft Server Query + + + +
+

Crafty McStatusFace

+
+
+
+

+ You can use this website to retrieve query information from a Minecraft server! +

+

+ For more information, see https://minecraft.wiki/w/Query. +

+ +
+ + +
+ +
+ +

+ Alternatively, you can cURL this website to get a raw JSON response: +

+ +
curl 'https://{{host}}?s=<server address>'
+ + {{response}} +
+
+
+ made with by ari, 2025. source +
+ + From f5ef25145828cc13179349ae595f057ce0396ad9 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 18:06:43 +0100 Subject: [PATCH 03/11] embed web source --- src/main.rs | 106 ++++++++++--------------- {public => src/public}/style/index.css | 0 {views => src/views}/index.html | 0 3 files changed, 44 insertions(+), 62 deletions(-) rename {public => src/public}/style/index.css (100%) rename {views => src/views}/index.html (100%) diff --git a/src/main.rs b/src/main.rs index c4ab06d..f3e5190 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::io::{Result}; use std::net::{ToSocketAddrs}; -use std::{env, fs}; +use std::{env}; use mcstatusface::http::{HttpServer, StatusCode}; use mcstatusface::{MinecraftStatus}; @@ -58,20 +58,11 @@ env!("CARGO_PKG_VERSION")); } if request.path() == "/style/index.css" { - match fs::read_to_string("./public/style/index.css") { - Ok(content) => { - response.set_header("Content-Type", "text/css".to_string()); - response.status(StatusCode::OK); - response.body(content.to_string()); - return response.send(); - } - Err(err) => { - eprint!("failed to load index.css: {}\n", err.to_string()); - response.status(StatusCode::InternalServerError); - response.body("Internal Server Error\n".to_string()); - return response.send(); - } - } + let content = include_str!("public/style/index.css"); + response.set_header("Content-Type", "text/css".to_string()); + response.status(StatusCode::OK); + response.body(content.to_string()); + return response.send(); } if request.path() == "/" { @@ -79,36 +70,35 @@ env!("CARGO_PKG_VERSION")); |accept| accept.contains("text/html") ) { // HTML response - match fs::read_to_string("./views/index.html") { - Ok(mut content) => { - response.set_header("Content-Type", "text/html".to_string()); - response.status(StatusCode::OK); - let query_response: String; - match request.query().get("s") { - Some(query_address) => { - let mut address = query_address.to_string(); - if !address.contains(":") { address.push_str(":25565"); } - match address.to_socket_addrs() { - Err(_) => { - response.set_header("Content-Type", "text/html".to_string()); - response.status(StatusCode::BadRequest); - response.body("Server address is invalid or unreachable.\n".to_string()); - return response.send(); - } - Ok(mut addrs_iter) => { - let address = addrs_iter.next().unwrap(); + let content = include_str!("views/index.html"); + response.set_header("Content-Type", "text/html".to_string()); + response.status(StatusCode::OK); + let query_response: String; + match request.query().get("s") { + Some(query_address) => { + let mut address = query_address.to_string(); + if !address.contains(":") { address.push_str(":25565"); } + match address.to_socket_addrs() { + Err(_) => { + response.set_header("Content-Type", "text/html".to_string()); + response.status(StatusCode::BadRequest); + response.body("Server address is invalid or unreachable.\n".to_string()); + return response.send(); + } + Ok(mut addrs_iter) => { + let address = addrs_iter.next().unwrap(); - let status = MinecraftStatus::fetch(address).unwrap(); + let status = MinecraftStatus::fetch(address).unwrap(); - let minecraft_status = MinecraftStatusResponse{ - version: &status.version.name, - players: status.players.online, - max_players: status.players.max, - motd: status.parse_description(), - }; + let minecraft_status = MinecraftStatusResponse{ + version: &status.version.name, + players: status.players.online, + max_players: status.players.max, + motd: status.parse_description(), + }; - query_response = format!( - "
+ query_response = format!( + "

Server Details

Version: {}
@@ -120,31 +110,23 @@ env!("CARGO_PKG_VERSION")); minecraft_status.players, minecraft_status.max_players, sanitize_html(&minecraft_status.motd).to_string(), - ); - } - } - } - None => { - query_response = String::from(""); + ); } } - - content = content - .replace("{{response}}", &query_response) - .replace("{{host}}", match request.headers().get("Host") { - Some(host) => { host } - None => { "mcq.bliss.town" } - }); - response.body(content.to_string()); - return response.send(); } - Err(err) => { - eprint!("failed to load index.html: {}\n", err.to_string()); - response.status(StatusCode::InternalServerError); - response.body("Internal Server Error\n".to_string()); - return response.send(); + None => { + query_response = String::from(""); } } + + let response_content = content + .replace("{{response}}", &query_response) + .replace("{{host}}", match request.headers().get("Host") { + Some(host) => { host } + None => { "mcq.bliss.town" } + }); + response.body(response_content.to_string()); + return response.send(); } // JSON response diff --git a/public/style/index.css b/src/public/style/index.css similarity index 100% rename from public/style/index.css rename to src/public/style/index.css diff --git a/views/index.html b/src/views/index.html similarity index 100% rename from views/index.html rename to src/views/index.html From d66b12caff4dc7ba91e8be65e49c8126236f80e4 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 19:27:01 +0100 Subject: [PATCH 04/11] read X-Forwarded-For header from trusted proxies --- src/http.rs | 46 +++++++++++++++++++++++++++++++++++++++------- src/main.rs | 21 ++++++++++++++++++--- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/http.rs b/src/http.rs index bc2d69e..27bbdc4 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,4 +1,10 @@ -use std::{collections::HashMap, io::{BufRead, BufReader, Error, ErrorKind, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}}; +use std::{ + collections::HashMap, + io::{BufRead, BufReader, Error, ErrorKind, Result, Write}, + net::{IpAddr, SocketAddr, TcpListener, TcpStream}, + str::FromStr, + sync::Arc, +}; use chrono::Local; @@ -45,10 +51,11 @@ pub struct Request<'a> { headers: HashMap<&'a str, &'a str>, query: HashMap<&'a str, &'a str>, body: Option, + real_address: IpAddr, } impl<'a> Request<'a> { - pub fn new(stream: &'a TcpStream, lines: &'a Vec) -> Result> { + pub fn new(stream: &'a TcpStream, lines: &'a Vec, trusted_proxies: Vec) -> Result> { let request_line = lines[0].as_str(); let request_line_split: Vec<&str> = request_line.split(" ").collect(); if request_line_split.len() < 3 { @@ -88,6 +95,22 @@ impl<'a> Request<'a> { } } + let mut real_address = IpAddr::from(stream.peer_addr().unwrap().ip()); + + headers.get("X-Forwarded-For").inspect(|address| { + match IpAddr::from_str(address) { + Ok(address) => { + for proxy in trusted_proxies { + if real_address == proxy { + real_address = address; + break; + } + } + } + Err(_) => {} + } + }); + let mut body: Option = None; if lines.len() > headers.len() + 2 && (method == "POST" || method == "PUT") && @@ -104,12 +127,16 @@ impl<'a> Request<'a> { headers, query, body, + real_address, }) } pub fn address(&self) -> Result { self.stream.peer_addr() } + pub fn real_address(&self) -> &IpAddr { + &self.real_address + } pub fn path(&self) -> &'a str { self.path } @@ -187,11 +214,12 @@ impl<'a> Response<'a> { pub struct HttpServer { address: String, port: u16, + trusted_proxies: Arc>, max_connections: usize, } impl HttpServer { - pub fn new(address: String, max_connections: usize) -> HttpServer { + pub fn new(address: String, max_connections: usize, trusted_proxies: Vec) -> HttpServer { let mut _address = address.clone(); let mut _port: u16 = 8080; match address.split_once(":") { @@ -204,6 +232,7 @@ impl HttpServer { HttpServer { address: _address, port: _port, + trusted_proxies: Arc::new(trusted_proxies), max_connections, } } @@ -217,8 +246,9 @@ impl HttpServer { for stream in listener.incoming() { match stream { Ok(stream) => { + let trusted_proxies = self.trusted_proxies.clone(); pool.execute(move || { - HttpServer::handle_client(&stream, handler); + HttpServer::handle_client(&stream, handler, trusted_proxies); }); } Err(e) => { @@ -230,7 +260,7 @@ impl HttpServer { Ok(()) } - fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc) { + fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc>) { let buf_reader = BufReader::new(stream); let http_request: Vec = buf_reader .lines() @@ -238,7 +268,7 @@ impl HttpServer { .take_while(|line| !line.is_empty()) .collect(); - let request = Request::new(stream, &http_request); + let request = Request::new(stream, &http_request, trusted_proxies.to_vec()); if request.is_err() { eprintln!("Failed to process request: {}", request.err().unwrap()); return; @@ -251,6 +281,8 @@ impl HttpServer { match handler(&request, response) { Ok(status) => { let end_date = Local::now(); + + println!( "[{}] {} {} {} - {} {} - {}ms - {} ({})", start_date.format("%Y-%m-%d %H:%M:%S"), @@ -265,7 +297,7 @@ impl HttpServer { (end_date - start_date).num_milliseconds(), request.headers().get("User-Agent").map_or("[]", |v| v), - request.address().unwrap().ip(), + request.real_address().to_string(), ); } Err(e) => { diff --git a/src/main.rs b/src/main.rs index f3e5190..75e4e5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::io::{Result}; -use std::net::{ToSocketAddrs}; +use std::net::{IpAddr, ToSocketAddrs}; +use std::str::FromStr; use std::{env}; use mcstatusface::http::{HttpServer, StatusCode}; @@ -46,8 +47,22 @@ env!("CARGO_PKG_VERSION")); let mut address = "0.0.0.0:8080".to_string(); if args.len() > 2 { address = args[2].to_string() } + let trusted_proxies: Vec = + match env::var("MCSTATUSFACE_TRUSTED_PROXIES") { + Ok(envar) => { + let mut trusted_proxies: Vec = Vec::new(); + for addr in envar.split(",") { + match IpAddr::from_str(addr) { + Ok(addr) => { trusted_proxies.push(addr); } + Err(_) => {} + } + } + trusted_proxies + } + Err(_) => { vec![] } + }; - HttpServer::new(address, 64).start(|request, mut response| { + HttpServer::new(address, 64, trusted_proxies).start(|request, mut response| { response.status(StatusCode::OK); response.set_header("Content-Type", "text/plain".to_string()); response.set_header("x-powered-by", "GIRL FUEL".to_string()); @@ -132,7 +147,7 @@ env!("CARGO_PKG_VERSION")); // JSON response match request.query().get("s") { None => { - response.status(StatusCode::BadRequest); + response.status(StatusCode::OK); response.body("?s=\n".to_string()); return response.send(); } From d2fb78108ee13ec13153d898a40d8c6471203df1 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 19:30:51 +0100 Subject: [PATCH 05/11] update readme --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9745dbb..10e2605 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,14 @@ For more information, see https://minecraft.wiki/w/Query ## Usage -McStatusFace can be run as a web server with `mcstatusface serve`. This will -provide server information in JSON format to requests on `GET /?s=`. -(e.g. `curl -sS "127.0.0.1:8080?s=127.0.0.1:25565" | jq .`) +McStatusFace can simply be run with `mcstatusface `. -Alternatively, you can simply run `mcstatusface `, and the +Alternatively, you can start a web interface with +`mcstatusface serve [address:[port]]`. + +This provides a web interface to query server information via a frontend, or +in JSON format if the request is cURLed. + +The server queries Minecraft servers when a `GET /?s=` request +is received. (e.g. `curl -sS "127.0.0.1:8080?s=127.0.0.1:25565" | jq .`) tool will provide server details in plain-text format. From fc064efb61813f9d77afcff868ed3f35034ed79e Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 19:50:48 +0100 Subject: [PATCH 06/11] ignore case on header keys --- src/http.rs | 9 ++++----- src/main.rs | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/http.rs b/src/http.rs index 27bbdc4..131c96e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -48,7 +48,7 @@ pub struct Request<'a> { path: &'a str, method: &'a str, version: &'a str, - headers: HashMap<&'a str, &'a str>, + headers: HashMap, query: HashMap<&'a str, &'a str>, body: Option, real_address: IpAddr, @@ -82,7 +82,7 @@ impl<'a> Request<'a> { None => {}, } - let mut headers: HashMap<&'a str, &'a str> = HashMap::new(); + let mut headers: HashMap = HashMap::new(); if lines.len() > 1 { let mut i: usize = 1; loop { @@ -90,7 +90,7 @@ impl<'a> Request<'a> { let line = &lines[i]; if line.len() == 0 || !line.contains(":") { break; } let (name, value) = line.split_once(":").unwrap(); - headers.insert(name, value.trim()); + headers.insert(name.to_lowercase(), value.trim()); i += 1; } } @@ -149,7 +149,7 @@ impl<'a> Request<'a> { pub fn body(&self) -> &Option { &self.body } - pub fn headers(&self) -> &HashMap<&'a str, &'a str> { + pub fn headers(&self) -> &HashMap { &self.headers } pub fn query(&self) -> &HashMap<&'a str, &'a str> { @@ -282,7 +282,6 @@ impl HttpServer { Ok(status) => { let end_date = Local::now(); - println!( "[{}] {} {} {} - {} {} - {}ms - {} ({})", start_date.format("%Y-%m-%d %H:%M:%S"), diff --git a/src/main.rs b/src/main.rs index 75e4e5e..664036b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,7 +81,7 @@ env!("CARGO_PKG_VERSION")); } if request.path() == "/" { - if request.headers().get("Accept").is_some_and( + if request.headers().get("accept").is_some_and( |accept| accept.contains("text/html") ) { // HTML response @@ -136,7 +136,7 @@ env!("CARGO_PKG_VERSION")); let response_content = content .replace("{{response}}", &query_response) - .replace("{{host}}", match request.headers().get("Host") { + .replace("{{host}}", match request.headers().get("host") { Some(host) => { host } None => { "mcq.bliss.town" } }); From dab6eefbdd7e40ceff05b0ae02d76389961d7247 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 19:53:01 +0100 Subject: [PATCH 07/11] css fix on motd --- src/main.rs | 2 +- src/public/style/index.css | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 664036b..c9d1ffc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,7 +120,7 @@ env!("CARGO_PKG_VERSION")); Players: {}/{}
MOTD:

-
{}
", +
{}
", sanitize_html(minecraft_status.version).to_string(), minecraft_status.players, minecraft_status.max_players, diff --git a/src/public/style/index.css b/src/public/style/index.css index 9aac8cf..59c28af 100644 --- a/src/public/style/index.css +++ b/src/public/style/index.css @@ -47,7 +47,11 @@ pre code { background-color: #303030; } -pre code#motd { +pre#motd { + margin-top: -1em +} + +pre#motd code { display: block; width: fit-content; min-width: 440px; From a0c6c910ebd9237bab9302aa4abb2ecd0d257e53 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 19:56:42 +0100 Subject: [PATCH 08/11] bump to v1.1.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a4a24c9..9ea234c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ authors = ["ari melody "] repository = "https://git.arimelody.me/ari/mcstatusface" license = "MIT" keywords = ["minecraft", "server", "query", "web"] -version = "1.0.1" +version = "1.1.0" edition = "2024" [dependencies] From 75bd0000bb2580c95721964430d0d3ef2a6fc92d Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 20:01:25 +0100 Subject: [PATCH 09/11] opengraph metadata --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/views/index.html | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7166ce7..2afe0a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "mcstatusface" -version = "1.0.1" +version = "1.1.1" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index 9ea234c..7140036 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ authors = ["ari melody "] repository = "https://git.arimelody.me/ari/mcstatusface" license = "MIT" keywords = ["minecraft", "server", "query", "web"] -version = "1.1.0" +version = "1.1.1" edition = "2024" [dependencies] diff --git a/src/views/index.html b/src/views/index.html index 8168602..5bdd9d3 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -3,8 +3,16 @@ - Minecraft Server Query + Crafty McStatusFace + + + + + + + +
From c765db81480a8bd4e26fa97d43fc6a171ce1a454 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 22:05:01 +0100 Subject: [PATCH 10/11] fix some sloppy unwraps, teensy refactor --- src/main.rs | 101 +++++++++++++++++++++++++++++++------------------- src/status.rs | 4 +- 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/src/main.rs b/src/main.rs index c9d1ffc..1011cdb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,17 +49,17 @@ env!("CARGO_PKG_VERSION")); if args.len() > 2 { address = args[2].to_string() } let trusted_proxies: Vec = match env::var("MCSTATUSFACE_TRUSTED_PROXIES") { + Err(_) => { vec![] } Ok(envar) => { let mut trusted_proxies: Vec = Vec::new(); for addr in envar.split(",") { match IpAddr::from_str(addr) { - Ok(addr) => { trusted_proxies.push(addr); } Err(_) => {} + Ok(addr) => { trusted_proxies.push(addr); } } } trusted_proxies } - Err(_) => { vec![] } }; HttpServer::new(address, 64, trusted_proxies).start(|request, mut response| { @@ -90,6 +90,9 @@ env!("CARGO_PKG_VERSION")); response.status(StatusCode::OK); let query_response: String; match request.query().get("s") { + None => { + query_response = String::from(""); + } Some(query_address) => { let mut address = query_address.to_string(); if !address.contains(":") { address.push_str(":25565"); } @@ -103,17 +106,25 @@ env!("CARGO_PKG_VERSION")); Ok(mut addrs_iter) => { let address = addrs_iter.next().unwrap(); - let status = MinecraftStatus::fetch(address).unwrap(); + match MinecraftStatus::fetch(address) { + Err(_) => { + query_response = format!( + "
+

Server Details

+
Failed to connect to {}.
", + sanitize_html(&address.to_string()), + ); + } + Ok(status) => { + let minecraft_status = MinecraftStatusResponse{ + version: &status.version.name, + players: status.players.online, + max_players: status.players.max, + motd: status.parse_description(), + }; - let minecraft_status = MinecraftStatusResponse{ - version: &status.version.name, - players: status.players.online, - max_players: status.players.max, - motd: status.parse_description(), - }; - - query_response = format!( - "
+ query_response = format!( + "

Server Details

Version: {}
@@ -125,20 +136,19 @@ env!("CARGO_PKG_VERSION")); minecraft_status.players, minecraft_status.max_players, sanitize_html(&minecraft_status.motd).to_string(), - ); + ); + } + } } } } - None => { - query_response = String::from(""); - } } let response_content = content .replace("{{response}}", &query_response) .replace("{{host}}", match request.headers().get("host") { - Some(host) => { host } None => { "mcq.bliss.town" } + Some(host) => { host } }); response.body(response_content.to_string()); return response.send(); @@ -154,31 +164,44 @@ env!("CARGO_PKG_VERSION")); Some(query_address) => { let mut address = query_address.to_string(); if !address.contains(":") { address.push_str(":25565"); } - let mut addrs_iter = address.to_socket_addrs().unwrap(); - let address = addrs_iter.next().unwrap(); - - let status = MinecraftStatus::fetch(address).unwrap(); - - let minecraft_status = MinecraftStatusResponse{ - version: &status.version.name, - players: status.players.online, - max_players: status.players.max, - motd: status.parse_description(), - }; - - match serde_json::to_string(&minecraft_status) { - Ok(json) => { - response.status(StatusCode::OK); - response.set_header("Content-Type", "application/json".to_string()); - response.body(json + "\n"); - } - Err(e) => { - eprintln!("Request to {address} failed: {e}"); + match address.to_socket_addrs() { + Err(_) => { response.status(StatusCode::InternalServerError); - response.body("Unable to reach the requested server.\n".to_string()); + response.body("Invalid server address.\n".to_string()); + } + Ok(mut addrs_iter) => { + let address = addrs_iter.next().unwrap(); + + match MinecraftStatus::fetch(address) { + Err(_) => { + response.status(StatusCode::InternalServerError); + response.body(format!("Failed to connect to {address}.\n")); + } + Ok(status) => { + let minecraft_status = MinecraftStatusResponse{ + version: &status.version.name, + players: status.players.online, + max_players: status.players.max, + motd: status.parse_description(), + }; + + match serde_json::to_string(&minecraft_status) { + Err(e) => { + eprintln!("Failed to parse status for {address}: {e}"); + response.status(StatusCode::InternalServerError); + response.body(format!("Failed to parse response from {address}.\n")); + } + Ok(json) => { + response.status(StatusCode::OK); + response.set_header("Content-Type", "application/json".to_string()); + response.body(json + "\n"); + } + } + } + } + } } - return response.send() } } diff --git a/src/status.rs b/src/status.rs index c0363ad..3af55f6 100644 --- a/src/status.rs +++ b/src/status.rs @@ -51,8 +51,10 @@ impl MinecraftStatus { pub fn fetch(address: SocketAddr) -> Result { // println!("Connecting to {address}..."); - let mut stream = TcpStream::connect(address.to_string()).unwrap(); + let stream = TcpStream::connect(address.to_string()); + if stream.is_err() { return Err(stream.unwrap_err()); } // println!("Connected!"); + let mut stream = stream.unwrap(); let mut send_buffer: Vec = Vec::new(); From eab99012e83ee7a003ca97b9eba80380f056fba8 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 22:17:19 +0100 Subject: [PATCH 11/11] improve error messages --- src/main.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1011cdb..a4309af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,16 +98,20 @@ env!("CARGO_PKG_VERSION")); if !address.contains(":") { address.push_str(":25565"); } match address.to_socket_addrs() { Err(_) => { - response.set_header("Content-Type", "text/html".to_string()); response.status(StatusCode::BadRequest); - response.body("Server address is invalid or unreachable.\n".to_string()); - return response.send(); + query_response = format!( + "


+

Server Details

+
Invalid server address: {}.
", + sanitize_html(&address.to_string()), + ); } Ok(mut addrs_iter) => { let address = addrs_iter.next().unwrap(); match MinecraftStatus::fetch(address) { Err(_) => { + response.status(StatusCode::InternalServerError); query_response = format!( "

Server Details

@@ -150,6 +154,7 @@ env!("CARGO_PKG_VERSION")); None => { "mcq.bliss.town" } Some(host) => { host } }); + response.set_header("Content-Type", "text/html".to_string()); response.body(response_content.to_string()); return response.send(); } @@ -166,7 +171,7 @@ env!("CARGO_PKG_VERSION")); if !address.contains(":") { address.push_str(":25565"); } match address.to_socket_addrs() { Err(_) => { - response.status(StatusCode::InternalServerError); + response.status(StatusCode::BadRequest); response.body("Invalid server address.\n".to_string()); } Ok(mut addrs_iter) => { @@ -187,9 +192,9 @@ env!("CARGO_PKG_VERSION")); match serde_json::to_string(&minecraft_status) { Err(e) => { - eprintln!("Failed to parse status for {address}: {e}"); + eprintln!("Failed to format response from {} to JSON: {}", address, e); response.status(StatusCode::InternalServerError); - response.body(format!("Failed to parse response from {address}.\n")); + response.body("Internal Server Error\n".to_string()); } Ok(json) => { response.status(StatusCode::OK);