First response

This commit is contained in:
maxstrb 2025-10-12 21:12:15 +02:00
parent 9a2a500eb0
commit 80fabcde47
4 changed files with 382 additions and 58 deletions

View file

@ -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"<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"/><title>Hello World!</title></head><body><h1>Ahojky</h1><p>Jou jou jou</p></body></html>".to_vec())
.with_header(ResponseHeader::ContentType(Content::html_utf8()));
response.respond(&mut writer)?;
writer.flush()?;
}
Ok(())

View file

@ -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>),
ContentType(Content),
Accept(Vec<Content>),
Other(Box<str>, Box<str>),
}
@ -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::<Result<Vec<ContentType>, io::Error>>()?,
.map(Content::from_str)
.collect::<Result<Vec<Content>, io::Error>>()?,
)),
_ => Ok(RequestHeader::Other(
header_type.to_string().into_boxed_str(),

View file

@ -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<str>,
code: ResponseCode,
headers: Vec<ResponseHeader>,
data: Vec<u8>,
}
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<str> {
format!(
"{} {}\r\n{}",
self.http_version,
self.code.to_code(),
self.headers
.iter()
.map(|header| header.to_str())
.collect::<Vec<_>>()
.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<u8>) -> 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<str> {
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",
}
}
}

View file

@ -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<str> {
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<str>, Box<str>),
}
impl Parameter {
fn to_str(&self) -> Box<str> {
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<Parameter>,
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<Vec<Parameter>>,
}
impl Content {
pub fn to_str(&self) -> Box<str> {
match &self.parameter {
Some(p) => format!(
"{}; {}",
self.content_type.to_str(),
p.iter()
.map(|par| par.to_str())
.collect::<Vec<_>>()
.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<Parameter>) -> 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<f32> {
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<Self, Self::Err> {
match s.split(';').collect::<Vec<&str>>().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::<Result<Vec<_>, _>>()?,
),
}),
_ => Err(invalid_data_error("Invalid content-type")),
}
}
}
@ -61,7 +192,7 @@ impl FromStr for Parameter {
["q", value] => {
let pref_val = match value.parse::<f32>() {
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<Self, Self::Err> {
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",
}
}
}