1use crate::layer::har::recorder::{
2 HttpRequestCapture, HttpResponseCapture, RecorderSession, StreamingRecorder, WebSocketCapture,
3 body_capture_channel,
4};
5use crate::layer::har::spec::{Request as HarRequest, Response as HarResponse};
6use crate::layer::har::toggle::Toggle;
7use crate::{Body, Request, Response, StreamingBody};
8
9use jiff::Timestamp;
10use rama_core::error::BoxError;
11use rama_core::extensions::ExtensionsRef;
12use rama_core::telemetry::tracing;
13use rama_core::{Service, bytes::Bytes};
14use rama_http_types::proto::h2::ext::Protocol;
15use tokio::time::Instant;
16
17#[derive(Clone)]
18pub struct HARExportService<R, S, T> {
19 pub(super) recorder: R,
20 pub(super) service: S,
21 pub(super) toggle: T,
22
23 pub(super) preserve_sensitive: bool,
24}
25
26struct ActiveRecording<S> {
27 session: S,
28 web_socket_capture: Option<WebSocketCapture>,
29}
30
31impl<R, S, T> HARExportService<R, S, T> {
32 pub fn recorder(&self) -> &R {
33 &self.recorder
34 }
35
36 pub fn toggle(&self) -> &T {
37 &self.toggle
38 }
39
40 rama_utils::macros::generate_set_and_with! {
41 pub fn preserve_sensitive(mut self) -> Self {
43 self.preserve_sensitive = true;
44 self
45 }
46 }
47}
48
49impl<R, S, W, ReqBody, ResBody> Service<Request<ReqBody>> for HARExportService<R, S, W>
50where
51 R: StreamingRecorder,
52 S: Service<Request, Output = Response<ResBody>, Error: Into<BoxError> + Send + Sync + 'static>,
53 W: Toggle,
54 ReqBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
55 ResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
56{
57 type Output = Response;
58 type Error = BoxError;
59
60 async fn serve(&self, req: Request<ReqBody>) -> Result<Self::Output, Self::Error> {
61 let (request, recording) = if self.toggle.status().await {
62 self.start_recording(req).await
63 } else {
64 self.recorder.stop_record().await;
65 (req.map(Body::new), None)
66 };
67
68 let result = self.service.serve(request).await;
69 let Some(recording) = recording else {
70 return result
71 .map(|response| response.map(Body::new))
72 .map_err(Into::into);
73 };
74
75 match result {
76 Ok(response) => self.record_response(recording, response).await,
77 Err(err) => {
78 if let Some(capture) = recording.web_socket_capture {
79 capture.close();
80 }
81 _ = recording.session.record_request_only().await;
82 Err(err.into())
83 }
84 }
85 }
86}
87
88impl<R, S, W> HARExportService<R, S, W>
89where
90 R: StreamingRecorder,
91{
92 async fn start_recording<ReqBody>(
93 &self,
94 request: Request<ReqBody>,
95 ) -> (Request, Option<ActiveRecording<R::Session>>)
96 where
97 ReqBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
98 {
99 let start_time = Timestamp::now();
100 let begin = Instant::now();
101 let (parts, body) = request.into_parts();
102 let har_request =
103 match HarRequest::from_http_request_parts(&parts, &[], self.preserve_sensitive) {
104 Ok(request) => request,
105 Err(err) => {
106 tracing::debug!(
107 "failed to create HAR request from incoming HTTP Request: {err}"
108 );
109 return (Request::from_parts(parts, Body::new(body)), None);
110 }
111 };
112
113 let mime_type = super::spec::get_mime(&parts.headers);
114 let web_socket = is_web_socket_request(&parts);
115 let (sink, body_stream) = body_capture_channel();
116 let capture = HttpRequestCapture {
117 started_date_time: start_time,
118 begin,
119 request: har_request,
120 body_mime_type: mime_type,
121 body: body_stream,
122 web_socket,
123 };
124
125 match self.recorder.start_http_recording(capture).await {
126 Some(session) => {
127 let web_socket_capture = if web_socket {
128 session.web_socket_capture()
129 } else {
130 None
131 };
132 if let Some(capture) = &web_socket_capture {
133 parts.extensions.insert(capture.clone());
134 }
135 (
136 Request::from_parts(parts, Body::new(body).capture(sink)),
137 Some(ActiveRecording {
138 session,
139 web_socket_capture,
140 }),
141 )
142 }
143 None => (Request::from_parts(parts, Body::new(body)), None),
144 }
145 }
146
147 async fn record_response<ResBody>(
148 &self,
149 recording: ActiveRecording<R::Session>,
150 response: Response<ResBody>,
151 ) -> Result<Response, BoxError>
152 where
153 ResBody: StreamingBody<Data = Bytes, Error: Into<BoxError>> + Send + Sync + 'static,
154 {
155 let (parts, body) = response.into_parts();
156 let har_response =
157 match HarResponse::from_http_response_parts(&parts, &[], self.preserve_sensitive) {
158 Ok(response) => response,
159 Err(err) => {
160 tracing::debug!(
161 "failed to create HAR response from returned HTTP Response: {err}"
162 );
163 if let Some(capture) = recording.web_socket_capture {
164 capture.close();
165 }
166 let extensions = recording.session.record_request_only().await;
167 let response = Response::from_parts(parts, Body::new(body));
168 extend_response(&response, extensions);
169 return Ok(response);
170 }
171 };
172
173 let (sink, body_stream) = body_capture_channel();
174 if let Some(capture) = recording.web_socket_capture {
175 if is_successful_web_socket_response(&parts) {
176 parts.extensions.insert(capture);
177 } else {
178 capture.close();
179 }
180 }
181 let extensions = recording
182 .session
183 .record_response(HttpResponseCapture {
184 response: har_response,
185 body: body_stream,
186 })
187 .await;
188 let response = Response::from_parts(parts, Body::new(body).capture(sink));
189 extend_response(&response, extensions);
190 Ok(response)
191 }
192}
193
194fn is_web_socket_request(parts: &rama_http_types::request::Parts) -> bool {
195 parts
196 .extensions
197 .get_ref::<Protocol>()
198 .is_some_and(|protocol| protocol.as_str().eq_ignore_ascii_case("websocket"))
199 || parts
200 .headers
201 .get_all(rama_http_types::header::UPGRADE)
202 .iter()
203 .filter_map(|value| value.to_str().ok())
204 .flat_map(|value| value.split(','))
205 .any(|protocol| protocol.trim().eq_ignore_ascii_case("websocket"))
206}
207
208fn is_successful_web_socket_response(parts: &rama_http_types::response::Parts) -> bool {
209 match parts.version {
210 rama_http_types::Version::HTTP_2 | rama_http_types::Version::HTTP_3 => {
211 parts.status.is_success()
212 }
213 _ => parts.status == rama_http_types::StatusCode::SWITCHING_PROTOCOLS,
214 }
215}
216
217fn extend_response(response: &Response, extensions: Option<rama_core::extensions::Extensions>) {
218 if let Some(extensions) = extensions {
219 tracing::trace!("extend response with HAR recorder extensions");
220 response.extensions().extend(&extensions);
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::{is_successful_web_socket_response, is_web_socket_request};
227 use crate::{Request, Response, StatusCode, Version, proto::h2::ext::Protocol};
228 use rama_core::extensions::ExtensionsRef;
229
230 fn response_parts(status: StatusCode, version: Version) -> rama_http_types::response::Parts {
231 let (mut parts, _) = Response::new(()).into_parts();
232 parts.status = status;
233 parts.version = version;
234 parts
235 }
236
237 #[test]
238 fn validates_web_socket_response_by_http_version() {
239 assert!(is_successful_web_socket_response(&response_parts(
240 StatusCode::SWITCHING_PROTOCOLS,
241 Version::HTTP_11,
242 )));
243 assert!(!is_successful_web_socket_response(&response_parts(
244 StatusCode::OK,
245 Version::HTTP_11,
246 )));
247 assert!(is_successful_web_socket_response(&response_parts(
248 StatusCode::OK,
249 Version::HTTP_2,
250 )));
251 assert!(is_successful_web_socket_response(&response_parts(
252 StatusCode::CREATED,
253 Version::HTTP_2,
254 )));
255 assert!(!is_successful_web_socket_response(&response_parts(
256 StatusCode::BAD_REQUEST,
257 Version::HTTP_2,
258 )));
259 }
260
261 #[test]
262 fn detects_h1_upgrade_and_h2_extended_connect_requests() {
263 let (h1, _) = Request::builder()
264 .header("upgrade", "h2c, WebSocket")
265 .body(())
266 .unwrap()
267 .into_parts();
268 assert!(is_web_socket_request(&h1));
269
270 let mut h2 = Request::new(());
271 *h2.version_mut() = Version::HTTP_2;
272 h2.extensions().insert(Protocol::from_static("websocket"));
273 let (h2, _) = h2.into_parts();
274 assert!(is_web_socket_request(&h2));
275
276 let (plain, _) = Request::new(()).into_parts();
277 assert!(!is_web_socket_request(&plain));
278 }
279}