Skip to main content

limon_core/collectors/monitor/
http.rs

1use std::time::Duration;
2
3use curl::easy::{Easy2, Handler, HttpVersion, List, WriteError};
4use tokio::task;
5
6use crate::collectors::monitor::errors::HttpError;
7use crate::models::monitor::config::HttpConfig;
8use crate::models::monitor::measurement::{Data, HttpData};
9
10#[derive(Default)]
11struct ResponseBody(Vec<u8>);
12
13impl Handler for ResponseBody {
14  fn write(&mut self, data: &[u8]) -> Result<usize, WriteError> {
15    self.0.extend_from_slice(data);
16
17    Ok(data.len())
18  }
19}
20
21impl ResponseBody {
22  pub fn get_body(&self) -> String {
23    String::from_utf8_lossy(&self.0).into()
24  }
25}
26
27/// Collector for receiving monitor data from `HTTP` request.
28pub struct Http;
29
30impl Http {
31  pub async fn measure(host: &String, config: &HttpConfig) -> Result<Data, HttpError> {
32    let url = format!(
33      "{}://{}{}{}",
34      config.protocol.to_lowercase(),
35      host,
36      config
37        .port
38        .map_or(String::new(), |port| format!(":{}", port)),
39      config.path.clone().unwrap_or_default()
40    );
41
42    let mut headers = List::new();
43
44    if let Some(header_values) = &config.headers {
45      for header in header_values {
46        headers.append(&format!("{}: {}", header.name, header.value))?;
47      }
48    }
49
50    let mut request = Easy2::new(ResponseBody::default());
51    request.url(url.as_str())?;
52    request.http_headers(headers)?;
53    request.timeout(Duration::from_secs(config.timeout as u64))?;
54    request.cookie_file("")?;
55    request.follow_location(config.follow_redirects)?;
56    request.http_version(HttpVersion::V2)?;
57
58    match config.method.to_lowercase().as_str() {
59      "get" => request.get(true)?,
60      "post" => request.post(true)?,
61      "put" => request.put(true)?,
62      "patch" => request.custom_request("PATCH")?,
63      "head" => {
64        request.nobody(true)?;
65        request.custom_request("HEAD")?
66      }
67      _ => unimplemented!("Unimplemented HTTP method"),
68    };
69
70    if let Some(body) = config.body.clone() {
71      request.post_fields_copy(body.as_bytes())?;
72    }
73
74    let response = task::spawn_blocking(move || match request.perform() {
75      Ok(()) => Ok(request),
76      Err(error) => Err(HttpError::Unknown(error)),
77    })
78    .await
79    .expect("curl request")?;
80
81    let response_status = response.response_code()? as u16;
82    let expected_status_code = config.expected_status_code as u16;
83
84    if response_status != expected_status_code {
85      return Err(HttpError::StatusMismatch {
86        expected: expected_status_code,
87        actual: response_status,
88      });
89    }
90
91    if let Some(keyword) = config.keyword.clone() {
92      let response_body = response.get_ref().get_body();
93
94      if !response_body.contains(keyword.as_str()) {
95        return Err(HttpError::KeywordNotFound { keyword });
96      }
97    }
98
99    let dns = response.namelookup_time()?.as_secs_f32();
100    let tcp = response.connect_time()?.as_secs_f32();
101    let tls = response.appconnect_time()?.as_secs_f32();
102    let ttfb = response.starttransfer_time()?.as_secs_f32();
103    let total = response.total_time()?.as_secs_f32();
104    let transfer = (total - ttfb).max(0.0);
105
106    Ok(Data::Http(HttpData {
107      dns,
108      tcp,
109      tls,
110      ttfb,
111      transfer,
112    }))
113  }
114}
115
116#[cfg(test)]
117mod tests {
118  use httpmock::prelude::*;
119
120  use super::*;
121  use crate::models::monitor::config::{Header, HttpConfig};
122
123  #[test]
124  fn response_body() {
125    let mut response_body = ResponseBody([0].into());
126
127    assert!(
128      response_body.write(&[0]).is_ok(),
129      "response body is writable"
130    );
131    assert_eq!(
132      response_body.get_body(),
133      "\0\0",
134      "response body is readable"
135    );
136  }
137
138  #[tokio::test]
139  async fn headers() {
140    let server = MockServer::start_async().await;
141
142    let mock = server
143      .mock_async(|when, then| {
144        when
145          .header("Authorization", "token")
146          .method(GET)
147          .path("/check");
148        then.status(200);
149      })
150      .await;
151
152    let result = Http::measure(
153      &server.host(),
154      &HttpConfig {
155        timeout: 3,
156        method: String::from("GET"),
157        protocol: String::from("HTTP"),
158        port: Some(server.port()),
159        path: Some(String::from("/check")),
160        headers: Some(vec![Header {
161          name: String::from("Authorization"),
162          value: String::from("token"),
163        }]),
164        expected_status_code: 200,
165        ..Default::default()
166      },
167    )
168    .await;
169
170    mock.assert();
171
172    assert!(result.is_ok(), "request header is correct");
173  }
174
175  #[tokio::test]
176  async fn body() {
177    let server = MockServer::start_async().await;
178
179    let mock = server
180      .mock_async(|when, then| {
181        when.method(POST).path("/check").body("test");
182        then.status(200);
183      })
184      .await;
185
186    let result = Http::measure(
187      &server.host(),
188      &HttpConfig {
189        timeout: 3,
190        method: String::from("POST"),
191        protocol: String::from("HTTP"),
192        port: Some(server.port()),
193        path: Some(String::from("/check")),
194        body: Some(String::from("test")),
195        expected_status_code: 200,
196        ..Default::default()
197      },
198    )
199    .await;
200
201    mock.assert();
202
203    assert!(result.is_ok(), "request body is correct");
204  }
205
206  #[tokio::test]
207  async fn methods() {
208    let server = MockServer::start_async().await;
209
210    for method in ["GET", "POST", "PUT", "PATCH", "HEAD"] {
211      let mock = server
212        .mock_async(|when, then| {
213          when.method(Method::from(method)).path("/check");
214          then.status(200);
215        })
216        .await;
217
218      let result = Http::measure(
219        &server.host(),
220        &HttpConfig {
221          timeout: 3,
222          method: String::from(method),
223          protocol: String::from("HTTP"),
224          port: Some(server.port()),
225          path: Some(String::from("/check")),
226          expected_status_code: 200,
227          ..Default::default()
228        },
229      )
230      .await;
231
232      mock.assert();
233
234      assert!(result.is_ok(), "request method is correct");
235    }
236  }
237
238  #[tokio::test]
239  async fn response_status_mismatch() {
240    let server = MockServer::start_async().await;
241
242    let mock = server
243      .mock_async(|when, then| {
244        when.method(GET).path("/check");
245        then.status(400);
246      })
247      .await;
248
249    let result = Http::measure(
250      &server.host(),
251      &HttpConfig {
252        timeout: 3,
253        method: String::from("GET"),
254        protocol: String::from("HTTP"),
255        port: Some(server.port()),
256        path: Some(String::from("/check")),
257        expected_status_code: 200,
258        ..Default::default()
259      },
260    )
261    .await;
262
263    mock.assert();
264
265    assert!(result.is_err(), "response has unexpected status");
266  }
267
268  #[tokio::test]
269  async fn response_doesnt_contain_keyword() {
270    let server = MockServer::start_async().await;
271
272    let mock = server
273      .mock_async(|when, then| {
274        when.method(GET).path("/check");
275        then.status(200).body("error");
276      })
277      .await;
278
279    let result = Http::measure(
280      &server.host(),
281      &HttpConfig {
282        timeout: 3,
283        method: String::from("GET"),
284        protocol: String::from("HTTP"),
285        port: Some(server.port()),
286        path: Some(String::from("/check")),
287        expected_status_code: 200,
288        keyword: Some(String::from("index")),
289        ..Default::default()
290      },
291    )
292    .await;
293
294    mock.assert();
295
296    assert!(result.is_err(), "response doesn't contain expected keyword");
297  }
298
299  #[tokio::test]
300  async fn unknown_error() {
301    let result = Http::measure(
302      &String::from("127.0.0.1"),
303      &HttpConfig {
304        method: String::from("GET"),
305        protocol: String::from("HTTP"),
306        port: Some(5555),
307        expected_status_code: 200,
308        ..Default::default()
309      },
310    )
311    .await;
312
313    assert!(result.is_err(), "Could not connect to server");
314  }
315}