From 75bd0000bb2580c95721964430d0d3ef2a6fc92d Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 20:01:25 +0100 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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); From 394419fa6345bb536befd8f893430d27e45438d9 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 01:14:45 +0000 Subject: [PATCH 04/12] properly accept ports in web requests --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index a4309af..086e4bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,6 +95,7 @@ env!("CARGO_PKG_VERSION")); } Some(query_address) => { let mut address = query_address.to_string(); + address = address.replace("%3A", ":"); if !address.contains(":") { address.push_str(":25565"); } match address.to_socket_addrs() { Err(_) => { @@ -168,6 +169,7 @@ env!("CARGO_PKG_VERSION")); } Some(query_address) => { let mut address = query_address.to_string(); + address = address.replace("%3A", ":"); if !address.contains(":") { address.push_str(":25565"); } match address.to_socket_addrs() { Err(_) => { From 8a30e93a89e45f58759b0d5afa09924c9dcbead0 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 01:40:30 +0000 Subject: [PATCH 05/12] UI refresh --- src/public/style/index.css | 79 ++++++++++++++++++++++++++++++-------- src/views/index.html | 6 +-- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/public/style/index.css b/src/public/style/index.css index 59c28af..ef8577a 100644 --- a/src/public/style/index.css +++ b/src/public/style/index.css @@ -1,10 +1,47 @@ :root { - --accent: #7ca82f; + --bg-0: #101010; + --bg-1: #181818; + --bg-2: #282828; + --fg-0: #c0c0c0; + --fg-1: #d0d0d0; + --fg-2: #e0e0e0; + --accent: #c0ea76; + --on-accent: #43600f; + + --shadow-md: 0 2px 4px rgba(0,0,0,.5); + --shadow-sm: 0 1px 2px rgba(0,0,0,.3); +} + +@media (prefers-color-scheme: light) { + :root { + --bg-0: #e8e8e8; + --bg-1: #f0f0f0; + --bg-2: #fcfcfc; + --fg-0: #606060; + --fg-1: #303030; + --fg-2: #101010; + --accent: #6a9321; + --on-accent: #f6ffe6; + + --shadow-md: 0 2px 4px rgba(0,0,0,.25);; + --shadow-sm: 0 1px 2px rgba(0,0,0,.15);; + } } body { - font-size: 16px; - font-family: 'Inter', sans-serif; + width: 720px; + max-width: calc(100vw - 1em); + margin: auto; + + font-size: 20px; + font-family: 'Inter', 'Arial', sans-serif; + + background-color: var(--bg-0); + color: var(--fg-0); +} + +* { + transition: background-color .2s ease-out, color .2s ease-out; } a { @@ -17,34 +54,45 @@ a:hover { form { width: fit-content; - padding: .5em; - border: 1px solid black; - border-radius: 4px; + padding: 1em; + border-radius: 8px; + background-color: var(--bg-1); + color: var(--fg-1); + box-shadow: var(--shadow-md); } form input[type="text"] { width: fit-content; min-width: 16em; + padding: .2em .4em; + font-size: inherit; + border: none; + border-radius: 4px; + background-color: var(--bg-2); + color: var(--fg-2); + box-shadow: var(--shadow-sm); } form button { margin-top: .5em; - padding: .2em .3em; + padding: .2em .4em; font-family: inherit; font-size: inherit; - border: 1px solid black; - border-radius: 4px; - color: white; + border: none; + border-radius: 8px; + color: var(--on-accent); background-color: var(--accent); + box-shadow: var(--shadow-sm); } pre code { padding: .5em; - border: 1px solid black; - border-radius: 4px; - font-size: .8em; - color: #e0e0e0; - background-color: #303030; + border-radius: 8px; + font-size: 16px; + + background-color: var(--fg-1); + color: var(--bg-1); + box-shadow: var(--shadow-sm); } pre#motd { @@ -55,6 +103,7 @@ pre#motd code { display: block; width: fit-content; min-width: 440px; + box-shadow: var(--shadow-sm); } footer { diff --git a/src/views/index.html b/src/views/index.html index 5bdd9d3..fcfc154 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -21,7 +21,7 @@

- You can use this website to retrieve query information from a Minecraft server! + This website retrieves query information from Minecraft servers!

For more information, see https://minecraft.wiki/w/Query. @@ -38,13 +38,13 @@ Alternatively, you can cURL this website to get a raw JSON response:

-
curl 'https://{{host}}?s=<server address>'
+
curl 'https://{{host}}?s=<server address>' | jq .
{{response}}

- made with by ari, 2025. source + made with by ari, 2025. source
From 694c2f07d8c431a4a346439f10be625d2087e9f0 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 01:58:05 +0000 Subject: [PATCH 06/12] improve query support simple string MOTDs; optional player sample --- src/status.rs | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/status.rs b/src/status.rs index 3af55f6..65c6eb3 100644 --- a/src/status.rs +++ b/src/status.rs @@ -19,7 +19,7 @@ pub struct MinecraftPlayer { pub struct MinecraftPlayers { pub online: u32, pub max: u32, - pub sample: Vec, + pub sample: Option>, } #[derive(serde::Serialize, serde::Deserialize)] @@ -30,7 +30,14 @@ enum MinecraftDescriptionExtra { } #[derive(serde::Serialize, serde::Deserialize)] -pub struct MinecraftDescription { +#[serde(untagged)] +pub enum MinecraftDescription { + Rich(MinecraftRichDescription), + Plain(String) +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct MinecraftRichDescription { text: String, extra: Option>, bold: Option, @@ -103,7 +110,6 @@ impl MinecraftStatus { } let msg = std::str::from_utf8(&data[offset..]).unwrap().trim(); let sanitised: String = msg.chars().filter(|&c| c >= '\u{20}' || c == '\n' || c == '\r' || c == '\t').collect(); - // println!("{sanitised}"); let status: MinecraftStatus = serde_json::from_slice(sanitised.as_bytes()).unwrap(); Ok(status) @@ -119,21 +125,28 @@ impl MinecraftStatus { } fn _description(description: &MinecraftDescription) -> String { - if description.extra.is_some() { - let mut extras = String::new(); - for extra in description.extra.as_ref().unwrap() { - match extra { - MinecraftDescriptionExtra::Extra(description) => { - extras += &_description(&description); - } - MinecraftDescriptionExtra::String(string) => { - extras += &string; + match description { + MinecraftDescription::Rich(description) => { + if description.extra.is_some() { + let mut extras = String::new(); + for extra in description.extra.as_ref().unwrap() { + match extra { + MinecraftDescriptionExtra::Extra(description) => { + extras += &_description(&description); + } + MinecraftDescriptionExtra::String(string) => { + extras += &string; + } + } } + return description.text.clone() + &extras; } + description.text.clone() + } + MinecraftDescription::Plain(description) => { + description.clone() } - return description.text.clone() + &extras; } - description.text.clone() } fn send_packet(stream: &mut TcpStream, data: &[u8]) -> Result<()> { From d7c4fac34cad6a9a82b2d75ab3640366cc046e53 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 02:06:57 +0000 Subject: [PATCH 07/12] report if server enforces secure chat --- src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main.rs b/src/main.rs index 086e4bb..a0afb95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ struct MinecraftStatusResponse<'a> { version: &'a String, players: u32, max_players: u32, + enforces_secure_chat: bool, motd: String, } @@ -125,6 +126,7 @@ env!("CARGO_PKG_VERSION")); version: &status.version.name, players: status.players.online, max_players: status.players.max, + enforces_secure_chat: status.enforces_secure_chat(), motd: status.parse_description(), }; @@ -134,12 +136,14 @@ env!("CARGO_PKG_VERSION"));

Version: {}
Players: {}/{}
+ Enforces Secure Chat: {}
MOTD:

{}
", sanitize_html(minecraft_status.version).to_string(), minecraft_status.players, minecraft_status.max_players, + if minecraft_status.enforces_secure_chat { "true" } else { "false" }, sanitize_html(&minecraft_status.motd).to_string(), ); } @@ -189,6 +193,7 @@ env!("CARGO_PKG_VERSION")); version: &status.version.name, players: status.players.online, max_players: status.players.max, + enforces_secure_chat: status.enforces_secure_chat(), motd: status.parse_description(), }; From 983ac7021e960752d0e53a9bad8d033cc8e2c9f9 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 02:18:33 +0000 Subject: [PATCH 08/12] add favicon support --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index a0afb95..3ae1e4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ struct MinecraftStatusResponse<'a> { players: u32, max_players: u32, enforces_secure_chat: bool, + favicon: Option<&'a String>, motd: String, } @@ -127,6 +128,7 @@ env!("CARGO_PKG_VERSION")); players: status.players.online, max_players: status.players.max, enforces_secure_chat: status.enforces_secure_chat(), + favicon: status.favicon.as_ref(), motd: status.parse_description(), }; @@ -134,12 +136,15 @@ env!("CARGO_PKG_VERSION")); "

Server Details

+ Favicon:
+
Version: {}
Players: {}/{}
Enforces Secure Chat: {}
MOTD:

{}
", + sanitize_html(&minecraft_status.favicon.map_or("", |s| s).to_string()), sanitize_html(minecraft_status.version).to_string(), minecraft_status.players, minecraft_status.max_players, @@ -194,6 +199,7 @@ env!("CARGO_PKG_VERSION")); players: status.players.online, max_players: status.players.max, enforces_secure_chat: status.enforces_secure_chat(), + favicon: status.favicon.as_ref(), motd: status.parse_description(), }; From 7c8c04573c4e7f204bd5d21ea966245f611412b1 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 19:23:00 +0000 Subject: [PATCH 09/12] add checks for SRV records --- Cargo.lock | 2 +- Cargo.toml | 6 +-- src/dns.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 33 ++++++++++++-- 5 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 src/dns.rs diff --git a/Cargo.lock b/Cargo.lock index 2afe0a5..e214ba7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "mcstatusface" -version = "1.1.1" +version = "1.2.0" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7140036..9c5b648 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "mcstatusface" -authors = ["ari melody "] -repository = "https://git.arimelody.me/ari/mcstatusface" +authors = ["ari melody "] +repository = "https://codeberg.org/arimelody/mcstatusface" license = "MIT" keywords = ["minecraft", "server", "query", "web"] -version = "1.1.1" +version = "1.2.0" edition = "2024" [dependencies] diff --git a/src/dns.rs b/src/dns.rs new file mode 100644 index 0000000..8cfb46c --- /dev/null +++ b/src/dns.rs @@ -0,0 +1,128 @@ +use std::{io::{Error, ErrorKind, Result}, net::UdpSocket}; + +const CLOUDFLARE_DNS: &str = "1.1.1.1"; + +const RECURSION_DESIRED: u16 = 1 << 8; + +const RECORD_TYPE_SRV: u16 = 33; +const RECORD_CLASS_IN: u16 = 1; + +pub fn create_dns_query(qname: &String, qtype: u16, qclass: u16) -> Result> { + let qname = match qname.is_ascii() { + true => qname, + false => return Err(Error::new(ErrorKind::InvalidInput, "domain is not valid ASCII")), + }; + + let txid = 1u16; + let flags = RECURSION_DESIRED; + let qdcount = 1u16; + let ancount = 0u16; + let nscount = 0u16; + let arcount = 0u16; + + let header: [u16; 6] = + [txid, flags, qdcount, ancount, nscount, arcount] + .map(|el| el.to_be()); + + // let mut qname_bytes = bitvec![u8, Msb0;]; + let mut query = Vec::::new(); + for mut label in qname.split(".") { + label = match label.len() > u8::MAX.into() { + true => label.split_at(u8::MAX.into()).0, + false => label + }; + let len: u8 = label.len().try_into().unwrap(); + query.push(len); + query.append(&mut label.as_bytes().to_vec()); + } + query.push(0); + query.append(&mut qtype.to_be_bytes().to_vec()); + query.append(&mut qclass.to_be_bytes().to_vec()); + + let mut request = Vec::::new(); + for el in header { + request.push(el as u8); + request.push((el >> 8) as u8); + } + request.append(&mut query); + + Ok(request) +} + +#[derive(Debug)] +pub struct DnsSrvResponse { + qref: u16, + rtype: u16, + class: u16, + ttl: u32, + priority: u16, + weight: u16, + port: u16, +} + +pub fn parse_srv_response(mut ptr: usize, recv: &[u8]) -> Option { + let mut data = DnsSrvResponse { + qref: 0, + rtype: 0, + class: 0, + ttl: 0, + priority: 0, + weight: 0, + port: 0, + }; + + fn read16(buf: &[u8], offset: usize) -> u16 { + let left = (buf[offset] as u16) << 8; + let right = buf[offset + 1] as u16; + left + right + } + + fn read32(buf: &[u8], offset: usize) -> u32 { + let left = (read16(buf, offset) as u32) << 16; + let right = read16(buf, offset + 2) as u32; + left + right + } + + data.qref = read16(recv, ptr); + ptr += 2; + data.rtype = read16(recv, ptr); + ptr += 2; + if data.rtype != RECORD_TYPE_SRV { + return None + } + data.class = read16(recv, ptr); + ptr += 2; + data.ttl = read32(recv, ptr); + ptr += 4; + let _rdata_len = read16(recv, ptr); + ptr += 2; + data.priority = read16(recv, ptr); + ptr += 2; + data.weight = read16(recv, ptr); + ptr += 2; + data.port = read16(recv, ptr); + // optional: read target? + + // print!("data: {:#?}\n", data); + + Some(data) +} + +pub fn resolve_srv_port(domain: &String) -> Option { + let request = create_dns_query( + &("_minecraft._tcp.".to_string() + domain), + RECORD_TYPE_SRV, + RECORD_CLASS_IN).unwrap(); + + let socket = UdpSocket::bind("0.0.0.0:0").expect("failed to bind to port!"); + socket.connect(CLOUDFLARE_DNS.to_string() + ":53").expect("failed to connect to dns server"); + socket.send(&request).expect("failed to send dns request"); + + let mut recv_buf: [u8; 1024] = [0; 1024]; + let _ = socket.recv_from(&mut recv_buf).unwrap(); + + match parse_srv_response(request.len(), &recv_buf) { + Some(data) => Some(data.port), + None => None, + } +} diff --git a/src/lib.rs b/src/lib.rs index 8a34bdd..6139176 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use std::{sync::{mpsc, Arc, Mutex}, thread}; pub mod leb128; pub mod status; pub mod http; +pub mod dns; pub use status::MinecraftStatus; pub struct ThreadPool { diff --git a/src/main.rs b/src/main.rs index 3ae1e4c..2dcd39d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::{env}; use mcstatusface::http::{HttpServer, StatusCode}; use mcstatusface::{MinecraftStatus}; +use mcstatusface::dns::{resolve_srv_port}; #[derive(serde::Serialize)] struct MinecraftStatusResponse<'a> { @@ -16,6 +17,8 @@ struct MinecraftStatusResponse<'a> { motd: String, } +const DEFAULT_PORT: u16 = 25565; + fn main() -> Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { @@ -33,7 +36,15 @@ env!("CARGO_PKG_VERSION")); if args[1] != "serve" { let mut address = String::from(args[1].as_str()); - if !address.contains(":") { address.push_str(":25565"); } + if !address.contains(":") { + let port: u16 = match resolve_srv_port(&address) { + Some(port) => port, + None => DEFAULT_PORT, + }; + + address.push_str(":"); + address.push_str(port.to_string().as_str()); + } let mut addrs_iter = address.to_socket_addrs().unwrap(); let address = addrs_iter.next().unwrap(); @@ -98,7 +109,15 @@ env!("CARGO_PKG_VERSION")); Some(query_address) => { let mut address = query_address.to_string(); address = address.replace("%3A", ":"); - if !address.contains(":") { address.push_str(":25565"); } + if !address.contains(":") { + let port: u16 = match resolve_srv_port(&address) { + Some(port) => port, + None => DEFAULT_PORT, + }; + + address.push_str(":"); + address.push_str(port.to_string().as_str()); + } match address.to_socket_addrs() { Err(_) => { response.status(StatusCode::BadRequest); @@ -179,7 +198,15 @@ env!("CARGO_PKG_VERSION")); Some(query_address) => { let mut address = query_address.to_string(); address = address.replace("%3A", ":"); - if !address.contains(":") { address.push_str(":25565"); } + if !address.contains(":") { + let port: u16 = match resolve_srv_port(&address) { + Some(port) => port, + None => DEFAULT_PORT, + }; + + address.push_str(":"); + address.push_str(port.to_string().as_str()); + } match address.to_socket_addrs() { Err(_) => { response.status(StatusCode::BadRequest); From 172a072c368a6e9ada4bc21b3b890faf880b5a80 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 20:04:31 +0000 Subject: [PATCH 10/12] add enforce secure chat to cli response --- src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.rs b/src/main.rs index 2dcd39d..d6299b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,10 @@ env!("CARGO_PKG_VERSION")); println!("Version: {} ({})", status.version.name, status.version.protocol); println!("Players: {}/{}", status.players.online, status.players.max); + println!( + "Enforces Secure Chat: {}", + if status.enforces_secure_chat() { "true" } else { "false" }, + ); println!("MOTD:"); println!("{}", status.parse_description()); From f64882b091c3735af21dab34f3489bf9f9fd0208 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 5 Nov 2025 20:06:59 +0000 Subject: [PATCH 11/12] reduce http workers to 16 --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index d6299b1..46ba212 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,7 @@ env!("CARGO_PKG_VERSION")); } }; - HttpServer::new(address, 64, trusted_proxies).start(|request, mut response| { + HttpServer::new(address, 16, 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()); From fc34b1e3282198fec516c62aee22b85ec0d063b7 Mon Sep 17 00:00:00 2001 From: ari melody Date: Thu, 6 Nov 2025 11:27:29 +0000 Subject: [PATCH 12/12] small code cleanup no functional changes, just minor restructuring --- src/http.rs | 2 +- src/leb128.rs | 58 ++++++++++++++++------------------- src/lib.rs | 85 +-------------------------------------------------- src/main.rs | 8 ++--- src/status.rs | 14 ++++----- src/thread.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 127 deletions(-) create mode 100644 src/thread.rs diff --git a/src/http.rs b/src/http.rs index 131c96e..90e0d49 100644 --- a/src/http.rs +++ b/src/http.rs @@ -8,7 +8,7 @@ use std::{ use chrono::Local; -use crate::ThreadPool; +use crate::thread::ThreadPool; #[derive(Clone, Copy)] pub enum StatusCode { diff --git a/src/leb128.rs b/src/leb128.rs index 7f7b091..60247ad 100644 --- a/src/leb128.rs +++ b/src/leb128.rs @@ -1,38 +1,34 @@ const CONTINUE_BIT: u8 = 0x80; -pub struct LEB128; - -impl LEB128 { - pub fn write_leb128(buffer: &mut Vec, mut value: u64) -> usize { - let mut size: usize = 0; - loop { - let mut byte: u8 = (value & (std::u8::MAX as u64)).try_into().unwrap(); - value >>= 7; - if value != 0 { - byte |= CONTINUE_BIT; - } - - buffer.push(byte); - size += 1; - - if value == 0 { - return size; - } +pub fn write_leb128(buffer: &mut Vec, mut value: u64) -> usize { + let mut size: usize = 0; + loop { + let mut byte: u8 = (value & (std::u8::MAX as u64)).try_into().unwrap(); + value >>= 7; + if value != 0 { + byte |= CONTINUE_BIT; } - } - pub fn read_leb128(buffer: &[u8]) -> (u32, usize) { - let mut result: u32 = 0; - let mut shift: usize = 0; - let mut offset: usize = 0; - loop { - let byte: u8 = buffer[offset]; - result |= ((byte & (std::i8::MAX as u8)) as u32) << (shift as u32); - offset += 1; - if byte & CONTINUE_BIT == 0 || offset == 4 { - return (result, offset); - } - shift += 7; + buffer.push(byte); + size += 1; + + if value == 0 { + return size; } } } + +pub fn read_leb128(buffer: &[u8]) -> (u32, usize) { + let mut result: u32 = 0; + let mut shift: usize = 0; + let mut offset: usize = 0; + loop { + let byte: u8 = buffer[offset]; + result |= ((byte & (std::i8::MAX as u8)) as u32) << (shift as u32); + offset += 1; + if byte & CONTINUE_BIT == 0 || offset == 4 { + return (result, offset); + } + shift += 7; + } +} diff --git a/src/lib.rs b/src/lib.rs index 6139176..954b787 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,88 +1,5 @@ -use std::{sync::{mpsc, Arc, Mutex}, thread}; - pub mod leb128; pub mod status; +pub mod thread; pub mod http; pub mod dns; -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 46ba212..fddb15f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ -use std::io::{Result}; +use std::io::Result; use std::net::{IpAddr, ToSocketAddrs}; use std::str::FromStr; -use std::{env}; +use std::env; use mcstatusface::http::{HttpServer, StatusCode}; -use mcstatusface::{MinecraftStatus}; -use mcstatusface::dns::{resolve_srv_port}; +use mcstatusface::status::MinecraftStatus; +use mcstatusface::dns::resolve_srv_port; #[derive(serde::Serialize)] struct MinecraftStatusResponse<'a> { diff --git a/src/status.rs b/src/status.rs index 65c6eb3..7d28f96 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,7 +1,7 @@ use std::io::{Error, ErrorKind, Read, Write, Result}; use std::net::{SocketAddr, TcpStream}; -use crate::leb128::LEB128; +use crate::leb128::{read_leb128, write_leb128}; #[derive(serde::Serialize, serde::Deserialize)] pub struct MinecraftVersion { @@ -67,11 +67,11 @@ impl MinecraftStatus { // println!("Sending payload..."); send_buffer.push(0x00); - LEB128::write_leb128(&mut send_buffer, 769); // 1.21.4 - LEB128::write_leb128(&mut send_buffer, address.ip().to_string().len().try_into().unwrap()); + write_leb128(&mut send_buffer, 769); // 1.21.4 + write_leb128(&mut send_buffer, address.ip().to_string().len().try_into().unwrap()); send_buffer.extend_from_slice(address.ip().to_string().as_bytes()); send_buffer.extend_from_slice(&address.port().to_be_bytes()); - LEB128::write_leb128(&mut send_buffer, 1); + write_leb128(&mut send_buffer, 1); send_packet(&mut stream, &send_buffer).unwrap(); send_packet(&mut stream, &[0x00]).unwrap(); @@ -89,7 +89,7 @@ impl MinecraftStatus { if len > 0 { if msg_len == 0 { let mut val: u32; - (val, offset) = LEB128::read_leb128(&recv_buffer); + (val, offset) = read_leb128(&recv_buffer); msg_len = val as usize; if recv_buffer[offset] != 0x00 { @@ -98,7 +98,7 @@ impl MinecraftStatus { offset += 1; // skip message type bit let offset2: usize; - (val, offset2) = LEB128::read_leb128(&recv_buffer[offset..]); + (val, offset2) = read_leb128(&recv_buffer[offset..]); object_len = val as usize; offset += offset2; } @@ -151,7 +151,7 @@ fn _description(description: &MinecraftDescription) -> String { fn send_packet(stream: &mut TcpStream, data: &[u8]) -> Result<()> { let mut packet: Vec = Vec::new(); - LEB128::write_leb128(&mut packet, data.len() as u64); + write_leb128(&mut packet, data.len() as u64); packet.extend_from_slice(&data); stream.write(&packet).unwrap(); diff --git a/src/thread.rs b/src/thread.rs new file mode 100644 index 0000000..a6cae8f --- /dev/null +++ b/src/thread.rs @@ -0,0 +1,81 @@ +use std::{sync::{mpsc, Arc, Mutex}, thread}; + +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(..) { + worker.thread.join().expect( + format!("Error in worker {}", worker.id).as_str()); + } + } +} + +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 } + } +} +