Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e61657090 | ||
| 72bf34127b | |||
| 7d4aabad2c | |||
| b8c0fbba0b | |||
| fb164ba137 |
@ -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
|
||||||
|
|||||||
26
README.md
26
README.md
@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
18
src/lib.rs
18
src/lib.rs
@ -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
94
src/message.rs
Normal 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());
|
||||||
|
}
|
||||||
@ -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
117
src/response.rs
Normal 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
54
src/status.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user