diff --git a/Cargo.lock b/Cargo.lock index 2afe0a5..7a04376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "mcstatusface" -version = "1.1.1" +version = "1.0.0" dependencies = [ "chrono", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7140036..70db94f 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.1" +version = "1.0.0" edition = "2024" [dependencies] diff --git a/README.md b/README.md index 10e2605..6ab3e2f 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,11 @@ 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 simply be run with `mcstatusface `. +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 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 .`) +Alternatively, you can simply run `./mcstatusface `, and the tool will provide server details in plain-text format. diff --git a/src/http.rs b/src/http.rs index 131c96e..18b406d 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,10 +1,4 @@ -use std::{ - collections::HashMap, - io::{BufRead, BufReader, Error, ErrorKind, Result, Write}, - net::{IpAddr, SocketAddr, TcpListener, TcpStream}, - str::FromStr, - sync::Arc, -}; +use std::{collections::HashMap, io::{BufRead, BufReader, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}}; use chrono::Local; @@ -48,27 +42,22 @@ pub struct Request<'a> { path: &'a str, method: &'a str, version: &'a str, - headers: HashMap, + 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, trusted_proxies: Vec) -> Result> { + 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 mut path = request_line_split[1]; + let path = request_line_split[1]; let version = request_line_split[2]; let mut query: HashMap<&'a str, &'a str> = HashMap::new(); - match request_line_split[1].split_once("?") { - Some((path_without_query, query_string)) => { - path = path_without_query; + match path.split_once("?") { + Some((_, query_string)) => { let query_splits: Vec<&'a str> = query_string.split("&").collect(); for pair in query_splits { match pair.split_once("=") { @@ -82,7 +71,7 @@ impl<'a> Request<'a> { None => {}, } - let mut headers: HashMap = HashMap::new(); + let mut headers: HashMap<&'a str, &'a str> = HashMap::new(); if lines.len() > 1 { let mut i: usize = 1; loop { @@ -90,27 +79,11 @@ 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.to_lowercase(), value.trim()); + headers.insert(name, value.trim()); i += 1; } } - 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") && @@ -127,16 +100,12 @@ 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 } @@ -149,7 +118,7 @@ impl<'a> Request<'a> { pub fn body(&self) -> &Option { &self.body } - pub fn headers(&self) -> &HashMap { + pub fn headers(&self) -> &HashMap<&'a str, &'a str> { &self.headers } pub fn query(&self) -> &HashMap<&'a str, &'a str> { @@ -214,12 +183,11 @@ 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, trusted_proxies: Vec) -> HttpServer { + pub fn new(address: String, max_connections: usize) -> HttpServer { let mut _address = address.clone(); let mut _port: u16 = 8080; match address.split_once(":") { @@ -232,7 +200,6 @@ impl HttpServer { HttpServer { address: _address, port: _port, - trusted_proxies: Arc::new(trusted_proxies), max_connections, } } @@ -246,9 +213,8 @@ 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, trusted_proxies); + HttpServer::handle_client(&stream, handler); }); } Err(e) => { @@ -260,7 +226,7 @@ impl HttpServer { Ok(()) } - fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc>) { + fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc) { let buf_reader = BufReader::new(stream); let http_request: Vec = buf_reader .lines() @@ -268,7 +234,7 @@ impl HttpServer { .take_while(|line| !line.is_empty()) .collect(); - let request = Request::new(stream, &http_request, trusted_proxies.to_vec()); + let request = Request::new(stream, &http_request); if request.is_err() { eprintln!("Failed to process request: {}", request.err().unwrap()); return; @@ -281,7 +247,6 @@ impl HttpServer { match handler(&request, response) { Ok(status) => { let end_date = Local::now(); - println!( "[{}] {} {} {} - {} {} - {}ms - {} ({})", start_date.format("%Y-%m-%d %H:%M:%S"), @@ -296,7 +261,7 @@ impl HttpServer { (end_date - start_date).num_milliseconds(), request.headers().get("User-Agent").map_or("[]", |v| v), - request.real_address().to_string(), + request.address().unwrap().ip(), ); } Err(e) => { diff --git a/src/main.rs b/src/main.rs index a4309af..645fec8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,15 @@ use std::io::{Result}; -use std::net::{IpAddr, ToSocketAddrs}; -use std::str::FromStr; -use std::{env}; +use std::net::{ToSocketAddrs}; +use std::env; 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 { - 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); + eprintln!("Usage: {} [serve] ", args[0]); + std::process::exit(1); } if args[1] != "serve" { @@ -47,24 +30,9 @@ 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") { - Err(_) => { vec![] } - Ok(envar) => { - let mut trusted_proxies: Vec = Vec::new(); - for addr in envar.split(",") { - match IpAddr::from_str(addr) { - Err(_) => {} - Ok(addr) => { trusted_proxies.push(addr); } - } - } - trusted_proxies - } - }; - HttpServer::new(address, 64, trusted_proxies).start(|request, mut response| { + 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" { @@ -72,159 +40,51 @@ env!("CARGO_PKG_VERSION")); return response.send() } - if request.path() == "/style/index.css" { - 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.query().contains_key("s") { + response.status(StatusCode::BadRequest); + // TODO: nice index landing page for browsers + response.body("?s=\n".to_string()); + return response.send() } - if request.path() == "/" { - if request.headers().get("accept").is_some_and( - |accept| accept.contains("text/html") - ) { - // HTML response - let content = include_str!("views/index.html"); - response.set_header("Content-Type", "text/html".to_string()); + 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); - 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"); } - match address.to_socket_addrs() { - Err(_) => { - response.status(StatusCode::BadRequest); - 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

-
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(), - }; - - 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(), - ); - } - } - } - } - } - } - - let response_content = content - .replace("{{response}}", &query_response) - .replace("{{host}}", match request.headers().get("host") { - 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(); + response.set_header("Content-Type", "application/json".to_string()); + response.body(json); } - - // JSON response - match request.query().get("s") { - None => { - response.status(StatusCode::OK); - 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"); } - match address.to_socket_addrs() { - Err(_) => { - response.status(StatusCode::BadRequest); - 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 format response from {} to JSON: {}", address, e); - response.status(StatusCode::InternalServerError); - response.body("Internal Server Error\n".to_string()); - } - Ok(json) => { - response.status(StatusCode::OK); - response.set_header("Content-Type", "application/json".to_string()); - response.body(json + "\n"); - } - } - } - } - - } - } - return response.send() - } + 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()); } } - response.status(StatusCode::NotFound); - response.body("Not Found".to_string()); - return response.send(); + response.send() }).unwrap(); Ok(()) } - -fn sanitize_html(input: &String) -> String { - input - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'") -} diff --git a/src/public/style/index.css b/src/public/style/index.css deleted file mode 100644 index 59c28af..0000000 --- a/src/public/style/index.css +++ /dev/null @@ -1,62 +0,0 @@ -: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#motd { - margin-top: -1em -} - -pre#motd code { - display: block; - width: fit-content; - min-width: 440px; -} - -footer { - font-family: monospace; -} diff --git a/src/status.rs b/src/status.rs index 3af55f6..c0363ad 100644 --- a/src/status.rs +++ b/src/status.rs @@ -51,10 +51,8 @@ impl MinecraftStatus { pub fn fetch(address: SocketAddr) -> Result { // println!("Connecting to {address}..."); - let stream = TcpStream::connect(address.to_string()); - if stream.is_err() { return Err(stream.unwrap_err()); } + let mut stream = TcpStream::connect(address.to_string()).unwrap(); // println!("Connected!"); - let mut stream = stream.unwrap(); let mut send_buffer: Vec = Vec::new(); diff --git a/src/views/index.html b/src/views/index.html deleted file mode 100644 index 5bdd9d3..0000000 --- a/src/views/index.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - Crafty McStatusFace - - - - - - - - - - - -
-

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}} -
-
- - -