changes
This commit is contained in:
commit
168b8937eb
12 changed files with 1044 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/Cargo.lock
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "http"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
multimap = "0.9.0"
|
17
src/error.rs
Normal file
17
src/error.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum HTTPError {
|
||||||
|
InvalidHeaderName(String),
|
||||||
|
InvalidHeader(String),
|
||||||
|
InvalidQueryFragment(String),
|
||||||
|
InvalidQueryFragmentName(String),
|
||||||
|
InvalidEncodedString(String),
|
||||||
|
InvalidPath(String),
|
||||||
|
InvalidScheme(String),
|
||||||
|
InvalidPort(String),
|
||||||
|
InvalidURI(String),
|
||||||
|
InvalidMethod(String),
|
||||||
|
InvalidStatus(String),
|
||||||
|
MissingHeader,
|
||||||
|
MissingHeaderBreak,
|
||||||
|
InvalidBody
|
||||||
|
}
|
174
src/header.rs
Normal file
174
src/header.rs
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
use std::{string::ToString, cmp::{PartialEq, Eq}, hash::{Hash, Hasher}};
|
||||||
|
use multimap::{MultiMap, IterAll};
|
||||||
|
use crate::{error::HTTPError, parse::{TryParse, Parse}};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq)]
|
||||||
|
pub struct HeaderName {
|
||||||
|
inner: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderName {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.inner.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for HeaderName {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let name = s.into();
|
||||||
|
if name.len() < 1 {
|
||||||
|
return Err(HTTPError::InvalidHeaderName(name))
|
||||||
|
} else {
|
||||||
|
Ok(Self { inner: name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for HeaderName {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.inner.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for HeaderName {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner.eq_ignore_ascii_case(&other.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for HeaderName {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
let mut hash: i32 = 0;
|
||||||
|
for char in self.inner.chars() {
|
||||||
|
let byte = char.to_ascii_lowercase() as u8;
|
||||||
|
hash = hash ^ ((byte as i32) << 0);
|
||||||
|
hash = hash ^ ((byte as i32) << 8);
|
||||||
|
hash = hash ^ ((byte as i32) << 16);
|
||||||
|
hash = hash ^ ((byte as i32) << 24);
|
||||||
|
hash = hash % 16777213;
|
||||||
|
}
|
||||||
|
state.write_i32(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct HeaderValue {
|
||||||
|
inner: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderValue {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.inner.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for HeaderValue {
|
||||||
|
fn parse(value: impl Into<String>) -> Self {
|
||||||
|
Self { inner: value.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for HeaderValue {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.inner.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Header {
|
||||||
|
pub name: HeaderName,
|
||||||
|
pub value: HeaderValue
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
pub fn new(name: impl Into<HeaderName>, value: impl Into<HeaderValue>) -> Self {
|
||||||
|
Self { name: name.into(), value: value.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for Header {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s = s.into();
|
||||||
|
let Some(mid) = s.find(": ") else {
|
||||||
|
return Err(HTTPError::InvalidHeader(s.to_string()))
|
||||||
|
};
|
||||||
|
if mid == 0 || mid >= s.len() - 2 {
|
||||||
|
return Err(HTTPError::InvalidHeader(s.to_string()))
|
||||||
|
}
|
||||||
|
let name = HeaderName::try_parse(&s[0..mid])?;
|
||||||
|
let value = HeaderValue::parse(&s[(mid+2)..]);
|
||||||
|
Ok(Header::new(name, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Header {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
s.push_str(self.name.as_str());
|
||||||
|
s.push_str(": ");
|
||||||
|
s.push_str(self.value.as_str());
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H,V> From<(H, V)> for Header
|
||||||
|
where
|
||||||
|
H: Into<HeaderName>,
|
||||||
|
V: Into<HeaderValue>
|
||||||
|
{
|
||||||
|
fn from(value: (H, V)) -> Self {
|
||||||
|
Self { name: value.0.into(), value: value.1.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeaderMap {
|
||||||
|
inner: MultiMap<HeaderName, Header>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderMap {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::with_headers(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_headers(headers: Vec<Header>) -> Self {
|
||||||
|
let mut inner = MultiMap::with_capacity(headers.len());
|
||||||
|
for header in headers {
|
||||||
|
inner.insert(header.name.clone(), Header::new(header.name, header.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, header: impl Into<Header>) {
|
||||||
|
let header = header.into();
|
||||||
|
self.inner.insert(header.name.clone(), Header::new(header.name, header.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, name: &HeaderName) -> Option<Vec<Header>> {
|
||||||
|
self.inner.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&mut self, name: &HeaderName) -> Option<&Vec<Header>> {
|
||||||
|
self.inner.get_vec(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> HeaderMapIter {
|
||||||
|
HeaderMapIter { inner: self.inner.iter_all() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeaderMapIter<'i> {
|
||||||
|
inner: IterAll<'i, HeaderName, Vec<Header>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'i> Iterator for HeaderMapIter<'i> {
|
||||||
|
type Item = &'i Vec<Header>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.inner.next() {
|
||||||
|
Some(h) => Some(h.1),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
pub mod error;
|
||||||
|
pub mod header;
|
||||||
|
pub mod uri;
|
||||||
|
pub mod method;
|
||||||
|
pub mod version;
|
||||||
|
pub mod status;
|
||||||
|
pub mod request;
|
||||||
|
pub mod response;
|
||||||
|
pub mod parse;
|
65
src/method.rs
Normal file
65
src/method.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::parse::Parse;
|
||||||
|
|
||||||
|
pub enum Method {
|
||||||
|
Get,
|
||||||
|
Head,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Connect,
|
||||||
|
Options,
|
||||||
|
Trace,
|
||||||
|
Patch,
|
||||||
|
Brew,
|
||||||
|
PropFind,
|
||||||
|
When,
|
||||||
|
Unkown(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Method {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Get => "GET",
|
||||||
|
Self::Head => "HEAD",
|
||||||
|
Self::Post => "POST",
|
||||||
|
Self::Put => "PUT",
|
||||||
|
Self::Delete => "DELETE",
|
||||||
|
Self::Connect => "CONNECT",
|
||||||
|
Self::Options => "OPTIONS",
|
||||||
|
Self::Trace => "TRACE",
|
||||||
|
Self::Patch => "PATCH",
|
||||||
|
Self::Brew => "BREW",
|
||||||
|
Self::PropFind => "PROPFIND",
|
||||||
|
Self::When => "WHEN",
|
||||||
|
Self::Unkown(ref s) => s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Method {
|
||||||
|
fn parse(s: impl Into<String>) -> Self {
|
||||||
|
let str: String = s.into();
|
||||||
|
match str.as_str() {
|
||||||
|
"GET" => Self::Get,
|
||||||
|
"HEAD" => Self::Head,
|
||||||
|
"POST" => Self::Post,
|
||||||
|
"PUT" => Self::Put,
|
||||||
|
"DELETE" => Self::Delete,
|
||||||
|
"CONNECT" => Self::Connect,
|
||||||
|
"OPTIONS" => Self::Options,
|
||||||
|
"TRACE" => Self::Trace,
|
||||||
|
"PATCH" => Self::Patch,
|
||||||
|
"BREW" => Self::Brew,
|
||||||
|
"PROPFIND" => Self::PropFind,
|
||||||
|
"WHEN" => Self::When,
|
||||||
|
_ => Self::Unkown(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Method {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.as_str().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
110
src/parse.rs
Normal file
110
src/parse.rs
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
use crate::header::{Header, HeaderValue, HeaderName};
|
||||||
|
use crate::error::HTTPError;
|
||||||
|
use crate::method::Method;
|
||||||
|
use crate::status::Status;
|
||||||
|
use crate::uri::{QueryMap, QueryFragment, Authority, Path, Scheme, URI};
|
||||||
|
use crate::version::Version;
|
||||||
|
use crate::response::Response;
|
||||||
|
use crate::request::Request;
|
||||||
|
use std::convert::{From, TryFrom};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub trait Parse
|
||||||
|
where
|
||||||
|
Self: Sized
|
||||||
|
{
|
||||||
|
fn parse(s: impl Into<String>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TryParse
|
||||||
|
where
|
||||||
|
Self: Sized
|
||||||
|
{
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! try_into {
|
||||||
|
($type:ty, $struct:ty) => {
|
||||||
|
impl TryFrom<$type> for $struct {
|
||||||
|
type Error = HTTPError;
|
||||||
|
|
||||||
|
fn try_from(value: $type) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_parse(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! try_into_struct {
|
||||||
|
($struct:ty) => {
|
||||||
|
impl From<$struct> for String {
|
||||||
|
fn from(value: $struct) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try_into!(String, $struct);
|
||||||
|
try_into!(&String, $struct);
|
||||||
|
try_into!(&str, $struct);
|
||||||
|
try_into!(Cow<'_, str>, $struct);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try_into_struct!(Header);
|
||||||
|
try_into_struct!(HeaderName);
|
||||||
|
try_into_struct!(QueryMap);
|
||||||
|
try_into_struct!(QueryFragment);
|
||||||
|
try_into_struct!(Authority);
|
||||||
|
try_into_struct!(Path);
|
||||||
|
try_into_struct!(URI);
|
||||||
|
try_into_struct!(Status);
|
||||||
|
|
||||||
|
macro_rules! try_into_res {
|
||||||
|
($type:ty, $struct:ty) => {
|
||||||
|
impl<T: FromStr + ToString> TryFrom<$type> for $struct {
|
||||||
|
type Error = HTTPError;
|
||||||
|
|
||||||
|
fn try_from(value: $type) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_parse(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! try_into_struct_res {
|
||||||
|
($struct:ty) => {
|
||||||
|
impl<T: FromStr + ToString> From<$struct> for String {
|
||||||
|
fn from(value: $struct) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try_into_res!(String, $struct);
|
||||||
|
try_into_res!(&String, $struct);
|
||||||
|
try_into_res!(&str, $struct);
|
||||||
|
try_into_res!(Cow<'_, str>, $struct);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try_into_struct_res!(Request<T>);
|
||||||
|
try_into_struct_res!(Response<T>);
|
||||||
|
|
||||||
|
macro_rules! into_struct {
|
||||||
|
($struct:ty) => {
|
||||||
|
impl<T> From<T> for $struct
|
||||||
|
where
|
||||||
|
T: Into<String>
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self::parse(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
into_struct!(Scheme);
|
||||||
|
into_struct!(HeaderValue);
|
||||||
|
into_struct!(Method);
|
||||||
|
into_struct!(Version);
|
85
src/request.rs
Normal file
85
src/request.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
use crate::{version::Version, method::Method, uri::URI, header::{HeaderMap, Header}, error::HTTPError, parse::TryParse};
|
||||||
|
|
||||||
|
pub struct Request<T: ToString + FromStr> {
|
||||||
|
pub method: Method,
|
||||||
|
pub uri: URI,
|
||||||
|
pub version: Version,
|
||||||
|
pub headers: HeaderMap,
|
||||||
|
pub body: Option<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString + FromStr> TryParse for Request<T> {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s = s.into();
|
||||||
|
let mut lines = s.lines();
|
||||||
|
let Some(head) = lines.next() else {
|
||||||
|
return Err(HTTPError::MissingHeader)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut head_parts = head.split_whitespace();
|
||||||
|
let err = HTTPError::InvalidHeader(s.clone());
|
||||||
|
|
||||||
|
let Some(method_str) = head_parts.next() else { return Err(err) };
|
||||||
|
let method = method_str.into();
|
||||||
|
|
||||||
|
let Some(uri_str) = head_parts.next() else { return Err(err) };
|
||||||
|
let uri = uri_str.try_into()?;
|
||||||
|
|
||||||
|
let Some(version_str) = head_parts.next() else { return Err(err) };
|
||||||
|
let version = version_str.into();
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(next) = lines.next() else {
|
||||||
|
return Err(HTTPError::MissingHeaderBreak)
|
||||||
|
};
|
||||||
|
|
||||||
|
if next.len() < 1 { break }
|
||||||
|
|
||||||
|
let header = Header::try_parse(next)?;
|
||||||
|
headers.insert(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rest: String = lines.collect();
|
||||||
|
let body;
|
||||||
|
if rest.len() < 1 {
|
||||||
|
body = None
|
||||||
|
} else {
|
||||||
|
body = match T::from_str(&rest) {
|
||||||
|
Ok(body) => Some(body),
|
||||||
|
Err(_) => return Err(HTTPError::InvalidBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { method, uri, version, headers, body })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString + FromStr> ToString for Request<T> {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
|
||||||
|
s.push_str(&self.method.as_str());
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(&self.uri.to_string());
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(self.version.as_str());
|
||||||
|
s.push_str("\r\n");
|
||||||
|
let iter = self.headers.iter();
|
||||||
|
for headers in iter {
|
||||||
|
for header in headers {
|
||||||
|
s.push_str(header.name.as_str());
|
||||||
|
s.push_str(": ");
|
||||||
|
s.push_str(header.value.as_str());
|
||||||
|
s.push_str("\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.push_str("\r\n");
|
||||||
|
if let Some(body) = &self.body {
|
||||||
|
s.push_str(&body.to_string());
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
111
src/response.rs
Normal file
111
src/response.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
use crate::{status::{Status, self}, version::Version, header::{HeaderMap, Header}, parse::TryParse, error::HTTPError};
|
||||||
|
|
||||||
|
pub struct Response<T: ToString + FromStr> {
|
||||||
|
version: Version,
|
||||||
|
status: Status,
|
||||||
|
headers: HeaderMap,
|
||||||
|
body: Option<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString + FromStr> Response<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
version: Version::HTTP11,
|
||||||
|
status: status::SUCCESS,
|
||||||
|
headers: HeaderMap::new(),
|
||||||
|
body: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_version(mut self, version: impl Into<Version>) -> Self {
|
||||||
|
self.version = version.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_status(mut self, status: impl Into<Status>) -> Self {
|
||||||
|
self.status = status.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_header(mut self, header: impl Into<Header>) -> Self {
|
||||||
|
self.headers.insert(header.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_body(mut self, body: T) -> Self {
|
||||||
|
self.body = Some(body);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString + FromStr> TryParse for Response<T> {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s = s.into();
|
||||||
|
let mut lines = s.lines();
|
||||||
|
|
||||||
|
let Some(header_str) = lines.next() else {
|
||||||
|
return Err(HTTPError::MissingHeader)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut header_parts = header_str.split(" ");
|
||||||
|
let Some(version_str) = header_parts.next() else {
|
||||||
|
return Err(HTTPError::InvalidHeader(header_str.into()))
|
||||||
|
};
|
||||||
|
let version = version_str.into();
|
||||||
|
|
||||||
|
let status_str: String = header_parts.collect();
|
||||||
|
let status = status_str.try_into()?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(next) = lines.next() else {
|
||||||
|
return Err(HTTPError::MissingHeaderBreak)
|
||||||
|
};
|
||||||
|
|
||||||
|
if next.len() < 1 { break }
|
||||||
|
|
||||||
|
let header = Header::try_parse(next)?;
|
||||||
|
headers.insert(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rest: String = lines.collect();
|
||||||
|
let body;
|
||||||
|
if rest.len() < 1 {
|
||||||
|
body = None
|
||||||
|
} else {
|
||||||
|
body = match T::from_str(&rest) {
|
||||||
|
Ok(body) => Some(body),
|
||||||
|
Err(_) => return Err(HTTPError::InvalidBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { version, status, headers, body })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ToString + FromStr> ToString for Response<T> {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
s.push_str(self.version.as_str());
|
||||||
|
s.push(' ');
|
||||||
|
s.push_str(&self.status.to_string());
|
||||||
|
s.push_str("\r\n");
|
||||||
|
let iter = self.headers.iter();
|
||||||
|
for headers in iter {
|
||||||
|
for header in headers {
|
||||||
|
s.push_str(header.name.as_str());
|
||||||
|
s.push_str(": ");
|
||||||
|
s.push_str(header.value.as_str());
|
||||||
|
s.push_str("\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.push_str("\r\n");
|
||||||
|
if let Some(body) = &self.body {
|
||||||
|
s.push_str(&body.to_string());
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
83
src/status.rs
Normal file
83
src/status.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use crate::{parse::TryParse, error::HTTPError};
|
||||||
|
|
||||||
|
pub struct Status(u16, Cow<'static, str>);
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
pub fn new(code: u16, msg: impl Into<String>) -> Self {
|
||||||
|
Self(code, Cow::Owned(msg.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn code(&self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn msg(&self) -> &str {
|
||||||
|
self.1.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Status {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
if self.1.len() < 1 {
|
||||||
|
format!("{}", self.0)
|
||||||
|
} else {
|
||||||
|
format!("{} {}", self.0, self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for Status {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s = s.into();
|
||||||
|
let mut parts = s.split(" ");
|
||||||
|
let Some(code_str) = parts.next() else {
|
||||||
|
return Err(HTTPError::InvalidStatus(s))
|
||||||
|
};
|
||||||
|
let Ok(code) = code_str.parse::<u16>() else {
|
||||||
|
return Err(HTTPError::InvalidStatus(s))
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg: String = parts.collect();
|
||||||
|
Ok(Self::new(code, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Status {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.0 == other.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! status_from {
|
||||||
|
($type:ty) => {
|
||||||
|
impl From<$type> for Status {
|
||||||
|
fn from(value: $type) -> Self {
|
||||||
|
Self(value as u16, Cow::Owned(String::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<($type, T)> for Status
|
||||||
|
where
|
||||||
|
T: Into<String>
|
||||||
|
{
|
||||||
|
fn from(value: ($type, T)) -> Self {
|
||||||
|
Self(value.0 as u16, Cow::Owned(value.1.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
status_from!(u8);
|
||||||
|
status_from!(u16);
|
||||||
|
status_from!(u32);
|
||||||
|
status_from!(u64);
|
||||||
|
status_from!(u128);
|
||||||
|
|
||||||
|
macro_rules! status {
|
||||||
|
($name:ident, $code:literal, $msg:literal) => {
|
||||||
|
pub const $name: Status = Status($code, Cow::Borrowed($msg));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
status!(SUCCESS, 200, "Ok");
|
345
src/uri.rs
Normal file
345
src/uri.rs
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{error::HTTPError, parse::{TryParse, Parse}};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Path {
|
||||||
|
inner: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.inner.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for Path {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s: String = s.into();
|
||||||
|
let Some(first) = s.chars().nth(0) else {
|
||||||
|
return Err(HTTPError::InvalidPath(s))
|
||||||
|
};
|
||||||
|
if first != '/' {
|
||||||
|
return Err(HTTPError::InvalidPath(s))
|
||||||
|
} else {
|
||||||
|
return Ok(Self { inner: s })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Path {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.as_str().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Scheme {
|
||||||
|
Http,
|
||||||
|
Https,
|
||||||
|
Unkown(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scheme {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Scheme::Http => "http",
|
||||||
|
Scheme::Https => "https",
|
||||||
|
Scheme::Unkown(ref s) => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Scheme {
|
||||||
|
fn parse(s: impl Into<String>) -> Self {
|
||||||
|
let s = s.into();
|
||||||
|
match s.as_str() {
|
||||||
|
"http" => Self::Http,
|
||||||
|
"https" => Self::Https,
|
||||||
|
_ => Self::Unkown(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Scheme {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.as_str().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Authority {
|
||||||
|
pub sub_domain: Option<String>,
|
||||||
|
pub domain: String,
|
||||||
|
pub tld: Option<String>,
|
||||||
|
pub port: Option<u16>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for Authority {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s = s.into();
|
||||||
|
let domain_end = s.find(":").unwrap_or(s.len());
|
||||||
|
let authority = &s[..domain_end];
|
||||||
|
let tld_start = authority.rfind(".").unwrap_or(authority.len());
|
||||||
|
let full_domain = &authority[..tld_start];
|
||||||
|
let domain_start = match full_domain.find(".") {
|
||||||
|
Some(u) => u + 1,
|
||||||
|
None => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
let domain = &full_domain[domain_start..];
|
||||||
|
|
||||||
|
let sub_domain;
|
||||||
|
if domain_start > 0 {
|
||||||
|
sub_domain = Some(full_domain[..(domain_start-1)].to_string());
|
||||||
|
} else {
|
||||||
|
sub_domain = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tld;
|
||||||
|
if tld_start < authority.len() {
|
||||||
|
tld = Some(authority[(tld_start+1)..domain_end].to_string());
|
||||||
|
} else {
|
||||||
|
tld = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let port;
|
||||||
|
if domain_end < s.len() {
|
||||||
|
let port_str = &s[(domain_end+1)..];
|
||||||
|
let num = match port_str.parse::<u16>() {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_) => return Err(HTTPError::InvalidPort(port_str.to_string()))
|
||||||
|
};
|
||||||
|
port = Some(num);
|
||||||
|
} else {
|
||||||
|
port = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { sub_domain, domain: domain.to_string(), tld, port } )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Authority {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
if let Some(sub_domain) = &self.sub_domain {
|
||||||
|
s.push_str(sub_domain);
|
||||||
|
s.push('.');
|
||||||
|
}
|
||||||
|
s.push_str(&self.domain);
|
||||||
|
if let Some(tld) = &self.tld {
|
||||||
|
s.push('.');
|
||||||
|
s.push_str(tld);
|
||||||
|
};
|
||||||
|
if let Some(port) = self.port {
|
||||||
|
s.push(':');
|
||||||
|
s.push_str(&format!("{port}"));
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct QueryFragment {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryFragment {
|
||||||
|
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let name = name.into();
|
||||||
|
if name.len() < 1 {
|
||||||
|
return Err(HTTPError::InvalidQueryFragmentName(name))
|
||||||
|
}
|
||||||
|
Ok(Self { name, value: value.into() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for QueryFragment {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s = s.into();
|
||||||
|
let Some(index) = s.find('=') else {
|
||||||
|
return Err(HTTPError::InvalidQueryFragment(s.to_string()))
|
||||||
|
};
|
||||||
|
if index == 0 {
|
||||||
|
return Err(HTTPError::InvalidQueryFragment(s.to_string()))
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
name: decode_str(&s[0..index])?,
|
||||||
|
value: decode_str(&s[(index+1)..])?
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for QueryFragment {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
s.push_str(&encode_str(&self.name));
|
||||||
|
s.push('=');
|
||||||
|
s.push_str(&encode_str(&self.value));
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct QueryMap {
|
||||||
|
inner: HashMap<String, QueryFragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryMap {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::with_fragments(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_fragments(fragments: Vec<QueryFragment>) -> Self {
|
||||||
|
let mut inner = HashMap::with_capacity(fragments.len());
|
||||||
|
for fragment in fragments {
|
||||||
|
inner.insert(fragment.name.clone(), fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, fragment: &QueryFragment) {
|
||||||
|
self.inner.insert(fragment.name.clone(), fragment.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, name: &str) -> Option<QueryFragment> {
|
||||||
|
self.inner.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, name: &str) -> Option<&QueryFragment> {
|
||||||
|
self.inner.get(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for QueryMap {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
let s = s.into();
|
||||||
|
let parts: Vec<QueryFragment> = s.split('&').map(|f| QueryFragment::try_parse(f)).flatten().collect();
|
||||||
|
Ok(Self::with_fragments(parts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for QueryMap {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
for (i, fragment) in self.inner.values().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
s.push('?');
|
||||||
|
} else {
|
||||||
|
s.push('&');
|
||||||
|
}
|
||||||
|
s.push_str(&fragment.to_string());
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct URI {
|
||||||
|
pub scheme: Option<Scheme>,
|
||||||
|
pub authority: Option<Authority>,
|
||||||
|
pub path: Path,
|
||||||
|
pub query: QueryMap
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryParse for URI {
|
||||||
|
fn try_parse(s: impl Into<String>) -> Result<Self, HTTPError> {
|
||||||
|
|
||||||
|
let s = s.into();
|
||||||
|
let mut n = s.as_str();
|
||||||
|
|
||||||
|
let scheme: Option<Scheme>;
|
||||||
|
if let Some(scheme_end) = n.find("://") {
|
||||||
|
scheme = Some(Scheme::parse(&n[..scheme_end]));
|
||||||
|
n = &n[(scheme_end + 3)..];
|
||||||
|
} else {
|
||||||
|
scheme = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let authority_end = match n.find("/") {
|
||||||
|
Some(i) => i,
|
||||||
|
None => return Err(HTTPError::InvalidURI(s.to_string()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let authority: Option<Authority>;
|
||||||
|
if authority_end == 0 {
|
||||||
|
if scheme.is_some() {
|
||||||
|
return Err(HTTPError::InvalidURI(s.to_string()))
|
||||||
|
}
|
||||||
|
authority = None;
|
||||||
|
} else {
|
||||||
|
authority = Some(Authority::try_parse(&n[..authority_end])?);
|
||||||
|
n = &n[authority_end..];
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_end = n.find("?").unwrap_or(n.len());
|
||||||
|
let path = Path::try_parse(&n[..path_end])?;
|
||||||
|
|
||||||
|
let query: QueryMap;
|
||||||
|
if path_end + 1 < n.len() {
|
||||||
|
query = QueryMap::try_parse(&n[(path_end+1)..])?;
|
||||||
|
} else {
|
||||||
|
query = QueryMap::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { scheme, authority, path, query })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for URI {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
if let Some(scheme) = &self.scheme {
|
||||||
|
s.push_str(scheme.as_str());
|
||||||
|
}
|
||||||
|
if let Some(authority) = &self.authority {
|
||||||
|
s.push_str(&authority.to_string());
|
||||||
|
}
|
||||||
|
s.push_str(self.path.as_str());
|
||||||
|
s.push_str(&self.query.to_string());
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encoded(c: char) -> bool {
|
||||||
|
match c {
|
||||||
|
'/' | '&' | '%' | '?' | '=' => true,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_str(s: &str) -> String {
|
||||||
|
let mut e = String::new();
|
||||||
|
for c in s.chars() {
|
||||||
|
if encoded(c) {
|
||||||
|
e.push('%');
|
||||||
|
e.push_str(&format!("{:02X}", c as u8));
|
||||||
|
} else {
|
||||||
|
e.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_str(s: &str) -> Result<String, HTTPError> {
|
||||||
|
let mut e = String::new();
|
||||||
|
let mut iter = s.chars();
|
||||||
|
loop {
|
||||||
|
let Some(char) = iter.next() else { break };
|
||||||
|
if char == '%' {
|
||||||
|
let err = HTTPError::InvalidEncodedString(s.to_string());
|
||||||
|
let f = iter.next().ok_or(err.clone())?;
|
||||||
|
let s = iter.next().ok_or(err.clone())?;
|
||||||
|
let Some(fd) = f.to_digit(16) else { return Err(err) };
|
||||||
|
let Some(sd) = s.to_digit(16) else { return Err(err) };
|
||||||
|
let b = (fd * 16 + sd) as u8;
|
||||||
|
e.push(b as char);
|
||||||
|
} else {
|
||||||
|
e.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(e)
|
||||||
|
}
|
34
src/version.rs
Normal file
34
src/version.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use crate::parse::Parse;
|
||||||
|
|
||||||
|
pub enum Version {
|
||||||
|
HTTP11,
|
||||||
|
HTTP2,
|
||||||
|
Unknown(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::HTTP11 => "HTTP/1.1",
|
||||||
|
Self::HTTP2 => "HTTP/2",
|
||||||
|
Self::Unknown(ref s) => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Version {
|
||||||
|
fn parse(s: impl Into<String>) -> Self {
|
||||||
|
let s = s.into();
|
||||||
|
match s.as_str() {
|
||||||
|
"HTTP/1.1" => Self::HTTP11,
|
||||||
|
"HTTP/2" => Self::HTTP2,
|
||||||
|
_ => Self::Unknown(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Version {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.as_str().to_string()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue