Compare commits

..

No commits in common. "53752c46c5efbe7dda76d11c30372c57baea1e9e" and "fc34b1e3282198fec516c62aee22b85ec0d063b7" have entirely different histories.

4 changed files with 217 additions and 252 deletions

View file

@ -108,7 +108,7 @@ pub fn parse_srv_response(mut ptr: usize, recv: &[u8]) -> Option<DnsSrvResponse>
Some(data) Some(data)
} }
pub fn resolve_srv_port(domain: &str) -> Option<u16> { pub fn resolve_srv_port(domain: &String) -> Option<u16> {
let request = create_dns_query( let request = create_dns_query(
&("_minecraft._tcp.".to_string() + domain), &("_minecraft._tcp.".to_string() + domain),
RECORD_TYPE_SRV, RECORD_TYPE_SRV,

View file

@ -48,8 +48,8 @@ pub struct Request<'a> {
path: &'a str, path: &'a str,
method: &'a str, method: &'a str,
version: &'a str, version: &'a str,
headers: HashMap<String, String>, headers: HashMap<String, &'a str>,
query: HashMap<String, String>, query: HashMap<&'a str, &'a str>,
body: Option<String>, body: Option<String>,
real_address: IpAddr, real_address: IpAddr,
} }
@ -65,15 +65,15 @@ impl<'a> Request<'a> {
let mut path = request_line_split[1]; let mut path = request_line_split[1];
let version = request_line_split[2]; let version = request_line_split[2];
let mut query: HashMap<String, String> = HashMap::new(); let mut query: HashMap<&'a str, &'a str> = HashMap::new();
match request_line_split[1].split_once("?") { match request_line_split[1].split_once("?") {
Some((path_without_query, query_string)) => { Some((path_without_query, query_string)) => {
path = path_without_query; 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 { for pair in query_splits {
match pair.split_once("=") { match pair.split_once("=") {
Some((name, value)) => { Some((name, value)) => {
query.insert(name.to_owned(), value.to_owned()); query.insert(name, value);
} }
None => { continue; } None => { continue; }
} }
@ -82,7 +82,7 @@ impl<'a> Request<'a> {
None => {}, None => {},
} }
let mut headers: HashMap<String, String> = HashMap::new(); let mut headers: HashMap<String, &'a str> = HashMap::new();
if lines.len() > 1 { if lines.len() > 1 {
let mut i: usize = 1; let mut i: usize = 1;
loop { loop {
@ -90,9 +90,7 @@ impl<'a> Request<'a> {
let line = &lines[i]; let line = &lines[i];
if line.len() == 0 || !line.contains(":") { break; } if line.len() == 0 || !line.contains(":") { break; }
let (name, value) = line.split_once(":").unwrap(); let (name, value) = line.split_once(":").unwrap();
headers.insert( headers.insert(name.to_lowercase(), value.trim());
name.to_lowercase().to_owned(),
value.trim().to_owned());
i += 1; i += 1;
} }
} }
@ -151,10 +149,10 @@ impl<'a> Request<'a> {
pub fn body(&self) -> &Option<String> { pub fn body(&self) -> &Option<String> {
&self.body &self.body
} }
pub fn headers(&self) -> &HashMap<String, String> { pub fn headers(&self) -> &HashMap<String, &'a str> {
&self.headers &self.headers
} }
pub fn query(&self) -> &HashMap<String, String> { pub fn query(&self) -> &HashMap<&'a str, &'a str> {
&self.query &self.query
} }
} }

View file

@ -1,199 +1,24 @@
use std::io::{Error, ErrorKind, Result}; use std::io::Result;
use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::net::{IpAddr, ToSocketAddrs};
use std::str::FromStr; use std::str::FromStr;
use std::env; use std::env;
use mcstatusface::http::{HttpServer, Request, Response, StatusCode}; use mcstatusface::http::{HttpServer, StatusCode};
use mcstatusface::status::MinecraftStatus; use mcstatusface::status::MinecraftStatus;
use mcstatusface::dns::resolve_srv_port; use mcstatusface::dns::resolve_srv_port;
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
struct MinecraftStatusResponse<'a> { struct MinecraftStatusResponse<'a> {
version: &'a str, version: &'a String,
players: u32, players: u32,
max_players: u32, max_players: u32,
enforces_secure_chat: bool, enforces_secure_chat: bool,
favicon: Option<&'a str>, favicon: Option<&'a String>,
motd: &'a str, motd: String,
} }
const DEFAULT_PORT: u16 = 25565; const DEFAULT_PORT: u16 = 25565;
fn parse_address(address: &str) -> Result<SocketAddr> {
// 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<StatusCode> {
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!(
"<hr/>
<h2>Server Details</h2>
<pre><code>Invalid server address: {}.</pre></code>",
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!(
"<hr/>
<h2>Server Details</h2>
<pre><code>Failed to connect to {} ({}).</pre></code>",
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!(
"<hr/>
<h2>Server Details</h2>
<p>
<strong>Favicon:</strong></br>
<img width=\"64\" height=\"64\" src=\"{}\"></br>
<strong>Version:</strong> <code>{}</code><br/>
<strong>Players:</strong> <code>{}/{}</code><br/>
<strong>Enforces Secure Chat:</strong> <code>{}</code></br>
<strong>MOTD:</strong>
</p>
<pre id=\"motd\"><code>{}</code></pre>",
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<StatusCode> {
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=<server address>\n".to_string());
}
return response.send();
}
fn main() -> Result<()> { fn main() -> Result<()> {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
if args.len() < 2 { if args.len() < 2 {
@ -210,8 +35,20 @@ env!("CARGO_PKG_VERSION"));
} }
if args[1] != "serve" { if args[1] != "serve" {
let address = parse_address(&args[1]).expect("Failed to parse address"); let mut address = String::from(args[1].as_str());
let status = MinecraftStatus::fetch(&address).unwrap(); 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!("Version: {} ({})", status.version.name, status.version.protocol);
println!("Players: {}/{}", status.players.online, status.players.max); println!("Players: {}/{}", status.players.online, status.players.max);
@ -264,22 +101,170 @@ env!("CARGO_PKG_VERSION"));
if request.headers().get("accept").is_some_and( if request.headers().get("accept").is_some_and(
|accept| accept.contains("text/html") |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!(
"<hr/>
<h2>Server Details</h2>
<pre><code>Invalid server address: {}.</pre></code>",
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!(
"<hr/>
<h2>Server Details</h2>
<pre><code>Failed to connect to {}.</pre></code>",
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!(
"<hr/>
<h2>Server Details</h2>
<p>
<strong>Favicon:</strong></br>
<img width=\"64\" height=\"64\" src=\"{}\"></br>
<strong>Version:</strong> <code>{}</code><br/>
<strong>Players:</strong> <code>{}/{}</code><br/>
<strong>Enforces Secure Chat:</strong> <code>{}</code></br>
<strong>MOTD:</strong>
</p>
<pre id=\"motd\"><code>{}</code></pre>",
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 // JSON response
return handle_json_request(request, &mut response); match request.query().get("s") {
None => {
response.status(StatusCode::OK);
response.body("?s=<server address>\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.status(StatusCode::NotFound);
response.body("Not Found".to_string()); response.body("Not Found".to_string());
return response.send(); return response.send();
}).expect("Failed to start HTTP server"); }).unwrap();
Ok(()) Ok(())
} }
fn sanitize_html(input: &str) -> String { fn sanitize_html(input: &String) -> String {
input input
.replace("&", "&amp;") .replace("&", "&amp;")
.replace("<", "&lt;") .replace("<", "&lt;")

View file

@ -1,6 +1,5 @@
use std::io::{Error, ErrorKind, Read, Write, Result}; use std::io::{Error, ErrorKind, Read, Write, Result};
use std::net::{SocketAddr, TcpStream}; use std::net::{SocketAddr, TcpStream};
use std::time::Duration;
use crate::leb128::{read_leb128, write_leb128}; use crate::leb128::{read_leb128, write_leb128};
@ -56,35 +55,27 @@ pub struct MinecraftStatus {
} }
impl MinecraftStatus { impl MinecraftStatus {
pub fn fetch(address: &SocketAddr) -> Result<MinecraftStatus> { pub fn fetch(address: SocketAddr) -> Result<MinecraftStatus> {
//println!("Connecting to {address}..."); // 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()); } if stream.is_err() { return Err(stream.unwrap_err()); }
// println!("Connected!");
//println!("Connected!");
let mut stream = stream.unwrap(); let mut stream = stream.unwrap();
let mut send_buffer: Vec<u8> = Vec::new(); let mut send_buffer: Vec<u8> = Vec::new();
//println!("Sending payload..."); // println!("Sending payload...");
send_buffer.push(0x00);
send_buffer.push(0x00); // packet ID write_leb128(&mut send_buffer, 769); // 1.21.4
write_leb128(&mut send_buffer, 769); // "i am 1.21.4" write_leb128(&mut send_buffer, address.ip().to_string().len().try_into().unwrap());
write_leb128(&mut send_buffer, // upcoming address length send_buffer.extend_from_slice(address.ip().to_string().as_bytes());
address.ip().to_string().len().try_into().unwrap()); send_buffer.extend_from_slice(&address.port().to_be_bytes());
send_buffer.extend_from_slice( // the address write_leb128(&mut send_buffer, 1);
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, &send_buffer).unwrap();
send_packet(&mut stream, &[0x00]).unwrap(); send_packet(&mut stream, &[0x00]).unwrap();
//println!("Payload sent, receiving...\n");
let mut data: Vec<u8> = Vec::new(); let mut data: Vec<u8> = Vec::new();
let mut len: usize = 0; let mut len: usize = 0;
let mut msg_len: usize = 0; let mut msg_len: usize = 0;
@ -93,42 +84,33 @@ impl MinecraftStatus {
loop { loop {
let mut recv_buffer: [u8; 10240] = [0; 10240]; let mut recv_buffer: [u8; 10240] = [0; 10240];
match stream.read(&mut recv_buffer) { len += stream.read(&mut recv_buffer)?;
Ok(_len) => len += _len,
Err(e) => return Err(e)
};
if len == 0 { if len > 0 {
return Err(Error::new(ErrorKind::HostUnreachable, format!("No data received from remote"))); 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 offset2: usize;
let mut val: u32; (val, offset2) = read_leb128(&recv_buffer[offset..]);
(val, offset) = read_leb128(&recv_buffer); object_len = val as usize;
msg_len = val as usize; offset += offset2;
}
if recv_buffer[offset] != 0x00 { data.extend_from_slice(&recv_buffer);
return Err(Error::new(ErrorKind::InvalidData, format!("Expected packet type 0x00, but got 0x{:02x?}!", recv_buffer[offset]))); 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 msg = std::str::from_utf8(&data[offset..]).unwrap().trim();
let data = std::str::from_utf8(&data[offset..]).expect("Failed to parse UTF-8 data"); let sanitised: String = msg.chars().filter(|&c| c >= '\u{20}' || c == '\n' || c == '\r' || c == '\t').collect();
let sanitised: String = data.chars().filter( let status: MinecraftStatus = serde_json::from_slice(sanitised.as_bytes()).unwrap();
|&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) 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) => { MinecraftDescription::Plain(description) => {
description.to_owned() description.clone()
} }
} }
} }