started response

This commit is contained in:
maxstrb 2025-10-11 18:52:40 +02:00
parent e209abe7a0
commit 9a2a500eb0
4 changed files with 278 additions and 25 deletions

View file

@ -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()?;
}

View file

@ -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<RequestHeader>,
data: Vec<u8>,
pub method: Method,
pub http_version: Box<str>,
pub path: ServerPath,
pub headers: Box<[RequestHeader]>,
pub body: Option<Box<[u8]>>,
}
#[derive(Debug)]
pub enum RequestHeader {
Host(String),
UserAgent(String),
ContentType(ContentType),
Accept(ContentType),
Other(String, String),
Accept(Vec<ContentType>),
Other(Box<str>, Box<str>),
}
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::<Result<Vec<ContentType>, 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<Discriminant<RequestHeader>> = 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(&current_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<String>,
query: Vec<(String, String)>,
pub path: Box<str>,
pub query: Option<Box<[(Box<str>, Box<str>)]>>,
}
impl FromStr for ServerPath {
@ -102,11 +144,19 @@ impl FromStr for ServerPath {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split("?").collect::<Vec<&str>>().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<String> =
std::collections::hash_set::HashSet::new();
let query_parsed = query
let query_parsed: Vec<(Box<str>, Box<str>)> = query
.split('&')
.filter_map(|s| match s.split('=').collect::<Vec<&str>>().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,

View file

@ -1,11 +1,88 @@
use crate::shared_enums::ContentType;
pub struct Response {
first_line: String,
http_version: String,
code: ResponseCode,
headers: Vec<ResponseHeader>,
data: Vec<u8>,
}
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),

View file

@ -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<str>, Box<str>),
}
#[derive(Debug)]
pub enum Charset {
UTF8,
}
#[derive(Debug)]
pub struct ContentType {
pub content_type: ContentTypeType,
pub parameter: Option<Parameter>,
}
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<Self, Self::Err> {
Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid content-type",
))
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)?,
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<Self, Self::Err> {
match s.split('=').collect::<Vec<&str>>().as_slice() {
["q", value] => {
let pref_val = match value.parse::<f32>() {
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<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)),
["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,
}