From ff75fa542a98cf9a79133a81b7716401e717bfd6 Mon Sep 17 00:00:00 2001 From: omagdy7 Date: Wed, 5 Jun 2024 18:13:08 +0300 Subject: codecrafters submit [skip ci] --- src/extractor.rs | 44 +++++++++ src/http_types.rs | 89 +++++++++++++----- src/lib.rs | 2 + src/main.rs | 268 +++++++++++++++++++++++++++++++++++------------------- src/request.rs | 75 ++++++++++++--- src/response.rs | 53 +++++++++++ src/router.rs | 62 +++++++++++++ 7 files changed, 467 insertions(+), 126 deletions(-) create mode 100644 src/extractor.rs create mode 100644 src/router.rs (limited to 'src') diff --git a/src/extractor.rs b/src/extractor.rs new file mode 100644 index 0000000..edc7b2e --- /dev/null +++ b/src/extractor.rs @@ -0,0 +1,44 @@ +use regex::{escape, Regex}; + +pub fn build_regex_from_path(path_template: &str) -> Regex { + // Escape literal parts of the path to safely convert to regex + let mut regex_string = "^".to_string(); + for component in path_template.split('/') { + if component.starts_with(':') { + // Replace placeholder with regex to capture alphanumeric, underscores, or hyphens + regex_string.push_str("/([a-zA-Z0-9_-]+)"); + } else if !component.is_empty() { + // Escape and add literal components to the regex + regex_string.push('/'); + regex_string.push_str(&escape(component)); + } + } + regex_string.push_str("/?$"); + + // Compile the regex + Regex::new(®ex_string).unwrap() +} + +// pub fn match_path(method: &Method) -> String { +// use Method::*; +// match method { +// Get(route) => { +// match re.captures(route) { +// Some(caps) => { +// println!("Matched route: {}", route); +// // Iterate over the captures to extract the path segments and parameters +// for cap in caps.iter().flatten().skip(1) { +// println!("Segment: {}", cap.as_str()); +// } +// "empty".to_string() +// } +// None => { +// println!("No match for route: {}", route); +// "empty none".to_string() +// } +// } +// } +// Post(_) => todo!(), +// Put(_) => todo!(), +// } +// } diff --git a/src/http_types.rs b/src/http_types.rs index a652e6c..a196f25 100644 --- a/src/http_types.rs +++ b/src/http_types.rs @@ -10,14 +10,25 @@ pub enum StatusCode { NotFound, } -type Endpoint = String; -type Target = String; +type Route = String; -#[derive(Debug)] -pub enum HTTPMethod { - Get((Endpoint, Target)), - Post((Endpoint, Target)), - Put((Endpoint, Target)), +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Method { + Get(Route), + Post(Route), + Put(Route), +} + +pub fn get(route: &str) -> Method { + Method::Get(route.to_string()) +} + +pub fn post(route: &str) -> Method { + Method::Post(route.to_string()) +} + +pub fn put(route: &str) -> Method { + Method::Put(route.to_string()) } impl From for String { @@ -31,20 +42,43 @@ impl From for String { } } -impl From for String { - fn from(val: HTTPMethod) -> Self { - use HTTPMethod::*; +impl From for String { + fn from(val: Method) -> Self { + use Method::*; match val { - Get((endpoint, target)) => "GET".to_string() + &endpoint + &target, - Post((endpoint, target)) => "POST".to_string() + &endpoint + &target, - Put((endpoint, target)) => "PUT".to_string() + &endpoint + &target, + Get(route) => "GET ".to_string() + &route, + Post(route) => "POST ".to_string() + &route, + Put(route) => "PUT ".to_string() + &route, + } + } +} +impl From<&Method> for String { + fn from(val: &Method) -> Self { + use Method::*; + match val { + Get(route) => "GET ".to_string() + &route, + Post(route) => "POST ".to_string() + &route, + Put(route) => "PUT ".to_string() + &route, } } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Headers(pub HashMap); +impl From> for Headers { + fn from(value: Vec) -> Self { + let mut header_map = HashMap::new(); + for header in value.iter().filter(|val| !val.is_empty()) { + let (key, val) = header + .split_once(": ") + .expect("Should be splitable by :"); + header_map.insert(key.to_string(), val.to_string()); + } + Headers(header_map) + } +} + impl From<&[&str]> for Headers { fn from(value: &[&str]) -> Self { let mut header_map = HashMap::new(); @@ -58,16 +92,29 @@ impl From<&[&str]> for Headers { } } -impl From<&str> for HTTPMethod { +impl From for Method { + fn from(val: String) -> Self { + use Method::*; + let request_line = val.split(' ').collect_vec(); + let (method, route) = (request_line[0], request_line[1]); + match method { + "GET" => Get(route.to_string()), + "POST" => Post(route.to_string()), + _ => { + eprintln!("{method} Not Supported Yet"); + unreachable!() + } + } + } +} +impl From<&str> for Method { fn from(val: &str) -> Self { - use HTTPMethod::*; + use Method::*; let request_line = val.split(' ').collect_vec(); - let (method, info) = (request_line[0], request_line[1]); - let info = info.chars().skip(1).collect::() + &"/"; - let (endpoint, target) = info.split_once("/").expect("Should be splitable by /"); + let (method, route) = (request_line[0], request_line[1]); match method { - "GET" => Get((endpoint.to_string(), target.to_string())), - "POST" => Post((endpoint.to_string(), target.to_string())), + "GET" => Get(route.to_string()), + "POST" => Post(route.to_string()), _ => { eprintln!("{method} Not Supported Yet"); unreachable!() diff --git a/src/lib.rs b/src/lib.rs index 4667e1d..f9749a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +pub mod extractor; pub mod http_types; pub mod request; pub mod response; +pub mod router; diff --git a/src/main.rs b/src/main.rs index 1ad6f26..ccdaa37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,128 @@ #![allow(unused)] +use http_server_starter_rust::router::Router; use itertools::Itertools; use std::collections::HashMap; +use std::fs::File; use std::io::{self, Read, Write}; use std::net::{TcpListener, TcpStream}; -use std::{str, thread}; +use std::str::Utf8Error; +use std::sync::{Arc, Mutex}; +use std::{str, thread, usize}; -use http_server_starter_rust::http_types::*; use http_server_starter_rust::request::*; use http_server_starter_rust::response::*; +use http_server_starter_rust::{extractor, http_types::*}; -fn handle_client(mut stream: TcpStream) { +fn read_file_as_bytes(path: &str) -> io::Result> { + // Open the file in read-only mode + let mut file = File::open(path)?; + + // Create a buffer to hold the file contents + let mut buffer = Vec::new(); + + // Read the file contents into the buffer + file.read_to_end(&mut buffer)?; + + // Return the buffer + Ok(buffer) +} + +fn handle_echo(request: &Request, ctx: Option<&HashMap>) -> Response { + let mut headers = HashMap::new(); + // Extract the route regardless of the variant + let mut echo_string = "".to_string(); + let route = match request.method() { + Method::Get(route) | Method::Post(route) | Method::Put(route) => route, + }; + + for ch in route.chars().skip(1).skip_while(|&ch| ch != '/').skip(1) { + echo_string.push(ch); + } + if echo_string.chars().last().unwrap() == '/' { + echo_string.pop(); + } + let len = echo_string.len().to_string(); + headers.insert("Content-Type".to_string(), "text/plain".to_string()); + headers.insert("Content-Length".to_string(), len); + let body = echo_string; + Response::new( + "1.1".to_string(), + StatusCode::Ok, + Some(Headers(headers)), + Some(body), + ) +} + +fn handle_files(request: &Request, ctx: Option<&HashMap>) -> Response { + // Extract the route regardless of the variant + let mut file = "".to_string(); + let route = match request.method() { + Method::Get(route) | Method::Post(route) | Method::Put(route) => route, + }; + + let mut directory = ctx.unwrap().get(&"dir".to_string()).unwrap().clone(); + directory.pop(); // remove last slash + + for ch in route.chars().skip(1).skip_while(|&ch| ch != '/') { + file.push(ch); + } + if file.chars().last().unwrap() == '/' { + file.pop(); + } + let len = file.len().to_string(); + + let full_path = &(directory + &file); + dbg!(full_path); + + match read_file_as_bytes(full_path) { + Ok(bytes) => { + let mut headers = HashMap::new(); + headers.insert( + "Content-Type".to_string(), + "application/octet-stream".to_string(), + ); + headers.insert("Content-Length".to_string(), bytes.len().to_string()); + let body = String::from_utf8(bytes).unwrap(); + Response::new( + "1.1".to_string(), + StatusCode::Ok, + Some(Headers(headers)), + Some(body), + ) + } + Err(_) => Response::new("1.1".to_string(), StatusCode::NotFound, None, None), + } +} + +fn handle_user_agent(request: &Request, ctx: Option<&HashMap>) -> Response { + let mut headers = HashMap::new(); + let user_agent = request.get_tag("User-Agent".to_string()); + let len = user_agent.len().to_string(); + headers.insert("Content-Type".to_string(), "text/plain".to_string()); + headers.insert("Content-Length".to_string(), len); + let body = user_agent.to_string(); + Response::new( + "1.1".to_string(), + StatusCode::Ok, + Some(Headers(headers)), + Some(body), + ) + .into() +} + +fn handle_success(request: &Request, ctx: Option<&HashMap>) -> Response { + Response::new("1.1".to_string(), StatusCode::Ok, None, None).into() +} + +fn handle_not_found(request: Request, ctx: Option<&HashMap>) -> Response { + Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into() +} + +fn serve( + mut stream: TcpStream, + router: Arc>, + ctx: Arc>>, +) -> io::Result { // Buffer to store the data received from the client let mut buffer = [0; 512]; @@ -17,107 +130,78 @@ fn handle_client(mut stream: TcpStream) { match stream.read(&mut buffer) { Ok(_) => { // Convert buffer to a string and print the received data - if let Ok(request) = str::from_utf8(&buffer) { - println!("Received request: {}", request); - let request = Request::from(request); - println!("Request after parsing: {:?}", request); - let succses: String = - Response::new("1.1".to_string(), StatusCode::Ok, None, None).into(); - let succses = succses.as_bytes(); - - let not_found: String = - Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into(); - let not_found = not_found.as_bytes(); - - match request.method { - HTTPMethod::Get((endpoint, target)) => { - match (endpoint.as_str(), target.as_str()) { - ("", "") => match stream.write(succses) { - Ok(_) => { - println!("Response sent successfully"); - } - Err(e) => eprintln!("Failed to send response: {}", e), - }, - ("echo", target) => { - let mut headers = HashMap::new(); - headers - .insert("Content-Type".to_string(), "text/plain".to_string()); - headers.insert( - "Content-Length".to_string(), - (target.len() - 1).to_string(), - ); - let body = target[0..target.len() - 1].to_string(); - let response: String = Response::new( - "1.1".to_string(), - StatusCode::Ok, - Some(Headers(headers)), - Some(body), - ) - .into(); - dbg!(&response); - let response = response.as_bytes(); - - match stream.write(response) { - Ok(_) => { - println!("Response sent successfully"); - println!("Hello echo"); - } - Err(e) => eprintln!("Failed to send response: {}", e), - } - } - ("user-agent", _) => { - let mut headers = HashMap::new(); - let user_agent: String = - request.headers.unwrap().0.get("User-Agent").unwrap().into(); - headers - .insert("Content-Type".to_string(), "text/plain".to_string()); - headers.insert( - "Content-Length".to_string(), - user_agent.len().to_string(), - ); - let body = user_agent; - let response: String = Response::new( - "1.1".to_string(), - StatusCode::Ok, - Some(Headers(headers)), - Some(body), - ) - .into(); - dbg!(&response); - let response = response.as_bytes(); - - match stream.write(response) { - Ok(_) => { - println!("Response sent successfully"); - println!("Hello user-agent"); - } - Err(e) => eprintln!("Failed to send response: {}", e), - } - } - _ => match stream.write(not_found) { - Ok(_) => println!("Response sent successfully"), - Err(e) => eprintln!("Failed to send response: {}", e), - }, - } - } - HTTPMethod::Post(_target) => todo!(), - HTTPMethod::Put(_target) => todo!(), + match str::from_utf8(&buffer) { + Ok(request) => { + use Method::*; + println!("Received request:\n{}", request); + let request_lines: Vec<&str> = request.split("\r\n").collect(); + let request = Request::from(request_lines); + let request_string: String = (&request).into(); + + println!("Request after parsing:\n{}", request_string); + dbg!(&request.method); + + let response: String = { + let router = router.lock().unwrap(); + let ctx = ctx.lock().unwrap(); + router.handle(&request, Some(&ctx)).into() + }; + stream.write(response.as_bytes()) } + Err(_) => todo!(), } } - Err(e) => { - eprintln!("Failed to read from stream: {}", e); - } + Err(_) => todo!(), } } fn main() -> io::Result<()> { + // Collect the command-line arguments + let args: Vec = std::env::args().collect(); + + let mut dir = "".to_string(); + + let ctx = Arc::new(Mutex::new(HashMap::new())); + + // Check if the correct number of arguments are provided + if args.len() == 3 { + // Parse the arguments + if args[1] == "--directory" { + dir += &args[2]; + println!("Directory: {}", dir); + } else { + eprintln!("Unknown argument: {}", args[1]); + eprintln!("Usage: {} --directory ", args[0]); + return Ok(()); + } + } else { + eprintln!("Usage: {} --directory ", args[0]); + } + let listener = TcpListener::bind("127.0.0.1:4221").unwrap(); + let router = Arc::new(Mutex::new(Router::new())); + ctx.lock().unwrap().insert("dir".to_string(), dir); + + { + let mut router = router.lock().unwrap(); + router + .route(get("/"), handle_success) + .route(get("/echo/:var/"), handle_echo) + .route(get("/user-agent/"), handle_user_agent) + .route(get("/files/:file/"), handle_files); + } + for stream in listener.incoming() { match stream { Ok(stream) => { - thread::spawn(move || handle_client(stream)); + let router = Arc::clone(&router); + let ctx = Arc::clone(&ctx); + thread::spawn(move || { + if let Err(e) = serve(stream, router, ctx) { + eprintln!("Failed to serve connection: {}", e); + } + }); } Err(e) => { eprintln!("error: {}", e); diff --git a/src/request.rs b/src/request.rs index 8728e79..27a41f5 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,14 +1,16 @@ +use std::collections::HashMap; + use crate::http_types::*; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Request { - pub method: HTTPMethod, + pub method: Method, pub headers: Option, body: Option, } impl Request { - fn new(method: HTTPMethod, headers: Headers, body: String) -> Self { + fn new(method: Method, headers: Headers, body: String) -> Self { let headers = if headers.0.len() == 0 { None } else { @@ -21,19 +23,31 @@ impl Request { body, } } + + pub fn get_tag(&self, key: String) -> String { + self.headers.as_ref().unwrap().0.get(&key).unwrap().clone() + } + + pub fn method(&self) -> &Method { + &self.method + } + + pub fn headers(&self) -> &Option { + &self.headers + } + + pub fn body(&self) -> &Option { + &self.body + } } -impl From<&str> for Request { - fn from(val: &str) -> Self { - let request: Vec<&str> = val.split("\r\n").collect(); - match &request[..] { +impl From> for Request { + fn from(value: Vec<&str>) -> Self { + match &value[..] { [request_line, headers @ .., body] => { - let (method, headers, body) = ( - HTTPMethod::from(*request_line), - Headers::from(headers), - body.to_string(), - ); - Request::new(method, headers, body) + let (method, headers, body) = + (Method::from(*request_line), Headers::from(headers), body); + Request::new(method, headers, (*body).to_string()) } _ => { unreachable!(); @@ -41,3 +55,38 @@ impl From<&str> for Request { } } } + +impl<'a> Into for Request { + fn into(self) -> String { + let method = String::from(self.method); + let (method, endpoint) = method.split_once(" ").unwrap(); + let status_line = format!("{} {} HTTP/1.1", method, endpoint); + let headers = self + .headers + .unwrap_or(Headers(HashMap::new())) + .0 + .iter() + .map(|(key, value)| format!("{key}: {value}\r\n")) + .collect::(); + let body = self.body.unwrap_or("".to_string()); + format!("{status_line}\r\n{headers}\r\n{body}") + } +} + +impl Into for &Request { + fn into(self) -> String { + let method = String::from(self.method.clone()); + let (method, endpoint) = method.split_once(" ").unwrap(); + let status_line = format!("{} {} HTTP/1.1", method, endpoint); + let headers = self + .headers() + .clone() + .unwrap_or(Headers(HashMap::new())) + .0 + .iter() + .map(|(key, value)| format!("{key}: {value}\r\n")) + .collect::(); + let body = self.body.clone().unwrap_or("".to_string()); + format!("{status_line}\r\n{headers}\r\n{body}") + } +} diff --git a/src/response.rs b/src/response.rs index 4219afd..c3540d2 100644 --- a/src/response.rs +++ b/src/response.rs @@ -38,3 +38,56 @@ impl Into for Response { format!("{status_line}\r\n{headers}\r\n{body}") } } + +impl Into for &str { + fn into(self) -> Response { + let mut lines = self.lines(); + + // Parse the status line + let status_line = lines.next().unwrap_or_default(); + let mut status_line_parts = status_line.split_whitespace(); + + let version = status_line_parts.next().unwrap_or_default().to_string(); + let status_code = status_line_parts.next().unwrap_or_default(); + + let status = match status_code { + "200" => StatusCode::Ok, + "404" => StatusCode::NotFound, + _ => StatusCode::Ok, + }; + + // Parse headers + let mut headers_map = HashMap::new(); + while let Some(line) = lines.next() { + if line.is_empty() { + break; + } + let mut parts = line.splitn(2, ':'); + let key = parts.next().unwrap_or_default().trim(); + let value = parts.next().unwrap_or_default().trim(); + headers_map.insert(key.to_string(), value.to_string()); + } + let headers = if headers_map.is_empty() { + None + } else { + Some(Headers(headers_map)) + }; + + // Parse body + let body: Option = { + let remaining_lines: Vec<&str> = lines.collect(); + if remaining_lines.is_empty() { + None + } else { + Some(remaining_lines.join("\n")) + } + }; + + Response { + version, + status, + headers, + body, + } + } +} diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..1d8e6f2 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,62 @@ +use crate::{ + extractor::build_regex_from_path, + http_types::{Method, StatusCode}, + request::Request, + response::Response, +}; +use regex::Regex; +use std::collections::HashMap; + +type Handler = fn(&Request, Option<&HashMap>) -> Response; +type Routes = HashMap; + +pub struct Router { + routes: Routes, +} + +impl Router { + // Create a new Router + pub fn new() -> Self { + Router { + routes: HashMap::new(), + } + } + + pub fn routes(&self) -> &Routes { + &self.routes + } + + // Add a route to the router + pub fn route(&mut self, method: Method, handler: Handler) -> &mut Self { + use Method::*; + + let method_string = match &method { + Get(s) | Post(s) | Put(s) => s, + }; + + let re = build_regex_from_path(method_string.as_str()); + let meth = Get(re.to_string()); + dbg!(&meth); + self.routes.insert(meth, handler); + self + } + + // Handle incoming requests + pub fn handle(&self, request: &Request, ctx: Option<&HashMap>) -> Response { + use Method::*; + let method_string = match &request.method { + Get(s) | Post(s) | Put(s) => s, + }; + for (method, handler) in self.routes() { + let route_method = match method { + Get(s) | Post(s) | Put(s) => s.as_str(), + }; + let re = Regex::new(route_method).unwrap(); + dbg!(&re, method_string); + if re.is_match(method_string) { + return handler(request, ctx); + } + } + Response::new("1.1".to_string(), StatusCode::NotFound, None, None).into() + } +} -- cgit v1.2.3