diff --git a/src/http.rs b/src/http.rs index bdedce5..18b406d 100644 --- a/src/http.rs +++ b/src/http.rs @@ -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; + 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, } @@ -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 { - 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 { + // 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::().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 = 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 { - 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#" - - - - hello world!~ - - -

it works!!

- - -"#.to_string()); - response.send() - } } diff --git a/src/main.rs b/src/main.rs index a5c7c1c..91872d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 = env::args().collect(); if args.len() < 2 { - eprintln!("Usage: {} ", args[0]); + eprintln!("Usage: {} [serve] ", 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=\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(()) }