diff --git a/src/dns.rs b/src/dns.rs index f3c2edc..8cfb46c 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( &("_minecraft._tcp.".to_string() + domain), RECORD_TYPE_SRV, diff --git a/src/http.rs b/src/http.rs index 3ad56ba..90e0d49 100644 --- a/src/http.rs +++ b/src/http.rs @@ -48,8 +48,8 @@ pub struct Request<'a> { 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 } } diff --git a/src/main.rs b/src/main.rs index 615da37..fddb15f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,199 +1,24 @@ -use std::io::{Error, ErrorKind, Result}; -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::io::Result; +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) -> 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) { - 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) -> 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) { - 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 main() -> Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { @@ -210,8 +35,20 @@ env!("CARGO_PKG_VERSION")); } if args[1] != "serve" { - let address = parse_address(&args[1]).expect("Failed to parse address"); - let status = MinecraftStatus::fetch(&address).unwrap(); + 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); @@ -264,22 +101,170 @@ env!("CARGO_PKG_VERSION")); if request.headers().get("accept").is_some_and( |accept| accept.contains("text/html") ) { - return handle_html_request(request, &mut response); + // 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(_) => { + 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); + 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(); - }).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 28bbba5..7d28f96 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,6 +1,5 @@ use std::io::{Error, ErrorKind, Read, Write, Result}; use std::net::{SocketAddr, TcpStream}; -use std::time::Duration; use crate::leb128::{read_leb128, write_leb128}; @@ -56,35 +55,27 @@ pub struct MinecraftStatus { } impl MinecraftStatus { - pub fn fetch(address: &SocketAddr) -> Result { - //println!("Connecting to {address}..."); + pub fn fetch(address: SocketAddr) -> Result { + // println!("Connecting to {address}..."); - let stream = TcpStream::connect_timeout(address, Duration::new(5, 0)); + let stream = TcpStream::connect(address.to_string()); if stream.is_err() { return Err(stream.unwrap_err()); } - - //println!("Connected!"); - + // println!("Connected!"); let mut stream = stream.unwrap(); let mut send_buffer: Vec = Vec::new(); - //println!("Sending payload..."); - - 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 + // 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(); - //println!("Payload sent, receiving...\n"); - let mut data: Vec = Vec::new(); let mut len: usize = 0; let mut msg_len: usize = 0; @@ -93,42 +84,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) } @@ -157,12 +139,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() } } }