Compare commits

..

5 Commits

8 changed files with 309 additions and 12 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "http" name = "http"
version = "0.1.1" version = "0.1.6"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,3 +1,27 @@
# http # http
A basic Rust lib to parse an HTTP request and build HTTP response. A basic Rust library to parse an HTTP request and build HTTP response.
**NOTE**: only few parts of the specification has been implemented and only JSON body are allowed.
## Integration
Get the latest version:
```toml
http = { git = "https://gitea.thegux.fr/rmanach/http" }
```
Or get a specific version:
```toml
http = { git = "https://gitea.thegux.fr/rmanach/http", version = "0.1.2" }
```
## Documentation
```bash
cargo doc -r --no-deps --open
```
## Launch unit tests
```bash
cargo test
```

View File

@ -7,8 +7,8 @@ pub struct HTTPBody {
data: JsonValue, data: JsonValue,
} }
/// HTTPBody represents an HTTP request body /// HTTPBody represents an HTTP request body.
/// for simplicity, only JSON body is allowed /// For simplicity, only JSON body is allowed.
impl HTTPBody { impl HTTPBody {
fn new(data: JsonValue) -> HTTPBody { fn new(data: JsonValue) -> HTTPBody {
HTTPBody { data } HTTPBody { data }

View File

@ -1,18 +1,26 @@
//! http parses the request according to the HTTP message specifications //! **http** library is a light HTTP parser and builder.
//! it also includes `HTTPResponse` to build an HTTPResponse
//! //!
//! see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages //! It parses the request according to the HTTP message specifications and includes `HTTPResponse` to build an HTTP Response.
//! NOTE: only few parts of the specification has been implemented
//! //!
//! * Only json body allowed //! See <https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages> for more details.
//!
//! NOTE: only few parts of the specification has been implemented.
//!
//! * Only JSON body allowed
//! * HTTP Headers not parsed //! * HTTP Headers not parsed
mod body; mod body;
mod message;
mod request; mod request;
mod response;
mod start_line; mod start_line;
mod status;
mod version; mod version;
pub use body::HTTPBody; pub use body::HTTPBody;
pub use message::JSONMessage;
pub use request::HTTPRequest; pub use request::HTTPRequest;
pub use response::HTTPResponse;
pub use start_line::HTTPStartLine; pub use start_line::HTTPStartLine;
pub use status::{HTTPStatusCode, HTTPStatusLine};
pub use version::HTTPVersion; pub use version::HTTPVersion;

94
src/message.rs Normal file
View File

@ -0,0 +1,94 @@
use json::JsonValue;
use std::collections::HashMap;
const JSON_DELIMITER: &'static str = ",";
/// JSONMessage aims to build a JSON object from an `HashMap`
pub struct JSONMessage {
message: HashMap<String, String>,
}
impl Default for JSONMessage {
fn default() -> Self {
JSONMessage {
message: HashMap::new(),
}
}
}
/// try to convert `JSONMessage` into `json::JsonValue`
impl TryInto<JsonValue> for JSONMessage {
type Error = String;
fn try_into(self) -> Result<JsonValue, Self::Error> {
let message = format!(r#"{{{}}}"#, self.build_json());
match json::parse(&message) {
Ok(r) => Ok(r),
Err(e) => Err(format!(
"unable to parse the HTTPMessage correctly: {}, err={}",
message, e
)),
}
}
}
impl JSONMessage {
pub fn put(&mut self, key: &str, value: &str) {
self.message.insert(key.to_string(), value.to_string());
}
// associated function to build an HTTPMessage error
pub fn error(message: &str) -> Option<JsonValue> {
let mut http_message = JSONMessage::default();
http_message.put("error", message);
match message.try_into() {
Ok(m) => Some(m),
Err(e) => {
log::error!(
"unable to parse the message: {} into JSON, err={}",
message,
e
);
return None;
}
}
}
/// build_json loops over all the HashMap keys, builds a JSON key value for each one and join them with `JSON_DELIMITER`
fn build_json(self) -> String {
let unstruct: Vec<String> = self
.message
.keys()
.map(|k| format!(r#""{}":{:?}"#, k, self.message.get(k).unwrap()))
.collect();
let joined = unstruct.join(JSON_DELIMITER);
joined
}
}
#[test]
fn test_message() {
let mut message = JSONMessage::default();
message.put("email", "toto@toto.fr");
message.put("password", "tata");
let mut json_result: Result<JsonValue, String> = message.try_into();
assert!(json_result.is_ok());
let mut json = json_result.unwrap();
assert!(json.has_key("email"));
assert!(json.has_key("password"));
let empty_message = JSONMessage::default();
json_result = empty_message.try_into();
assert!(json_result.is_ok());
json = json_result.unwrap();
assert_eq!("{}", json.dump().to_string());
let mut bad_message = JSONMessage::default();
bad_message.put("\"", "");
json_result = bad_message.try_into();
assert!(json_result.is_err());
}

View File

@ -16,14 +16,14 @@ pub struct HTTPRequest<'a> {
impl<'a> HTTPRequest<'a> { impl<'a> HTTPRequest<'a> {
/// get_request_parts splits correctly the incoming request in order to get: /// get_request_parts splits correctly the incoming request in order to get:
/// * start_line /// * start line
/// * headers /// * headers
/// * data (if exists) /// * data (if exists)
fn get_request_parts(request: &str) -> Result<RequestParts, &str> { fn get_request_parts(request: &str) -> Result<RequestParts, &str> {
let mut request_parts: VecDeque<&str> = request.split(HTTP_REQUEST_SEPARATOR).collect(); let mut request_parts: VecDeque<&str> = request.split(HTTP_REQUEST_SEPARATOR).collect();
if request_parts.len() < 3 { if request_parts.len() < 3 {
return Err("request has no enough informations to be correctly parsed"); return Err("request has not enough informations to be correctly parsed");
} }
let start_line = request_parts.pop_front().unwrap(); let start_line = request_parts.pop_front().unwrap();
@ -55,7 +55,7 @@ impl<'a> HTTPRequest<'a> {
} }
} }
/// get_bodyèvalue retrieves JSON value in `HTTPBody` /// get_body_value retrieves JSON value in `HTTPBody`
pub fn get_body_value(&self, key: &str) -> Option<String> { pub fn get_body_value(&self, key: &str) -> Option<String> {
match self.body { match self.body {
Some(ref b) => match &b.get_data() { Some(ref b) => match &b.get_data() {

117
src/response.rs Normal file
View File

@ -0,0 +1,117 @@
use json::JsonValue;
use crate::{HTTPStatusCode, HTTPStatusLine};
const UNEXPECTED_ERROR: &'static str = r#"{"error":"unexpected error occurred"}"#;
const NOT_FOUND_ERROR: &'static str = r#"{"error":"url not found"}"#;
const FORBIDDEN_ERROR: &'static str = r#"{"error":"url forbidden"}"#;
const BAD_REQUEST_ERROR: &'static str = r#"{"error":"bad request"}"#;
const STATUS_OK: &'static str = r#"{"status":"ok"}"#;
/// HTTPResponse represents an HTTP response (headers are not parsed)
///
/// NOTE: for simplicity, only JSON body are allowed
pub struct HTTPResponse {
status_line: HTTPStatusLine,
body: JsonValue,
}
impl Into<String> for HTTPResponse {
fn into(self) -> String {
let body: String = json::stringify(self.body.clone());
let status_line: String = self.status_line.into();
format!(
"{}\r\nContent-Type: application/json\r\nContent-Length:{}\r\n\r\n{}",
status_line,
body.len(),
body
)
}
}
impl Default for HTTPResponse {
fn default() -> Self {
HTTPResponse {
status_line: HTTPStatusLine::default(),
body: json::parse("{}").unwrap(),
}
}
}
impl HTTPResponse {
pub fn get_status_code(&self) -> HTTPStatusCode {
self.status_line.get_status_code()
}
pub fn as_500(message: Option<json::JsonValue>) -> Self {
let mut response = Self::default();
response
.status_line
.set_status_code(HTTPStatusCode::Http500);
response.body = {
match message {
Some(m) => m,
None => json::parse(UNEXPECTED_ERROR).unwrap(),
}
};
response
}
pub fn as_404() -> Self {
let mut response = Self::default();
response
.status_line
.set_status_code(HTTPStatusCode::Http404);
response.body = json::parse(NOT_FOUND_ERROR).unwrap();
response
}
pub fn as_403() -> Self {
let mut response = HTTPResponse {
status_line: HTTPStatusLine::default(),
body: json::parse(FORBIDDEN_ERROR).unwrap(),
};
response
.status_line
.set_status_code(HTTPStatusCode::Http403);
response
}
pub fn as_400() -> Self {
let mut response = HTTPResponse {
status_line: HTTPStatusLine::default(),
body: json::parse(BAD_REQUEST_ERROR).unwrap(),
};
response
.status_line
.set_status_code(HTTPStatusCode::Http400);
response
}
pub fn as_200(message: Option<json::JsonValue>) -> Self {
let mut response = Self::default();
response
.status_line
.set_status_code(HTTPStatusCode::Http200);
response.body = {
match message {
Some(m) => m,
None => json::parse(STATUS_OK).unwrap(),
}
};
response
}
}

54
src/status.rs Normal file
View File

@ -0,0 +1,54 @@
use crate::HTTPVersion;
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum HTTPStatusCode {
Http200,
Http400,
Http403,
Http404,
Http500,
}
impl Into<String> for HTTPStatusCode {
fn into(self) -> String {
match self {
Self::Http200 => "200".to_string(),
Self::Http400 => "400".to_string(),
Self::Http404 => "404".to_string(),
Self::Http403 => "403".to_string(),
Self::Http500 => "500".to_string(),
}
}
}
pub struct HTTPStatusLine {
version: HTTPVersion,
status_code: HTTPStatusCode,
}
impl Default for HTTPStatusLine {
fn default() -> HTTPStatusLine {
HTTPStatusLine {
version: HTTPVersion::Http1_1,
status_code: HTTPStatusCode::Http400,
}
}
}
impl Into<String> for HTTPStatusLine {
fn into(self) -> String {
let version: String = self.version.into();
let status_code: String = self.status_code.into();
format! {"{} {}", version, status_code}
}
}
impl HTTPStatusLine {
pub fn get_status_code(&self) -> HTTPStatusCode {
self.status_code
}
pub fn set_status_code(&mut self, code: HTTPStatusCode) {
self.status_code = code;
}
}