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::{ContentType, Cookie as RamaCookie, HeaderMapExt, Location};
20use rama_http_headers::{HeaderEncode, SetCookie};
21use rama_http_types::mime::Mime;
22use rama_http_types::proto::h1::ext::ReasonPhrase;
23use rama_http_types::{HeaderMap, HeaderName, Version as RamaHttpVersion};
24
25use base64::Engine;
26use base64::engine::general_purpose::STANDARD as ENGINE;
27use jiff::Timestamp;
28use rama_utils::str::arcstr::ArcStr;
29use rama_utils::str::smol_str::{ToSmolStr, format_smolstr};
30use rama_utils::str::{NonEmptyStr, non_empty_str};
31use serde::{Deserialize, Serialize};
32
33mod websocket;
34pub use websocket::{WebSocketMessage, WebSocketMessageOpcode, WebSocketMessageType};
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
122pub(super) fn 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 #[serde(skip_serializing_if = "Option::is_none")]
175 pub pages: Option<Vec<Page>>,
176 pub entries: Vec<Entry>,
178 pub comment: Option<ArcStr>,
180}
181
182impl Default for Log {
183 fn default() -> Self {
184 Self {
185 version: non_empty_str!("1.2"),
186 creator: Creator {
187 name: rama_utils::info::NAME.into(),
188 version: rama_utils::info::VERSION.into(),
189 comment: None,
190 },
191 browser: None,
192 pages: None,
193 entries: vec![],
194 comment: None,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Creator {
202 pub name: ArcStr,
203 pub version: ArcStr,
204 pub comment: Option<ArcStr>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct Browser {
209 pub name: ArcStr,
211 pub version: Option<ArcStr>,
213 pub comment: Option<ArcStr>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Page {
219 #[serde(rename = "startedDateTime")]
221 pub started_date_time: Timestamp,
222 pub id: ArcStr,
224 pub title: ArcStr,
226 #[serde(rename = "pageTimings")]
228 pub page_timings: PageTimings,
229 pub comment: Option<ArcStr>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct PageTimings {
239 #[serde(rename = "onContentLoad")]
244 pub on_content_load: Option<i64>,
245 #[serde(rename = "onLoad")]
250 pub on_load: Option<i64>,
251 pub comment: Option<ArcStr>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Entry {
258 #[serde(rename = "pageref")]
262 pub page_ref: Option<ArcStr>,
263 #[serde(rename = "startedDateTime")]
265 pub started_date_time: Timestamp,
266 pub time: i64,
270 pub request: Request,
272 pub response: Response,
274 pub cache: Cache,
276 pub timings: Timings,
278 #[serde(rename = "serverIPAddress")]
282 pub server_ip_address: Option<IpAddr>, pub connection: Option<ArcStr>,
293 pub comment: Option<ArcStr>,
295 #[serde(rename = "_resourceType", skip_serializing_if = "Option::is_none")]
301 pub resource_type: Option<ArcStr>,
302 #[serde(rename = "_webSocketMessages", skip_serializing_if = "Option::is_none")]
312 pub web_socket_messages: Option<Vec<WebSocketMessage>>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct Request {
318 pub method: ArcStr,
320 pub url: ArcStr,
322 #[serde(rename = "httpVersion")]
324 pub http_version: HttpVersion,
325 pub cookies: Vec<Cookie>,
327 pub headers: Vec<Header>,
329 #[serde(rename = "queryString")]
331 pub query_string: Vec<QueryStringPair>,
332 #[serde(rename = "postData")]
334 pub post_data: Option<PostData>,
335 #[serde(rename = "headersSize")]
341 pub headers_size: i64,
342 #[serde(rename = "bodySize")]
346 pub body_size: i64,
347 pub comment: Option<ArcStr>,
349}
350
351impl TryFrom<Request> for crate::Request {
352 type Error = BoxError;
353
354 fn try_from(har_request: Request) -> Result<Self, Self::Error> {
355 let body = if let Some(text) = har_request.post_data.and_then(|pd| pd.text) {
356 if let Ok(bin) = ENGINE.decode(&text) {
357 crate::Body::from(bin)
358 } else {
359 crate::Body::from(text)
360 }
361 } else {
362 crate::Body::empty()
363 };
364
365 let mut headers = HeaderMap::with_capacity(har_request.headers.len());
366 for header in har_request.headers {
367 headers.append(
368 HeaderName::from_str(&header.name).context("convert http header name")?,
369 crate::HeaderValue::from_maybe_shared(header.value)
370 .context("convert http header value")?,
371 );
372 }
373
374 let builder = crate::Request::builder()
375 .method(
376 har_request
377 .method
378 .parse::<crate::Method>()
379 .context("parse HAR HTTP Method")?,
380 )
381 .uri(har_request.url.as_str());
382
383 let builder = if let Ok(ver) = har_request.http_version.try_into() {
384 builder.version(ver)
385 } else {
386 builder
387 };
388
389 let mut req = builder
390 .body(body)
391 .context("build http request from HAR data")?;
392
393 *req.headers_mut() = headers;
394
395 if let Some(comment) = har_request.comment {
396 req.extensions().insert(RequestComment(comment));
397 }
398
399 Ok(req)
400 }
401}
402
403impl Request {
404 pub fn from_http_request_parts(
405 parts: &ReqParts,
406 payload: &[u8],
407 preserve_sensitive: bool,
408 ) -> Result<Self, BoxError> {
409 let post_data = if !payload.is_empty() {
410 let mime_type = get_mime(&parts.headers);
411 let params = if mime_type
412 .as_ref()
413 .map(|m| m.subtype() == crate::mime::WWW_FORM_URLENCODED)
414 .unwrap_or_default()
415 {
416 Some(serde_html_form::from_bytes(payload).context("decode form body payload")?)
417 } else {
418 None
419 };
420
421 let text = match std::str::from_utf8(payload) {
422 Ok(s) => s.into(),
423 Err(_) => ENGINE.encode(payload).into(),
424 };
425
426 Some(PostData {
427 mime_type,
428 params,
429 text: Some(text),
430 comment: None,
431 })
432 } else {
433 None
434 };
435
436 let comment = parts
437 .extensions
438 .get_ref::<RequestComment>()
439 .map(|req_comment| req_comment.0.clone());
440
441 let cookies = parts
442 .headers
443 .typed_get::<RamaCookie>()
444 .map(|c| {
445 c.iter()
446 .map(|(k, v)| Cookie {
447 name: k.into(),
448 value: v.into(),
449 path: None,
450 domain: None,
451 expires: None,
452 http_only: None,
453 secure: None,
454 comment: None,
455 })
456 .collect()
457 })
458 .unwrap_or_default();
459
460 let query_string = into_query_string(parts);
461 let mut headers = parts.headers.clone();
462 if !preserve_sensitive {
463 remove_sensitive_request_headers(&mut headers);
464 }
465
466 let headers_size_ext = parts.extensions.get_ref::<HeaderByteLength>();
467 let headers_size = headers_size_ext.map(|v| v.0 as i64).unwrap_or(-1);
468
469 Ok(Self {
470 method: parts.method.to_smolstr().into(),
471 url: parts.uri.to_string().into(),
472 http_version: parts.version.into(),
473 cookies,
474 headers: into_har_headers(headers),
475 query_string,
476 post_data,
477 headers_size,
478 body_size: payload.len() as i64,
479 comment,
480 })
481 }
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct Response {
487 pub status: u16,
489 #[serde(rename = "statusText")]
491 pub status_text: Option<ArcStr>,
492 #[serde(rename = "httpVersion")]
494 pub http_version: HttpVersion,
495 pub cookies: Vec<Cookie>,
497 pub headers: Vec<Header>,
499 pub content: Content,
501 #[serde(rename = "redirectURL")]
503 pub redirect_url: Option<ArcStr>,
504 #[serde(rename = "headersSize")]
510 pub headers_size: i64,
511 #[serde(rename = "bodySize")]
520 pub body_size: i64,
521 pub comment: Option<ArcStr>,
523}
524
525impl TryFrom<Response> for crate::Response {
526 type Error = BoxError;
527
528 fn try_from(har_response: Response) -> Result<Self, Self::Error> {
529 let body = match har_response.content.text {
530 Some(s) if har_response.content.encoding.as_deref() == Some("base64") => {
531 crate::Body::from(
532 ENGINE
533 .decode(&s)
534 .context("decode base64 HAR response body")?,
535 )
536 }
537 Some(s) => crate::Body::from(s),
538 None => crate::Body::empty(),
539 };
540
541 let mut headers = HeaderMap::with_capacity(har_response.headers.len());
542 for header in har_response.headers {
543 headers.append(
544 HeaderName::from_str(&header.name).context("convert http header name")?,
545 crate::HeaderValue::from_maybe_shared(header.value)
546 .context("convert http header value")?,
547 );
548 }
549
550 let builder = crate::Response::builder().status(
551 crate::StatusCode::from_u16(har_response.status).context("convert HAR status code")?,
552 );
553
554 let builder = if let Ok(ver) = har_response.http_version.try_into() {
555 builder.version(ver)
556 } else {
557 builder
558 };
559
560 let mut res = builder
561 .body(body)
562 .context("build http response from HAR data")?;
563
564 *res.headers_mut() = headers;
565
566 Ok(res)
567 }
568}
569
570impl Response {
571 pub fn from_http_response_parts(
572 parts: &RespParts,
573 payload: &[u8],
574 preserve_sensitive: bool,
575 ) -> Result<Self, BoxError> {
576 let (text, encoding) = if payload.is_empty() {
577 (None, None)
578 } else {
579 match std::str::from_utf8(payload) {
580 Ok(s) => (Some(s.into()), None),
581 Err(_) => (Some(ENGINE.encode(payload).into()), Some("base64".into())),
582 }
583 };
584 let content = Content {
585 size: payload.len() as i64,
586 compression: None,
587 mime_type: get_mime(&parts.headers),
588 text,
589 encoding,
590 comment: None,
591 };
592
593 let redirect_url = parts
594 .headers
595 .typed_get::<Location>()
596 .and_then(|h| h.encode_to_value())
597 .and_then(|v| v.to_str().ok().map(Into::into));
598
599 let cookies = parts
600 .headers
601 .typed_get::<SetCookie>()
602 .map(|sc| {
603 sc.iter_header_values()
604 .filter_map(|v| {
605 v.to_str().ok().and_then(|s| {
606 let raw = s.split(';').next()?;
607 parse_cookie_part(raw)
608 })
609 })
610 .collect()
611 })
612 .unwrap_or_default();
613
614 let mut headers = parts.headers.clone();
615 if !preserve_sensitive {
616 remove_sensitive_response_headers(&mut headers);
617 }
618
619 let headers_size_ext = parts.extensions.get_ref::<HeaderByteLength>();
620 let headers_size = headers_size_ext.map(|v| v.0 as i64).unwrap_or(-1);
621
622 Ok(Self {
623 status: parts.status.as_u16(),
624 status_text: match parts.extensions.get_ref::<ReasonPhrase>() {
625 Some(reason) => Some(
626 String::from_utf8_lossy(reason.as_bytes())
627 .into_owned()
628 .into(),
629 ),
630 None => parts.status.canonical_reason().map(Into::into),
631 },
632 http_version: parts.version.into(),
633 cookies,
634 headers: into_har_headers(headers),
635 content,
636 redirect_url,
637 headers_size,
638 body_size: payload.len() as i64,
639 comment: None,
640 })
641 }
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize, Default)]
648pub struct Cookie {
652 pub name: ArcStr,
654 pub value: ArcStr,
656 pub path: Option<ArcStr>,
658 pub domain: Option<ArcStr>,
660 pub expires: Option<Timestamp>,
664 #[serde(rename = "httpOnly")]
666 pub http_only: Option<bool>,
667 pub secure: Option<bool>,
669 pub comment: Option<ArcStr>,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct Header {
676 pub name: ArcStr,
678 pub value: ArcStr,
680 pub comment: Option<ArcStr>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize)]
685pub struct QueryStringPair {
688 pub name: ArcStr,
690 pub value: ArcStr,
692 pub comment: Option<ArcStr>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct PostData {
701 #[serde(with = "mime_serde", rename = "mimeType")]
702 pub mime_type: Option<Mime>,
704 pub params: Option<Vec<PostParam>>,
708 pub text: Option<ArcStr>,
710 pub comment: Option<ArcStr>,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
715pub struct PostParam {
716 pub name: ArcStr,
717 pub value: Option<ArcStr>,
718 #[serde(rename = "fileName")]
719 pub file_name: Option<ArcStr>,
720 #[serde(rename = "contentType")]
721 pub content_type: Option<ArcStr>,
722 pub comment: Option<ArcStr>,
723}
724
725#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct Content {
737 pub size: i64, pub compression: Option<i64>, #[serde(with = "mime_serde", rename = "mimeType")]
747 pub mime_type: Option<Mime>,
754 pub text: Option<ArcStr>,
755 pub encoding: Option<ArcStr>,
763 pub comment: Option<ArcStr>,
765}
766
767#[derive(Debug, Clone, Serialize, Deserialize, Default)]
768pub struct Cache {
770 #[serde(rename = "beforeRequest")]
774 pub before_request: Option<CacheState>,
775 #[serde(rename = "afterRequest")]
779 pub after_request: Option<CacheState>,
780 pub comment: Option<ArcStr>,
782} #[derive(Debug, Clone, Serialize, Deserialize)]
785pub struct CacheState {
786 pub expires: Timestamp,
791 #[serde(rename = "lastAccess")]
793 pub last_access: Option<ArcStr>,
794 #[serde(rename = "eTag")]
796 pub e_tag: Option<ArcStr>,
797 #[serde(rename = "hitCount")]
799 pub hit_count: Option<i64>,
800 pub comment: Option<ArcStr>,
802}
803
804#[derive(Debug, Clone, Serialize, Deserialize, Default)]
805pub struct Timings {
809 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>,
838}
839
840#[cfg(test)]
841mod tests {
842 use rama_http_types::body::util::BodyExt as _;
843
844 use super::*;
845
846 #[test]
847 fn into_har_headers_preserves_original_header_name_casing() {
848 let mut headers = HeaderMap::new();
849 headers.insert(
850 HeaderName::from_static("Content-Length"),
851 "42".parse().unwrap(),
852 );
853 headers.append(HeaderName::from_static("X-CuStOm"), "rama".parse().unwrap());
854
855 let har_headers = into_har_headers(headers);
856 assert_eq!("Content-Length", har_headers[0].name);
857 assert_eq!("X-CuStOm", har_headers[1].name);
858 }
859
860 #[test]
861 #[tracing_test::traced_test]
862 fn test_load_har_entries() {
863 let log_file: LogFile = serde_json::from_str(HAR_LOG_FILE_EXAMPLE).unwrap();
864
865 assert_eq!(6, log_file.log.entries.len());
866
867 let entry0 = &log_file.log.entries[0];
868 assert_eq!("http://www.igvita.com/", entry0.request.url);
869 assert_eq!("GET", entry0.request.method);
870 assert!(matches!(entry0.request.http_version, HttpVersion::Http11));
871 assert_eq!(0, entry0.request.query_string.len());
872
873 let entry1 = &log_file.log.entries[1];
874 assert_eq!(
875 "http://fonts.googleapis.com/css?family=Open+Sans:400,600",
876 entry1.request.url
877 );
878 assert_eq!(1, entry1.request.query_string.len());
879 assert_eq!("family", entry1.request.query_string[0].name);
880 assert_eq!("Open+Sans:400,600", entry1.request.query_string[0].value);
881
882 let entry5 = &log_file.log.entries[5];
883 assert_eq!(
884 "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",
885 entry5.request.url
886 );
887 assert_eq!(5, entry5.request.query_string.len());
888
889 let req0: crate::Request = entry0.request.clone().try_into().unwrap();
891 let (req0_parts, req0_body) = req0.into_parts();
892 drop(req0_body);
893
894 assert_eq!(crate::Method::GET, req0_parts.method);
895 assert_eq!(entry0.request.url, req0_parts.uri.to_string());
896 assert_eq!(RamaHttpVersion::HTTP_11, req0_parts.version);
897
898 let host = req0_parts
899 .headers
900 .get("Host")
901 .and_then(|v| v.to_str().ok())
902 .unwrap();
903 assert_eq!("www.igvita.com", host);
904
905 let req0_back = Request::from_http_request_parts(&req0_parts, &[], false).unwrap();
907 assert_eq!(entry0.request.method, req0_back.method);
908 assert_eq!(entry0.request.url, req0_back.url);
909 assert!(matches!(req0_back.http_version, HttpVersion::Http11));
910 assert_eq!(0, req0_back.body_size);
911
912 let ua = req0_back
913 .headers
914 .iter()
915 .find(|h| h.name.eq_ignore_ascii_case("User-Agent"))
916 .map(|h| h.value.as_str())
917 .unwrap();
918 assert!(ua.contains("Chrome/21.0.1180.82"));
919
920 let req5: crate::Request = entry5.request.clone().try_into().unwrap();
922 let (req5_parts, req5_body) = req5.into_parts();
923 drop(req5_body);
924
925 let req5_back = Request::from_http_request_parts(&req5_parts, &[], false).unwrap();
926 assert_eq!(5, req5_back.query_string.len(), "req: {req5_back:?}");
927 assert!(
928 req5_back
929 .query_string
930 .iter()
931 .any(|p| p.name == "org" && p.value == "50_1_cn"),
932 "query string: {:?}",
933 req5_back.query_string
934 );
935 assert!(
936 req5_back
937 .query_string
938 .iter()
939 .any(|p| p.name == "ets" && p.value == "load:93"),
940 "query string: {:?}",
941 req5_back.query_string
942 );
943 assert!(
944 req5_back
945 .query_string
946 .iter()
947 .any(|p| p.name == "url" && p.value == "http://www.igvita.com/"),
948 "query string: {:?}",
949 req5_back.query_string
950 );
951
952 let har_res0 = entry0.response.clone();
954 let res0: crate::Response = har_res0.try_into().unwrap();
955 let (res0_parts, res0_body) = res0.into_parts();
956 drop(res0_body);
957
958 assert_eq!(crate::StatusCode::OK, res0_parts.status);
959 assert_eq!(RamaHttpVersion::HTTP_11, res0_parts.version);
960
961 let ct = res0_parts
962 .headers
963 .get("Content-Type")
964 .and_then(|v| v.to_str().ok())
965 .unwrap();
966 assert!(ct.starts_with("text/html"));
967
968 let ce = res0_parts
969 .headers
970 .get("Content-Encoding")
971 .and_then(|v| v.to_str().ok())
972 .unwrap();
973 assert_eq!("gzip", ce);
974
975 let res0_back = Response::from_http_response_parts(&res0_parts, &[], false).unwrap();
977 assert_eq!(200, res0_back.status);
978 assert_eq!(Some("OK"), res0_back.status_text.as_deref());
979 assert!(matches!(res0_back.http_version, HttpVersion::Http11));
980
981 let mime = res0_back.content.mime_type.unwrap();
982 assert_eq!("text/html; charset=utf-8", mime.as_ref());
983
984 assert!(res0_back.content.encoding.is_none());
985 }
986
987 #[tokio::test]
988 #[tracing_test::traced_test]
989 async fn test_load_har_entries_payload_roundtrip() {
990 let log_file: LogFile = serde_json::from_str(HAR_LOG_FILE_PAYLOAD_EXAMPLE).unwrap();
991 assert_eq!(1, log_file.log.entries.len());
992
993 let entry = &log_file.log.entries[0];
994
995 let req: crate::Request = entry.request.clone().try_into().unwrap();
997 let (req_parts, req_body) = req.into_parts();
998
999 let req_payload = vec![0u8, 255, 1, 2, 3];
1000 let req_bytes = req_body.collect().await.unwrap().to_bytes().to_vec();
1001 assert_eq!(req_payload, req_bytes);
1002
1003 let har_req_back =
1005 Request::from_http_request_parts(&req_parts, &req_payload, false).unwrap();
1006 let post_data = har_req_back.post_data.unwrap();
1007 assert_eq!(
1008 Some(Mime::from_str("application/octet-stream").unwrap()),
1009 post_data.mime_type
1010 );
1011 assert_eq!(Some("AP8BAgM="), post_data.text.as_deref());
1012 assert_eq!(req_payload.len() as i64, har_req_back.body_size);
1013
1014 let har_res = entry.response.clone();
1016 let res: crate::Response = har_res.try_into().unwrap();
1017 let (res_parts, res_body) = res.into_parts();
1018
1019 let res_payload = vec![10u8, 20, 30, 255, 0];
1020 let res_bytes = res_body.collect().await.unwrap().to_bytes().to_vec();
1021 assert_eq!(res_payload, res_bytes);
1022
1023 let har_res_back =
1025 Response::from_http_response_parts(&res_parts, &res_payload, false).unwrap();
1026 assert_eq!(res_payload.len() as i64, har_res_back.content.size);
1027 assert_eq!(
1028 Some(Mime::from_str("application/octet-stream").unwrap()),
1029 har_res_back.content.mime_type
1030 );
1031 assert_eq!(Some("ChQe/wA="), har_res_back.content.text.as_deref());
1032 assert_eq!(Some("base64"), har_res_back.content.encoding.as_deref());
1033 assert_eq!(res_payload.len() as i64, har_res_back.body_size);
1034 }
1035
1036 #[test]
1037 fn unsupported_pages_are_omitted_instead_of_serialized_as_null() {
1038 let value = serde_json::to_value(Log::default()).unwrap();
1039 assert!(value.get("pages").is_none());
1040 }
1041
1042 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"}]}}"##;
1043
1044 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=","encoding":"base64"},"redirectURL":"","headersSize":-1,"bodySize":5},"cache":{},"timings":{"send":0,"wait":1,"receive":0}}]}}"##;
1045}