1use std::fmt::Debug;
4use std::net::IpAddr;
5use std::str::FromStr;
6
7use crate::layer::har::extensions::RequestComment;
8use crate::layer::remove_header::{
9 remove_sensitive_request_headers, remove_sensitive_response_headers,
10};
11use crate::proto::HeaderByteLength;
12use crate::request::Parts as ReqParts;
13use crate::response::Parts as RespParts;
14use crate::service::web::extract::Query;
15
16use rama_core::error::{BoxError, ErrorContext};
17use rama_core::extensions::ExtensionsRef;
18use rama_core::telemetry::tracing;
19use rama_http_headers::{
20 ContentEncoding, ContentEncodingDirective, ContentType, Cookie as RamaCookie, HeaderMapExt,
21 Location,
22};
23use rama_http_headers::{HeaderEncode, SetCookie};
24use rama_http_types::mime::Mime;
25use rama_http_types::proto::h1::ext::ReasonPhrase;
26use rama_http_types::{HeaderMap, HeaderName, Version as RamaHttpVersion};
27
28use base64::Engine;
29use base64::engine::general_purpose::STANDARD as ENGINE;
30use jiff::Timestamp;
31use rama_utils::str::arcstr::ArcStr;
32use rama_utils::str::smol_str::{ToSmolStr, format_smolstr};
33use rama_utils::str::{NonEmptyStr, non_empty_str};
34use serde::{Deserialize, Serialize};
35
36mod mime_serde {
37 use rama_http_types::mime::Mime;
38 use serde::{Deserialize, Deserializer, Serializer, de::Error};
39 use std::{borrow::Cow, str::FromStr};
40
41 #[expect(clippy::ref_option)]
42 pub(super) fn serialize<S>(mime: &Option<Mime>, serializer: S) -> Result<S::Ok, S::Error>
43 where
44 S: Serializer,
45 {
46 if let Some(mime) = mime {
47 serializer.serialize_str(mime.as_ref())
48 } else {
49 serializer.serialize_none()
50 }
51 }
52
53 pub(super) fn deserialize<'de, D>(d: D) -> Result<Option<Mime>, D::Error>
54 where
55 D: Deserializer<'de>,
56 {
57 let opt = <Option<Cow<'de, str>>>::deserialize(d)?;
58 if let Some(s) = opt {
59 Mime::from_str(&s).map_err(Error::custom).map(Some)
60 } else {
61 Ok(None)
62 }
63 }
64}
65
66rama_utils::macros::enums::enum_builder! {
67 @String
68 pub enum HttpVersion {
69 Http09 => "HTTP/0.9" | "0.9",
70 Http10 => "HTTP/1.0" | "1.0" | "HTTP/1",
71 Http11 => "HTTP/1.1" | "1.1",
72 Http2 => "HTTP/2" | "2" | "h2",
73 Http3 => "HTTP/3" | "3" | "h3",
74 }
75}
76
77impl From<RamaHttpVersion> for HttpVersion {
78 fn from(rhv: RamaHttpVersion) -> Self {
79 match rhv {
80 RamaHttpVersion::HTTP_09 => Self::Http09,
81 RamaHttpVersion::HTTP_10 => Self::Http10,
82 RamaHttpVersion::HTTP_11 => Self::Http11,
83 RamaHttpVersion::HTTP_2 => Self::Http2,
84 RamaHttpVersion::HTTP_3 => Self::Http3,
85 }
86 }
87}
88
89impl TryFrom<HttpVersion> for RamaHttpVersion {
90 type Error = HttpVersion;
91
92 fn try_from(rhv: HttpVersion) -> Result<Self, Self::Error> {
93 match rhv {
94 HttpVersion::Http09 => Ok(Self::HTTP_09),
95 HttpVersion::Http10 => Ok(Self::HTTP_10),
96 HttpVersion::Http11 => Ok(Self::HTTP_11),
97 HttpVersion::Http2 => Ok(Self::HTTP_2),
98 HttpVersion::Http3 => Ok(Self::HTTP_3),
99 v @ HttpVersion::Unknown(_) => Err(v),
100 }
101 }
102}
103
104fn into_query_string(parts: &ReqParts) -> Vec<QueryStringPair> {
105 let query = parts.uri.query_or_empty();
106 match Query::<Vec<(ArcStr, ArcStr)>>::parse_query_str(query.as_ref()) {
107 Ok(Query(v)) => v
108 .into_iter()
109 .map(|(name, value)| QueryStringPair {
110 name,
111 value,
112 comment: None,
113 })
114 .collect(),
115 Err(err) => {
116 tracing::debug!("failure to parse query string: {err:?}");
117 vec![]
118 }
119 }
120}
121
122fn get_mime(headers: &HeaderMap) -> Option<Mime> {
123 headers.typed_get::<ContentType>().map(|ct| ct.into_mime())
124}
125
126fn parse_cookie_part(part: &str) -> Option<Cookie> {
127 let trimmed = part.trim();
128 if trimmed.is_empty() {
129 return None;
130 }
131 let mut split = trimmed.splitn(2, '=');
132 let name = split.next()?.trim().into();
133 let value = split.next()?.trim().into();
134 Some(Cookie {
135 name,
136 value,
137 ..Default::default()
138 })
139}
140
141fn into_har_headers(header_map: HeaderMap) -> Vec<Header> {
142 header_map
143 .into_ordered_iter()
144 .map(|(name, value)| Header {
145 name: name.as_original_str().into_owned().into(),
146 value: match value.to_str() {
147 Ok(s) => s.into(),
148 Err(_) => format_smolstr!("{value:x?}").into(),
149 },
150 comment: None,
151 })
152 .collect()
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct LogFile {
158 pub log: Log,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Log {
165 pub version: NonEmptyStr,
167 pub creator: Creator,
169 pub browser: Option<Browser>,
171 pub pages: Option<Vec<Page>>,
175 pub entries: Vec<Entry>,
177 pub comment: Option<ArcStr>,
179}
180
181impl Default for Log {
182 fn default() -> Self {
183 Self {
184 version: non_empty_str!("1.2"),
185 creator: Creator {
186 name: rama_utils::info::NAME.into(),
187 version: rama_utils::info::VERSION.into(),
188 comment: None,
189 },
190 browser: None,
191 pages: None,
192 entries: vec![],
193 comment: None,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Creator {
201 pub name: ArcStr,
202 pub version: ArcStr,
203 pub comment: Option<ArcStr>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct Browser {
208 pub name: ArcStr,
210 pub version: Option<ArcStr>,
212 pub comment: Option<ArcStr>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Page {
218 #[serde(rename = "startedDateTime")]
220 pub started_date_time: Timestamp,
221 pub id: ArcStr,
223 pub title: ArcStr,
225 #[serde(rename = "pageTimings")]
227 pub page_timings: PageTimings,
228 pub comment: Option<ArcStr>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct PageTimings {
238 #[serde(rename = "onContentLoad")]
243 pub on_content_load: Option<i64>,
244 #[serde(rename = "onLoad")]
249 pub on_load: Option<i64>,
250 pub comment: Option<ArcStr>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct Entry {
257 #[serde(rename = "pageref")]
261 pub page_ref: Option<ArcStr>,
262 #[serde(rename = "startedDateTime")]
264 pub started_date_time: Timestamp,
265 pub time: i64,
269 pub request: Request,
271 pub response: Option<Response>,
273 pub cache: Cache,
275 pub timings: Timings,
277 #[serde(rename = "serverIPAddress")]
281 pub server_ip_address: Option<IpAddr>, pub connection: Option<ArcStr>,
292 pub comment: Option<ArcStr>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct Request {
299 pub method: ArcStr,
301 pub url: ArcStr,
303 #[serde(rename = "httpVersion")]
305 pub http_version: HttpVersion,
306 pub cookies: Vec<Cookie>,
308 pub headers: Vec<Header>,
310 #[serde(rename = "queryString")]
312 pub query_string: Vec<QueryStringPair>,
313 #[serde(rename = "postData")]
315 pub post_data: Option<PostData>,
316 #[serde(rename = "headersSize")]
322 pub headers_size: i64,
323 #[serde(rename = "bodySize")]
327 pub body_size: i64,
328 pub comment: Option<ArcStr>,
330}
331
332impl TryFrom<Request> for crate::Request {
333 type Error = BoxError;
334
335 fn try_from(har_request: Request) -> Result<Self, Self::Error> {
336 let body = if let Some(text) = har_request.post_data.and_then(|pd| pd.text) {
337 if let Ok(bin) = ENGINE.decode(&text) {
338 crate::Body::from(bin)
339 } else {
340 crate::Body::from(text)
341 }
342 } else {
343 crate::Body::empty()
344 };
345
346 let mut headers = HeaderMap::with_capacity(har_request.headers.len());
347 for header in har_request.headers {
348 headers.append(
349 HeaderName::from_str(&header.name).context("convert http header name")?,
350 crate::HeaderValue::from_maybe_shared(header.value)
351 .context("convert http header value")?,
352 );
353 }
354
355 let builder = crate::Request::builder()
356 .method(
357 har_request
358 .method
359 .parse::<crate::Method>()
360 .context("parse HAR HTTP Method")?,
361 )
362 .uri(har_request.url.as_str());
363
364 let builder = if let Ok(ver) = har_request.http_version.try_into() {
365 builder.version(ver)
366 } else {
367 builder
368 };
369
370 let mut req = builder
371 .body(body)
372 .context("build http request from HAR data")?;
373
374 *req.headers_mut() = headers;
375
376 if let Some(comment) = har_request.comment {
377 req.extensions().insert(RequestComment(comment));
378 }
379
380 Ok(req)
381 }
382}
383
384impl Request {
385 pub fn from_http_request_parts(
386 parts: &ReqParts,
387 payload: &[u8],
388 preserve_sensitive: bool,
389 ) -> Result<Self, BoxError> {
390 let post_data = if !payload.is_empty() {
391 let mime_type = get_mime(&parts.headers);
392 let params = if mime_type
393 .as_ref()
394 .map(|m| m.subtype() == crate::mime::WWW_FORM_URLENCODED)
395 .unwrap_or_default()
396 {
397 Some(serde_html_form::from_bytes(payload).context("decode form body payload")?)
398 } else {
399 None
400 };
401
402 let text = match std::str::from_utf8(payload) {
403 Ok(s) => s.into(),
404 Err(_) => ENGINE.encode(payload).into(),
405 };
406
407 Some(PostData {
408 mime_type,
409 params,
410 text: Some(text),
411 comment: None,
412 })
413 } else {
414 None
415 };
416
417 let comment = parts
418 .extensions
419 .get_ref::<RequestComment>()
420 .map(|req_comment| req_comment.0.clone());
421
422 let cookies = parts
423 .headers
424 .typed_get::<RamaCookie>()
425 .map(|c| {
426 c.iter()
427 .map(|(k, v)| Cookie {
428 name: k.into(),
429 value: v.into(),
430 path: None,
431 domain: None,
432 expires: None,
433 http_only: None,
434 secure: None,
435 comment: None,
436 })
437 .collect()
438 })
439 .unwrap_or_default();
440
441 let query_string = into_query_string(parts);
442 let mut headers = parts.headers.clone();
443 if !preserve_sensitive {
444 remove_sensitive_request_headers(&mut headers);
445 }
446
447 let headers_size_ext = parts.extensions.get_ref::<HeaderByteLength>();
448 let headers_size = headers_size_ext.map(|v| v.0 as i64).unwrap_or(-1);
449
450 Ok(Self {
451 method: parts.method.to_smolstr().into(),
452 url: parts.uri.to_string().into(),
453 http_version: parts.version.into(),
454 cookies,
455 headers: into_har_headers(headers),
456 query_string,
457 post_data,
458 headers_size,
459 body_size: payload.len() as i64,
460 comment,
461 })
462 }
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct Response {
468 pub status: u16,
470 #[serde(rename = "statusText")]
472 pub status_text: Option<ArcStr>,
473 #[serde(rename = "httpVersion")]
475 pub http_version: HttpVersion,
476 pub cookies: Vec<Cookie>,
478 pub headers: Vec<Header>,
480 pub content: Content,
482 #[serde(rename = "redirectURL")]
484 pub redirect_url: Option<ArcStr>,
485 #[serde(rename = "headersSize")]
491 pub headers_size: i64,
492 #[serde(rename = "bodySize")]
501 pub body_size: i64,
502 pub comment: Option<ArcStr>,
504}
505
506impl TryFrom<Response> for crate::Response {
507 type Error = BoxError;
508
509 fn try_from(har_response: Response) -> Result<Self, Self::Error> {
510 let body = match har_response.content.text {
511 Some(s) => match ENGINE.decode(&s) {
512 Ok(v) => crate::Body::from(v),
513 Err(_) => crate::Body::from(s),
514 },
515 None => crate::Body::empty(),
516 };
517
518 let mut headers = HeaderMap::with_capacity(har_response.headers.len());
519 for header in har_response.headers {
520 headers.append(
521 HeaderName::from_str(&header.name).context("convert http header name")?,
522 crate::HeaderValue::from_maybe_shared(header.value)
523 .context("convert http header value")?,
524 );
525 }
526
527 let builder = crate::Response::builder().status(
528 crate::StatusCode::from_u16(har_response.status).context("convert HAR status code")?,
529 );
530
531 let builder = if let Ok(ver) = har_response.http_version.try_into() {
532 builder.version(ver)
533 } else {
534 builder
535 };
536
537 let mut res = builder
538 .body(body)
539 .context("build http response from HAR data")?;
540
541 *res.headers_mut() = headers;
542
543 Ok(res)
544 }
545}
546
547impl Response {
548 pub fn from_http_response_parts(
549 parts: &RespParts,
550 payload: &[u8],
551 preserve_sensitive: bool,
552 ) -> Result<Self, BoxError> {
553 let content = Content {
554 size: payload.len() as i64,
555 compression: None,
556 mime_type: get_mime(&parts.headers),
557 text: (!payload.is_empty()).then(|| match std::str::from_utf8(payload) {
558 Ok(s) => s.into(),
559 Err(_) => ENGINE.encode(payload).into(),
560 }),
561 encoding: parts
562 .headers
563 .typed_get::<ContentEncoding>()
564 .map(|ContentEncoding(ce)| ce.head),
565 comment: None,
566 };
567
568 let redirect_url = parts
569 .headers
570 .typed_get::<Location>()
571 .and_then(|h| h.encode_to_value())
572 .and_then(|v| v.to_str().ok().map(Into::into));
573
574 let cookies = parts
575 .headers
576 .typed_get::<SetCookie>()
577 .map(|sc| {
578 sc.iter_header_values()
579 .filter_map(|v| {
580 v.to_str().ok().and_then(|s| {
581 let raw = s.split(';').next()?;
582 parse_cookie_part(raw)
583 })
584 })
585 .collect()
586 })
587 .unwrap_or_default();
588
589 let mut headers = parts.headers.clone();
590 if !preserve_sensitive {
591 remove_sensitive_response_headers(&mut headers);
592 }
593
594 let headers_size_ext = parts.extensions.get_ref::<HeaderByteLength>();
595 let headers_size = headers_size_ext.map(|v| v.0 as i64).unwrap_or(-1);
596
597 Ok(Self {
598 status: parts.status.as_u16(),
599 status_text: match parts.extensions.get_ref::<ReasonPhrase>() {
600 Some(reason) => Some(
601 String::from_utf8_lossy(reason.as_bytes())
602 .into_owned()
603 .into(),
604 ),
605 None => parts.status.canonical_reason().map(Into::into),
606 },
607 http_version: parts.version.into(),
608 cookies,
609 headers: into_har_headers(headers),
610 content,
611 redirect_url,
612 headers_size,
613 body_size: payload.len() as i64,
614 comment: None,
615 })
616 }
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize, Default)]
623pub struct Cookie {
627 pub name: ArcStr,
629 pub value: ArcStr,
631 pub path: Option<ArcStr>,
633 pub domain: Option<ArcStr>,
635 pub expires: Option<Timestamp>,
639 #[serde(rename = "httpOnly")]
641 pub http_only: Option<bool>,
642 pub secure: Option<bool>,
644 pub comment: Option<ArcStr>,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
649pub struct Header {
651 pub name: ArcStr,
653 pub value: ArcStr,
655 pub comment: Option<ArcStr>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct QueryStringPair {
663 pub name: ArcStr,
665 pub value: ArcStr,
667 pub comment: Option<ArcStr>,
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct PostData {
676 #[serde(with = "mime_serde", rename = "mimeType")]
677 pub mime_type: Option<Mime>,
679 pub params: Option<Vec<PostParam>>,
683 pub text: Option<ArcStr>,
685 pub comment: Option<ArcStr>,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct PostParam {
691 pub name: ArcStr,
692 pub value: Option<ArcStr>,
693 #[serde(rename = "fileName")]
694 pub file_name: Option<ArcStr>,
695 #[serde(rename = "contentType")]
696 pub content_type: Option<ArcStr>,
697 pub comment: Option<ArcStr>,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize)]
701pub struct Content {
712 pub size: i64, pub compression: Option<i64>, #[serde(with = "mime_serde", rename = "mimeType")]
722 pub mime_type: Option<Mime>,
729 pub text: Option<ArcStr>,
730 pub encoding: Option<ContentEncodingDirective>,
738 pub comment: Option<ArcStr>,
740}
741
742#[derive(Debug, Clone, Serialize, Deserialize, Default)]
743pub struct Cache {
745 #[serde(rename = "beforeRequest")]
749 pub before_request: Option<CacheState>,
750 #[serde(rename = "afterRequest")]
754 pub after_request: Option<CacheState>,
755 pub comment: Option<ArcStr>,
757} #[derive(Debug, Clone, Serialize, Deserialize)]
760pub struct CacheState {
761 pub expires: Timestamp,
766 #[serde(rename = "lastAccess")]
768 pub last_access: Option<ArcStr>,
769 #[serde(rename = "eTag")]
771 pub e_tag: Option<ArcStr>,
772 #[serde(rename = "hitCount")]
774 pub hit_count: Option<i64>,
775 pub comment: Option<ArcStr>,
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize, Default)]
780pub struct Timings {
784 pub blocked: Option<i64>, pub dns: Option<i64>, pub connect: Option<i64>, pub send: i64, pub wait: i64, pub receive: i64, pub ssl: Option<i64>, pub comment: Option<ArcStr>,
813}
814
815#[cfg(test)]
816mod tests {
817 use rama_http_types::body::util::BodyExt as _;
818
819 use super::*;
820
821 #[test]
822 fn into_har_headers_preserves_original_header_name_casing() {
823 let mut headers = HeaderMap::new();
824 headers.insert(
825 HeaderName::from_static("Content-Length"),
826 "42".parse().unwrap(),
827 );
828 headers.append(HeaderName::from_static("X-CuStOm"), "rama".parse().unwrap());
829
830 let har_headers = into_har_headers(headers);
831 assert_eq!("Content-Length", har_headers[0].name);
832 assert_eq!("X-CuStOm", har_headers[1].name);
833 }
834
835 #[test]
836 #[tracing_test::traced_test]
837 fn test_load_har_entries() {
838 let log_file: LogFile = serde_json::from_str(HAR_LOG_FILE_EXAMPLE).unwrap();
839
840 assert_eq!(6, log_file.log.entries.len());
841
842 let entry0 = &log_file.log.entries[0];
843 assert_eq!("http://www.igvita.com/", entry0.request.url);
844 assert_eq!("GET", entry0.request.method);
845 assert!(matches!(entry0.request.http_version, HttpVersion::Http11));
846 assert_eq!(0, entry0.request.query_string.len());
847
848 let entry1 = &log_file.log.entries[1];
849 assert_eq!(
850 "http://fonts.googleapis.com/css?family=Open+Sans:400,600",
851 entry1.request.url
852 );
853 assert_eq!(1, entry1.request.query_string.len());
854 assert_eq!("family", entry1.request.query_string[0].name);
855 assert_eq!("Open+Sans:400,600", entry1.request.query_string[0].value);
856
857 let entry5 = &log_file.log.entries[5];
858 assert_eq!(
859 "http://1-ps.googleusercontent.com/beacon?org=50_1_cn&ets=load:93&ifr=0&hft=32&url=http%3A%2F%2Fwww.igvita.com%2F",
860 entry5.request.url
861 );
862 assert_eq!(5, entry5.request.query_string.len());
863
864 let req0: crate::Request = entry0.request.clone().try_into().unwrap();
866 let (req0_parts, req0_body) = req0.into_parts();
867 drop(req0_body);
868
869 assert_eq!(crate::Method::GET, req0_parts.method);
870 assert_eq!(entry0.request.url, req0_parts.uri.to_string());
871 assert_eq!(RamaHttpVersion::HTTP_11, req0_parts.version);
872
873 let host = req0_parts
874 .headers
875 .get("Host")
876 .and_then(|v| v.to_str().ok())
877 .unwrap();
878 assert_eq!("www.igvita.com", host);
879
880 let req0_back = Request::from_http_request_parts(&req0_parts, &[], false).unwrap();
882 assert_eq!(entry0.request.method, req0_back.method);
883 assert_eq!(entry0.request.url, req0_back.url);
884 assert!(matches!(req0_back.http_version, HttpVersion::Http11));
885 assert_eq!(0, req0_back.body_size);
886
887 let ua = req0_back
888 .headers
889 .iter()
890 .find(|h| h.name.eq_ignore_ascii_case("User-Agent"))
891 .map(|h| h.value.as_str())
892 .unwrap();
893 assert!(ua.contains("Chrome/21.0.1180.82"));
894
895 let req5: crate::Request = entry5.request.clone().try_into().unwrap();
897 let (req5_parts, req5_body) = req5.into_parts();
898 drop(req5_body);
899
900 let req5_back = Request::from_http_request_parts(&req5_parts, &[], false).unwrap();
901 assert_eq!(5, req5_back.query_string.len(), "req: {req5_back:?}");
902 assert!(
903 req5_back
904 .query_string
905 .iter()
906 .any(|p| p.name == "org" && p.value == "50_1_cn"),
907 "query string: {:?}",
908 req5_back.query_string
909 );
910 assert!(
911 req5_back
912 .query_string
913 .iter()
914 .any(|p| p.name == "ets" && p.value == "load:93"),
915 "query string: {:?}",
916 req5_back.query_string
917 );
918 assert!(
919 req5_back
920 .query_string
921 .iter()
922 .any(|p| p.name == "url" && p.value == "http://www.igvita.com/"),
923 "query string: {:?}",
924 req5_back.query_string
925 );
926
927 let har_res0 = entry0.response.clone().unwrap();
929 let res0: crate::Response = har_res0.try_into().unwrap();
930 let (res0_parts, res0_body) = res0.into_parts();
931 drop(res0_body);
932
933 assert_eq!(crate::StatusCode::OK, res0_parts.status);
934 assert_eq!(RamaHttpVersion::HTTP_11, res0_parts.version);
935
936 let ct = res0_parts
937 .headers
938 .get("Content-Type")
939 .and_then(|v| v.to_str().ok())
940 .unwrap();
941 assert!(ct.starts_with("text/html"));
942
943 let ce = res0_parts
944 .headers
945 .get("Content-Encoding")
946 .and_then(|v| v.to_str().ok())
947 .unwrap();
948 assert_eq!("gzip", ce);
949
950 let res0_back = Response::from_http_response_parts(&res0_parts, &[], false).unwrap();
952 assert_eq!(200, res0_back.status);
953 assert_eq!(Some("OK"), res0_back.status_text.as_deref());
954 assert!(matches!(res0_back.http_version, HttpVersion::Http11));
955
956 let mime = res0_back.content.mime_type.unwrap();
957 assert_eq!("text/html; charset=utf-8", mime.as_ref());
958
959 let encoding = res0_back.content.encoding.unwrap();
960 assert_eq!(ContentEncodingDirective::Gzip, encoding);
961 }
962
963 #[tokio::test]
964 #[tracing_test::traced_test]
965 async fn test_load_har_entries_payload_roundtrip() {
966 let log_file: LogFile = serde_json::from_str(HAR_LOG_FILE_PAYLOAD_EXAMPLE).unwrap();
967 assert_eq!(1, log_file.log.entries.len());
968
969 let entry = &log_file.log.entries[0];
970
971 let req: crate::Request = entry.request.clone().try_into().unwrap();
973 let (req_parts, req_body) = req.into_parts();
974
975 let req_payload = vec![0u8, 255, 1, 2, 3];
976 let req_bytes = req_body.collect().await.unwrap().to_bytes().to_vec();
977 assert_eq!(req_payload, req_bytes);
978
979 let har_req_back =
981 Request::from_http_request_parts(&req_parts, &req_payload, false).unwrap();
982 let post_data = har_req_back.post_data.unwrap();
983 assert_eq!(
984 Some(Mime::from_str("application/octet-stream").unwrap()),
985 post_data.mime_type
986 );
987 assert_eq!(Some("AP8BAgM="), post_data.text.as_deref());
988 assert_eq!(req_payload.len() as i64, har_req_back.body_size);
989
990 let har_res = entry.response.clone().unwrap();
992 let res: crate::Response = har_res.try_into().unwrap();
993 let (res_parts, res_body) = res.into_parts();
994
995 let res_payload = vec![10u8, 20, 30, 255, 0];
996 let res_bytes = res_body.collect().await.unwrap().to_bytes().to_vec();
997 assert_eq!(res_payload, res_bytes);
998
999 let har_res_back =
1001 Response::from_http_response_parts(&res_parts, &res_payload, false).unwrap();
1002 assert_eq!(res_payload.len() as i64, har_res_back.content.size);
1003 assert_eq!(
1004 Some(Mime::from_str("application/octet-stream").unwrap()),
1005 har_res_back.content.mime_type
1006 );
1007 assert_eq!(Some("ChQe/wA="), har_res_back.content.text.as_deref());
1008 assert_eq!(res_payload.len() as i64, har_res_back.body_size);
1009 }
1010
1011 const HAR_LOG_FILE_EXAMPLE: &str = r##"{"log":{"version":"1.2","creator":{"name":"WebInspector","version":"537.1"},"pages":[{"startedDateTime":"2012-08-28T05:14:24.803Z","id":"page_1","title":"http://www.igvita.com/","pageTimings":{"onContentLoad":299,"onLoad":301}}],"entries":[{"startedDateTime":"2012-08-28T05:14:24.803Z","time":121,"request":{"method":"GET","url":"http://www.igvita.com/","httpVersion":"HTTP/1.1","headers":[{"name":"Accept-Encoding","value":"gzip,deflate,sdch"},{"name":"Accept-Language","value":"en-US,en;q=0.8"},{"name":"Connection","value":"keep-alive"},{"name":"Accept-Charset","value":"ISO-8859-1,utf-8;q=0.7,*;q=0.3"},{"name":"Host","value":"www.igvita.com"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.82 Safari/537.1"},{"name":"Accept","value":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},{"name":"Cache-Control","value":"max-age=0"}],"queryString":[],"cookies":[],"headersSize":678,"bodySize":0},"response":{"status":200,"statusText":"OK","httpVersion":"HTTP/1.1","headers":[{"name":"Date","value":"Tue, 28 Aug 2012 05:14:24 GMT"},{"name":"Via","value":"HTTP/1.1 GWA"},{"name":"Transfer-Encoding","value":"chunked"},{"name":"Content-Encoding","value":"gzip"},{"name":"X-XSS-Protection","value":"1; mode=block"},{"name":"X-UA-Compatible","value":"IE=Edge,chrome=1"},{"name":"X-Page-Speed","value":"50_1_cn"},{"name":"Server","value":"nginx/1.0.11"},{"name":"Vary","value":"Accept-Encoding"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Cache-Control","value":"max-age=0, no-cache"},{"name":"Expires","value":"Tue, 28 Aug 2012 05:14:24 GMT"}],"cookies":[],"content":{"size":9521,"mimeType":"text/html","compression":5896},"redirectURL":"","headersSize":379,"bodySize":3625},"cache":{},"timings":{"blocked":0,"dns":-1,"connect":-1,"send":1,"wait":112,"receive":6,"ssl":-1},"pageref":"page_1"},{"startedDateTime":"2012-08-28T05:14:25.011Z","time":10,"request":{"method":"GET","url":"http://fonts.googleapis.com/css?family=Open+Sans:400,600","httpVersion":"HTTP/1.1","headers":[],"queryString":[{"name":"family","value":"Open+Sans:400,600"}],"cookies":[],"headersSize":71,"bodySize":0},"response":{"status":200,"statusText":"OK","httpVersion":"HTTP/1.1","headers":[],"cookies":[],"content":{"size":542,"mimeType":"text/css"},"redirectURL":"","headersSize":17,"bodySize":0},"cache":{},"timings":{"blocked":0,"dns":-1,"connect":-1,"send":-1,"wait":-1,"receive":2,"ssl":-1},"pageref":"page_1"},{"startedDateTime":"2012-08-28T05:14:25.017Z","time":31,"request":{"method":"GET","url":"http://1-ps.googleusercontent.com/h/www.igvita.com/css/style.css.pagespeed.ce.LzjUDNB25e.css","httpVersion":"HTTP/1.1","headers":[{"name":"Accept-Encoding","value":"gzip,deflate,sdch"},{"name":"Accept-Language","value":"en-US,en;q=0.8"},{"name":"Connection","value":"keep-alive"},{"name":"If-Modified-Since","value":"Mon, 27 Aug 2012 15:28:34 GMT"},{"name":"Accept-Charset","value":"ISO-8859-1,utf-8;q=0.7,*;q=0.3"},{"name":"Host","value":"1-ps.googleusercontent.com"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.82 Safari/537.1"},{"name":"Accept","value":"text/css,*/*;q=0.1"},{"name":"Cache-Control","value":"max-age=0"},{"name":"If-None-Match","value":"W/0"},{"name":"Referer","value":"http://www.igvita.com/"}],"queryString":[],"cookies":[],"headersSize":539,"bodySize":0},"response":{"status":304,"statusText":"Not Modified","httpVersion":"HTTP/1.1","headers":[{"name":"Date","value":"Mon, 27 Aug 2012 06:01:49 GMT"},{"name":"Age","value":"83556"},{"name":"Server","value":"GFE/2.0"},{"name":"ETag","value":"W/0"},{"name":"Expires","value":"Tue, 27 Aug 2013 06:01:49 GMT"}],"cookies":[],"content":{"size":14679,"mimeType":"text/css"},"redirectURL":"","headersSize":146,"bodySize":0},"cache":{},"timings":{"blocked":0,"dns":-1,"connect":-1,"send":1,"wait":24,"receive":2,"ssl":-1},"pageref":"page_1"},{"startedDateTime":"2012-08-28T05:14:25.021Z","time":30,"request":{"method":"GET","url":"http://1-ps.googleusercontent.com/h/www.igvita.com/js/libs/modernizr.84728.js.pagespeed.jm._DgXLhVY42.js","httpVersion":"HTTP/1.1","headers":[{"name":"Accept-Encoding","value":"gzip,deflate,sdch"},{"name":"Accept-Language","value":"en-US,en;q=0.8"},{"name":"Connection","value":"keep-alive"},{"name":"If-Modified-Since","value":"Sat, 25 Aug 2012 14:30:37 GMT"},{"name":"Accept-Charset","value":"ISO-8859-1,utf-8;q=0.7,*;q=0.3"},{"name":"Host","value":"1-ps.googleusercontent.com"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.82 Safari/537.1"},{"name":"Accept","value":"*/*"},{"name":"Cache-Control","value":"max-age=0"},{"name":"If-None-Match","value":"W/0"},{"name":"Referer","value":"http://www.igvita.com/"}],"queryString":[],"cookies":[],"headersSize":536,"bodySize":0},"response":{"status":304,"statusText":"Not Modified","httpVersion":"HTTP/1.1","headers":[{"name":"Date","value":"Sat, 25 Aug 2012 14:30:37 GMT"},{"name":"Age","value":"225828"},{"name":"Server","value":"GFE/2.0"},{"name":"ETag","value":"W/0"},{"name":"Expires","value":"Sun, 25 Aug 2013 14:30:37 GMT"}],"cookies":[],"content":{"size":11831,"mimeType":"text/javascript"},"redirectURL":"","headersSize":147,"bodySize":0},"cache":{},"timings":{"blocked":0,"dns":-1,"connect":0,"send":1,"wait":27,"receive":1,"ssl":-1},"pageref":"page_1"},{"startedDateTime":"2012-08-28T05:14:25.103Z","time":0,"request":{"method":"GET","url":"http://www.google-analytics.com/ga.js","httpVersion":"HTTP/1.1","headers":[],"queryString":[],"cookies":[],"headersSize":52,"bodySize":0},"response":{"status":200,"statusText":"OK","httpVersion":"HTTP/1.1","headers":[{"name":"Date","value":"Mon, 27 Aug 2012 21:57:00 GMT"},{"name":"Content-Encoding","value":"gzip"},{"name":"X-Content-Type-Options","value":"nosniff, nosniff"},{"name":"Age","value":"23052"},{"name":"Last-Modified","value":"Thu, 16 Aug 2012 07:05:05 GMT"},{"name":"Server","value":"GFE/2.0"},{"name":"Vary","value":"Accept-Encoding"},{"name":"Content-Type","value":"text/javascript"},{"name":"Expires","value":"Tue, 28 Aug 2012 09:57:00 GMT"},{"name":"Cache-Control","value":"max-age=43200, public"},{"name":"Content-Length","value":"14804"}],"cookies":[],"content":{"size":36893,"mimeType":"text/javascript"},"redirectURL":"","headersSize":17,"bodySize":0},"cache":{},"timings":{"blocked":0,"dns":-1,"connect":-1,"send":-1,"wait":-1,"receive":0,"ssl":-1},"pageref":"page_1"},{"startedDateTime":"2012-08-28T05:14:25.123Z","time":91,"request":{"method":"GET","url":"http://1-ps.googleusercontent.com/beacon?org=50_1_cn&ets=load:93&ifr=0&hft=32&url=http%3A%2F%2Fwww.igvita.com%2F","httpVersion":"HTTP/1.1","headers":[{"name":"Accept-Encoding","value":"gzip,deflate,sdch"},{"name":"Accept-Language","value":"en-US,en;q=0.8"},{"name":"Connection","value":"keep-alive"},{"name":"Accept-Charset","value":"ISO-8859-1,utf-8;q=0.7,*;q=0.3"},{"name":"Host","value":"1-ps.googleusercontent.com"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_4) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.82 Safari/537.1"},{"name":"Accept","value":"*/*"},{"name":"Referer","value":"http://www.igvita.com/"}],"queryString":[{"name":"org","value":"50_1_cn"},{"name":"ets","value":"load:93"},{"name":"ifr","value":"0"},{"name":"hft","value":"32"},{"name":"url","value":"http%3A%2F%2Fwww.igvita.com%2F"}],"cookies":[],"headersSize":448,"bodySize":0},"response":{"status":204,"statusText":"No Content","httpVersion":"HTTP/1.1","headers":[{"name":"Date","value":"Tue, 28 Aug 2012 05:14:25 GMT"},{"name":"Content-Length","value":"0"},{"name":"X-XSS-Protection","value":"1; mode=block"},{"name":"Server","value":"PagespeedRewriteProxy 0.1"},{"name":"Content-Type","value":"text/plain"},{"name":"Cache-Control","value":"no-cache"}],"cookies":[],"content":{"size":0,"mimeType":"text/plain","compression":0},"redirectURL":"","headersSize":202,"bodySize":0},"cache":{},"timings":{"blocked":0,"dns":-1,"connect":-1,"send":0,"wait":70,"receive":7,"ssl":-1},"pageref":"page_1"}]}}"##;
1012
1013 const HAR_LOG_FILE_PAYLOAD_EXAMPLE: &str = r##"{"log":{"version":"1.2","creator":{"name":"rama-test","version":"0.0"},"entries":[{"startedDateTime":"2012-08-28T05:14:24.803Z","time":1,"request":{"method":"POST","url":"http://example.test/upload","httpVersion":"HTTP/1.1","headers":[{"name":"Host","value":"example.test"},{"name":"Content-Type","value":"application/octet-stream"}],"queryString":[],"cookies":[],"postData":{"mimeType":"application/octet-stream","text":"AP8BAgM="},"headersSize":-1,"bodySize":5},"response":{"status":200,"statusText":"OK","httpVersion":"HTTP/1.1","headers":[{"name":"Content-Type","value":"application/octet-stream"}],"cookies":[],"content":{"size":5,"mimeType":"application/octet-stream","text":"ChQe/wA="},"redirectURL":"","headersSize":-1,"bodySize":5},"cache":{},"timings":{"send":0,"wait":1,"receive":0}}]}}"##;
1014}