diff --git a/src/main.rs b/src/main.rs index d679fbd..e4df8de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ use std::{ net::TcpListener, }; +use crate::request::RequestHeader; + fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:8080")?; for incoming_stream in listener.incoming() { @@ -16,7 +18,10 @@ fn main() -> std::io::Result<()> { let req = request::Request::from_bufreader(reader)?; + println!("{req:?}"); + let mut writer = stream; + writer.write_all("Ok".as_bytes())?; writer.flush()?; } diff --git a/src/request.rs b/src/request.rs index cbb2918..4682649 100644 --- a/src/request.rs +++ b/src/request.rs @@ -9,20 +9,22 @@ const MAX_LINE_WIDTH: u64 = 4096; // 4 KiB const MAX_BODY_LENGTH: u64 = 8388608; // 5 MiB const MAX_HEADER_COUNT: u64 = 512; +#[derive(Debug)] pub struct Request { - method: Method, - http_version: String, - path: ServerPath, - headers: Vec, - data: Vec, + pub method: Method, + pub http_version: Box, + pub path: ServerPath, + pub headers: Box<[RequestHeader]>, + pub body: Option>, } +#[derive(Debug)] pub enum RequestHeader { Host(String), UserAgent(String), ContentType(ContentType), - Accept(ContentType), - Other(String, String), + Accept(Vec), + Other(Box, Box), } impl FromStr for RequestHeader { @@ -36,11 +38,15 @@ impl FromStr for RequestHeader { "Host" => Ok(RequestHeader::Host(value.to_string())), "UserAgent" => Ok(RequestHeader::UserAgent(value.to_string())), "ContentType" => Ok(RequestHeader::ContentType(ContentType::from_str(value)?)), - "Accept" => Ok(RequestHeader::Accept(ContentType::from_str(value)?)), - + "Accept" => Ok(RequestHeader::Accept( + value + .split(',') + .map(ContentType::from_str) + .collect::, io::Error>>()?, + )), _ => Ok(RequestHeader::Other( - header_type.to_string(), - value.to_string(), + header_type.to_string().into_boxed_str(), + value.to_string().into_boxed_str(), )), }, _ => Err(io::Error::new( @@ -58,12 +64,40 @@ impl Request { let first_line = Self::read_line(&mut limited_buffer)?; let parsed_first_line = Self::parse_first_line(first_line)?; + use std::collections::hash_set::HashSet; + use std::mem::{Discriminant, discriminant}; + + let mut header_set: HashSet> = HashSet::new(); + let mut headers = vec![]; + + for _ in 0..MAX_HEADER_COUNT { + let current_line = Self::read_line(&mut limited_buffer)?; + + if current_line.is_empty() || current_line == "\r\n" { + break; + } + + let header = RequestHeader::from_str(¤t_line)?; + headers.push(header); + + if let RequestHeader::Other(_, _) = headers.last().unwrap() { + continue; + } + + if !header_set.insert(discriminant(headers.last().unwrap())) { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Multiple headers of the same type", + )); + } + } + Ok(Self { method: parsed_first_line.0, path: parsed_first_line.1, - http_version: parsed_first_line.2, - headers: vec![], - data: vec![], + http_version: parsed_first_line.2.into_boxed_str(), + headers: headers.into_boxed_slice(), + body: None, }) } @@ -72,6 +106,13 @@ impl Request { buffer.set_limit(MAX_LINE_WIDTH); buffer.read_until(b'\n', &mut read_buffer)?; + if read_buffer.len() < 2 { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid line")); + } + + read_buffer.remove(read_buffer.len() - 1); + read_buffer.remove(read_buffer.len() - 1); + Ok(String::from_utf8_lossy(&read_buffer).to_string()) } @@ -92,9 +133,10 @@ impl Request { } } +#[derive(Debug)] pub struct ServerPath { - path: Vec, - query: Vec<(String, String)>, + pub path: Box, + pub query: Option, Box)]>>, } impl FromStr for ServerPath { @@ -102,11 +144,19 @@ impl FromStr for ServerPath { fn from_str(s: &str) -> Result { match s.split("?").collect::>().as_slice() { + [path] => Ok(Self { + path: path + .split('/') + .filter(|s| !s.is_empty() && !s.starts_with('.')) + .map(|s| s.to_string()) + .collect(), + query: None, + }), [path, query] => { let mut query_hashset: std::collections::hash_set::HashSet = std::collections::hash_set::HashSet::new(); - let query_parsed = query + let query_parsed: Vec<(Box, Box)> = query .split('&') .filter_map(|s| match s.split('=').collect::>().as_slice() { [parameter, value] => { @@ -114,7 +164,10 @@ impl FromStr for ServerPath { None } else { query_hashset.insert(parameter.to_string()); - Some((parameter.to_string(), value.to_string())) + Some(( + parameter.to_string().into_boxed_str(), + value.to_string().into_boxed_str(), + )) } } _ => None, @@ -127,7 +180,7 @@ impl FromStr for ServerPath { .filter(|s| !s.is_empty() && !s.starts_with('.')) .map(|s| s.to_string()) .collect(), - query: query_parsed, + query: Some(query_parsed.into_boxed_slice()), }) } _ => Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid path")), @@ -135,6 +188,7 @@ impl FromStr for ServerPath { } } +#[derive(Debug)] pub enum Method { Get, Post, diff --git a/src/response.rs b/src/response.rs index 9f54057..f11f4f4 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,11 +1,88 @@ use crate::shared_enums::ContentType; pub struct Response { - first_line: String, + http_version: String, + code: ResponseCode, headers: Vec, data: Vec, } +pub enum ResponseCode { + Continue, + SwitchingProtocols, + Processing, + EarlyHints, + OK, + Created, + Accepted, + NonAuthoritativeInformation, + NoContent, + ResetContent, + PartialContent, + MultiStatus, + AlreadyReported, + IMUsed, + MultipleChoices, + MovedPermanently, + Found, + SeeOther, + NotModified, + TemporaryRedirect, + PermanentRedirect, + BadRequest, + Unauthorized, + PaymentRequired, + Forbidden, + NotFound, + MethodNotAllowed, + NotAcceptable, + ProxyAuthenticationRequired, + RequestTimeout, + Conflict, + Gone, + LengthRequired, + PreconditionFailed, + ContentTooLarge, + URITooLong, + UnsupportedMediaType, + RangeNotSatisfiable, + ExpectationFailed, + IAmTeapot, + MisdirectedRequest, + UnprocessableContent, + Locked, + FailedDependency, + TooEarly, + UpgradeRequired, + PreconditionRequired, + TooManyRequests, + RequestHeaderFieldsTooLarge, + UnavailableForLegalReasons, + InternalServerError, + NotImplemented, + BadGateway, + ServiceUnavailable, + GatewayTimeout, + HTTPVersionNotSupported, + VariantAlsoNegotiates, + InsufficientStorage, + LoopDetected, + NotExtended, + NetworkAuthenticationRequired, +} + +impl ResponseCode { + fn to_code(&self) -> u32 { + match self { + ResponseCode::Continue => 100, + ResponseCode::SwitchingProtocols => 101, + ResponseCode::Processing => 102, + ResponseCode::EarlyHints => 103, + ResponseCode::OK => 200, + } + } +} + pub enum ResponseHeader { ContentLength(u32), ContentType(ContentType), diff --git a/src/shared_enums.rs b/src/shared_enums.rs index c5add9e..d8fea1d 100644 --- a/src/shared_enums.rs +++ b/src/shared_enums.rs @@ -1,22 +1,136 @@ use std::{io, str::FromStr}; -pub enum ContentType { +#[derive(Debug)] +pub enum ContentTypeType { Text(TextType), Aplication(ApplicationType), + Image(Image), Any, } +#[derive(Debug)] +pub enum Parameter { + Preference(f32), + Charset(Charset), + Other(Box, Box), +} + +#[derive(Debug)] +pub enum Charset { + UTF8, +} + +#[derive(Debug)] +pub struct ContentType { + pub content_type: ContentTypeType, + pub parameter: Option, +} + +pub fn InvalidDataError(error: &str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, error) +} + impl FromStr for ContentType { type Err = io::Error; fn from_str(s: &str) -> Result { - Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid content-type", - )) + match s.split(';').collect::>().as_slice() { + [val, par] => Ok(Self { + content_type: ContentTypeType::from_str(val)?, + parameter: Some(Parameter::from_str(par)?), + }), + + [val] => Ok(Self { + content_type: ContentTypeType::from_str(val)?, + parameter: None, + }), + + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid content-type", + )), + } } } +impl FromStr for Parameter { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + match s.split('=').collect::>().as_slice() { + ["q", value] => { + let pref_val = match value.parse::() { + Ok(v) => Ok(v), + Err(_) => Err(InvalidDataError("Invalid preference")), + }?; + + Ok(Parameter::Preference(pref_val)) + } + + ["charset", "utf-8"] => Ok(Parameter::Charset(Charset::UTF8)), + + [t, v] => Ok(Parameter::Other( + t.to_string().into_boxed_str(), + v.to_string().into_boxed_str(), + )), + + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid parameter", + )), + } + } +} + +impl FromStr for ContentTypeType { + type Err = io::Error; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split("/").collect(); + + match parts.as_slice() { + ["*", "*"] => Ok(ContentTypeType::Any), + ["text", "*"] => Ok(ContentTypeType::Text(TextType::Any)), + ["text", "html"] => Ok(ContentTypeType::Text(TextType::Html)), + ["text", "css"] => Ok(ContentTypeType::Text(TextType::Css)), + ["text", "javascript"] => Ok(ContentTypeType::Text(TextType::Javascript)), + + ["application", "json"] => Ok(ContentTypeType::Aplication(ApplicationType::Json)), + ["application", "xhtml+xml"] => { + Ok(ContentTypeType::Aplication(ApplicationType::XhtmlXml)) + } + ["application", "xml"] => Ok(ContentTypeType::Aplication(ApplicationType::Xml)), + ["application", "*"] => Ok(ContentTypeType::Aplication(ApplicationType::Any)), + + ["image", "png"] => Ok(ContentTypeType::Image(Image::Png)), + ["image", "jpeg"] | ["image", "jpg"] => Ok(ContentTypeType::Image(Image::Jpeg)), + ["image", "avif"] => Ok(ContentTypeType::Image(Image::Avif)), + ["image", "webp"] => Ok(ContentTypeType::Image(Image::Webp)), + ["image", "svg"] | ["image", "svg+xml"] => Ok(ContentTypeType::Image(Image::Svg)), + ["image", "*"] => Ok(ContentTypeType::Image(Image::Any)), + + _ => { + println!("{parts:?}"); + Err(io::Error::new( + io::ErrorKind::InvalidData, + "Invalid content-type-type", + )) + } + } + } +} + +#[derive(Debug)] +pub enum Image { + Png, + Avif, + Jpeg, + Webp, + Svg, + Any, +} + +#[derive(Debug)] pub enum TextType { Html, Css, @@ -24,7 +138,10 @@ pub enum TextType { Any, } +#[derive(Debug)] pub enum ApplicationType { Json, Any, + XhtmlXml, + Xml, }