Compare commits
No commits in common. "ec4d6035e8d8052fe64ac6f3f02de3202edb452b" and "5742898412d608819e0e0d31c6d1dd24e5369a05" have entirely different histories.
ec4d6035e8
...
5742898412
4 changed files with 273 additions and 301 deletions
|
|
@ -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(
|
||||||
&format!("_minecraft._tcp.{}", domain),
|
&format!("_minecraft._tcp.{}", domain),
|
||||||
RECORD_TYPE_SRV,
|
RECORD_TYPE_SRV,
|
||||||
|
|
|
||||||
43
src/http.rs
43
src/http.rs
|
|
@ -41,15 +41,15 @@ impl StatusCode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpHandlerFunc = fn(&Request, Response, bool) -> Result<StatusCode>;
|
type HttpHandlerFunc = fn(&Request, Response) -> Result<StatusCode>;
|
||||||
|
|
||||||
pub struct Request<'a> {
|
pub struct Request<'a> {
|
||||||
stream: &'a TcpStream,
|
stream: &'a TcpStream,
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,21 +211,20 @@ impl<'a> Response<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HttpServer<'a> {
|
pub struct HttpServer {
|
||||||
address: &'a str,
|
address: String,
|
||||||
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: &'_ str, max_connections: usize, trusted_proxies: Vec<IpAddr>, verbose: bool) -> HttpServer<'_> {
|
pub fn new(address: String, max_connections: usize, trusted_proxies: Vec<IpAddr>) -> HttpServer {
|
||||||
let mut _address = address;
|
let mut _address = address.clone();
|
||||||
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;
|
_address = ip.to_string();
|
||||||
_port = port.parse::<u16>().expect(format!("Invalid port {}", port).as_str());
|
_port = port.parse::<u16>().expect(format!("Invalid port {}", port).as_str());
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
|
|
@ -237,14 +234,12 @@ 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);
|
||||||
|
|
||||||
|
|
@ -253,7 +248,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, verbose);
|
HttpServer::handle_client(&stream, handler, trusted_proxies);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -265,7 +260,7 @@ impl HttpServer <'_> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc<Vec<IpAddr>>, verbose: bool) {
|
fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc, trusted_proxies: Arc<Vec<IpAddr>>) {
|
||||||
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()
|
||||||
|
|
@ -283,7 +278,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, verbose) {
|
match handler(&request, response) {
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
let end_date = Local::now();
|
let end_date = Local::now();
|
||||||
|
|
||||||
|
|
|
||||||
420
src/main.rs
420
src/main.rs
|
|
@ -1,200 +1,27 @@
|
||||||
use std::io::{Error, ErrorKind, Result};
|
use std::io::{Error, ErrorKind, 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> {
|
fn main() -> Result<()> {
|
||||||
// attempt to parse as IP + port
|
let args: Vec<String> = env::args().collect();
|
||||||
if let Ok(socket_addr) = SocketAddr::from_str(address) {
|
if args.len() < 2 {
|
||||||
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, verbose: bool) -> 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, verbose) {
|
|
||||||
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, verbose: bool) -> 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, verbose) {
|
|
||||||
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 print_help() {
|
|
||||||
println!(
|
println!(
|
||||||
r#"Crafty McStatusFace, v{} - made with <3 by ari melody
|
r#"Crafty McStatusFace, v{} - made with <3 by ari melody
|
||||||
|
|
||||||
|
|
@ -204,29 +31,51 @@ $ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
let verbose = args.contains(&String::from("-v"));
|
let verbose = args.contains(&"-v".to_string());
|
||||||
|
|
||||||
if !args[1..].contains(&String::from("serve")) {
|
if args[1] != "serve" {
|
||||||
let address_arg = match args[1..].iter().find(|arg| !arg.starts_with("-")) {
|
let mut address = String::from(args[args.len() - 1].as_str());
|
||||||
Some(arg) => arg,
|
if !address.contains(":") {
|
||||||
None => {
|
let port: u16 = match resolve_srv_port(&address) {
|
||||||
print_help();
|
Some(port) => port,
|
||||||
std::process::exit(0);
|
None => DEFAULT_PORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
address.push_str(":");
|
||||||
|
address.push_str(port.to_string().as_str());
|
||||||
|
}
|
||||||
|
let addrs_iter = match address.to_socket_addrs() {
|
||||||
|
Ok(addr) => addr,
|
||||||
|
Err(e) => {
|
||||||
|
panic!("Failed to parse address {}: {}", address, e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut error: Option<Error> = None;
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
println!(
|
||||||
|
"{} address{} available.",
|
||||||
|
addrs_iter.len(),
|
||||||
|
// horrible no good code but it's really funny to look at
|
||||||
|
// i'm sure this is awesome if you're into functional programming
|
||||||
|
(addrs_iter.len() == 1).then(|| "es").unwrap_or(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
for address in addrs_iter {
|
||||||
|
let status = match MinecraftStatus::fetch(address, verbose) {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err (e) => {
|
||||||
|
error = Some(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
format!("Failed to fetch status: {}", e)));
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let address = parse_address(&address_arg).expect("Failed to parse address");
|
|
||||||
|
|
||||||
match MinecraftStatus::fetch(&address, verbose) {
|
|
||||||
Ok(status) => {
|
|
||||||
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);
|
||||||
println!(
|
println!(
|
||||||
|
|
@ -237,20 +86,15 @@ fn main() -> Result<()> {
|
||||||
println!("{}", status.parse_description());
|
println!("{}", status.parse_description());
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
},
|
|
||||||
Err (e) => {
|
|
||||||
return Err(Error::new(
|
|
||||||
ErrorKind::Other,
|
|
||||||
format!("Failed to fetch status: {}", e)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut address = "0.0.0.0:8080";
|
if let Some(error) = error {
|
||||||
let address_args = args[1..].iter()
|
return Err(error);
|
||||||
.filter(|arg| *arg != "-v" && *arg != "serve")
|
}
|
||||||
.collect::<Vec<_>>();
|
}
|
||||||
if !address_args.is_empty() { address = address_args[0] }
|
|
||||||
|
let mut address = "0.0.0.0:8080".to_string();
|
||||||
|
if args.len() > 2 { address = args[2].to_string() }
|
||||||
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![] }
|
||||||
|
|
@ -266,7 +110,7 @@ fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
HttpServer::new(address, 16, trusted_proxies, verbose).start(|request, mut response, verbose| {
|
HttpServer::new(address, 16, trusted_proxies).start(|request, mut response| {
|
||||||
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());
|
||||||
|
|
@ -288,22 +132,170 @@ fn main() -> Result<()> {
|
||||||
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, verbose);
|
// 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, false) {
|
||||||
|
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, verbose);
|
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, false) {
|
||||||
|
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("&", "&")
|
.replace("&", "&")
|
||||||
.replace("<", "<")
|
.replace("<", "<")
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use crate::leb128::{read_leb128, write_leb128};
|
use crate::leb128::{read_leb128, write_leb128};
|
||||||
|
|
||||||
const TIMEOUT_SECS: u64 = 5;
|
const TIMEOUT_SECS: u64 = 10;
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
pub struct MinecraftVersion {
|
pub struct MinecraftVersion {
|
||||||
|
|
@ -58,30 +58,24 @@ pub struct MinecraftStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MinecraftStatus {
|
impl MinecraftStatus {
|
||||||
pub fn fetch(address: &SocketAddr, verbose: bool) -> Result<MinecraftStatus> {
|
pub fn fetch(address: SocketAddr, verbose: bool) -> Result<MinecraftStatus> {
|
||||||
if verbose { println!("Connecting to {address}..."); }
|
if verbose { println!("Connecting to {address}..."); }
|
||||||
|
|
||||||
let mut stream = match TcpStream::connect_timeout(
|
let stream = TcpStream::connect_timeout(
|
||||||
address, Duration::from_secs(TIMEOUT_SECS)) {
|
&address, Duration::from_secs(TIMEOUT_SECS));
|
||||||
Ok(stream) => stream,
|
if stream.is_err() { return Err(stream.unwrap_err()); }
|
||||||
Err(e) => {
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if verbose { println!("Connected!"); }
|
if verbose { println!("Connected!"); }
|
||||||
|
let mut stream = stream.unwrap();
|
||||||
if verbose { println!("Sending payload..."); }
|
|
||||||
|
|
||||||
let mut send_buffer: Vec<u8> = Vec::new();
|
let mut send_buffer: Vec<u8> = Vec::new();
|
||||||
send_buffer.push(0x00); // packet ID
|
|
||||||
write_leb128(&mut send_buffer, 769); // "i am 1.21.4"
|
if verbose { println!("Sending payload..."); }
|
||||||
write_leb128(&mut send_buffer, // upcoming address length
|
send_buffer.push(0x00);
|
||||||
address.ip().to_string().len().try_into().unwrap());
|
write_leb128(&mut send_buffer, 769); // 1.21.4
|
||||||
send_buffer.extend_from_slice( // the address
|
write_leb128(&mut send_buffer, address.ip().to_string().len().try_into().unwrap());
|
||||||
address.ip().to_string().as_bytes());
|
send_buffer.extend_from_slice(address.ip().to_string().as_bytes());
|
||||||
send_buffer.extend_from_slice( // the port
|
send_buffer.extend_from_slice(&address.port().to_be_bytes());
|
||||||
&address.port().to_be_bytes());
|
write_leb128(&mut send_buffer, 1);
|
||||||
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();
|
||||||
|
|
@ -96,17 +90,9 @@ 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 {
|
|
||||||
return Err(Error::new(ErrorKind::HostUnreachable, format!("No data received from remote")));
|
|
||||||
}
|
|
||||||
|
|
||||||
//println!("< {} bytes\n", len);
|
|
||||||
|
|
||||||
|
if len > 0 {
|
||||||
if msg_len == 0 {
|
if msg_len == 0 {
|
||||||
let mut val: u32;
|
let mut val: u32;
|
||||||
(val, offset) = read_leb128(&recv_buffer);
|
(val, offset) = read_leb128(&recv_buffer);
|
||||||
|
|
@ -127,11 +113,10 @@ impl MinecraftStatus {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let data = std::str::from_utf8(&data[offset..]).expect("Failed to parse UTF-8 data");
|
let msg = std::str::from_utf8(&data[offset..]).unwrap().trim();
|
||||||
let sanitised: String = data.chars().filter(
|
let sanitised: String = msg.chars().filter(|&c| c >= '\u{20}' || c == '\n' || c == '\r' || c == '\t').collect();
|
||||||
|&c| c >= '\u{20}' || c == '\n' || c == '\r' || c == '\t').collect();
|
let status: MinecraftStatus = serde_json::from_slice(sanitised.as_bytes()).unwrap();
|
||||||
let status: MinecraftStatus = serde_json::from_str(&sanitised).expect("Failed to parse JSON");
|
|
||||||
|
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
@ -160,12 +145,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue