commit 0017c77637fae937c7f99c46c60083d39acccb10 Author: Winter Hille Date: Sat Mar 15 22:59:42 2025 -0700 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d1d85aa --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,40 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "fluffle" +version = "0.0.0" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f896a5d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "fluffle" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +bytes = "1.10.1" +http = "1.2.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63e3564 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Winter Hille + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..f612b42 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +{ + pkgs ? import { }, +}: +with pkgs; +mkShell { + buildInputs = [ + rustc + cargo + ]; +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..50d5c66 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,118 @@ +use std::{ + io::{Read, Write}, + net::{IpAddr, SocketAddr, TcpListener}, + sync::{Arc, Mutex}, + thread, +}; + +use bytes::{Bytes, BytesMut}; +use http::{Method, Request, Response, Version}; + +pub use http; + +pub struct Server { + listener: TcpListener, + request_handler: + Arc) -> Response + Send + Sync>>>>, +} + +impl Server { + pub fn new(ip: IpAddr, port: u16) -> Self { + let addr = SocketAddr::from((ip, port)); + let listener = TcpListener::bind(addr).unwrap(); + + Server { + listener, + request_handler: Arc::new(Mutex::new(None)), + } + } + + pub fn run(&mut self) { + for stream in self.listener.incoming() { + let mut stream = stream.unwrap(); + + let request_handler = Arc::clone(&self.request_handler); + + thread::spawn(move || { + let mut buffer = vec![0; 1024]; + let bytes_read = stream.read(&mut buffer).unwrap(); + + if bytes_read > 0 { + let request_bytes = BytesMut::from(&buffer[..bytes_read]); + if let Some(request_handler) = &*request_handler + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + { + let request = parse_request(request_bytes).unwrap(); + let response = parse_response(request_handler(request)); + + stream.write(&response).unwrap(); + stream.flush().unwrap(); + } + } + }); + } + } + + pub fn on_request(&self, handler: F) + where + F: Fn(Request) -> Response + Send + Sync + 'static, + { + *self.request_handler.lock().unwrap() = Some(Box::new(handler)) + } +} + +fn parse_request(request: BytesMut) -> Option> { + let request_str = std::str::from_utf8(&request).ok()?; + let mut lines = request_str.lines(); + let request_line = lines.next()?; + let mut parts = request_line.split_whitespace(); + + let method = parts.next()?.parse::().ok()?; + let uri = parts.next()?; + let version = match parts.next()? { + "HTTP/0.9" => Version::HTTP_09, + "HTTP/1.0" => Version::HTTP_10, + "HTTP/1.1" => Version::HTTP_11, + "HTTP/2.0" => Version::HTTP_2, + "HTTP/3.0" => Version::HTTP_3, + _ => unreachable!(), + }; + + let mut builder = Request::builder().method(method).uri(uri).version(version); + + for line in lines.by_ref() { + if let Some((k, v)) = line.split_once(": ") { + builder = builder.header(k, v); + }; + } + + let body = lines.collect::>().join("\n"); + + builder.body(body.into()).ok() +} + +fn parse_response(response: Response) -> BytesMut { + let mut response_bytes = BytesMut::new(); + + let version = match response.version() { + Version::HTTP_09 => "HTTP/0.9", + Version::HTTP_10 => "HTTP/1.0", + Version::HTTP_11 => "HTTP/1.1", + Version::HTTP_2 => "HTTP/2.0", + Version::HTTP_3 => "HTTP/3.0", + _ => unreachable!(), + }; + let status = response.status(); + + response_bytes.extend_from_slice(format!("{} {}\r\n", version, status).as_bytes()); + + for (k, v) in response.headers() { + response_bytes.extend_from_slice(format!("{}: {}\r\n", k, v.to_str().unwrap()).as_bytes()) + } + + response_bytes.extend_from_slice(b"\r\n"); + response_bytes.extend_from_slice(response.body()); + + response_bytes +}