Initial commit

This commit is contained in:
Ben Bridle 2022-08-25 21:55:02 +12:00
commit 56af313687
9 changed files with 308 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "wetstring_http"
version = "1.0.0"
authors = ["Ben Bridle <bridle.benjamin@gmail.com>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wetstring = { git = "https://tildegit.org/toast/wetstring", tag = "v1.0" }
log = "0.4.14"

22
src/error.rs Normal file
View File

@ -0,0 +1,22 @@
pub enum RequestError {
StartLineMissingMethod,
StartLineMissingTarget,
TargetTooLong,
TargetCouldNotParse,
MethodNotSupported(String),
QueryParametersCouldNotParse,
}
impl RequestError {
#[rustfmt::skip]
pub fn description(&self) -> String {
match self {
Self::StartLineMissingMethod => "The request method is missing from the start line".to_string(),
Self::StartLineMissingTarget => "The request target URI is missing from the start line".to_string(),
Self::TargetTooLong => "The length of the request target URI in the start line exceeds the maximum length".to_string(),
Self::TargetCouldNotParse => "The request target URI is invalid".to_string(),
Self::MethodNotSupported(method) => format!("The '{}' method is not supported", method),
Self::QueryParametersCouldNotParse => "QueryParametersCouldNotParse".to_string(),
}
}
}

16
src/lib.rs Normal file
View File

@ -0,0 +1,16 @@
mod error;
mod method;
mod request;
mod request_parser;
mod response;
mod status;
pub use error::RequestError;
pub use method::Method;
pub use request::{HttpRequest, RequestTarget};
pub use request_parser::HttpRequestParser;
pub use response::HttpResponse;
pub use status::Status;
pub use wetstring::RequestProcessor;
pub type HttpServer<Proc> = wetstring::Server<HttpRequestParser, Proc>;

44
src/method.rs Normal file
View File

@ -0,0 +1,44 @@
use crate::RequestError;
pub enum Method {
GET,
POST,
PUT,
PATCH,
DELETE,
OPTION,
HEAD,
}
impl Method {
pub fn parse(method: &str) -> Result<Self, RequestError> {
Ok(match method {
"GET" => Self::GET,
"POST" => Self::POST,
"PUT" => Self::PUT,
"PATCH" => Self::PATCH,
"DELETE" => Self::DELETE,
"OPTION" => Self::OPTION,
"HEAD" => Self::HEAD,
method => return Err(RequestError::MethodNotSupported(method.to_string())),
})
}
pub fn name(&self) -> &str {
match self {
Self::GET => "GET",
Self::POST => "POST",
Self::PUT => "PUT",
Self::PATCH => "PATCH",
Self::DELETE => "DELETE",
Self::OPTION => "OPTION",
Self::HEAD => "HEAD",
}
}
}
impl std::fmt::Debug for Method {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.name())
}
}

48
src/request.rs Normal file
View File

@ -0,0 +1,48 @@
use crate::*;
use log::info;
use std::collections::HashMap;
use std::net::SocketAddr;
use wetstring::Request;
pub struct HttpRequest {
pub client_address: SocketAddr,
pub method: Method,
pub target: RequestTarget,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}
impl Request for HttpRequest {}
pub struct RequestTarget {
/// `path` excludes host
pub path: String,
pub query_parameters: HashMap<String, String>,
}
impl RequestTarget {
pub fn parse(text: &str) -> Result<Self, RequestError> {
info!("URI is '{}'", text);
match text.split_once('?') {
Some((path, query)) => Ok(Self {
path: path.to_string(),
query_parameters: Self::parse_query_parameters(query)?,
}),
None => Ok(Self {
path: text.to_string(),
query_parameters: HashMap::new(),
}),
}
}
fn parse_query_parameters(query: &str) -> Result<HashMap<String, String>, RequestError> {
let mut query_parameters = HashMap::new();
for pair_text in query.split('&') {
match pair_text.split_once('=') {
Some((key, value)) => query_parameters.insert(key.to_string(), value.to_string()),
None => return Err(RequestError::QueryParametersCouldNotParse),
};
}
return Ok(query_parameters);
}
}

90
src/request_parser.rs Normal file
View File

@ -0,0 +1,90 @@
use crate::*;
use log::info;
use std::collections::HashMap;
use std::net::SocketAddr;
use wetstring::{RequestParseResult, RequestParser};
pub struct HttpRequestParser {
bytes: Vec<u8>,
client_address: SocketAddr,
headers: HashMap<String, String>,
body: Option<Vec<u8>>,
}
impl RequestParser for HttpRequestParser {
type Req = HttpRequest;
type Res = HttpResponse;
fn new(client_address: SocketAddr) -> Self {
Self {
bytes: Vec::new(),
client_address,
headers: HashMap::new(),
body: None,
}
}
fn push_bytes(&mut self, bytes: &[u8]) {
self.bytes.extend(bytes);
}
fn try_parse(&mut self) -> RequestParseResult<HttpRequest, HttpResponse> {
let client_error = HttpResponse::client_error();
if self.bytes.len() > 8096 {
return RequestParseResult::Invalid(client_error);
}
let string = match String::from_utf8(self.bytes.clone()) {
Ok(string) => string,
Err(_) => return RequestParseResult::Incomplete,
};
let mut lines = string.lines();
// Parse request start line
let (method, target) = match lines.next() {
Some(line) => match parse_start_line(line) {
Ok(v) => v,
Err(e) => {
info!("{}", e.description());
return RequestParseResult::Invalid(client_error);
}
},
None => {
info!("Missing start line from request");
return RequestParseResult::Invalid(client_error);
}
};
// Parse request headers
// for line in lines {
// if line == "" {
// break;
// };
// match parse_header_line(line) {
// Ok((key, value)) => self.headers.insert(key, value),
// Err(_) => return RequestParseResult::Invalid(client_error),
// };
// }
let request = HttpRequest {
client_address: self.client_address,
method,
target,
headers: std::mem::take(&mut self.headers),
body: std::mem::take(&mut self.body),
};
RequestParseResult::Complete(request)
}
}
fn parse_start_line(line: &str) -> Result<(Method, RequestTarget), RequestError> {
let mut split = line.split_whitespace();
let method_str = split.next().ok_or(RequestError::StartLineMissingMethod)?;
let target_str = split.next().ok_or(RequestError::StartLineMissingTarget)?;
let method = Method::parse(method_str)?;
let target = RequestTarget::parse(target_str)?;
Ok((method, target))
}

40
src/response.rs Normal file
View File

@ -0,0 +1,40 @@
use crate::Status;
use std::collections::HashMap;
use wetstring::Response;
pub struct HttpResponse {
pub status: Status,
pub headers: HashMap<String, String>,
pub body: Option<Vec<u8>>,
}
impl HttpResponse {
pub fn new(status: Status) -> Self {
Self {
status,
headers: HashMap::new(),
body: None,
}
}
pub fn with_utf8_body(mut self, body: &str) -> Self {
self.body = Some(body.as_bytes().to_vec());
return self;
}
pub fn client_error() -> Self {
Self::new(Status::ClientError).with_utf8_body("Client error")
}
}
impl Response for HttpResponse {
fn to_bytes(self) -> Vec<u8> {
let mut bytes = format!("HTTP/1.1 {}\n\n", self.status.code())
.as_bytes()
.to_vec();
if let Some(body) = self.body {
bytes.extend(body);
}
return bytes;
}
}

35
src/status.rs Normal file
View File

@ -0,0 +1,35 @@
pub enum Status {
Information,
Success,
Redirect,
ClientError,
ServerError,
}
impl Status {
pub fn code(&self) -> &str {
match self {
Self::Information => "100",
Self::Success => "200",
Self::Redirect => "300",
Self::ClientError => "400",
Self::ServerError => "500",
}
}
pub fn description(&self) -> &str {
match self {
Self::Information => "Information",
Self::Success => "Success",
Self::Redirect => "Redirect",
Self::ClientError => "ClientError",
Self::ServerError => "ServerError",
}
}
}
impl std::fmt::Debug for Status {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(f, "{} {}", self.code(), self.description())
}
}