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",
+ }
+ }
+}