From 9a1cd8bb5020b644acc42b85996be624357c6c48 Mon Sep 17 00:00:00 2001 From: Winter Hille Date: Thu, 14 Nov 2024 20:37:26 -0800 Subject: [PATCH] version 0.1.0 --- Cargo.lock | 111 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/client.rs | 41 ++++++++++++++++ src/http/mod.rs | 7 +++ src/http/request.rs | 34 +++++++++++++ src/http/response.rs | 55 +++++++++++++++++++++ src/http/version.rs | 37 +++++++++++++++ src/lib.rs | 6 +++ src/server.rs | 102 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 394 insertions(+) create mode 100644 src/client.rs create mode 100644 src/http/mod.rs create mode 100644 src/http/request.rs create mode 100644 src/http/response.rs create mode 100644 src/http/version.rs create mode 100644 src/server.rs diff --git a/Cargo.lock b/Cargo.lock index 0b90ab3..9286175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,117 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "fluffle" version = "0.1.0" +dependencies = [ + "lazy-regex", +] + +[[package]] +name = "lazy-regex" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" diff --git a/Cargo.toml b/Cargo.toml index 6ab1367..2cca5d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +lazy-regex = { version = "3.3.0", features = ["std"] } diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..4452fbf --- /dev/null +++ b/src/client.rs @@ -0,0 +1,41 @@ +use std::{ + io::{self, Read, Write}, + net::TcpStream, +}; + +pub struct Client { + stream: TcpStream, +} + +impl Client { + pub(crate) fn new(stream: TcpStream) -> Self { + Client { stream } + } + + // basically an alias for write (might remove) + pub(crate) fn write_bytes(&mut self, mut bytes: Vec) -> io::Result { + bytes.extend(b"\r\n"); + self.stream.write(&bytes) + } + + pub(crate) fn write_string(&mut self, mut string: String) -> io::Result { + string.push_str("\r\n"); + self.stream.write(string.as_bytes()) + } +} + +impl Read for Client { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.stream.read(buf) + } +} + +impl Write for Client { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.stream.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.stream.flush() + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..a0c19c0 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,7 @@ +pub mod request; +pub mod response; +mod version; + +pub use self::request::Request; +pub use self::response::Response; +pub use self::version::Version; diff --git a/src/http/request.rs b/src/http/request.rs new file mode 100644 index 0000000..7e07f3c --- /dev/null +++ b/src/http/request.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use lazy_regex::regex_captures; + +use crate::http; + +/// HTTP request line +/// +/// ` HTTP/` +#[derive(Debug)] +pub struct RequestLine<'a> { + pub method: &'a str, + pub target: &'a str, + pub version: http::Version, +} + +impl RequestLine<'_> { + pub(crate) fn from_str(str: &str) -> RequestLine<'_> { + let (_, method, target, version) = + regex_captures!(r#"([^\s]+)\s+([^\s]+)\s+HTTP\/([0-9.]+)"#, str).unwrap(); + + RequestLine { + method, + target, + version: http::Version::from_str(version), + } + } +} + +#[derive(Debug)] +pub struct Request<'a> { + pub request_line: RequestLine<'a>, + pub headers: HashMap<&'a str, &'a str>, +} diff --git a/src/http/response.rs b/src/http/response.rs new file mode 100644 index 0000000..8e88de9 --- /dev/null +++ b/src/http/response.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; + +use crate::http; + +#[derive(Debug)] +pub enum Status { + OK, + NotFound, +} + +impl Status { + fn code(&self) -> u16 { + match self { + Status::OK => 200, + Status::NotFound => 404, + } + } + + fn message(&self) -> &str { + match self { + Status::OK => "OK", + Status::NotFound => "Not Found", + } + } + + pub(crate) fn to_string(&self) -> String { + format!("{} {}", self.code(), self.message()) + } +} + +/// HTTP response line +/// +/// `HTTP/ ` +#[derive(Debug)] +pub struct ResponseLine { + pub version: http::Version, + pub status: Status, +} + +impl ResponseLine { + pub fn new(version: http::Version, status: Status) -> Self { + ResponseLine { version, status } + } + + pub(crate) fn to_string(&self) -> String { + format!("HTTP/{} {}", self.version.to_str(), self.status.to_string()) + } +} + +#[derive(Debug)] +pub struct Response<'a> { + pub response_line: ResponseLine, + pub headers: Option>, + pub content: Option>, +} diff --git a/src/http/version.rs b/src/http/version.rs new file mode 100644 index 0000000..f72a056 --- /dev/null +++ b/src/http/version.rs @@ -0,0 +1,37 @@ +#[derive(Debug)] +pub enum Version { + Http0_9, + Http1_0, + Http1_1, + Http2, + Http3, +} + +impl Version { + pub(crate) fn from_str(str: &str) -> Self { + match str { + "0.9" => Version::Http0_9, + "1.0" => Version::Http1_0, + "1.1" => Version::Http1_1, + "2" => Version::Http2, + "3" => Version::Http3, + _ => unreachable!(), + } + } + + pub(crate) fn to_str(&self) -> &str { + match self { + Version::Http0_9 => "0.9", + Version::Http1_0 => "1.0", + Version::Http1_1 => "1.1", + Version::Http2 => "2", + Version::Http3 => "3", + } + } +} + +impl Default for Version { + fn default() -> Self { + Version::Http1_1 + } +} diff --git a/src/lib.rs b/src/lib.rs index 8b13789..f8bc083 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,7 @@ +mod client; +pub mod http; +mod server; +pub use client::Client; +pub use http::Version as HttpVersion; +pub use server::Server; diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..7ee39ff --- /dev/null +++ b/src/server.rs @@ -0,0 +1,102 @@ +use std::{ + collections::HashMap, + io::{Read, Write}, + net::TcpListener, + str::from_utf8, + sync::{Arc, Mutex}, + thread, +}; + +use crate::{ + http::{request::RequestLine, Request, Response}, + Client, +}; + +pub struct Server { + listener: TcpListener, + + on_request: Arc Response + Send + 'static>>>>, +} + +impl Server { + pub fn new(ip: &str, port: i32) -> Self { + let addr = format!("{}:{}", ip, port); + let listener = TcpListener::bind(addr).unwrap(); + + Server { + listener, + + on_request: Arc::new(Mutex::new(None)), + } + } + + pub fn run(&mut self) { + for stream in self.listener.incoming() { + let stream = stream.unwrap(); + + let on_request = Arc::clone(&self.on_request); + + thread::spawn(move || { + let mut client = Client::new(stream); + + let mut buffer = vec![0; 1024]; + let bytes_read = client.read(&mut buffer).unwrap(); + + if bytes_read > 0 { + if let Ok(request_str) = from_utf8(&buffer[..bytes_read]) { + if let Some(on_request) = &*on_request.lock().unwrap() { + let request; + { + let mut lines = request_str.lines(); + + let request_line = RequestLine::from_str(lines.next().unwrap()); + + let mut headers = HashMap::new(); + for line in lines { + if let Some((k, v)) = line.split_once(": ") { + headers.insert(k, v); + } + } + + request = Request { + request_line, + headers, + }; + } + + let response = on_request(request); + { + let response_line = response.response_line.to_string(); + client.write_string(response_line).unwrap(); + + if let Some(headers) = response.headers { + for (k, v) in headers { + client.write_string(format!("{}: {}", k, v)).unwrap(); + } + } else { + client.write(b"\r\n").unwrap(); + } + + if let Some(content) = response.content { + client.write(b"\r\n").unwrap(); + client.write(&content).unwrap(); + } else { + client.write(b"\r\n").unwrap(); + } + } + + client.flush().unwrap(); + } + } + } + }); + } + } + + pub fn on_request(&mut self, f: F) + where + F: Fn(Request) -> Response + Send + 'static, + { + *self.on_request.lock().unwrap() = Some(Box::new(f)) + } +}