Skip to main content

rama_http/layer/har/
spec.rs

1// NOTE: spec can be found in ./spec.md
2
3use 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)]
156/// This object represents the exported data structure.
157pub struct LogFile {
158    /// The HAR log data.
159    pub log: Log,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163/// This object represents the root of exported data.
164pub struct Log {
165    /// Version number of the format. If empty, string "1.1" is assumed by default.
166    pub version: NonEmptyStr,
167    /// Name and version info of the log creator application.
168    pub creator: Creator,
169    /// Name and version info of used browser.
170    pub browser: Option<Browser>,
171    /// List of all exported (tracked) pages.
172    ///
173    /// Leave out this field if the application does not support grouping by pages.
174    pub pages: Option<Vec<Page>>,
175    /// List of all exported (tracked) requests.
176    pub entries: Vec<Entry>,
177    /// A comment provided by the user or the application.
178    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)]
199/// Creator and browser objects share the same structure.
200pub 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    /// Name of the application/browser used to export the log.
209    pub name: ArcStr,
210    /// Version of the application/browser used to export the log.
211    pub version: Option<ArcStr>,
212    /// A comment provided by the user or the application.
213    pub comment: Option<ArcStr>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Page {
218    /// Date and time stamp of the request start (ISO 8601 - YYYY-MM-DDThh:mm:ss.sTZD)
219    #[serde(rename = "startedDateTime")]
220    pub started_date_time: Timestamp,
221    /// Unique identifier of a page within the [Log]. Entries use it to refer the parent page.
222    pub id: ArcStr,
223    /// Page title
224    pub title: ArcStr,
225    /// Detailed timing info about page load.
226    #[serde(rename = "pageTimings")]
227    pub page_timings: PageTimings,
228    /// A comment provided by the user or the application.
229    pub comment: Option<ArcStr>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233/// This object describes timings for various events (states) fired during the page load.
234///
235/// All times are specified in milliseconds.
236/// If a time info is not available appropriate field is set to -1.
237pub struct PageTimings {
238    /// Content of the page loaded.
239    ///
240    /// Number of milliseconds since page load started (page.startedDateTime).
241    /// Use -1 if the timing does not apply to the current request.
242    #[serde(rename = "onContentLoad")]
243    pub on_content_load: Option<i64>,
244    /// Page is loaded (onLoad event fired).
245    ///
246    /// Number of milliseconds since page load started (page.startedDateTime).
247    /// Use -1 if the timing does not apply to the current request.
248    #[serde(rename = "onLoad")]
249    pub on_load: Option<i64>,
250    /// A comment provided by the user or the application.
251    pub comment: Option<ArcStr>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255/// This object represents a single exportred request with its response and metadata.
256pub struct Entry {
257    /// Reference to the parent page.
258    ///
259    /// Leave out this field if the application does not support grouping by pages.
260    #[serde(rename = "pageref")]
261    pub page_ref: Option<ArcStr>,
262    /// Date and time stamp of the request start (ISO 8601 - YYYY-MM-DDThh:mm:ss.sTZD)
263    #[serde(rename = "startedDateTime")]
264    pub started_date_time: Timestamp,
265    /// Total elapsed time of the request in milliseconds.
266    ///
267    /// This is the sum of all timings available in the timings object (i.e. not including -1 values).
268    pub time: i64,
269    /// Detailed info about the request.
270    pub request: Request,
271    /// Detailed info about the response.
272    pub response: Option<Response>,
273    /// Info about cache usage.
274    pub cache: Cache,
275    /// Detailed timing info about request/response round trip.
276    pub timings: Timings,
277    /// IP address of the server that was connected
278    ///
279    /// (result of DNS resolution).
280    #[serde(rename = "serverIPAddress")]
281    pub server_ip_address: Option<IpAddr>, // TODO: be able to provide for client middleware
282    /// Unique ID of the parent TCP/IP connection,
283    /// can be the client or server port number.
284    ///
285    /// Note that a port number doesn't have to be unique identifier
286    /// in cases where the port is shared for more connections.
287    /// If the port isn't available for the application,
288    /// any other unique connection ID can be used instead (e.g. connection index).
289    ///
290    /// Leave out this field if the application doesn't support this info.
291    pub connection: Option<ArcStr>,
292    /// A comment provided by the user or the application.
293    pub comment: Option<ArcStr>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
297/// This object contains detailed info about performed request.
298pub struct Request {
299    /// Request method (GET, POST, ...).
300    pub method: ArcStr,
301    /// Absolute URL of the request (fragments are not included).
302    pub url: ArcStr,
303    /// Request HTTP Version.
304    #[serde(rename = "httpVersion")]
305    pub http_version: HttpVersion,
306    /// List of cookie objects.
307    pub cookies: Vec<Cookie>,
308    /// List of header objects.
309    pub headers: Vec<Header>,
310    /// List of query parameter objects.
311    #[serde(rename = "queryString")]
312    pub query_string: Vec<QueryStringPair>,
313    /// Posted data info.
314    #[serde(rename = "postData")]
315    pub post_data: Option<PostData>,
316    /// Total number of bytes from the start of the HTTP request message
317    ///
318    /// Until (and including) the double CRLF before the body.
319    ///
320    /// Set to -1 if the info is not available.
321    #[serde(rename = "headersSize")]
322    pub headers_size: i64,
323    /// Size of the request body (POST data payload) in bytes.
324    ///
325    /// Set to -1 if the info is not available.
326    #[serde(rename = "bodySize")]
327    pub body_size: i64,
328    /// A comment provided by the user or the application.
329    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)]
466/// This object contains detailed info about the response.
467pub struct Response {
468    /// Response status.
469    pub status: u16,
470    /// Response status description.
471    #[serde(rename = "statusText")]
472    pub status_text: Option<ArcStr>,
473    /// Response HTTP Version.
474    #[serde(rename = "httpVersion")]
475    pub http_version: HttpVersion,
476    /// List of cookie objects.
477    pub cookies: Vec<Cookie>,
478    /// List of header objects.
479    pub headers: Vec<Header>,
480    /// Details about the response body.
481    pub content: Content,
482    /// Redirection target URL from the Location response header.
483    #[serde(rename = "redirectURL")]
484    pub redirect_url: Option<ArcStr>,
485    /// Total number of bytes from the start of the HTTP response message
486    ///
487    /// Until (and including) the double CRLF before the body.
488    ///
489    /// Set to -1 if the info is not available.
490    #[serde(rename = "headersSize")]
491    pub headers_size: i64,
492    /// Size of the received response body in bytes.
493    ///
494    /// Set to zero in case of responses coming from the cache (304). Set to -1 if the info is not available.
495    ///
496    /// The size of received response-headers is computed only from headers
497    /// that are really received from the server. Additional headers appended
498    /// by the browser are not included in this number,
499    /// but they appear in the list of header objects.
500    #[serde(rename = "bodySize")]
501    pub body_size: i64,
502    /// A comment provided by the user or the application.
503    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// TODO: https://github.com/plabayo/rama/issues/44
620// For now this will have to be manually parsed. Needs an http-cookie logic
621
622#[derive(Debug, Clone, Serialize, Deserialize, Default)]
623/// This object contains list of all cookies
624///
625/// (used in [Request] and [Response] objects).
626pub struct Cookie {
627    /// The name of the cookie.
628    pub name: ArcStr,
629    /// The cookie value.
630    pub value: ArcStr,
631    /// The path pertaining to the cookie.
632    pub path: Option<ArcStr>,
633    /// The host of the cookie.
634    pub domain: Option<ArcStr>,
635    /// Date and time stamp of the request start
636    ///
637    /// (ISO 8601 - YYYY-MM-DDThh:mm:ss.sTZD)
638    pub expires: Option<Timestamp>,
639    /// Set to true if the cookie is HTTP only, false otherwise.
640    #[serde(rename = "httpOnly")]
641    pub http_only: Option<bool>,
642    /// True if the cookie was transmitted over ssl, false otherwise.
643    pub secure: Option<bool>,
644    /// A comment provided by the user or the application.
645    pub comment: Option<ArcStr>,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
649/// Single HTTP Header.
650pub struct Header {
651    /// Name of header.
652    pub name: ArcStr,
653    /// Value of header.
654    pub value: ArcStr,
655    /// A comment provided by the user or the application.
656    pub comment: Option<ArcStr>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
660/// This object contains list of all parameters & values parsed from a query string,
661/// if any (embedded in [Request] object).
662pub struct QueryStringPair {
663    /// Name of parameter.
664    pub name: ArcStr,
665    /// Value of parameter.
666    pub value: ArcStr,
667    /// A comment provided by the user or the application.
668    pub comment: Option<ArcStr>,
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
672/// This object describes posted data,
673///
674/// if any (embedded in [Request] object).
675pub struct PostData {
676    #[serde(with = "mime_serde", rename = "mimeType")]
677    /// Mime type of posted data.
678    pub mime_type: Option<Mime>,
679    /// List of posted parameters
680    ///
681    /// (in case of URL encoded parameters).
682    pub params: Option<Vec<PostParam>>,
683    /// Plain text posted data
684    pub text: Option<ArcStr>,
685    /// A comment provided by the user or the application.
686    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)]
701/// This object describes details about response content
702///
703/// (embedded in `<response>` object).
704///
705/// Before setting the text field,
706/// the HTTP response is decoded (decompressed & unchunked),
707/// than trans-coded from its original character set into UTF-8. Additionally,
708/// it can be encoded using e.g. base64. Ideally,
709/// the application should be able to unencode a
710/// base64 blob and get a byte-for-byte identical resource to what the browser operated on.
711pub struct Content {
712    /// Length of the returned content in bytes.
713    ///
714    /// Should be equal to response.bodySize if there is no compression
715    /// and bigger when the content has been compressed.
716    pub size: i64, // TODO: support
717    /// Number of bytes saved.
718    ///
719    /// Leave out this field if the information is not available.
720    pub compression: Option<i64>, // TODO: support
721    #[serde(with = "mime_serde", rename = "mimeType")]
722    /// MIME type of the response text
723    ///
724    /// (value of the Content-Type response header).
725    ///
726    /// The charset attribute of the MIME type is included
727    /// (if available).
728    pub mime_type: Option<Mime>,
729    pub text: Option<ArcStr>,
730    /// Response body sent from the server or loaded from the browser cache.
731    ///
732    /// This field is populated with textual content only.
733    /// The text field is either HTTP decoded text or a encoded
734    /// (e.g. "base64") representation of the response body.
735    ///
736    /// Leave out this field if the information is not available.
737    pub encoding: Option<ContentEncodingDirective>,
738    /// A comment provided by the user or the application.
739    pub comment: Option<ArcStr>,
740}
741
742#[derive(Debug, Clone, Serialize, Deserialize, Default)]
743/// This objects contains info about a request coming from browser cache.
744pub struct Cache {
745    /// State of a cache entry before the request.
746    ///
747    /// Leave out this field if the information is not available.
748    #[serde(rename = "beforeRequest")]
749    pub before_request: Option<CacheState>,
750    /// State of a cache entry after the request.
751    ///
752    /// Leave out this field if the information is not available.
753    #[serde(rename = "afterRequest")]
754    pub after_request: Option<CacheState>,
755    /// A comment provided by the user or the application.
756    pub comment: Option<ArcStr>,
757} // TODO: support this once we have cache support in rama, e.g. based on extension info
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
760pub struct CacheState {
761    /// Date and time stamp of the request start
762    ///
763    /// (ISO 8601 - YYYY-MM-DDThh:mm:ss.sTZD)
764    /// Expiration time of the cache entry.
765    pub expires: Timestamp,
766    /// The last time the cache entry was opened.
767    #[serde(rename = "lastAccess")]
768    pub last_access: Option<ArcStr>,
769    /// Etag
770    #[serde(rename = "eTag")]
771    pub e_tag: Option<ArcStr>,
772    /// The number of times the cache entry has been opened.
773    #[serde(rename = "hitCount")]
774    pub hit_count: Option<i64>,
775    /// A comment provided by the user or the application.
776    pub comment: Option<ArcStr>,
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize, Default)]
780/// This object describes various phases within request-response round trip.
781///
782/// All times are specified in milliseconds.
783pub struct Timings {
784    /// Time spent in a queue waiting for a network connection.
785    ///
786    /// Use -1 if the timing does not apply to the current request.
787    pub blocked: Option<i64>, // TODO
788    /// DNS resolution time.
789    ///
790    /// The time required to resolve a host name.
791    ///
792    /// Use -1 if the timing does not apply to the current request.
793    pub dns: Option<i64>, // TODO
794    /// Time required to create TCP connection.
795    ///
796    /// Use -1 if the timing does not apply to the current request.
797    pub connect: Option<i64>, // TODO
798    /// Time required to send HTTP request to the server.
799    pub send: i64, // TODO
800    /// Waiting for a response from the server.
801    pub wait: i64, // TODO
802    /// Time required to read entire response from the server (or cache).
803    pub receive: i64, // TODO
804    /// Time required for SSL/TLS negotiation.
805    ///
806    /// If this field is defined then the time is also included in the connect field
807    /// (to ensure backward compatibility with HAR 1.1).
808    ///
809    /// Use -1 if the timing does not apply to the current request.
810    pub ssl: Option<i64>, // TODO
811    /// A comment provided by the user or the application.
812    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        // HAR Request to rama Request
865        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        // rama Request to HAR Request
881        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        // Query parsing sanity check when converting rama Request parts back into HAR Request
896        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        // HAR Response to rama Response
928        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        // rama Response to HAR Response
951        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        // HAR -> rama request payload
972        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        // rama request parts + payload -> HAR request payload
980        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        // HAR -> rama response payload
991        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        // rama response parts + payload -> HAR response payload
1000        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}