From 6bd7379df3447cc533f5c6fec69d2b443368cb94 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Jun 2025 17:57:51 +0100 Subject: [PATCH] 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}} +
+
+ + +