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 crate::ThreadPool;
#[derive(Clone, Copy)]
pub enum StatusCode {
OK,
BadRequest,
NotFound,
// InternalServerError,
InternalServerError,
// 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> {
stream: &'a TcpStream,
path: &'a str,
method: &'a str,
version: &'a str,
headers: HashMap<&'a str, &'a str>,
query: HashMap<&'a str, &'a str>,
body: Option<String>,
}
@ -28,6 +55,22 @@ impl<'a> Request<'a> {
let 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)) => {
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();
if lines.len() > 1 {
let mut i: usize = 1;
@ -55,6 +98,7 @@ impl<'a> Request<'a> {
method,
version,
headers,
query,
body,
})
}
@ -77,6 +121,9 @@ impl<'a> Request<'a> {
pub fn headers(&self) -> &HashMap<&'a str, &'a str> {
&self.headers
}
pub fn query(&self) -> &HashMap<&'a str, &'a str> {
&self.query
}
}
pub struct Response<'a> {
@ -112,21 +159,9 @@ impl<'a> Response<'a> {
self.body = Some(body);
}
pub fn send(&mut self) -> Result<usize> {
let mut len: usize = 0;
let code = match self.status {
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();
pub fn send(&mut self) -> Result<StatusCode> {
// let mut len: usize = 0;
self.stream.write(format!("HTTP/1.1 {} {}\r\n", self.status.code(), self.status.reason()).as_bytes()).unwrap();
let mut content_length: usize = 0;
if self.body.is_some() {
@ -134,14 +169,14 @@ impl<'a> Response<'a> {
}
self.set_header("Content-Length", content_length.to_string());
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() {
len += self.stream.write("\r\n".as_bytes()).unwrap();
len += self.stream.write(self.body.as_ref().unwrap().as_bytes()).unwrap();
self.stream.write("\r\n".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 {
pub fn new(address: &str) -> HttpServer {
HttpServer {
address: address.to_string(),
port: 8080,
max_connections: 16,
pub fn new(address: String, max_connections: usize) -> HttpServer {
let mut _address = address.clone();
let mut _port: u16 = 8080;
match address.split_once(":") {
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 listener = TcpListener::bind(format!("{}:{}", self.address, self.port)).expect("Failed to bind to port");
@ -171,7 +214,7 @@ impl HttpServer {
match stream {
Ok(stream) => {
pool.execute(move || {
HttpServer::handle_client(&stream);
HttpServer::handle_client(&stream, handler);
});
}
Err(e) => {
@ -183,7 +226,7 @@ impl HttpServer {
Ok(())
}
fn handle_client(stream: &TcpStream) {
fn handle_client(stream: &TcpStream, handler: HttpHandlerFunc) {
let buf_reader = BufReader::new(stream);
let http_request: Vec<String> = buf_reader
.lines()
@ -201,16 +244,23 @@ impl HttpServer {
let response = Response::new(stream);
let start_date = Local::now();
match HttpServer::handle_request(&request, response) {
Ok(_) => {
match handler(&request, response) {
Ok(status) => {
let end_date = Local::now();
println!(
"[{}] {} {} {} - {}ms - {}",
"[{}] {} {} {} - {} {} - {}ms - {} ({})",
start_date.format("%Y-%m-%d %H:%M:%S"),
request.method(),
request.path(),
request.version(),
status.code(),
status.reason(),
(end_date - start_date).num_milliseconds(),
request.headers().get("User-Agent").map_or("[]", |v| v),
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::env;
use mcstatusface::http::HttpServer;
use mcstatusface::http::{HttpServer, StatusCode};
use mcstatusface::{MinecraftStatus};
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <serve | address[:port]>", args[0]);
eprintln!("Usage: {} [serve] <address[:port]>", args[0]);
std::process::exit(1);
}
@ -30,7 +30,63 @@ fn main() -> Result<()> {
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(())
}