you ever get bored and just write a raw TCP HTTP server

This commit is contained in:
ari melody 2025-04-14 17:14:22 +01:00
parent b0de168dda
commit eda7f79fb0
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
5 changed files with 605 additions and 14 deletions

247
src/http.rs Normal file
View file

@ -0,0 +1,247 @@
use std::{collections::HashMap, io::{BufRead, BufReader, Result, Write}, net::{SocketAddr, TcpListener, TcpStream}, thread, time::Duration};
use chrono::Local;
use crate::ThreadPool;
pub enum StatusCode {
OK,
NotFound,
// InternalServerError,
// ImATeapot,
}
pub struct Request<'a> {
stream: &'a TcpStream,
path: &'a str,
method: &'a str,
version: &'a str,
headers: HashMap<&'a str, &'a str>,
body: Option<String>,
}
impl<'a> 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_split: Vec<&str> = request_line.split(" ").collect();
let method = request_line_split[0];
let path = request_line_split[1];
let version = request_line_split[2];
let mut headers: HashMap<&'a str, &'a str> = HashMap::new();
if lines.len() > 1 {
let mut i: usize = 1;
loop {
if i >= lines.len() { break; }
let line = &lines[i];
if line.len() == 0 || !line.contains(":") { break; }
let (name, value) = line.split_once(":").unwrap();
headers.insert(name, value.trim());
i += 1;
}
}
let mut body: Option<String> = None;
if lines.len() > headers.len() + 2 &&
(method == "POST" || method == "PUT") &&
lines[headers.len() + 1] == "\r\n"
{
body = Some(lines[headers.len() + 2..].join("\n"));
}
Ok(Request {
stream,
path,
method,
version,
headers,
body,
})
}
pub fn address(&self) -> Result<SocketAddr> {
self.stream.peer_addr()
}
pub fn path(&self) -> &'a str {
self.path
}
pub fn method(&self) -> &'a str {
self.method
}
pub fn version(&self) -> &'a str {
self.version
}
pub fn body(&self) -> &Option<String> {
&self.body
}
pub fn headers(&self) -> &HashMap<&'a str, &'a str> {
&self.headers
}
}
pub struct Response<'a> {
stream: &'a TcpStream,
status: StatusCode,
headers: HashMap<&'a str, String>,
body: Option<String>,
}
impl<'a> Response<'a> {
pub fn new(stream: &'a TcpStream) -> Response<'a> {
Response {
stream,
status: StatusCode::OK,
headers: HashMap::from([
("Server", "mcstatusface".to_string()),
("Content-Type", "text/plain".to_string()),
]),
body: None,
}
}
pub fn status(&mut self, status: StatusCode) {
self.status = status;
}
pub fn headers(&self) -> &HashMap<&'a str, String> {
&self.headers
}
pub fn set_header(&mut self, name: &'a str, value: String) {
self.headers.insert(name, value);
}
pub fn body(&mut self, body: String) {
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();
let mut content_length: usize = 0;
if self.body.is_some() {
content_length = self.body.as_ref().unwrap().len();
}
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()
}
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();
}
Ok(len)
}
}
pub struct HttpServer {
address: String,
port: u16,
max_connections: usize,
}
impl HttpServer {
pub fn new(address: &str) -> HttpServer {
HttpServer {
address: address.to_string(),
port: 8080,
max_connections: 16,
}
}
pub fn start(&self) -> Result<()> {
let pool = ThreadPool::new(self.max_connections);
let listener = TcpListener::bind(format!("{}:{}", self.address, self.port)).expect("Failed to bind to port");
println!("Now listening on {}:{}", self.address, self.port);
for stream in listener.incoming() {
match stream {
Ok(stream) => {
pool.execute(move || {
HttpServer::handle_client(&stream);
});
}
Err(e) => {
eprintln!("Failed to handle incoming connection: {e}");
}
}
}
Ok(())
}
fn handle_client(stream: &TcpStream) {
let buf_reader = BufReader::new(stream);
let http_request: Vec<String> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let request = Request::new(stream, &http_request);
if request.is_err() {
eprintln!("Failed to process request: {}", request.err().unwrap());
return;
}
let request = request.unwrap();
let response = Response::new(stream);
let start_date = Local::now();
match HttpServer::handle_request(&request, response) {
Ok(_) => {
let end_date = Local::now();
println!(
"[{}] {} {} {} - {}ms - {}",
start_date.format("%Y-%m-%d %H:%M:%S"),
request.method(),
request.path(),
request.version(),
(end_date - start_date).num_milliseconds(),
request.address().unwrap().ip(),
);
}
Err(e) => {
eprintln!("Failed to handle request: {e}")
}
}
}
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

@ -1,4 +1,87 @@
use std::{sync::{mpsc, Arc, Mutex}, thread};
pub mod leb128;
pub mod status;
pub mod http;
pub use status::MinecraftStatus;
pub struct ThreadPool {
workers: Vec<ThreadWorker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
// Create a new ThreadPool with `size` available threads.
//
// # Panics
//
// `new` will panic if `size` is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(ThreadWorker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender)
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in &mut self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct ThreadWorker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl ThreadWorker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> ThreadWorker {
let thread = thread::spawn(move || loop {
let msg = receiver.lock().unwrap().recv();
match msg {
Ok(job) => {
// println!("Job received by worker {id}");
job();
}
Err(_) => {
// println!("Worker {id} disconnected. Shutting down...");
break;
}
}
});
ThreadWorker { id, thread }
}
}

View file

@ -2,28 +2,35 @@ use std::io::{Result};
use std::net::{ToSocketAddrs};
use std::env;
use mcstatusface::MinecraftStatus;
use mcstatusface::http::HttpServer;
use mcstatusface::{MinecraftStatus};
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <address[:port]>", args[0]);
eprintln!("Usage: {} <serve | address[:port]>", args[0]);
std::process::exit(1);
}
let mut address = String::from(args[1].as_str());
if !address.contains(":") {
address += ":25565";
if args[1] != "serve" {
let mut address = String::from(args[1].as_str());
if !address.contains(":") {
address += ":25565";
}
let mut addrs_iter = args[1].to_socket_addrs().unwrap();
let address = addrs_iter.next().unwrap();
let status = MinecraftStatus::fetch(address).unwrap();
println!("\nVersion: {} ({})", status.version.name, status.version.protocol);
println!("Players: {}/{}", status.players.online, status.players.max);
println!("MOTD:");
println!("{}", status.parse_description());
return Ok(());
}
let mut addrs_iter = args[1].to_socket_addrs().unwrap();
let address = addrs_iter.next().unwrap();
let status = MinecraftStatus::fetch(address).unwrap();
println!("\nVersion: {} ({})", status.version.name, status.version.protocol);
println!("Players: {}/{}", status.players.online, status.players.max);
println!("MOTD:");
println!("{}", status.parse_description());
HttpServer::new("0.0.0.0").start().unwrap();
Ok(())
}