Compare commits

..

2 commits

Author SHA1 Message Date
d2fb78108e
update readme 2025-06-16 19:30:51 +01:00
d66b12caff
read X-Forwarded-For header from trusted proxies 2025-06-16 19:27:01 +01:00
3 changed files with 66 additions and 14 deletions

View file

@ -7,9 +7,14 @@ For more information, see https://minecraft.wiki/w/Query
## Usage ## Usage
McStatusFace can be run as a web server with `mcstatusface serve`. This will McStatusFace can simply be run with `mcstatusface <address:[port]>`.
provide server information in JSON format to requests on `GET /?s=<address[:port]>`.
(e.g. `curl -sS "127.0.0.1:8080?s=127.0.0.1:25565" | jq .`)
Alternatively, you can simply run `mcstatusface <address[:port]>`, and the Alternatively, you can start a web interface with
`mcstatusface serve [address:[port]]`.
This provides a web interface to query server information via a frontend, or
in JSON format if the request is cURLed.
The server queries Minecraft servers when a `GET /?s=<address[:port]>` request
is received. (e.g. `curl -sS "127.0.0.1:8080?s=127.0.0.1:25565" | jq .`)
tool will provide server details in plain-text format. tool will provide server details in plain-text format.

View file

@ -1,4 +1,10 @@
use std::{collections::HashMap, io::{BufRead, BufReader, Error, ErrorKind, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}}; use std::{
collections::HashMap,
io::{BufRead, BufReader, Error, ErrorKind, Result, Write},
net::{IpAddr, SocketAddr, TcpListener, TcpStream},
str::FromStr,
sync::Arc,
};
use chrono::Local; use chrono::Local;
@ -45,10 +51,11 @@ pub struct Request<'a> {
headers: HashMap<&'a str, &'a str>, headers: HashMap<&'a str, &'a str>,
query: HashMap<&'a str, &'a str>, query: HashMap<&'a str, &'a str>,
body: Option<String>, body: Option<String>,
real_address: IpAddr,
} }
impl<'a> Request<'a> { impl<'a> Request<'a> {
pub fn new(stream: &'a TcpStream, lines: &'a Vec<String>) -> Result<Request<'a>> { pub fn new(stream: &'a TcpStream, lines: &'a Vec<String>, trusted_proxies: Vec<IpAddr>) -> Result<Request<'a>> {
let request_line = lines[0].as_str(); let request_line = lines[0].as_str();
let request_line_split: Vec<&str> = request_line.split(" ").collect(); let request_line_split: Vec<&str> = request_line.split(" ").collect();
if request_line_split.len() < 3 { if request_line_split.len() < 3 {
@ -88,6 +95,22 @@ impl<'a> Request<'a> {
} }
} }
let mut real_address = IpAddr::from(stream.peer_addr().unwrap().ip());
headers.get("X-Forwarded-For").inspect(|address| {
match IpAddr::from_str(address) {
Ok(address) => {
for proxy in trusted_proxies {
if real_address == proxy {
real_address = address;
break;
}
}
}
Err(_) => {}
}
});
let mut body: Option<String> = None; let mut body: Option<String> = None;
if lines.len() > headers.len() + 2 && if lines.len() > headers.len() + 2 &&
(method == "POST" || method == "PUT") && (method == "POST" || method == "PUT") &&
@ -104,12 +127,16 @@ impl<'a> Request<'a> {
headers, headers,
query, query,
body, body,
real_address,
}) })
} }
pub fn address(&self) -> Result<SocketAddr> { pub fn address(&self) -> Result<SocketAddr> {
self.stream.peer_addr() self.stream.peer_addr()
} }
pub fn real_address(&self) -> &IpAddr {
&self.real_address
}
pub fn path(&self) -> &'a str { pub fn path(&self) -> &'a str {
self.path self.path
} }
@ -187,11 +214,12 @@ impl<'a> Response<'a> {
pub struct HttpServer { pub struct HttpServer {
address: String, address: String,
port: u16, port: u16,
trusted_proxies: Arc<Vec<IpAddr>>,
max_connections: usize, max_connections: usize,
} }
impl HttpServer { impl HttpServer {
pub fn new(address: String, max_connections: usize) -> HttpServer { pub fn new(address: String, max_connections: usize, trusted_proxies: Vec<IpAddr>) -> HttpServer {
let mut _address = address.clone(); let mut _address = address.clone();
let mut _port: u16 = 8080; let mut _port: u16 = 8080;
match address.split_once(":") { match address.split_once(":") {
@ -204,6 +232,7 @@ impl HttpServer {
HttpServer { HttpServer {
address: _address, address: _address,
port: _port, port: _port,
trusted_proxies: Arc::new(trusted_proxies),
max_connections, max_connections,
} }
} }
@ -217,8 +246,9 @@ impl HttpServer {
for stream in listener.incoming() { for stream in listener.incoming() {
match stream { match stream {
Ok(stream) => { Ok(stream) => {
let trusted_proxies = self.trusted_proxies.clone();
pool.execute(move || { pool.execute(move || {
HttpServer::handle_client(&stream, handler); HttpServer::handle_client(&stream, handler, trusted_proxies);
}); });
} }
Err(e) => { Err(e) => {
@ -230,7 +260,7 @@ impl HttpServer {
Ok(()) Ok(())
} }
fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc) { 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()
@ -238,7 +268,7 @@ impl HttpServer {
.take_while(|line| !line.is_empty()) .take_while(|line| !line.is_empty())
.collect(); .collect();
let request = Request::new(stream, &http_request); let request = Request::new(stream, &http_request, trusted_proxies.to_vec());
if request.is_err() { if request.is_err() {
eprintln!("Failed to process request: {}", request.err().unwrap()); eprintln!("Failed to process request: {}", request.err().unwrap());
return; return;
@ -251,6 +281,8 @@ impl HttpServer {
match handler(&request, response) { match handler(&request, response) {
Ok(status) => { Ok(status) => {
let end_date = Local::now(); let end_date = Local::now();
println!( println!(
"[{}] {} {} {} - {} {} - {}ms - {} ({})", "[{}] {} {} {} - {} {} - {}ms - {} ({})",
start_date.format("%Y-%m-%d %H:%M:%S"), start_date.format("%Y-%m-%d %H:%M:%S"),
@ -265,7 +297,7 @@ impl HttpServer {
(end_date - start_date).num_milliseconds(), (end_date - start_date).num_milliseconds(),
request.headers().get("User-Agent").map_or("[]", |v| v), request.headers().get("User-Agent").map_or("[]", |v| v),
request.address().unwrap().ip(), request.real_address().to_string(),
); );
} }
Err(e) => { Err(e) => {

View file

@ -1,5 +1,6 @@
use std::io::{Result}; use std::io::{Result};
use std::net::{ToSocketAddrs}; use std::net::{IpAddr, ToSocketAddrs};
use std::str::FromStr;
use std::{env}; use std::{env};
use mcstatusface::http::{HttpServer, StatusCode}; use mcstatusface::http::{HttpServer, StatusCode};
@ -46,8 +47,22 @@ env!("CARGO_PKG_VERSION"));
let mut address = "0.0.0.0:8080".to_string(); let mut address = "0.0.0.0:8080".to_string();
if args.len() > 2 { address = args[2].to_string() } if args.len() > 2 { address = args[2].to_string() }
let trusted_proxies: Vec<IpAddr> =
match env::var("MCSTATUSFACE_TRUSTED_PROXIES") {
Ok(envar) => {
let mut trusted_proxies: Vec<IpAddr> = Vec::new();
for addr in envar.split(",") {
match IpAddr::from_str(addr) {
Ok(addr) => { trusted_proxies.push(addr); }
Err(_) => {}
}
}
trusted_proxies
}
Err(_) => { vec![] }
};
HttpServer::new(address, 64).start(|request, mut response| { HttpServer::new(address, 64, 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());
@ -132,7 +147,7 @@ env!("CARGO_PKG_VERSION"));
// JSON response // JSON response
match request.query().get("s") { match request.query().get("s") {
None => { None => {
response.status(StatusCode::BadRequest); response.status(StatusCode::OK);
response.body("?s=<server address>\n".to_string()); response.body("?s=<server address>\n".to_string());
return response.send(); return response.send();
} }