277 lines
12 KiB
Rust
277 lines
12 KiB
Rust
use std::io::Result;
|
|
use std::net::{IpAddr, ToSocketAddrs};
|
|
use std::str::FromStr;
|
|
use std::env;
|
|
|
|
use mcstatusface::http::{HttpServer, StatusCode};
|
|
use mcstatusface::status::MinecraftStatus;
|
|
use mcstatusface::dns::resolve_srv_port;
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct MinecraftStatusResponse<'a> {
|
|
version: &'a String,
|
|
players: u32,
|
|
max_players: u32,
|
|
enforces_secure_chat: bool,
|
|
favicon: Option<&'a String>,
|
|
motd: String,
|
|
}
|
|
|
|
const DEFAULT_PORT: u16 = 25565;
|
|
|
|
fn main() -> Result<()> {
|
|
let args: Vec<String> = 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]]
|
|
|
|
Query a server:
|
|
$ mcstatusface <address[:port]>"#,
|
|
env!("CARGO_PKG_VERSION"));
|
|
std::process::exit(0);
|
|
}
|
|
|
|
if args[1] != "serve" {
|
|
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);
|
|
println!(
|
|
"Enforces Secure Chat: {}",
|
|
if status.enforces_secure_chat() { "true" } else { "false" },
|
|
);
|
|
println!("MOTD:");
|
|
println!("{}", status.parse_description());
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
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
|
|
}
|
|
};
|
|
|
|
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());
|
|
|
|
if request.method() != "GET" {
|
|
response.status(StatusCode::NotFound);
|
|
return response.send()
|
|
}
|
|
|
|
if request.path() == "/style/index.css" {
|
|
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();
|
|
}
|
|
|
|
if request.path() == "/" {
|
|
if request.headers().get("accept").is_some_and(
|
|
|accept| accept.contains("text/html")
|
|
) {
|
|
// 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(e) => {
|
|
println!(
|
|
"Failed to connect to {} ({}): {}",
|
|
query_address, address, e);
|
|
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
|
|
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.body("Not Found".to_string());
|
|
return response.send();
|
|
}).unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn sanitize_html(input: &String) -> String {
|
|
input
|
|
.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">")
|
|
.replace("\"", """)
|
|
.replace("'", "'")
|
|
}
|