use std::io::Result; use std::net::{IpAddr, ToSocketAddrs}; use std::str::FromStr; use std::env; use mcstatusface::http::{HttpServer, StatusCode}; use mcstatusface::status::MinecraftStatus; use mcstatusface::dns::resolve_srv_port; #[derive(serde::Serialize)] struct MinecraftStatusResponse<'a> { version: &'a String, players: u32, max_players: u32, enforces_secure_chat: bool, favicon: Option<&'a String>, motd: String, } const DEFAULT_PORT: u16 = 25565; 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); } if args[1] != "serve" { let mut address = String::from(args[1].as_str()); 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(); let status = MinecraftStatus::fetch(address).unwrap(); 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()); return Ok(()); } 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, 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()); if request.method() != "GET" { response.status(StatusCode::NotFound); 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.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()); 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(); address = address.replace("%3A", ":"); 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); 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(e) => { println!( "Failed to connect to {} ({}): {}", query_address, address, e); 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, enforces_secure_chat: status.enforces_secure_chat(), favicon: status.favicon.as_ref(), motd: status.parse_description(), }; query_response = format!( "

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, if minecraft_status.enforces_secure_chat { "true" } else { "false" }, 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(); } // 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(); address = address.replace("%3A", ":"); 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); 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, enforces_secure_chat: status.enforces_secure_chat(), favicon: status.favicon.as_ref(), 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() } } } 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("'", "'") }