From 80fabcde47e20169ec07c7325d6e660de34b7206 Mon Sep 17 00:00:00 2001 From: maxstrb Date: Sun, 12 Oct 2025 21:12:15 +0200 Subject: [PATCH] First response --- src/main.rs | 13 ++- src/request.rs | 13 +-- src/response.rs | 173 +++++++++++++++++++++++++++++-- src/shared_enums.rs | 241 ++++++++++++++++++++++++++++++++++++-------- 4 files changed, 382 insertions(+), 58 deletions(-) diff --git a/src/main.rs b/src/main.rs index e4df8de..77c2b4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,10 @@ use std::{ net::TcpListener, }; -use crate::request::RequestHeader; +use crate::{ + response::{Response, ResponseCode, ResponseHeader}, + shared_enums::Content, +}; fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:8080")?; @@ -22,7 +25,13 @@ fn main() -> std::io::Result<()> { let mut writer = stream; - writer.write_all("Ok".as_bytes())?; + let response = Response::new() + .with_code(ResponseCode::Ok) + .with_data(b"Hello World!

Ahojky

Jou jou jou

".to_vec()) + .with_header(ResponseHeader::ContentType(Content::html_utf8())); + + response.respond(&mut writer)?; + writer.flush()?; } Ok(()) diff --git a/src/request.rs b/src/request.rs index 4682649..a7cb462 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,10 +1,11 @@ -use crate::shared_enums::ContentType; use std::{ io::{self, BufRead, BufReader, Read, Take}, net::TcpStream, str::FromStr, }; +use crate::shared_enums::Content; + const MAX_LINE_WIDTH: u64 = 4096; // 4 KiB const MAX_BODY_LENGTH: u64 = 8388608; // 5 MiB const MAX_HEADER_COUNT: u64 = 512; @@ -22,8 +23,8 @@ pub struct Request { pub enum RequestHeader { Host(String), UserAgent(String), - ContentType(ContentType), - Accept(Vec), + ContentType(Content), + Accept(Vec), Other(Box, Box), } @@ -37,12 +38,12 @@ impl FromStr for RequestHeader { [header_type, value] => match *header_type { "Host" => Ok(RequestHeader::Host(value.to_string())), "UserAgent" => Ok(RequestHeader::UserAgent(value.to_string())), - "ContentType" => Ok(RequestHeader::ContentType(ContentType::from_str(value)?)), + "ContentType" => Ok(RequestHeader::ContentType(Content::from_str(value)?)), "Accept" => Ok(RequestHeader::Accept( value .split(',') - .map(ContentType::from_str) - .collect::, io::Error>>()?, + .map(Content::from_str) + .collect::, io::Error>>()?, )), _ => Ok(RequestHeader::Other( header_type.to_string().into_boxed_str(), diff --git a/src/response.rs b/src/response.rs index f11f4f4..7ce93b2 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,18 +1,92 @@ -use crate::shared_enums::ContentType; +use std::{ + io::{self, Write}, + net::TcpStream, +}; + +use crate::shared_enums::Content; pub struct Response { - http_version: String, + http_version: Box, code: ResponseCode, headers: Vec, data: Vec, } +impl Response { + pub fn respond(self, stream: &mut TcpStream) -> Result<(), io::Error> { + let binding = self.to_str(); + let mut output = binding.as_bytes().to_vec(); + output.extend_from_slice(b"\r\n"); + + if !self.data.is_empty() { + output.extend_from_slice(b"\r\n"); + output.extend_from_slice(&self.data); + } + + stream.write_all(output.as_slice())?; + + Ok(()) + } + + fn to_str(&self) -> Box { + format!( + "{} {}\r\n{}", + self.http_version, + self.code.to_code(), + self.headers + .iter() + .map(|header| header.to_str()) + .collect::>() + .join("\r\n") + ) + .into_boxed_str() + } + + pub fn new() -> Self { + Self { + http_version: "HTTP/1.1".to_owned().into_boxed_str(), + code: ResponseCode::Ok, + headers: vec![], + data: vec![], + } + } + + pub fn with_data(self, data: Vec) -> Self { + Self { + http_version: self.http_version, + code: self.code, + headers: self.headers, + data, + } + } + + pub fn with_code(self, code: ResponseCode) -> Self { + Self { + http_version: self.http_version, + code, + headers: self.headers, + data: self.data, + } + } + + pub fn with_header(mut self, header: ResponseHeader) -> Self { + self.headers.push(header); + + Self { + http_version: self.http_version, + code: self.code, + headers: self.headers, + data: self.data, + } + } +} + pub enum ResponseCode { Continue, SwitchingProtocols, Processing, EarlyHints, - OK, + Ok, Created, Accepted, NonAuthoritativeInformation, @@ -72,23 +146,102 @@ pub enum ResponseCode { } impl ResponseCode { - fn to_code(&self) -> u32 { + fn to_code(&self) -> &'static str { + type R = ResponseCode; match self { - ResponseCode::Continue => 100, - ResponseCode::SwitchingProtocols => 101, - ResponseCode::Processing => 102, - ResponseCode::EarlyHints => 103, - ResponseCode::OK => 200, + R::Continue => "100 Continue", + R::SwitchingProtocols => "101 Switching Protocols", + R::Processing => "102 Processing", + R::EarlyHints => "103 Early Hints", + R::Ok => "200 OK", + R::Created => "201 Created", + R::Accepted => "202 Accepted", + R::NonAuthoritativeInformation => "203 Non-Authoritative Information", + R::NoContent => "204 No Content", + R::ResetContent => "205 Reset Content", + R::PartialContent => "206 Partial Content", + R::MultiStatus => "207 Multi-Status", + R::AlreadyReported => "208 Already Reported", + R::IMUsed => "226 IM Used", + R::MultipleChoices => "300 Multiple Choices", + R::MovedPermanently => "301 Moved Permanently", + R::Found => "302 Found", + R::SeeOther => "303 See Other", + R::NotModified => "304 Not Modified", + R::TemporaryRedirect => "307 Temporary Redirect", + R::PermanentRedirect => "308 Permanent Redirect", + R::BadRequest => "400 Bad Request", + R::Unauthorized => "401 Unauthorized", + R::PaymentRequired => "402 Payment Required", + R::Forbidden => "403 Forbidden", + R::NotFound => "404 Not Found", + R::MethodNotAllowed => "405 Method Not Allowed", + R::NotAcceptable => "406 Not Acceptable", + R::ProxyAuthenticationRequired => "407 Proxy Authentication Required", + R::RequestTimeout => "408 Request Timeout", + R::Conflict => "409 Conflict", + R::Gone => "410 Gone", + R::LengthRequired => "411 Length Required", + R::PreconditionFailed => "412 Precondition Failed", + R::ContentTooLarge => "413 Content Too Large", + R::URITooLong => "414 URI Too Long", + R::UnsupportedMediaType => "415 Unsupported Media Type", + R::RangeNotSatisfiable => "416 Range Not Satisfiable", + R::ExpectationFailed => "417 Expectation Failed", + R::IAmTeapot => "418 I'm a teapot", + R::MisdirectedRequest => "421 Misdirected Request", + R::UnprocessableContent => "422 Unprocessable Content", + R::Locked => "423 Locked", + R::FailedDependency => "424 Failed Dependency", + R::TooEarly => "425 Too Early", + R::UpgradeRequired => "426 Upgrade Required", + R::PreconditionRequired => "428 Precondition Required", + R::TooManyRequests => "429 Too Many Requests", + R::RequestHeaderFieldsTooLarge => "431 Request Header Fields Too Large", + R::UnavailableForLegalReasons => "451 Unavailable For Legal Reasons", + R::InternalServerError => "500 Internal Server Error", + R::NotImplemented => "501 Not Implemented", + R::BadGateway => "502 Bad Gateway", + R::ServiceUnavailable => "503 Service Unavailable", + R::GatewayTimeout => "504 Gateway Timeout", + R::HTTPVersionNotSupported => "505 HTTP Version Not Supported", + R::VariantAlsoNegotiates => "506 Variant Also Negotiates", + R::InsufficientStorage => "507 Insufficient Storage", + R::LoopDetected => "508 Loop Detected", + R::NotExtended => "510 Not Extended", + R::NetworkAuthenticationRequired => "511 Network Authentication Required", } } } pub enum ResponseHeader { ContentLength(u32), - ContentType(ContentType), + ContentType(Content), CacheControl(CacheControl), } +impl ResponseHeader { + fn to_str(&self) -> Box { + type R = ResponseHeader; + match self { + R::ContentLength(length) => format!("Content-Length: {length}").into_boxed_str(), + R::ContentType(content) => { + format!("Content-Type: {}", content.to_str()).into_boxed_str() + } + R::CacheControl(c) => format!("Cache-Control: {}", c.to_str()).into_boxed_str(), + } + } +} + pub enum CacheControl { NoStore, } + +impl CacheControl { + fn to_str(&self) -> &'static str { + type C = CacheControl; + match self { + C::NoStore => "no-store", + } + } +} diff --git a/src/shared_enums.rs b/src/shared_enums.rs index d8fea1d..78ca4ea 100644 --- a/src/shared_enums.rs +++ b/src/shared_enums.rs @@ -1,13 +1,26 @@ use std::{io, str::FromStr}; #[derive(Debug)] -pub enum ContentTypeType { +pub enum ContentType { Text(TextType), Aplication(ApplicationType), Image(Image), Any, } +impl ContentType { + fn to_str(&self) -> Box { + match self { + ContentType::Text(text) => format!("text/{}", text.to_str()).into_boxed_str(), + ContentType::Aplication(app) => { + format!("application/{}", app.to_str()).into_boxed_str() + } + ContentType::Image(img) => format!("image/{}", img.to_str()).into_boxed_str(), + ContentType::Any => "*/*".to_string().into_boxed_str(), + } + } +} + #[derive(Debug)] pub enum Parameter { Preference(f32), @@ -15,40 +28,158 @@ pub enum Parameter { Other(Box, Box), } +impl Parameter { + fn to_str(&self) -> Box { + match &self { + Parameter::Preference(val) => format!("q={val}").into_boxed_str(), + Parameter::Charset(ch) => format!("charset={}", ch.to_str()).into_boxed_str(), + Parameter::Other(p, v) => format!("{p}={v}").into_boxed_str(), + } + } +} + #[derive(Debug)] pub enum Charset { UTF8, } -#[derive(Debug)] -pub struct ContentType { - pub content_type: ContentTypeType, - pub parameter: Option, +impl Charset { + fn to_str(&self) -> &'static str { + match self { + Charset::UTF8 => "utf-8", + } + } } -pub fn InvalidDataError(error: &str) -> io::Error { +#[derive(Debug)] +pub struct Content { + pub content_type: ContentType, + pub parameter: Option>, +} + +impl Content { + pub fn to_str(&self) -> Box { + match &self.parameter { + Some(p) => format!( + "{}; {}", + self.content_type.to_str(), + p.iter() + .map(|par| par.to_str()) + .collect::>() + .join("; ") + ) + .into_boxed_str(), + None => self.content_type.to_str(), + } + } +} + +impl Content { + pub fn new(content_type: ContentType) -> Self { + Self { + content_type, + parameter: None, + } + } + + pub fn with_params(content_type: ContentType, params: Vec) -> Self { + Self { + content_type, + parameter: Some(params), + } + } + + pub fn add_parameter(&mut self, param: Parameter) { + match &mut self.parameter { + Some(params) => params.push(param), + None => self.parameter = Some(vec![param]), + } + } + + pub fn quality(&self) -> Option { + self.parameter.as_ref()?.iter().find_map(|p| { + if let Parameter::Preference(q) = p { + Some(*q) + } else { + None + } + }) + } + + pub fn charset(&self) -> Option<&Charset> { + self.parameter.as_ref()?.iter().find_map(|p| { + if let Parameter::Charset(cs) = p { + Some(cs) + } else { + None + } + }) + } + + pub fn matches(&self, other: &ContentType) -> bool { + type C = ContentType; + match (&self.content_type, other) { + (C::Any, _) | (_, C::Any) => true, + (C::Text(TextType::Any), C::Text(_)) => true, + (C::Text(_), C::Text(TextType::Any)) => true, + (C::Aplication(ApplicationType::Any), C::Aplication(_)) => true, + (C::Aplication(_), C::Aplication(ApplicationType::Any)) => true, + (C::Image(Image::Any), C::Image(_)) => true, + (C::Image(_), C::Image(Image::Any)) => true, + (a, b) => std::mem::discriminant(a) == std::mem::discriminant(b), + } + } + + pub fn is_text(&self) -> bool { + matches!(self.content_type, ContentType::Text(_)) + } + + pub fn is_application(&self) -> bool { + matches!(self.content_type, ContentType::Aplication(_)) + } + + pub fn is_image(&self) -> bool { + matches!(self.content_type, ContentType::Image(_)) + } + + pub fn html_utf8() -> Self { + Self::with_params( + ContentType::Text(TextType::Html), + vec![Parameter::Charset(Charset::UTF8)], + ) + } + + pub fn json_utf8() -> Self { + Self::with_params( + ContentType::Aplication(ApplicationType::Json), + vec![Parameter::Charset(Charset::UTF8)], + ) + } +} + +pub fn invalid_data_error(error: &str) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, error) } -impl FromStr for ContentType { +impl FromStr for Content { type Err = io::Error; fn from_str(s: &str) -> Result { 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)?, + content_type: ContentType::from_str(val)?, parameter: None, }), - _ => Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid content-type", - )), + [val, par @ ..] => Ok(Self { + content_type: ContentType::from_str(val)?, + parameter: Some( + par.iter() + .map(|p| Parameter::from_str(p)) + .collect::, _>>()?, + ), + }), + _ => Err(invalid_data_error("Invalid content-type")), } } } @@ -61,7 +192,7 @@ impl FromStr for Parameter { ["q", value] => { let pref_val = match value.parse::() { Ok(v) => Ok(v), - Err(_) => Err(InvalidDataError("Invalid preference")), + Err(_) => Err(invalid_data_error("Invalid preference")), }?; Ok(Parameter::Preference(pref_val)) @@ -82,39 +213,34 @@ impl FromStr for Parameter { } } -impl FromStr for ContentTypeType { +impl FromStr for ContentType { 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)), + ["*", "*"] => Ok(ContentType::Any), + ["text", "*"] => Ok(ContentType::Text(TextType::Any)), + ["text", "html"] => Ok(ContentType::Text(TextType::Html)), + ["text", "css"] => Ok(ContentType::Text(TextType::Css)), + ["text", "javascript"] => Ok(ContentType::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)), + ["application", "json"] => Ok(ContentType::Aplication(ApplicationType::Json)), + ["application", "xhtml+xml"] => Ok(ContentType::Aplication(ApplicationType::XhtmlXml)), + ["application", "xml"] => Ok(ContentType::Aplication(ApplicationType::Xml)), + ["application", "*"] => Ok(ContentType::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)), + ["image", "png"] => Ok(ContentType::Image(Image::Png)), + ["image", "jpeg"] | ["image", "jpg"] => Ok(ContentType::Image(Image::Jpeg)), + ["image", "avif"] => Ok(ContentType::Image(Image::Avif)), + ["image", "webp"] => Ok(ContentType::Image(Image::Webp)), + ["image", "svg"] | ["image", "svg+xml"] => Ok(ContentType::Image(Image::Svg)), + ["image", "*"] => Ok(ContentType::Image(Image::Any)), _ => { println!("{parts:?}"); - Err(io::Error::new( - io::ErrorKind::InvalidData, - "Invalid content-type-type", - )) + Err(invalid_data_error("Invalid content-type-type")) } } } @@ -130,6 +256,19 @@ pub enum Image { Any, } +impl Image { + fn to_str(&self) -> &'static str { + match self { + Image::Png => "png", + Image::Avif => "avif", + Image::Jpeg => "jpeg", + Image::Webp => "webp", + Image::Svg => "svg", + Image::Any => "*", + } + } +} + #[derive(Debug)] pub enum TextType { Html, @@ -138,6 +277,17 @@ pub enum TextType { Any, } +impl TextType { + fn to_str(&self) -> &'static str { + match self { + TextType::Html => "html", + TextType::Css => "css", + TextType::Javascript => "javascript", + TextType::Any => "*", + } + } +} + #[derive(Debug)] pub enum ApplicationType { Json, @@ -145,3 +295,14 @@ pub enum ApplicationType { XhtmlXml, Xml, } + +impl ApplicationType { + fn to_str(&self) -> &'static str { + match self { + ApplicationType::Json => "json", + ApplicationType::Any => "*", + ApplicationType::XhtmlXml => "xhtml+xml", + ApplicationType::Xml => "xml", + } + } +}