mcstatusface/src/main.rs

226 lines
9.4 KiB
Rust
Raw Normal View History

2025-04-14 07:20:05 +01:00
use std::io::{Result};
use std::net::{IpAddr, ToSocketAddrs};
use std::str::FromStr;
2025-06-16 18:06:43 +01:00
use std::{env};
2025-04-14 07:20:05 +01:00
2025-04-14 18:36:16 +01:00
use mcstatusface::http::{HttpServer, StatusCode};
use mcstatusface::{MinecraftStatus};
2025-04-14 07:20:05 +01:00
2025-06-16 17:57:51 +01:00
#[derive(serde::Serialize)]
struct MinecraftStatusResponse<'a> {
version: &'a String,
players: u32,
max_players: u32,
motd: String,
}
2025-04-14 07:20:05 +01:00
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
2025-04-14 23:53:03 +01:00
println!(
r#"Crafty McStatusFace, v{} - made with <3 by ari melody
Host a web API:
$ mcstatusface serve [address[:port]]
Query a server:
$ mcstatusface <address[:port]>"#,
env!("CARGO_PKG_VERSION"));
std::process::exit(0);
2025-04-14 07:20:05 +01:00
}
if args[1] != "serve" {
let mut address = String::from(args[1].as_str());
2025-04-14 18:49:04 +01:00
if !address.contains(":") { address.push_str(":25565"); }
let mut addrs_iter = address.to_socket_addrs().unwrap();
let address = addrs_iter.next().unwrap();
let status = MinecraftStatus::fetch(address).unwrap();
2025-04-14 07:20:05 +01:00
2025-04-14 18:49:04 +01:00
println!("Version: {} ({})", status.version.name, status.version.protocol);
println!("Players: {}/{}", status.players.online, status.players.max);
println!("MOTD:");
println!("{}", status.parse_description());
return Ok(());
}
2025-04-14 07:20:05 +01:00
2025-04-14 18:36:16 +01:00
let mut address = "0.0.0.0:8080".to_string();
if args.len() > 2 { address = args[2].to_string() }
let trusted_proxies: Vec<IpAddr> =
match env::var("MCSTATUSFACE_TRUSTED_PROXIES") {
Err(_) => { vec![] }
Ok(envar) => {
let mut trusted_proxies: Vec<IpAddr> = Vec::new();
for addr in envar.split(",") {
match IpAddr::from_str(addr) {
Err(_) => {}
Ok(addr) => { trusted_proxies.push(addr); }
}
}
trusted_proxies
}
};
2025-04-14 18:36:16 +01:00
HttpServer::new(address, 64, trusted_proxies).start(|request, mut response| {
2025-04-14 18:36:16 +01:00
response.status(StatusCode::OK);
2025-06-16 17:57:51 +01:00
response.set_header("Content-Type", "text/plain".to_string());
2025-04-14 18:36:16 +01:00
response.set_header("x-powered-by", "GIRL FUEL".to_string());
if request.method() != "GET" {
response.status(StatusCode::NotFound);
return response.send()
}
2025-06-16 17:57:51 +01:00
if request.path() == "/style/index.css" {
2025-06-16 18:06:43 +01:00
let content = include_str!("public/style/index.css");
response.set_header("Content-Type", "text/css".to_string());
response.status(StatusCode::OK);
response.body(content.to_string());
return response.send();
2025-04-14 18:36:16 +01:00
}
2025-06-16 17:57:51 +01:00
if request.path() == "/" {
2025-06-16 19:50:48 +01:00
if request.headers().get("accept").is_some_and(
2025-06-16 17:57:51 +01:00
|accept| accept.contains("text/html")
) {
// HTML response
2025-06-16 18:06:43 +01:00
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("");
}
2025-06-16 18:06:43 +01:00
Some(query_address) => {
let mut address = query_address.to_string();
if !address.contains(":") { address.push_str(":25565"); }
match address.to_socket_addrs() {
Err(_) => {
response.set_header("Content-Type", "text/html".to_string());
response.status(StatusCode::BadRequest);
response.body("Server address is invalid or unreachable.\n".to_string());
return response.send();
}
Ok(mut addrs_iter) => {
let address = addrs_iter.next().unwrap();
match MinecraftStatus::fetch(address) {
Err(_) => {
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,
motd: status.parse_description(),
};
query_response = format!(
"<hr/>
2025-06-16 17:57:51 +01:00
<h2>Server Details</h2>
<p>
<strong>Version:</strong> <code>{}</code><br/>
<strong>Players:</strong> <code>{}/{}</code><br/>
<strong>MOTD:</strong>
</p>
2025-06-16 19:53:01 +01:00
<pre id=\"motd\"><code>{}</code></pre>",
2025-06-16 17:57:51 +01:00
sanitize_html(minecraft_status.version).to_string(),
minecraft_status.players,
minecraft_status.max_players,
sanitize_html(&minecraft_status.motd).to_string(),
);
}
}
2025-06-16 17:57:51 +01:00
}
}
}
}
2025-06-16 18:06:43 +01:00
let response_content = content
.replace("{{response}}", &query_response)
2025-06-16 19:50:48 +01:00
.replace("{{host}}", match request.headers().get("host") {
2025-06-16 18:06:43 +01:00
None => { "mcq.bliss.town" }
Some(host) => { host }
2025-06-16 18:06:43 +01:00
});
response.body(response_content.to_string());
return response.send();
2025-04-14 18:36:16 +01:00
}
2025-06-16 17:57:51 +01:00
// JSON response
match request.query().get("s") {
None => {
response.status(StatusCode::OK);
2025-06-16 17:57:51 +01:00
response.body("?s=<server address>\n".to_string());
return response.send();
}
Some(query_address) => {
let mut address = query_address.to_string();
if !address.contains(":") { address.push_str(":25565"); }
match address.to_socket_addrs() {
Err(_) => {
2025-06-16 17:57:51 +01:00
response.status(StatusCode::InternalServerError);
response.body("Invalid server address.\n".to_string());
2025-06-16 17:57:51 +01:00
}
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,
motd: status.parse_description(),
};
match serde_json::to_string(&minecraft_status) {
Err(e) => {
eprintln!("Failed to parse status for {address}: {e}");
response.status(StatusCode::InternalServerError);
response.body(format!("Failed to parse response from {address}.\n"));
}
Ok(json) => {
response.status(StatusCode::OK);
response.set_header("Content-Type", "application/json".to_string());
response.body(json + "\n");
}
}
}
}
2025-06-16 17:57:51 +01:00
}
}
2025-06-16 17:57:51 +01:00
return response.send()
}
2025-04-14 18:36:16 +01:00
}
}
2025-06-16 17:57:51 +01:00
response.status(StatusCode::NotFound);
response.body("Not Found".to_string());
return response.send();
2025-04-14 18:36:16 +01:00
}).unwrap();
2025-04-14 07:20:05 +01:00
Ok(())
}
2025-06-16 17:57:51 +01:00
fn sanitize_html(input: &String) -> String {
input
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;")
}