Merge branch 'main' into dev

This commit is contained in:
ari melody 2026-04-24 01:04:23 +01:00
commit ec4d6035e8
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
4 changed files with 79 additions and 49 deletions

View file

@ -7,7 +7,7 @@ const RECURSION_DESIRED: u16 = 1 << 8;
const RECORD_TYPE_SRV: u16 = 33; const RECORD_TYPE_SRV: u16 = 33;
const RECORD_CLASS_IN: u16 = 1; const RECORD_CLASS_IN: u16 = 1;
pub fn create_dns_query(qname: &String, qtype: u16, qclass: u16) -> Result<Vec<u8>> { pub fn create_dns_query(qname: &str, qtype: u16, qclass: u16) -> Result<Vec<u8>> {
let qname = match qname.is_ascii() { let qname = match qname.is_ascii() {
true => qname, true => qname,
false => return Err(Error::new(ErrorKind::InvalidInput, "domain is not valid ASCII")), false => return Err(Error::new(ErrorKind::InvalidInput, "domain is not valid ASCII")),
@ -110,7 +110,7 @@ pub fn parse_srv_response(mut ptr: usize, recv: &[u8]) -> Option<DnsSrvResponse>
pub fn resolve_srv_port(domain: &str) -> Option<u16> { pub fn resolve_srv_port(domain: &str) -> Option<u16> {
let request = create_dns_query( let request = create_dns_query(
&("_minecraft._tcp.".to_string() + domain), &format!("_minecraft._tcp.{}", domain),
RECORD_TYPE_SRV, RECORD_TYPE_SRV,
RECORD_CLASS_IN).unwrap(); RECORD_CLASS_IN).unwrap();

View file

@ -41,7 +41,7 @@ impl StatusCode {
} }
} }
type HttpHandlerFunc = fn(&Request, Response) -> Result<StatusCode>; type HttpHandlerFunc = fn(&Request, Response, bool) -> Result<StatusCode>;
pub struct Request<'a> { pub struct Request<'a> {
stream: &'a TcpStream, stream: &'a TcpStream,
@ -213,20 +213,21 @@ impl<'a> Response<'a> {
} }
} }
pub struct HttpServer { pub struct HttpServer<'a> {
address: String, address: &'a str,
port: u16, port: u16,
trusted_proxies: Arc<Vec<IpAddr>>, trusted_proxies: Arc<Vec<IpAddr>>,
max_connections: usize, max_connections: usize,
verbose: bool,
} }
impl HttpServer { impl HttpServer <'_> {
pub fn new(address: String, max_connections: usize, trusted_proxies: Vec<IpAddr>) -> HttpServer { pub fn new(address: &'_ str, max_connections: usize, trusted_proxies: Vec<IpAddr>, verbose: bool) -> HttpServer<'_> {
let mut _address = address.clone(); let mut _address = address;
let mut _port: u16 = 8080; let mut _port: u16 = 8080;
match address.split_once(":") { match address.split_once(":") {
Some((ip, port)) => { Some((ip, port)) => {
_address = ip.to_string(); _address = ip;
_port = port.parse::<u16>().expect(format!("Invalid port {}", port).as_str()); _port = port.parse::<u16>().expect(format!("Invalid port {}", port).as_str());
} }
None => {} None => {}
@ -236,12 +237,14 @@ impl HttpServer {
port: _port, port: _port,
trusted_proxies: Arc::new(trusted_proxies), trusted_proxies: Arc::new(trusted_proxies),
max_connections, max_connections,
verbose,
} }
} }
pub fn start(&self, handler: HttpHandlerFunc) -> Result<()> { pub fn start(&self, handler: HttpHandlerFunc) -> Result<()> {
let pool = ThreadPool::new(self.max_connections); let pool = ThreadPool::new(self.max_connections);
let listener = TcpListener::bind(format!("{}:{}", self.address, self.port)).expect("Failed to bind to port"); 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); println!("Now listening on {}:{}", self.address, self.port);
@ -250,7 +253,7 @@ impl HttpServer {
Ok(stream) => { Ok(stream) => {
let trusted_proxies = self.trusted_proxies.clone(); let trusted_proxies = self.trusted_proxies.clone();
pool.execute(move || { pool.execute(move || {
HttpServer::handle_client(&stream, handler, trusted_proxies); HttpServer::handle_client(&stream, handler, trusted_proxies, verbose);
}); });
} }
Err(e) => { Err(e) => {
@ -262,7 +265,7 @@ impl HttpServer {
Ok(()) Ok(())
} }
fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc<Vec<IpAddr>>) { fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc<Vec<IpAddr>>, verbose: bool) {
let buf_reader = BufReader::new(stream); let buf_reader = BufReader::new(stream);
let http_request: Vec<String> = buf_reader let http_request: Vec<String> = buf_reader
.lines() .lines()
@ -280,7 +283,7 @@ impl HttpServer {
let response = Response::new(stream); let response = Response::new(stream);
let start_date = Local::now(); let start_date = Local::now();
match handler(&request, response) { match handler(&request, response, verbose) {
Ok(status) => { Ok(status) => {
let end_date = Local::now(); let end_date = Local::now();

View file

@ -53,7 +53,7 @@ fn parse_address(address: &str) -> Result<SocketAddr> {
} }
} }
fn handle_html_request(request: &Request, response: &mut Response) -> Result<StatusCode> { fn handle_html_request(request: &Request, response: &mut Response, verbose: bool) -> Result<StatusCode> {
let content = include_str!("views/index.html"); let content = include_str!("views/index.html");
response.set_header("Content-Type", "text/html".to_string()); response.set_header("Content-Type", "text/html".to_string());
response.status(StatusCode::OK); response.status(StatusCode::OK);
@ -72,7 +72,7 @@ fn handle_html_request(request: &Request, response: &mut Response) -> Result<Sta
); );
}, },
Ok(address) => { Ok(address) => {
match MinecraftStatus::fetch(&address) { match MinecraftStatus::fetch(&address, verbose) {
Err(err) => { Err(err) => {
println!( println!(
"Failed to connect to {} ({}): {}", "Failed to connect to {} ({}): {}",
@ -136,7 +136,7 @@ fn handle_html_request(request: &Request, response: &mut Response) -> Result<Sta
return response.send(); return response.send();
} }
fn handle_json_request(request: &Request, response: &mut Response) -> Result<StatusCode> { fn handle_json_request(request: &Request, response: &mut Response, verbose: bool) -> Result<StatusCode> {
response.set_header("Content-Type", "text/plain".to_string()); response.set_header("Content-Type", "text/plain".to_string());
response.status(StatusCode::OK); response.status(StatusCode::OK);
@ -149,7 +149,7 @@ fn handle_json_request(request: &Request, response: &mut Response) -> Result<Sta
response.body("Invalid server address.\n".to_string()); response.body("Invalid server address.\n".to_string());
} }
Ok(address) => { Ok(address) => {
match MinecraftStatus::fetch(&address) { match MinecraftStatus::fetch(&address, verbose) {
Err(err) => { Err(err) => {
println!( println!(
"Failed to connect to {} ({}): {}", "Failed to connect to {} ({}): {}",
@ -194,11 +194,9 @@ fn handle_json_request(request: &Request, response: &mut Response) -> Result<Sta
return response.send(); return response.send();
} }
fn main() -> Result<()> { fn print_help() {
let args: Vec<String> = env::args().collect(); println!(
if args.len() < 2 { r#"Crafty McStatusFace, v{} - made with <3 by ari melody
println!(
r#"Crafty McStatusFace, v{} - made with <3 by ari melody
Host a web API: Host a web API:
$ mcstatusface serve [address[:port]] $ mcstatusface serve [address[:port]]
@ -206,27 +204,53 @@ $ mcstatusface serve [address[:port]]
Query a server: Query a server:
$ mcstatusface <address[:port]>"#, $ mcstatusface <address[:port]>"#,
env!("CARGO_PKG_VERSION")); env!("CARGO_PKG_VERSION"));
}
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
print_help();
std::process::exit(0); std::process::exit(0);
} }
if args[1] != "serve" { let verbose = args.contains(&String::from("-v"));
let address = parse_address(&args[1]).expect("Failed to parse address");
let status = MinecraftStatus::fetch(&address).unwrap();
println!("Version: {} ({})", status.version.name, status.version.protocol); if !args[1..].contains(&String::from("serve")) {
println!("Players: {}/{}", status.players.online, status.players.max); let address_arg = match args[1..].iter().find(|arg| !arg.starts_with("-")) {
println!( Some(arg) => arg,
"Enforces Secure Chat: {}", None => {
if status.enforces_secure_chat() { "true" } else { "false" }, print_help();
); std::process::exit(0);
println!("MOTD:"); }
println!("{}", status.parse_description()); };
let address = parse_address(&address_arg).expect("Failed to parse address");
return Ok(()); 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());
return Ok(());
},
Err (e) => {
return Err(Error::new(
ErrorKind::Other,
format!("Failed to fetch status: {}", e)));
}
};
} }
let mut address = "0.0.0.0:8080".to_string(); let mut address = "0.0.0.0:8080";
if args.len() > 2 { address = args[2].to_string() } let address_args = args[1..].iter()
.filter(|arg| *arg != "-v" && *arg != "serve")
.collect::<Vec<_>>();
if !address_args.is_empty() { address = address_args[0] }
let trusted_proxies: Vec<IpAddr> = let trusted_proxies: Vec<IpAddr> =
match env::var("MCSTATUSFACE_TRUSTED_PROXIES") { match env::var("MCSTATUSFACE_TRUSTED_PROXIES") {
Err(_) => { vec![] } Err(_) => { vec![] }
@ -242,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.status(StatusCode::OK);
response.set_header("Content-Type", "text/plain".to_string()); response.set_header("Content-Type", "text/plain".to_string());
response.set_header("x-powered-by", "GIRL FUEL".to_string()); response.set_header("x-powered-by", "GIRL FUEL".to_string());
@ -264,11 +288,11 @@ 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); return handle_html_request(request, &mut response, verbose);
} }
// JSON response // JSON response
return handle_json_request(request, &mut response); return handle_json_request(request, &mut response, verbose);
} }
response.status(StatusCode::NotFound); response.status(StatusCode::NotFound);

View file

@ -4,6 +4,8 @@ use std::time::Duration;
use crate::leb128::{read_leb128, write_leb128}; use crate::leb128::{read_leb128, write_leb128};
const TIMEOUT_SECS: u64 = 5;
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
pub struct MinecraftVersion { pub struct MinecraftVersion {
pub name: String, pub name: String,
@ -56,20 +58,21 @@ pub struct MinecraftStatus {
} }
impl MinecraftStatus { impl MinecraftStatus {
pub fn fetch(address: &SocketAddr) -> Result<MinecraftStatus> { pub fn fetch(address: &SocketAddr, verbose: bool) -> Result<MinecraftStatus> {
//println!("Connecting to {address}..."); if verbose { println!("Connecting to {address}..."); }
let stream = TcpStream::connect_timeout(address, Duration::new(5, 0)); let mut stream = match TcpStream::connect_timeout(
if stream.is_err() { return Err(stream.unwrap_err()); } address, Duration::from_secs(TIMEOUT_SECS)) {
Ok(stream) => stream,
Err(e) => {
return Err(e);
}
};
if verbose { println!("Connected!"); }
//println!("Connected!"); if verbose { println!("Sending payload..."); }
let mut stream = stream.unwrap();
let mut send_buffer: Vec<u8> = Vec::new(); let mut send_buffer: Vec<u8> = Vec::new();
//println!("Sending payload...");
send_buffer.push(0x00); // packet ID send_buffer.push(0x00); // packet ID
write_leb128(&mut send_buffer, 769); // "i am 1.21.4" write_leb128(&mut send_buffer, 769); // "i am 1.21.4"
write_leb128(&mut send_buffer, // upcoming address length write_leb128(&mut send_buffer, // upcoming address length
@ -83,7 +86,7 @@ impl MinecraftStatus {
send_packet(&mut stream, &[0x00]).unwrap(); send_packet(&mut stream, &[0x00]).unwrap();
//println!("Payload sent, receiving...\n"); if verbose { println!("Awaiting response..."); }
let mut data: Vec<u8> = Vec::new(); let mut data: Vec<u8> = Vec::new();
let mut len: usize = 0; let mut len: usize = 0;