browser-friendly frontend :3

This commit is contained in:
ari melody 2025-06-16 17:57:51 +01:00
parent bac3204572
commit 6bd7379df3
Signed by: ari
GPG key ID: CF99829C92678188
5 changed files with 259 additions and 46 deletions

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, io::{BufRead, BufReader, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}};
use std::{collections::HashMap, io::{BufRead, BufReader, Error, ErrorKind, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}};
use chrono::Local;
@ -51,13 +51,17 @@ impl<'a> Request<'a> {
pub fn new(stream: &'a TcpStream, lines: &'a Vec<String>) -> Result<Request<'a>> {
let request_line = lines[0].as_str();
let request_line_split: Vec<&str> = request_line.split(" ").collect();
if request_line_split.len() < 3 {
return Err(Error::new(ErrorKind::Other, "invalid request start-line"));
}
let method = request_line_split[0];
let path = request_line_split[1];
let mut path = request_line_split[1];
let version = request_line_split[2];
let mut query: HashMap<&'a str, &'a str> = HashMap::new();
match path.split_once("?") {
Some((_, query_string)) => {
match request_line_split[1].split_once("?") {
Some((path_without_query, query_string)) => {
path = path_without_query;
let query_splits: Vec<&'a str> = query_string.split("&").collect();
for pair in query_splits {
match pair.split_once("=") {

View file

@ -1,10 +1,18 @@
use std::io::{Result};
use std::net::{ToSocketAddrs};
use std::env;
use std::{env, fs};
use mcstatusface::http::{HttpServer, StatusCode};
use mcstatusface::{MinecraftStatus};
#[derive(serde::Serialize)]
struct MinecraftStatusResponse<'a> {
version: &'a String,
players: u32,
max_players: u32,
motd: String,
}
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
@ -41,6 +49,7 @@ env!("CARGO_PKG_VERSION"));
HttpServer::new(address, 64).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" {
@ -48,51 +57,149 @@ env!("CARGO_PKG_VERSION"));
return response.send()
}
if !request.query().contains_key("s") {
response.status(StatusCode::BadRequest);
// TODO: nice index landing page for browsers
response.body("?s=<server address>\n".to_string());
return response.send()
}
let mut address = request.query().get("s").unwrap().to_string();
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();
#[derive(serde::Serialize)]
struct MinecraftStatusResponse<'a> {
version: &'a String,
players: u32,
max_players: u32,
motd: String,
}
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) {
Ok(json) => {
response.status(StatusCode::OK);
response.set_header("Content-Type", "application/json".to_string());
response.body(json);
}
Err(e) => {
eprintln!("Request to {address} failed: {e}");
response.status(StatusCode::InternalServerError);
response.set_header("Content-Type", "text/plain".to_string());
response.body("Unable to reach the requested server.\n".to_string());
if request.path() == "/style/index.css" {
match fs::read_to_string("./public/style/index.css") {
Ok(content) => {
response.set_header("Content-Type", "text/css".to_string());
response.status(StatusCode::OK);
response.body(content.to_string());
return response.send();
}
Err(err) => {
eprint!("failed to load index.css: {}\n", err.to_string());
response.status(StatusCode::InternalServerError);
response.body("Internal Server Error\n".to_string());
return response.send();
}
}
}
response.send()
if request.path() == "/" {
if request.headers().get("Accept").is_some_and(
|accept| accept.contains("text/html")
) {
// HTML response
match fs::read_to_string("./views/index.html") {
Ok(mut content) => {
response.set_header("Content-Type", "text/html".to_string());
response.status(StatusCode::OK);
let query_response: String;
match request.query().get("s") {
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();
let status = MinecraftStatus::fetch(address).unwrap();
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/>
<h2>Server Details</h2>
<p>
<strong>Version:</strong> <code>{}</code><br/>
<strong>Players:</strong> <code>{}/{}</code><br/>
<strong>MOTD:</strong>
</p>
<pre><code id=\"motd\">{}</code></pre>",
sanitize_html(minecraft_status.version).to_string(),
minecraft_status.players,
minecraft_status.max_players,
sanitize_html(&minecraft_status.motd).to_string(),
);
}
}
}
None => {
query_response = String::from("");
}
}
content = content
.replace("{{response}}", &query_response)
.replace("{{host}}", match request.headers().get("Host") {
Some(host) => { host }
None => { "mcq.bliss.town" }
});
response.body(content.to_string());
return response.send();
}
Err(err) => {
eprint!("failed to load index.html: {}\n", err.to_string());
response.status(StatusCode::InternalServerError);
response.body("Internal Server Error\n".to_string());
return response.send();
}
}
}
// JSON response
match request.query().get("s") {
None => {
response.status(StatusCode::BadRequest);
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"); }
let mut addrs_iter = address.to_socket_addrs().unwrap();
let address = addrs_iter.next().unwrap();
let status = MinecraftStatus::fetch(address).unwrap();
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) {
Ok(json) => {
response.status(StatusCode::OK);
response.set_header("Content-Type", "application/json".to_string());
response.body(json + "\n");
}
Err(e) => {
eprintln!("Request to {address} failed: {e}");
response.status(StatusCode::InternalServerError);
response.body("Unable to reach the requested server.\n".to_string());
}
}
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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#x27;")
}