yea that oughta do it for now i think
This commit is contained in:
parent
eda7f79fb0
commit
cf28e189cd
2 changed files with 142 additions and 61 deletions
141
src/http.rs
141
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 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 {
|
||||||
|
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 {
|
HttpServer {
|
||||||
address: address.to_string(),
|
address: _address,
|
||||||
port: 8080,
|
port: _port,
|
||||||
max_connections: 16,
|
max_connections,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
pub fn start(&self, handler: HttpHandlerFunc) -> Result<()> {
|
||||||
|
|
||||||
pub fn start(&self) -> 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
62
src/main.rs
62
src/main.rs
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue