browser-friendly frontend :3
This commit is contained in:
parent
bac3204572
commit
6bd7379df3
5 changed files with 259 additions and 46 deletions
|
@ -3,6 +3,8 @@
|
||||||
A light application that serves Minecraft server query information in a
|
A light application that serves Minecraft server query information in a
|
||||||
convenient format!
|
convenient format!
|
||||||
|
|
||||||
|
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 be run as a web server with `mcstatusface serve`. This will
|
||||||
|
|
58
public/style/index.css
Normal file
58
public/style/index.css
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
:root {
|
||||||
|
--accent: #7ca82f;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: fit-content;
|
||||||
|
padding: .5em;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input[type="text"] {
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 16em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button {
|
||||||
|
margin-top: .5em;
|
||||||
|
padding: .2em .3em;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
padding: .5em;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: .8em;
|
||||||
|
color: #e0e0e0;
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code#motd {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
12
src/http.rs
12
src/http.rs
|
@ -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;
|
use chrono::Local;
|
||||||
|
|
||||||
|
@ -51,13 +51,17 @@ 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>) -> 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 {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid request start-line"));
|
||||||
|
}
|
||||||
let method = request_line_split[0];
|
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 version = request_line_split[2];
|
||||||
|
|
||||||
let mut query: HashMap<&'a str, &'a str> = HashMap::new();
|
let mut query: HashMap<&'a str, &'a str> = HashMap::new();
|
||||||
match path.split_once("?") {
|
match request_line_split[1].split_once("?") {
|
||||||
Some((_, query_string)) => {
|
Some((path_without_query, query_string)) => {
|
||||||
|
path = path_without_query;
|
||||||
let query_splits: Vec<&'a 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("=") {
|
||||||
|
|
191
src/main.rs
191
src/main.rs
|
@ -1,10 +1,18 @@
|
||||||
use std::io::{Result};
|
use std::io::{Result};
|
||||||
use std::net::{ToSocketAddrs};
|
use std::net::{ToSocketAddrs};
|
||||||
use std::env;
|
use std::{env, fs};
|
||||||
|
|
||||||
use mcstatusface::http::{HttpServer, StatusCode};
|
use mcstatusface::http::{HttpServer, StatusCode};
|
||||||
use mcstatusface::{MinecraftStatus};
|
use mcstatusface::{MinecraftStatus};
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct MinecraftStatusResponse<'a> {
|
||||||
|
version: &'a String,
|
||||||
|
players: u32,
|
||||||
|
max_players: u32,
|
||||||
|
motd: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
|
@ -41,6 +49,7 @@ env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
HttpServer::new(address, 64).start(|request, mut response| {
|
HttpServer::new(address, 64).start(|request, mut response| {
|
||||||
response.status(StatusCode::OK);
|
response.status(StatusCode::OK);
|
||||||
|
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());
|
||||||
|
|
||||||
if request.method() != "GET" {
|
if request.method() != "GET" {
|
||||||
|
@ -48,51 +57,149 @@ env!("CARGO_PKG_VERSION"));
|
||||||
return response.send()
|
return response.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !request.query().contains_key("s") {
|
if request.path() == "/style/index.css" {
|
||||||
response.status(StatusCode::BadRequest);
|
match fs::read_to_string("./public/style/index.css") {
|
||||||
// TODO: nice index landing page for browsers
|
Ok(content) => {
|
||||||
response.body("?s=<server address>\n".to_string());
|
response.set_header("Content-Type", "text/css".to_string());
|
||||||
return response.send()
|
response.status(StatusCode::OK);
|
||||||
}
|
response.body(content.to_string());
|
||||||
|
return response.send();
|
||||||
let mut address = request.query().get("s").unwrap().to_string();
|
}
|
||||||
if !address.contains(":") { address.push_str(":25565"); }
|
Err(err) => {
|
||||||
let mut addrs_iter = address.to_socket_addrs().unwrap();
|
eprint!("failed to load index.css: {}\n", err.to_string());
|
||||||
let address = addrs_iter.next().unwrap();
|
response.status(StatusCode::InternalServerError);
|
||||||
|
response.body("Internal Server Error\n".to_string());
|
||||||
let status = MinecraftStatus::fetch(address).unwrap();
|
return response.send();
|
||||||
|
}
|
||||||
#[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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
}).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sanitize_html(input: &String) -> String {
|
||||||
|
input
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\"", """)
|
||||||
|
.replace("'", "'")
|
||||||
|
}
|
||||||
|
|
42
views/index.html
Normal file
42
views/index.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Minecraft Server Query</title>
|
||||||
|
<link href="style/index.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1><a href="/">Crafty McStatusFace</a></h1>
|
||||||
|
</header>
|
||||||
|
<hr/>
|
||||||
|
<main>
|
||||||
|
<p>
|
||||||
|
You can use this website to retrieve query information from a Minecraft server!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
For more information, see <a href="https://minecraft.wiki/w/Query">https://minecraft.wiki/w/Query</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form action="/" method="GET">
|
||||||
|
<label for="s">Server Address</label>
|
||||||
|
<input type="text" name="s" value="" placeholder="e.g. mc.bliss.town or 104.17.17.42">
|
||||||
|
<br/>
|
||||||
|
<button type="submit">Check Status</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Alternatively, you can cURL this website to get a raw JSON response:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>curl 'https://{{host}}?s=<server address>'</code></pre>
|
||||||
|
|
||||||
|
{{response}}
|
||||||
|
</main>
|
||||||
|
<hr/>
|
||||||
|
<footer>
|
||||||
|
<em>made with <span aria-label="love">♥</span> by ari, 2025. <a href="https://git.arimelody.me/ari/mcstatusface" target="_blank">source</a></em>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Add table
Add a link
Reference in a new issue