started response
This commit is contained in:
parent
e209abe7a0
commit
9a2a500eb0
4 changed files with 278 additions and 25 deletions
|
|
@ -7,6 +7,8 @@ use std::{
|
||||||
net::TcpListener,
|
net::TcpListener,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::request::RequestHeader;
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
let listener = TcpListener::bind("127.0.0.1:8080")?;
|
let listener = TcpListener::bind("127.0.0.1:8080")?;
|
||||||
for incoming_stream in listener.incoming() {
|
for incoming_stream in listener.incoming() {
|
||||||
|
|
@ -16,7 +18,10 @@ fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
let req = request::Request::from_bufreader(reader)?;
|
let req = request::Request::from_bufreader(reader)?;
|
||||||
|
|
||||||
|
println!("{req:?}");
|
||||||
|
|
||||||
let mut writer = stream;
|
let mut writer = stream;
|
||||||
|
|
||||||
writer.write_all("Ok".as_bytes())?;
|
writer.write_all("Ok".as_bytes())?;
|
||||||
writer.flush()?;
|
writer.flush()?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,22 @@ const MAX_LINE_WIDTH: u64 = 4096; // 4 KiB
|
||||||
const MAX_BODY_LENGTH: u64 = 8388608; // 5 MiB
|
const MAX_BODY_LENGTH: u64 = 8388608; // 5 MiB
|
||||||
const MAX_HEADER_COUNT: u64 = 512;
|
const MAX_HEADER_COUNT: u64 = 512;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
method: Method,
|
pub method: Method,
|
||||||
http_version: String,
|
pub http_version: Box<str>,
|
||||||
path: ServerPath,
|
pub path: ServerPath,
|
||||||
headers: Vec<RequestHeader>,
|
pub headers: Box<[RequestHeader]>,
|
||||||
data: Vec<u8>,
|
pub body: Option<Box<[u8]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum RequestHeader {
|
pub enum RequestHeader {
|
||||||
Host(String),
|
Host(String),
|
||||||
UserAgent(String),
|
UserAgent(String),
|
||||||
ContentType(ContentType),
|
ContentType(ContentType),
|
||||||
Accept(ContentType),
|
Accept(Vec<ContentType>),
|
||||||
Other(String, String),
|
Other(Box<str>, Box<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for RequestHeader {
|
impl FromStr for RequestHeader {
|
||||||
|
|
@ -36,11 +38,15 @@ impl FromStr for RequestHeader {
|
||||||
"Host" => Ok(RequestHeader::Host(value.to_string())),
|
"Host" => Ok(RequestHeader::Host(value.to_string())),
|
||||||
"UserAgent" => Ok(RequestHeader::UserAgent(value.to_string())),
|
"UserAgent" => Ok(RequestHeader::UserAgent(value.to_string())),
|
||||||
"ContentType" => Ok(RequestHeader::ContentType(ContentType::from_str(value)?)),
|
"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(
|
_ => Ok(RequestHeader::Other(
|
||||||
header_type.to_string(),
|
header_type.to_string().into_boxed_str(),
|
||||||
value.to_string(),
|
value.to_string().into_boxed_str(),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
_ => Err(io::Error::new(
|
_ => Err(io::Error::new(
|
||||||
|
|
@ -58,12 +64,40 @@ impl Request {
|
||||||
let first_line = Self::read_line(&mut limited_buffer)?;
|
let first_line = Self::read_line(&mut limited_buffer)?;
|
||||||
let parsed_first_line = Self::parse_first_line(first_line)?;
|
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(¤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 {
|
Ok(Self {
|
||||||
method: parsed_first_line.0,
|
method: parsed_first_line.0,
|
||||||
path: parsed_first_line.1,
|
path: parsed_first_line.1,
|
||||||
http_version: parsed_first_line.2,
|
http_version: parsed_first_line.2.into_boxed_str(),
|
||||||
headers: vec![],
|
headers: headers.into_boxed_slice(),
|
||||||
data: vec![],
|
body: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,6 +106,13 @@ impl Request {
|
||||||
buffer.set_limit(MAX_LINE_WIDTH);
|
buffer.set_limit(MAX_LINE_WIDTH);
|
||||||
buffer.read_until(b'\n', &mut read_buffer)?;
|
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())
|
Ok(String::from_utf8_lossy(&read_buffer).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,9 +133,10 @@ impl Request {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ServerPath {
|
pub struct ServerPath {
|
||||||
path: Vec<String>,
|
pub path: Box<str>,
|
||||||
query: Vec<(String, String)>,
|
pub query: Option<Box<[(Box<str>, Box<str>)]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ServerPath {
|
impl FromStr for ServerPath {
|
||||||
|
|
@ -102,11 +144,19 @@ impl FromStr for ServerPath {
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.split("?").collect::<Vec<&str>>().as_slice() {
|
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] => {
|
[path, query] => {
|
||||||
let mut query_hashset: std::collections::hash_set::HashSet<String> =
|
let mut query_hashset: std::collections::hash_set::HashSet<String> =
|
||||||
std::collections::hash_set::HashSet::new();
|
std::collections::hash_set::HashSet::new();
|
||||||
|
|
||||||
let query_parsed = query
|
let query_parsed: Vec<(Box<str>, Box<str>)> = query
|
||||||
.split('&')
|
.split('&')
|
||||||
.filter_map(|s| match s.split('=').collect::<Vec<&str>>().as_slice() {
|
.filter_map(|s| match s.split('=').collect::<Vec<&str>>().as_slice() {
|
||||||
[parameter, value] => {
|
[parameter, value] => {
|
||||||
|
|
@ -114,7 +164,10 @@ impl FromStr for ServerPath {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
query_hashset.insert(parameter.to_string());
|
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,
|
_ => None,
|
||||||
|
|
@ -127,7 +180,7 @@ impl FromStr for ServerPath {
|
||||||
.filter(|s| !s.is_empty() && !s.starts_with('.'))
|
.filter(|s| !s.is_empty() && !s.starts_with('.'))
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
query: query_parsed,
|
query: Some(query_parsed.into_boxed_slice()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid path")),
|
_ => Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid path")),
|
||||||
|
|
@ -135,6 +188,7 @@ impl FromStr for ServerPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum Method {
|
pub enum Method {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,88 @@
|
||||||
use crate::shared_enums::ContentType;
|
use crate::shared_enums::ContentType;
|
||||||
|
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
first_line: String,
|
http_version: String,
|
||||||
|
code: ResponseCode,
|
||||||
headers: Vec<ResponseHeader>,
|
headers: Vec<ResponseHeader>,
|
||||||
data: Vec<u8>,
|
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 {
|
pub enum ResponseHeader {
|
||||||
ContentLength(u32),
|
ContentLength(u32),
|
||||||
ContentType(ContentType),
|
ContentType(ContentType),
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,136 @@
|
||||||
use std::{io, str::FromStr};
|
use std::{io, str::FromStr};
|
||||||
|
|
||||||
pub enum ContentType {
|
#[derive(Debug)]
|
||||||
|
pub enum ContentTypeType {
|
||||||
Text(TextType),
|
Text(TextType),
|
||||||
Aplication(ApplicationType),
|
Aplication(ApplicationType),
|
||||||
|
Image(Image),
|
||||||
Any,
|
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 {
|
impl FromStr for ContentType {
|
||||||
type Err = io::Error;
|
type Err = io::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
Err(io::Error::new(
|
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,
|
io::ErrorKind::InvalidData,
|
||||||
"Invalid content-type",
|
"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 {
|
pub enum TextType {
|
||||||
Html,
|
Html,
|
||||||
Css,
|
Css,
|
||||||
|
|
@ -24,7 +138,10 @@ pub enum TextType {
|
||||||
Any,
|
Any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum ApplicationType {
|
pub enum ApplicationType {
|
||||||
Json,
|
Json,
|
||||||
Any,
|
Any,
|
||||||
|
XhtmlXml,
|
||||||
|
Xml,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue