diff --git a/src/dns.rs b/src/dns.rs index 5b1750f..82ab7d4 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: &str) -> Option { +pub fn resolve_srv_port(domain: &String) -> Option { let request = create_dns_query( &format!("_minecraft._tcp.{}", domain), RECORD_TYPE_SRV, diff --git a/src/http.rs b/src/http.rs index 1379016..90e0d49 100644 --- a/src/http.rs +++ b/src/http.rs @@ -41,15 +41,15 @@ impl StatusCode { } } -type HttpHandlerFunc = fn(&Request, Response, bool) -> Result; +type HttpHandlerFunc = fn(&Request, Response) -> Result; pub struct Request<'a> { stream: &'a TcpStream, path: &'a str, method: &'a str, version: &'a str, - headers: HashMap, - query: HashMap, + headers: HashMap, + query: HashMap<&'a str, &'a str>, 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 = HashMap::new(); + 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; - let query_splits: Vec<&str> = query_string.split("&").collect(); + let query_splits: Vec<&'a str> = query_string.split("&").collect(); for pair in query_splits { match pair.split_once("=") { Some((name, value)) => { - query.insert(name.to_owned(), value.to_owned()); + query.insert(name, value); } 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,9 +90,7 @@ 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().to_owned(), - value.trim().to_owned()); + headers.insert(name.to_lowercase(), value.trim()); i += 1; } } @@ -151,10 +149,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 { + pub fn query(&self) -> &HashMap<&'a str, &'a str> { &self.query } } @@ -213,21 +211,20 @@ impl<'a> Response<'a> { } } -pub struct HttpServer<'a> { - address: &'a str, +pub struct HttpServer { + address: String, port: u16, trusted_proxies: Arc>, max_connections: usize, - verbose: bool, } -impl HttpServer <'_> { - pub fn new(address: &'_ str, max_connections: usize, trusted_proxies: Vec, verbose: bool) -> HttpServer<'_> { - let mut _address = address; +impl HttpServer { + pub fn new(address: String, max_connections: usize, trusted_proxies: Vec) -> HttpServer { + let mut _address = address.clone(); let mut _port: u16 = 8080; match address.split_once(":") { Some((ip, port)) => { - _address = ip; + _address = ip.to_string(); _port = port.parse::().expect(format!("Invalid port {}", port).as_str()); } None => {} @@ -237,14 +234,12 @@ 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); @@ -253,7 +248,7 @@ impl HttpServer <'_> { Ok(stream) => { let trusted_proxies = self.trusted_proxies.clone(); pool.execute(move || { - HttpServer::handle_client(&stream, handler, trusted_proxies, verbose); + HttpServer::handle_client(&stream, handler, trusted_proxies); }); } Err(e) => { @@ -265,7 +260,7 @@ impl HttpServer <'_> { Ok(()) } - fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc>, verbose: bool) { + fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc>) { let buf_reader = BufReader::new(stream); let http_request: Vec = buf_reader .lines() @@ -283,7 +278,7 @@ impl HttpServer <'_> { let response = Response::new(stream); let start_date = Local::now(); - match handler(&request, response, verbose) { + match handler(&request, response) { Ok(status) => { let end_date = Local::now(); diff --git a/src/main.rs b/src/main.rs index 5bd96c2..1eb88cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,202 +1,29 @@ use std::io::{Error, ErrorKind, Result}; -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::net::{IpAddr, ToSocketAddrs}; use std::str::FromStr; use std::env; -use mcstatusface::http::{HttpServer, Request, Response, StatusCode}; +use mcstatusface::http::{HttpServer, StatusCode}; use mcstatusface::status::MinecraftStatus; use mcstatusface::dns::resolve_srv_port; #[derive(serde::Serialize)] struct MinecraftStatusResponse<'a> { - version: &'a str, + version: &'a String, players: u32, max_players: u32, enforces_secure_chat: bool, - favicon: Option<&'a str>, - motd: &'a str, + favicon: Option<&'a String>, + motd: String, } const DEFAULT_PORT: u16 = 25565; -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 +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]] @@ -204,53 +31,70 @@ $ 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(&String::from("-v")); + let verbose = args.contains(&"-v".to_string()); - 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); + 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) } }; - let address = parse_address(&address_arg).expect("Failed to parse address"); - 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()); + let mut error: Option = None; - return Ok(()); - }, - Err (e) => { - return Err(Error::new( + 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( 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"; - let address_args = args[1..].iter() - .filter(|arg| *arg != "-v" && *arg != "serve") - .collect::>(); - if !address_args.is_empty() { address = address_args[0] } + 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![] } @@ -266,7 +110,7 @@ fn main() -> Result<()> { } }; - HttpServer::new(address, 16, trusted_proxies, verbose).start(|request, mut response, verbose| { + 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()); @@ -288,22 +132,170 @@ fn main() -> Result<()> { if request.headers().get("accept").is_some_and( |accept| accept.contains("text/html") ) { - return handle_html_request(request, &mut response, verbose); + // 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(); } // JSON response - return handle_json_request(request, &mut response, verbose); + 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() + } + } } response.status(StatusCode::NotFound); response.body("Not Found".to_string()); return response.send(); - }).expect("Failed to start HTTP server"); + }).unwrap(); Ok(()) } -fn sanitize_html(input: &str) -> String { +fn sanitize_html(input: &String) -> String { input .replace("&", "&") .replace("<", "<") diff --git a/src/status.rs b/src/status.rs index 8e1185d..f62a2ae 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 = 5; +const TIMEOUT_SECS: u64 = 10; #[derive(serde::Serialize, serde::Deserialize)] pub struct MinecraftVersion { @@ -58,30 +58,24 @@ 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 mut stream = match TcpStream::connect_timeout( - address, Duration::from_secs(TIMEOUT_SECS)) { - Ok(stream) => stream, - Err(e) => { - return Err(e); - } - }; + let stream = TcpStream::connect_timeout( + &address, Duration::from_secs(TIMEOUT_SECS)); + if stream.is_err() { return Err(stream.unwrap_err()); } if verbose { println!("Connected!"); } - - if verbose { println!("Sending payload..."); } + let mut stream = stream.unwrap(); 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 + + 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); send_packet(&mut stream, &send_buffer).unwrap(); send_packet(&mut stream, &[0x00]).unwrap(); @@ -96,42 +90,33 @@ impl MinecraftStatus { loop { let mut recv_buffer: [u8; 10240] = [0; 10240]; - match stream.read(&mut recv_buffer) { - Ok(_len) => len += _len, - Err(e) => return Err(e) - }; + len += stream.read(&mut recv_buffer)?; - if len == 0 { - return Err(Error::new(ErrorKind::HostUnreachable, format!("No data received from remote"))); - } + if len > 0 { + if msg_len == 0 { + let mut val: u32; + (val, offset) = read_leb128(&recv_buffer); + msg_len = val as usize; - //println!("< {} bytes\n", len); + 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 - 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]))); + 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; } - 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 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"); + 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(); Ok(status) } @@ -160,12 +145,12 @@ fn _description(description: &MinecraftDescription) -> String { } } } - return description.text.to_owned() + &extras; + return description.text.clone() + &extras; } - description.text.to_owned() + description.text.clone() } MinecraftDescription::Plain(description) => { - description.to_owned() + description.clone() } } }