yea that oughta do it for now i think

This commit is contained in:
ari melody 2025-04-14 18:36:16 +01:00
parent eda7f79fb0
commit cf28e189cd
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
2 changed files with 142 additions and 61 deletions

View file

@ -1,22 +1,49 @@
use std::{collections::HashMap, io::{BufRead, BufReader, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}, thread, time::Duration}; use std::{collections::HashMap, io::{BufRead, BufReader, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}};
use chrono::Local; use chrono::Local;
use crate::ThreadPool; use crate::ThreadPool;
#[derive(Clone, Copy)]
pub enum StatusCode { pub enum StatusCode {
OK, OK,
BadRequest,
NotFound, NotFound,
// InternalServerError, InternalServerError,
// ImATeapot, // ImATeapot,
} }
impl StatusCode {
pub fn code(&self) -> u16 {
match self {
StatusCode::OK => 200,
StatusCode::BadRequest => 400,
StatusCode::NotFound => 404,
// StatusCode::ImATeapot => 418,
StatusCode::InternalServerError => 500,
}
}
pub fn reason(&self) -> &str {
match self {
StatusCode::OK => "OK",
StatusCode::BadRequest => "Bad Request",
StatusCode::NotFound => "Not Found",
// StatusCode::ImATeapot => "I'm a teapot",
StatusCode::InternalServerError => "Internal Server Error",
}
}
}
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<&'a str, &'a str>, headers: HashMap<&'a str, &'a str>,
query: HashMap<&'a str, &'a str>,
body: Option<String>, body: Option<String>,
} }
@ -28,6 +55,22 @@ impl<'a> Request<'a> {
let path = request_line_split[1]; let 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();
match path.split_once("?") {
Some((_, query_string)) => {
let query_splits: Vec<&'a str> = query_string.split("&").collect();
for pair in query_splits {
match pair.split_once("=") {
Some((name, value)) => {
query.insert(name, value);
}
None => { continue; }
}
}
}
None => {},
}
let mut headers: HashMap<&'a str, &'a str> = HashMap::new(); let mut headers: HashMap<&'a str, &'a str> = HashMap::new();
if lines.len() > 1 { if lines.len() > 1 {
let mut i: usize = 1; let mut i: usize = 1;
@ -55,6 +98,7 @@ impl<'a> Request<'a> {
method, method,
version, version,
headers, headers,
query,
body, body,
}) })
} }
@ -77,6 +121,9 @@ impl<'a> Request<'a> {
pub fn headers(&self) -> &HashMap<&'a str, &'a str> { pub fn headers(&self) -> &HashMap<&'a str, &'a str> {
&self.headers &self.headers
} }
pub fn query(&self) -> &HashMap<&'a str, &'a str> {
&self.query
}
} }
pub struct Response<'a> { pub struct Response<'a> {
@ -112,21 +159,9 @@ impl<'a> Response<'a> {
self.body = Some(body); self.body = Some(body);
} }
pub fn send(&mut self) -> Result<usize> { pub fn send(&mut self) -> Result<StatusCode> {
let mut len: usize = 0; // let mut len: usize = 0;
let code = match self.status { self.stream.write(format!("HTTP/1.1 {} {}\r\n", self.status.code(), self.status.reason()).as_bytes()).unwrap();
StatusCode::OK => 200,
StatusCode::NotFound => 404,
// StatusCode::ImATeapot => 418,
// StatusCode::InternalServerError => 500,
};
let reason = match self.status {
StatusCode::OK => "OK",
StatusCode::NotFound => "Not Found",
// StatusCode::ImATeapot => "I'm a teapot",
// StatusCode::InternalServerError => "Internal Server Error",
};
len += self.stream.write(format!("HTTP/1.1 {} {}\r\n", code, reason).as_bytes()).unwrap();
let mut content_length: usize = 0; let mut content_length: usize = 0;
if self.body.is_some() { if self.body.is_some() {
@ -134,14 +169,14 @@ impl<'a> Response<'a> {
} }
self.set_header("Content-Length", content_length.to_string()); self.set_header("Content-Length", content_length.to_string());
for (name, value) in &self.headers { for (name, value) in &self.headers {
len += self.stream.write(format!("{name}: {value}\r\n").as_bytes()).unwrap() self.stream.write(format!("{name}: {value}\r\n").as_bytes()).unwrap();
} }
if self.body.is_some() { if self.body.is_some() {
len += self.stream.write("\r\n".as_bytes()).unwrap(); self.stream.write("\r\n".as_bytes()).unwrap();
len += self.stream.write(self.body.as_ref().unwrap().as_bytes()).unwrap(); self.stream.write(self.body.as_ref().unwrap().as_bytes()).unwrap();
} }
Ok(len) Ok(self.status)
} }
} }
@ -152,16 +187,24 @@ pub struct HttpServer {
} }
impl HttpServer { impl HttpServer {
pub fn new(address: &str) -> HttpServer { pub fn new(address: String, max_connections: usize) -> HttpServer {
HttpServer { let mut _address = address.clone();
address: address.to_string(), let mut _port: u16 = 8080;
port: 8080, match address.split_once(":") {
max_connections: 16, Some((ip, port)) => {
_address = ip.to_string();
_port = port.parse::<u16>().expect(format!("Invalid port {}", port).as_str());
}
None => {}
}
HttpServer {
address: _address,
port: _port,
max_connections,
} }
} }
pub fn start(&self) -> 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");
@ -171,7 +214,7 @@ impl HttpServer {
match stream { match stream {
Ok(stream) => { Ok(stream) => {
pool.execute(move || { pool.execute(move || {
HttpServer::handle_client(&stream); HttpServer::handle_client(&stream, handler);
}); });
} }
Err(e) => { Err(e) => {
@ -183,7 +226,7 @@ impl HttpServer {
Ok(()) Ok(())
} }
fn handle_client(stream: &TcpStream) { fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc) {
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()
@ -201,16 +244,23 @@ impl HttpServer {
let response = Response::new(stream); let response = Response::new(stream);
let start_date = Local::now(); let start_date = Local::now();
match HttpServer::handle_request(&request, response) { match handler(&request, response) {
Ok(_) => { 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"),
request.method(), request.method(),
request.path(), request.path(),
request.version(), request.version(),
status.code(),
status.reason(),
(end_date - start_date).num_milliseconds(), (end_date - start_date).num_milliseconds(),
request.headers().get("User-Agent").map_or("[]", |v| v),
request.address().unwrap().ip(), request.address().unwrap().ip(),
); );
} }
@ -219,29 +269,4 @@ impl HttpServer {
} }
} }
} }
fn handle_request(request: &Request, mut response: Response) -> Result<usize> {
response.status(StatusCode::OK);
response.set_header("x-powered-by", "GIRL FUEL".to_string());
if request.method != "GET" {
response.status(StatusCode::NotFound);
return response.send()
}
response.status(StatusCode::OK);
response.set_header("Content-Type", "text/html".to_string());
response.body(r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello world!~</title>
</head>
<body>
<h1>it works!!</h1>
</body>
</html>
"#.to_string());
response.send()
}
} }

View file

@ -2,13 +2,13 @@ use std::io::{Result};
use std::net::{ToSocketAddrs}; use std::net::{ToSocketAddrs};
use std::env; use std::env;
use mcstatusface::http::HttpServer; use mcstatusface::http::{HttpServer, StatusCode};
use mcstatusface::{MinecraftStatus}; use mcstatusface::{MinecraftStatus};
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 {
eprintln!("Usage: {} <serve | address[:port]>", args[0]); eprintln!("Usage: {} [serve] <address[:port]>", args[0]);
std::process::exit(1); std::process::exit(1);
} }
@ -30,7 +30,63 @@ fn main() -> Result<()> {
return Ok(()); return Ok(());
} }
HttpServer::new("0.0.0.0").start().unwrap(); let mut address = "0.0.0.0:8080".to_string();
if args.len() > 2 { address = args[2].to_string() }
HttpServer::new(address, 64).start(|request, mut response| {
response.status(StatusCode::OK);
response.set_header("x-powered-by", "GIRL FUEL".to_string());
if request.method() != "GET" {
response.status(StatusCode::NotFound);
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());
}
}
response.send()
}).unwrap();
Ok(()) Ok(())
} }