diff --git a/src/dns.rs b/src/dns.rs index 82ab7d4..5b1750f 100644 --- a/src/dns.rs +++ b/src/dns.rs @@ -108,7 +108,7 @@ pub fn parse_srv_response(mut ptr: usize, recv: &[u8]) -> Option Some(data) } -pub fn resolve_srv_port(domain: &String) -> Option { +pub fn resolve_srv_port(domain: &str) -> Option { let request = create_dns_query( &format!("_minecraft._tcp.{}", domain), RECORD_TYPE_SRV, diff --git a/src/http.rs b/src/http.rs index 90e0d49..1379016 100644 --- a/src/http.rs +++ b/src/http.rs @@ -41,15 +41,15 @@ impl StatusCode { } } -type HttpHandlerFunc = fn(&Request, Response) -> Result; +type HttpHandlerFunc = fn(&Request, Response, bool) -> Result; pub struct Request<'a> { stream: &'a TcpStream, path: &'a str, method: &'a str, version: &'a str, - headers: HashMap, - query: HashMap<&'a str, &'a str>, + headers: HashMap, + query: HashMap, body: Option, real_address: IpAddr, } @@ -65,15 +65,15 @@ impl<'a> Request<'a> { let mut path = request_line_split[1]; let version = request_line_split[2]; - let mut query: HashMap<&'a str, &'a str> = HashMap::new(); + let mut query: HashMap = HashMap::new(); 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(); + let query_splits: Vec<&str> = query_string.split("&").collect(); for pair in query_splits { match pair.split_once("=") { Some((name, value)) => { - query.insert(name, value); + query.insert(name.to_owned(), value.to_owned()); } None => { continue; } } @@ -82,7 +82,7 @@ impl<'a> Request<'a> { None => {}, } - let mut headers: HashMap = HashMap::new(); + let mut headers: HashMap = HashMap::new(); if lines.len() > 1 { let mut i: usize = 1; loop { @@ -90,7 +90,9 @@ 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.to_lowercase().to_owned(), + value.trim().to_owned()); i += 1; } } @@ -149,10 +151,10 @@ impl<'a> Request<'a> { pub fn body(&self) -> &Option { &self.body } - pub fn headers(&self) -> &HashMap { + pub fn headers(&self) -> &HashMap { &self.headers } - pub fn query(&self) -> &HashMap<&'a str, &'a str> { + pub fn query(&self) -> &HashMap { &self.query } } @@ -211,20 +213,21 @@ impl<'a> Response<'a> { } } -pub struct HttpServer { - address: String, +pub struct HttpServer<'a> { + address: &'a str, port: u16, trusted_proxies: Arc>, max_connections: usize, + verbose: bool, } -impl HttpServer { - pub fn new(address: String, max_connections: usize, trusted_proxies: Vec) -> HttpServer { - let mut _address = address.clone(); +impl HttpServer <'_> { + pub fn new(address: &'_ str, max_connections: usize, trusted_proxies: Vec, verbose: bool) -> HttpServer<'_> { + let mut _address = address; let mut _port: u16 = 8080; match address.split_once(":") { Some((ip, port)) => { - _address = ip.to_string(); + _address = ip; _port = port.parse::().expect(format!("Invalid port {}", port).as_str()); } None => {} @@ -234,12 +237,14 @@ impl HttpServer { port: _port, trusted_proxies: Arc::new(trusted_proxies), max_connections, + verbose, } } pub fn start(&self, handler: HttpHandlerFunc) -> Result<()> { let pool = ThreadPool::new(self.max_connections); let listener = TcpListener::bind(format!("{}:{}", self.address, self.port)).expect("Failed to bind to port"); + let verbose = self.verbose; println!("Now listening on {}:{}", self.address, self.port); @@ -248,7 +253,7 @@ impl HttpServer { Ok(stream) => { let trusted_proxies = self.trusted_proxies.clone(); pool.execute(move || { - HttpServer::handle_client(&stream, handler, trusted_proxies); + HttpServer::handle_client(&stream, handler, trusted_proxies, verbose); }); } Err(e) => { @@ -260,7 +265,7 @@ impl HttpServer { Ok(()) } - fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc>) { + fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc>, verbose: bool) { let buf_reader = BufReader::new(stream); let http_request: Vec = buf_reader .lines() @@ -278,7 +283,7 @@ impl HttpServer { let response = Response::new(stream); let start_date = Local::now(); - match handler(&request, response) { + match handler(&request, response, verbose) { Ok(status) => { let end_date = Local::now(); diff --git a/src/main.rs b/src/main.rs index 1eb88cb..5bd96c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,202 @@ use std::io::{Error, ErrorKind, Result}; -use std::net::{IpAddr, ToSocketAddrs}; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::str::FromStr; use std::env; -use mcstatusface::http::{HttpServer, StatusCode}; +use mcstatusface::http::{HttpServer, Request, Response, StatusCode}; use mcstatusface::status::MinecraftStatus; use mcstatusface::dns::resolve_srv_port; #[derive(serde::Serialize)] struct MinecraftStatusResponse<'a> { - version: &'a String, + version: &'a str, players: u32, max_players: u32, enforces_secure_chat: bool, - favicon: Option<&'a String>, - motd: String, + favicon: Option<&'a str>, + motd: &'a str, } 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 +fn parse_address(address: &str) -> Result { + // attempt to parse as IP + port + if let Ok(socket_addr) = SocketAddr::from_str(address) { + return Ok(socket_addr); + } + + // handle IPv6 address between brackets + let mut address = address; + if address.starts_with("[") && address.ends_with("]") { + address = &address[1..address.len() - 1]; + } + + let port: u16 = match resolve_srv_port(address) { + Some(port) => port, + None => DEFAULT_PORT, + }; + + // handle IP address with no port + if let Ok(ip) = IpAddr::from_str(address) { + return Ok(SocketAddr::new(ip, port)) + } + + match format!("{}:{}", address, port).to_socket_addrs() { + Ok(mut addr) => { + return Ok(addr.next().unwrap()) + } + Err(_) => { + return Err(Error::new( + ErrorKind::InvalidInput, + format!("Invalid server address: {}", address))) + } + } +} + +fn handle_html_request(request: &Request, response: &mut Response, verbose: bool) -> Result { + let content = include_str!("views/index.html"); + response.set_header("Content-Type", "text/html".to_string()); + response.status(StatusCode::OK); + let mut query_response = "".to_string(); + + let query_address = request.query().get("s"); + if let Some(query_address) = query_address { + match parse_address(&query_address.replace("%3A", ":")) { + Err(_) => { + response.status(StatusCode::BadRequest); + query_response = format!( + "
+

Server Details

+
Invalid server address: {}.
", + sanitize_html(query_address), + ); + }, + Ok(address) => { + match MinecraftStatus::fetch(&address, verbose) { + Err(err) => { + println!( + "Failed to connect to {} ({}): {}", + query_address, address, err); + + response.body(format!( + "Failed to connect to {} ({}).\n", + query_address, address)); + response.status(StatusCode::InternalServerError); + query_response = format!( + "
+

Server Details

+
Failed to connect to {} ({}).
", + sanitize_html(query_address), + 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_deref(), + motd: &status.parse_description(), + }; + + query_response = format!( + "
+

Server Details

+

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

+
{}
", + minecraft_status.favicon.map_or("", |fav| fav), + 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(); +} + +fn handle_json_request(request: &Request, response: &mut Response, verbose: bool) -> Result { + response.set_header("Content-Type", "text/plain".to_string()); + response.status(StatusCode::OK); + + let query_address = request.query().get("s"); + + if let Some(query_address) = query_address { + match parse_address(&query_address.replace("%3A", ":")) { + Err(_) => { + response.status(StatusCode::BadRequest); + response.body("Invalid server address.\n".to_string()); + } + Ok(address) => { + match MinecraftStatus::fetch(&address, verbose) { + Err(err) => { + println!( + "Failed to connect to {} ({}): {}", + query_address, address, err); + + response.status(StatusCode::InternalServerError); + response.body(format!( + "Failed to connect to {} ({}).\n", + query_address, address)); + } + 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_deref(), + 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"); + } + } + } + } + } + } + } else { + response.status(StatusCode::OK); + response.body("?s=\n".to_string()); + } + + return response.send(); +} + +fn print_help() { + println!( + r#"Crafty McStatusFace, v{} - made with <3 by ari melody Host a web API: $ mcstatusface serve [address[:port]] @@ -31,70 +204,53 @@ $ mcstatusface serve [address[:port]] Query a server: $ mcstatusface "#, env!("CARGO_PKG_VERSION")); +} + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + if args.len() < 2 { + print_help(); std::process::exit(0); } - let verbose = args.contains(&"-v".to_string()); + let verbose = args.contains(&String::from("-v")); - if args[1] != "serve" { - let mut address = String::from(args[args.len() - 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 addrs_iter = match address.to_socket_addrs() { - Ok(addr) => addr, - Err(e) => { - panic!("Failed to parse address {}: {}", address, e) + if !args[1..].contains(&String::from("serve")) { + let address_arg = match args[1..].iter().find(|arg| !arg.starts_with("-")) { + Some(arg) => arg, + None => { + print_help(); + std::process::exit(0); } }; + let address = parse_address(&address_arg).expect("Failed to parse address"); - let mut error: Option = None; + match MinecraftStatus::fetch(&address, verbose) { + Ok(status) => { + 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()); - if verbose { - println!( - "{} address{} available.", - addrs_iter.len(), - // horrible no good code but it's really funny to look at - // i'm sure this is awesome if you're into functional programming - (addrs_iter.len() == 1).then(|| "es").unwrap_or("")) - } - - for address in addrs_iter { - let status = match MinecraftStatus::fetch(address, verbose) { - Ok(status) => status, - Err (e) => { - error = Some(Error::new( + return Ok(()); + }, + Err (e) => { + return Err(Error::new( ErrorKind::Other, format!("Failed to fetch status: {}", e))); - continue - } - }; - - 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(()); - } - - if let Some(error) = error { - return Err(error); - } + } + }; } - let mut address = "0.0.0.0:8080".to_string(); - if args.len() > 2 { address = args[2].to_string() } + let mut address = "0.0.0.0:8080"; + let address_args = args[1..].iter() + .filter(|arg| *arg != "-v" && *arg != "serve") + .collect::>(); + if !address_args.is_empty() { address = address_args[0] } let trusted_proxies: Vec = match env::var("MCSTATUSFACE_TRUSTED_PROXIES") { Err(_) => { vec![] } @@ -110,7 +266,7 @@ env!("CARGO_PKG_VERSION")); } }; - HttpServer::new(address, 16, trusted_proxies).start(|request, mut response| { + HttpServer::new(address, 16, trusted_proxies, verbose).start(|request, mut response, verbose| { response.status(StatusCode::OK); response.set_header("Content-Type", "text/plain".to_string()); response.set_header("x-powered-by", "GIRL FUEL".to_string()); @@ -132,170 +288,22 @@ env!("CARGO_PKG_VERSION")); 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, false) { - 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, - 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(); + return handle_html_request(request, &mut response, verbose); } // 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, false) { - 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() - } - } + return handle_json_request(request, &mut response, verbose); } response.status(StatusCode::NotFound); response.body("Not Found".to_string()); return response.send(); - }).unwrap(); + }).expect("Failed to start HTTP server"); Ok(()) } -fn sanitize_html(input: &String) -> String { +fn sanitize_html(input: &str) -> String { input .replace("&", "&") .replace("<", "<") diff --git a/src/status.rs b/src/status.rs index f62a2ae..8e1185d 100644 --- a/src/status.rs +++ b/src/status.rs @@ -4,7 +4,7 @@ use std::time::Duration; use crate::leb128::{read_leb128, write_leb128}; -const TIMEOUT_SECS: u64 = 10; +const TIMEOUT_SECS: u64 = 5; #[derive(serde::Serialize, serde::Deserialize)] pub struct MinecraftVersion { @@ -58,24 +58,30 @@ pub struct MinecraftStatus { } impl MinecraftStatus { - pub fn fetch(address: SocketAddr, verbose: bool) -> Result { + pub fn fetch(address: &SocketAddr, verbose: bool) -> Result { if verbose { println!("Connecting to {address}..."); } - let stream = TcpStream::connect_timeout( - &address, Duration::from_secs(TIMEOUT_SECS)); - if stream.is_err() { return Err(stream.unwrap_err()); } + let mut stream = match TcpStream::connect_timeout( + address, Duration::from_secs(TIMEOUT_SECS)) { + Ok(stream) => stream, + Err(e) => { + return Err(e); + } + }; if verbose { println!("Connected!"); } - let mut stream = stream.unwrap(); - - let mut send_buffer: Vec = Vec::new(); if verbose { println!("Sending payload..."); } - send_buffer.push(0x00); - 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()); - write_leb128(&mut send_buffer, 1); + + let mut send_buffer: Vec = Vec::new(); + send_buffer.push(0x00); // packet ID + write_leb128(&mut send_buffer, 769); // "i am 1.21.4" + write_leb128(&mut send_buffer, // upcoming address length + address.ip().to_string().len().try_into().unwrap()); + send_buffer.extend_from_slice( // the address + address.ip().to_string().as_bytes()); + send_buffer.extend_from_slice( // the port + &address.port().to_be_bytes()); + write_leb128(&mut send_buffer, 1); // give me status please <3 send_packet(&mut stream, &send_buffer).unwrap(); send_packet(&mut stream, &[0x00]).unwrap(); @@ -90,33 +96,42 @@ impl MinecraftStatus { loop { let mut recv_buffer: [u8; 10240] = [0; 10240]; - len += stream.read(&mut recv_buffer)?; + match stream.read(&mut recv_buffer) { + Ok(_len) => len += _len, + Err(e) => return Err(e) + }; - if len > 0 { - if msg_len == 0 { - let mut val: u32; - (val, offset) = read_leb128(&recv_buffer); - msg_len = val as usize; + if len == 0 { + return Err(Error::new(ErrorKind::HostUnreachable, format!("No data received from remote"))); + } - if recv_buffer[offset] != 0x00 { - return Err(Error::new(ErrorKind::InvalidData, format!("Expected packet type 0x00, but got 0x{:02x?}!", recv_buffer[offset]))); - } - offset += 1; // skip message type bit + //println!("< {} bytes\n", len); - let offset2: usize; - (val, offset2) = read_leb128(&recv_buffer[offset..]); - object_len = val as usize; - offset += offset2; - } - data.extend_from_slice(&recv_buffer); - if len >= offset + object_len { - break; + if msg_len == 0 { + let mut val: u32; + (val, offset) = read_leb128(&recv_buffer); + msg_len = val as usize; + + if recv_buffer[offset] != 0x00 { + return Err(Error::new(ErrorKind::InvalidData, format!("Expected packet type 0x00, but got 0x{:02x?}!", recv_buffer[offset]))); } + offset += 1; // skip message type bit + + let offset2: usize; + (val, offset2) = read_leb128(&recv_buffer[offset..]); + object_len = val as usize; + offset += offset2; + } + data.extend_from_slice(&recv_buffer); + if len >= offset + object_len { + break; } } - 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(); - let status: MinecraftStatus = serde_json::from_slice(sanitised.as_bytes()).unwrap(); + + let data = std::str::from_utf8(&data[offset..]).expect("Failed to parse UTF-8 data"); + let sanitised: String = data.chars().filter( + |&c| c >= '\u{20}' || c == '\n' || c == '\r' || c == '\t').collect(); + let status: MinecraftStatus = serde_json::from_str(&sanitised).expect("Failed to parse JSON"); Ok(status) } @@ -145,12 +160,12 @@ fn _description(description: &MinecraftDescription) -> String { } } } - return description.text.clone() + &extras; + return description.text.to_owned() + &extras; } - description.text.clone() + description.text.to_owned() } MinecraftDescription::Plain(description) => { - description.clone() + description.to_owned() } } }