ilmen_http/http/requests/
http_request.rs

1use std::collections::HashMap;
2use std::str::FromStr;
3use std::usize;
4use std::str;
5use log::trace;
6
7use crate::http::errors::http_errors::HttpError;
8use crate::http::header::Headers;
9use crate::Verb;
10
11#[derive(Clone)]
12pub struct HTTPRequest {
13    pub protocol: Protocol,
14    pub verb: Verb,
15    pub resource: Resource, 
16    pub query_params: Option<QueryParams>,
17    pub headers: Option<Headers>,
18    pub body: Option<Body>
19}
20
21pub type Resource = String; 
22pub type Protocol = String; 
23pub type QueryParams = HashMap<String, String>;
24type Body = String;
25
26impl Default for HTTPRequest {
27    fn default() -> Self {
28        Self { protocol: Default::default(), verb: Verb::GET, resource: "/".to_string(), query_params: Default::default(), headers: Default::default(), body: Default::default() }
29    }
30}
31
32impl TryFrom<Vec<u8>> for HTTPRequest {
33    type Error = HttpError;
34    fn try_from(value: Vec<u8>) -> Result<Self, HttpError> {
35        str::from_utf8(&value)
36        .map_err(|error| 
37            HttpError::BadRequest(error.to_string()))
38        .map(HTTPRequest::try_from)?
39    }
40}
41
42impl TryFrom<Vec<String>> for Verb {
43    type Error = HttpError;
44
45    fn try_from(value: Vec<String>) -> Result<Self, HttpError> {
46        value.first()
47            .ok_or(HttpError::BadRequest("Missing verb".to_string()))
48            .and_then(|verb| Verb::from_str(verb))
49    }
50}
51
52
53impl TryFrom<&str> for HTTPRequest {
54    type Error = HttpError;
55    fn try_from(buffer: &str) -> Result<Self, HttpError> {
56        let parsed_request = parse(buffer);
57        
58        let decomposed_start_line = parsed_request.first()
59            .ok_or(HttpError::BadRequest("Mising ressources".to_string()))?
60            .split(' ')
61            .take(3)
62            .map(String::from)
63            .collect::<Vec<String>>();
64
65        let verb = Verb::try_from(decomposed_start_line.clone())?;
66
67        let protocol = decomposed_start_line
68            .get(2)
69            .ok_or(HttpError::BadRequest("Missing protocol".to_string()))?
70            .to_string();
71
72
73        let requested_resource = decomposed_start_line
74            .get(1)
75            .ok_or(HttpError::BadRequest("No ressource".to_string()))
76            .map(|str| str.split("?").collect::<Vec<&str>>())?; 
77
78        let resource = requested_resource.first().ok_or(HttpError::BadRequest("Missing path".to_string()))?;
79
80        let query_params = requested_resource.get(1)
81            .map(|params| params.split("&").collect::<Vec<&str>>())
82            .map(|vec_params| 
83                vec_params.iter()
84                    .map(|couple| couple
85                        .split_once("=")
86                        .unwrap_or((couple, "")))
87                    .map(|(a,b)|(a.to_string(), b.to_string()))
88                    .collect::<QueryParams>());
89
90
91        let headers = extract_headers(parsed_request.clone());
92        trace!("Headers: {:?}", headers);
93        
94        let body = get_header(&headers, "Content-Length")
95            .map(|(_, length)| length.parse::<usize>())
96            .transpose()
97            .map_err(|_| HttpError::BadRequest("Content Length not a number".to_string()))?
98            .map(|length| extract_body(buffer, length));
99
100        Ok (HTTPRequest {protocol, 
101            verb, 
102            query_params, 
103            headers: Some(headers), 
104            body,
105            resource: resource.to_string()})
106    }
107}
108
109
110fn extract_body(request : &str, content_length: usize) -> Body {
111    return request.split_once("\r\n\r\n")
112        .map(|(_, b)| b[0..content_length].to_string())
113        .unwrap_or_default();
114}
115
116
117fn parse(buffer : &str) -> Vec<String> {
118    return buffer
119        .trim_matches(char::from(0))
120        .split("\r\n")
121        .map(|str| str.to_string())
122        .collect::<Vec<String>>();
123}
124
125impl HTTPRequest {
126    pub fn get_header(&self, key: &str) -> Option<(String, String)> {
127        self.headers.clone().unwrap_or_default()
128            .iter()
129            .find(|(header, _)| header.to_lowercase().starts_with(&key.to_lowercase())).cloned()
130    }
131}
132
133fn get_header(headers: &Headers, key: &str) -> Option<(String, String)> {
134    headers
135        .iter()
136        .find(|(header, _)| header.to_lowercase().starts_with(&key.to_lowercase()))
137        .cloned()
138}
139
140fn extract_headers(request : Vec<String>) -> Headers {
141    request.iter()
142        .skip(1)
143        .take_while(|&str| str.trim() != "")
144        .map(|header| header.split_once(':'))
145        .map(|spliterator| spliterator.unwrap_or_default())
146        .map(|(cle, value)| (cle.trim().to_owned(), value.trim().to_owned()))
147        .collect::<Headers>()
148}
149
150
151
152// UNIT TEST
153#[cfg(test)]
154mod tests {
155    use std::vec;
156    use crate::Verb;
157
158    use super::*;
159
160    #[test]
161    fn request_try_from_ok() {
162        let buffer = "POST rappel/1?moi=toi&toi=moi HTTP/1.1\r\nContent-Length: 10\r\n\r\ntoto\r\ntata";
163
164        let mut expected_query_params = HashMap::new();
165        expected_query_params.insert("moi".to_string(), "toi".to_string());
166        expected_query_params.insert("toi".to_string(),"moi".to_string());
167        
168        let  request = HTTPRequest::try_from(buffer);
169        
170        let http_request = request.unwrap();
171        assert_eq!(http_request.verb, Verb::POST);
172        assert_eq!(http_request.resource, "rappel/1");
173        assert_eq!(http_request.query_params, Some(expected_query_params));
174        assert_eq!(http_request.body, Some("toto\r\ntata".to_string()));
175        assert_eq!(http_request.headers, Some(vec![("Content-Length".to_string(), "10".to_string())]))
176    }
177
178    #[test]
179    fn request_try_from_ko() {
180        let buffer = "POST rappel/1 HTTP/1.1\r\nContent-Length: 4\r\n\r\ntoto";
181        
182        let  request = HTTPRequest::try_from(buffer);
183        
184        assert!(request.is_ok());
185    }
186
187
188    #[test]
189    fn extract_headers_ok() {
190        let request = vec!["ressource".to_string(),"Content-Length: 1".to_string(),"Content-type: x and y".to_string(), "".to_string()];
191        
192        let  request = extract_headers(request);
193        
194        assert_eq!(request, vec![("Content-Length".to_string(), "1".to_string()),("Content-type".to_string(), "x and y".to_string())]);
195    }
196}
197