1use std::net::SocketAddr;
2use std::sync::mpsc::Sender;
3use std::sync::Arc;
4use std::time::{Instant, SystemTime, UNIX_EPOCH};
5
6use serde::{Deserialize, Serialize};
7use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
8use tokio::net::{lookup_host, TcpListener, TcpStream};
9use tokio_rustls::rustls::pki_types::ServerName;
10use tokio_rustls::rustls::{ClientConfig, RootCertStore};
11use tokio_rustls::TlsConnector;
12use url::Url;
13use webpki_roots::TLS_SERVER_ROOTS;
14
15use crate::engine::pool::ConnectionPool;
16use crate::engine::proxy::ProxyMeasurement;
17use crate::engine::tls::{report_for_http, report_for_https_failure, report_for_https_success};
18use crate::error::{Result, TloxError};
19use crate::models::{TimingReport, TlsReport};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CaptureSessionExport {
25 pub version: u8,
26 pub listen_addr: String,
27 pub upstream: String,
28 pub exported_at_ms: i64,
29 pub event_count: usize,
30 pub events: Vec<CapturedExchange>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct CapturedExchange {
35 pub target: String,
36 pub request_host: String,
37 pub request_method: String,
38 pub request_path: String,
39 pub request_headers: Vec<(String, String)>,
40 pub response_headers: Vec<(String, String)>,
41 pub protocol_version: Option<String>,
42 pub redirect_location: Option<String>,
43 pub response_status_code: Option<u16>,
44 pub response_status_text: Option<String>,
45 pub response_bytes: u64,
46 pub report: TimingReport,
47 pub tls: Option<TlsReport>,
48 pub error_message: Option<String>,
49 pub created_at_ms: i64,
50 #[serde(default = "default_connection_reuse")]
57 pub connection_reuse: String,
58}
59
60fn default_connection_reuse() -> String {
61 "new".to_string()
62}
63
64#[derive(Debug)]
65pub struct CaptureProxy {
66 listen_addr: String,
67 upstream: Url,
68 sender: Sender<CapturedExchange>,
69 pool: ConnectionPool,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76struct IncomingRequest {
77 method: String,
78 path: String,
79 version: String,
80 headers: Vec<(String, String)>,
81 body: Vec<u8>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85struct ForwardResponse {
86 raw_response: Vec<u8>,
87 metadata: ResponseMetadata,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91struct ResponseMetadata {
92 total_ms: u64,
93 status_code: Option<u16>,
94 status_text: Option<String>,
95 bytes: u64,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99struct UpstreamResponse {
100 status_code: u16,
101 status_text: String,
102 protocol_version: String,
103 response_headers: Vec<(String, String)>,
104 redirect_location: Option<String>,
105 body: Vec<u8>,
106}
107
108impl CaptureProxy {
109 pub fn new(listen_addr: String, upstream: &str, sender: Sender<CapturedExchange>) -> Result<Self> {
110 let upstream = Url::parse(upstream)
111 .map_err(|_| TloxError::InvalidTarget(upstream.to_string()))?;
112
113 Ok(Self {
114 listen_addr,
115 upstream,
116 sender,
117 pool: ConnectionPool::new(),
118 })
119 }
120
121 pub fn listen_addr(&self) -> &str {
122 &self.listen_addr
123 }
124
125 pub fn upstream(&self) -> &Url {
126 &self.upstream
127 }
128
129 pub async fn run(self) -> Result<()> {
130 let listener = TcpListener::bind(&self.listen_addr).await?;
131 let upstream = self.upstream.clone();
132 let sender = self.sender.clone();
133 let pool = self.pool.clone();
134
135 loop {
136 let (stream, _) = listener.accept().await?;
137 let upstream = upstream.clone();
138 let sender = sender.clone();
139 let pool = pool.clone();
140
141 tokio::spawn(async move {
142 let error_target = upstream.to_string();
143 if let Err(error) =
144 handle_connection(stream, upstream, sender.clone(), pool).await
145 {
146 let exchange = CapturedExchange {
147 target: error_target,
148 request_host: "n/a".to_string(),
149 request_method: "UNKNOWN".to_string(),
150 request_path: "/".to_string(),
151 request_headers: Vec::new(),
152 response_headers: Vec::new(),
153 protocol_version: None,
154 redirect_location: None,
155 response_status_code: Some(502),
156 response_status_text: Some("Bad Gateway".to_string()),
157 response_bytes: 0,
158 report: TimingReport::default(),
159 tls: None,
160 error_message: Some(error.to_string()),
161 created_at_ms: current_time_ms().unwrap_or_default(),
162 connection_reuse: "new".to_string(),
163 };
164 let _ = sender.send(exchange);
165 }
166 });
167 }
168 }
169}
170
171async fn handle_connection(
172 mut inbound: TcpStream,
173 upstream: Url,
174 sender: Sender<CapturedExchange>,
175 pool: ConnectionPool,
176) -> Result<()> {
177 let Some(request) = read_http_request(&mut inbound).await? else {
178 return Ok(());
179 };
180
181 let result = forward_request(&upstream, &request, &pool).await;
182
183 match result {
184 Ok((response, exchange)) => {
185 inbound.write_all(&response.raw_response).await?;
186 inbound.flush().await?;
187 let _ = sender.send(exchange);
188 }
189 Err((exchange, response_bytes)) => {
190 inbound.write_all(&response_bytes).await?;
191 inbound.flush().await?;
192 let _ = sender.send(exchange);
193 }
194 }
195
196 Ok(())
197}
198
199async fn forward_request(
200 upstream: &Url,
201 request: &IncomingRequest,
202 pool: &ConnectionPool,
203) -> std::result::Result<(ForwardResponse, CapturedExchange), (CapturedExchange, Vec<u8>)> {
204 let is_https = upstream.scheme() == "https";
205 let target_url = match build_upstream_url(upstream, &request.path) {
206 Ok(url) => url,
207 Err(error) => {
208 let exchange = error_exchange(
209 upstream,
210 request,
211 TimingReport::default(),
212 tls_report_for_failure(is_https, error.to_string()),
213 error.to_string(),
214 );
215 return Err((exchange, bad_gateway_response("invalid upstream url")));
216 }
217 };
218
219 let host = match target_url.host_str() {
220 Some(host) => host.to_string(),
221 None => {
222 let message = "upstream is missing a host".to_string();
223 let exchange = error_exchange(
224 upstream,
225 request,
226 TimingReport::default(),
227 tls_report_for_failure(is_https, message.clone()),
228 message,
229 );
230 return Err((exchange, bad_gateway_response("upstream missing host")));
231 }
232 };
233
234 let port = match target_url.port_or_known_default() {
235 Some(port) => port,
236 None => {
237 let message = "upstream is missing a port".to_string();
238 let exchange = error_exchange(
239 upstream,
240 request,
241 TimingReport::default(),
242 tls_report_for_failure(is_https, message.clone()),
243 message,
244 );
245 return Err((exchange, bad_gateway_response("upstream missing port")));
246 }
247 };
248
249 let mut measurement = ProxyMeasurement::new();
250 let total_start = Instant::now();
251
252 if is_https {
257 if let Some(cached_sender) = pool.get_h2(host.as_str(), port).await {
258 let request_target = build_request_target(&target_url);
259 let warm_outcome = send_over_h2(
260 cached_sender,
261 &host,
262 &request_target,
263 request,
264 &mut measurement,
265 )
266 .await;
267
268 match warm_outcome {
269 Ok(upstream_response) => {
270 let total_ms = total_start.elapsed().as_millis() as u64;
271 let mut report = measurement.finish_report();
272 report.total_ms = total_ms;
273
274 let raw_response = build_raw_response(
275 upstream_response.status_code,
276 &upstream_response.status_text,
277 &upstream_response.response_headers,
278 &upstream_response.body,
279 );
280 let metadata = ResponseMetadata {
281 total_ms,
282 status_code: Some(upstream_response.status_code),
283 status_text: Some(upstream_response.status_text.clone()),
284 bytes: upstream_response.body.len() as u64,
285 };
286 let tls_report = report_for_https_success(None, None, None);
290 let mut exchange = success_exchange(
291 &target_url,
292 request,
293 report,
294 request.headers.clone(),
295 upstream_response.response_headers.clone(),
296 Some(upstream_response.protocol_version.clone()),
297 upstream_response.redirect_location.clone(),
298 Some(tls_report),
299 metadata.clone(),
300 );
301 exchange.connection_reuse = "reused".to_string();
302
303 return Ok((ForwardResponse { raw_response, metadata }, exchange));
304 }
305 Err(_error) => {
306 pool.invalidate_h2(host.as_str(), port).await;
310 }
311 }
312 }
313 }
314
315 let dns_start = Instant::now();
316 let addr: SocketAddr = match lookup_host((host.as_str(), port)).await {
317 Ok(mut addresses) => match addresses.next() {
318 Some(addr) => addr,
319 None => {
320 let message = "no addresses resolved".to_string();
321 let report = finish_report(&measurement, total_start);
322 let tls = tls_report_for_failure(is_https, message.clone());
323 let exchange = error_exchange(upstream, request, report, tls, message);
324 return Err((exchange, bad_gateway_response("upstream unresolved")));
325 }
326 },
327 Err(error) => {
328 let report = finish_report(&measurement, total_start);
329 let tls = tls_report_for_failure(is_https, error.to_string());
330 let exchange = error_exchange(upstream, request, report, tls, error.to_string());
331 return Err((exchange, bad_gateway_response("dns resolution failed")));
332 }
333 };
334 measurement.record_dns_ms(dns_start.elapsed().as_millis() as u64);
335
336 let tcp_start = Instant::now();
337 let stream = match TcpStream::connect(addr).await {
338 Ok(stream) => stream,
339 Err(error) => {
340 let report = finish_report(&measurement, total_start);
341 let tls = tls_report_for_failure(is_https, error.to_string());
342 let exchange = error_exchange(upstream, request, report, tls, error.to_string());
343 return Err((exchange, bad_gateway_response("tcp connect failed")));
344 }
345 };
346 measurement.record_tcp_ms(tcp_start.elapsed().as_millis() as u64);
347
348 let request_target = build_request_target(&target_url);
349
350 let outcome = if is_https {
351 exchange_https(
352 stream,
353 &host,
354 port,
355 &request_target,
356 request,
357 &mut measurement,
358 pool,
359 )
360 .await
361 } else {
362 exchange_http(stream, &host, &request_target, request, &mut measurement).await
363 };
364
365 match outcome {
366 Ok((upstream_response, tls_report)) => {
367 let total_ms = total_start.elapsed().as_millis() as u64;
368 let mut report = measurement.finish_report();
369 report.total_ms = total_ms;
370
371 let raw_response = build_raw_response(
372 upstream_response.status_code,
373 &upstream_response.status_text,
374 &upstream_response.response_headers,
375 &upstream_response.body,
376 );
377
378 let metadata = ResponseMetadata {
379 total_ms,
380 status_code: Some(upstream_response.status_code),
381 status_text: Some(upstream_response.status_text.clone()),
382 bytes: upstream_response.body.len() as u64,
383 };
384
385 let exchange = success_exchange(
386 &target_url,
387 request,
388 report,
389 request.headers.clone(),
390 upstream_response.response_headers.clone(),
391 Some(upstream_response.protocol_version.clone()),
392 upstream_response.redirect_location.clone(),
393 Some(tls_report),
394 metadata.clone(),
395 );
396
397 Ok((
398 ForwardResponse {
399 raw_response,
400 metadata,
401 },
402 exchange,
403 ))
404 }
405 Err(error) => {
406 let report = finish_report(&measurement, total_start);
407 let tls = tls_report_for_failure(is_https, error.to_string());
408 let exchange = error_exchange(upstream, request, report, tls, error.to_string());
409 Err((exchange, bad_gateway_response("upstream exchange failed")))
410 }
411 }
412}
413
414async fn exchange_http(
415 mut stream: TcpStream,
416 host: &str,
417 request_target: &str,
418 request: &IncomingRequest,
419 measurement: &mut ProxyMeasurement,
420) -> std::io::Result<(UpstreamResponse, TlsReport)> {
421 let response = exchange_over_stream(&mut stream, host, request_target, request, measurement).await?;
422 Ok((response, report_for_http()))
423}
424
425async fn exchange_https(
426 stream: TcpStream,
427 host: &str,
428 port: u16,
429 request_target: &str,
430 request: &IncomingRequest,
431 measurement: &mut ProxyMeasurement,
432 pool: &ConnectionPool,
433) -> std::io::Result<(UpstreamResponse, TlsReport)> {
434 let tls_start = Instant::now();
435 let connector = build_tls_connector();
436 let server_name = ServerName::try_from(host.to_string())
437 .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error.to_string()))?;
438
439 let mut tls_stream = connector
440 .connect(server_name, stream)
441 .await
442 .map_err(|error| std::io::Error::other(error))?;
443 measurement.record_tls_ms(tls_start.elapsed().as_millis() as u64);
444
445 let negotiated_h2 = {
450 let (_, connection) = tls_stream.get_ref();
451 connection.alpn_protocol() == Some(b"h2".as_slice())
452 };
453
454 let tls_metadata = {
457 let (_, connection) = tls_stream.get_ref();
458 (
459 connection.protocol_version().map(|value| format!("{value:?}")),
460 connection
461 .negotiated_cipher_suite()
462 .map(|value| format!("{:?}", value.suite())),
463 )
464 };
465
466 let response = if negotiated_h2 {
467 exchange_over_h2(
468 tls_stream,
469 host,
470 port,
471 request_target,
472 request,
473 measurement,
474 pool,
475 )
476 .await?
477 } else {
478 exchange_over_stream(&mut tls_stream, host, request_target, request, measurement).await?
479 };
480
481 let (tls_version, cipher_suite) = tls_metadata;
482 let tls_report = report_for_https_success(tls_version, cipher_suite, None);
483
484 Ok((response, tls_report))
485}
486
487async fn exchange_over_stream<S>(
488 stream: &mut S,
489 host: &str,
490 request_target: &str,
491 request: &IncomingRequest,
492 measurement: &mut ProxyMeasurement,
493) -> std::io::Result<UpstreamResponse>
494where
495 S: AsyncRead + AsyncWrite + Unpin,
496{
497 let request_bytes = serialize_request(&request.method, request_target, host, &request.headers, &request.body);
498
499 let ttfb_start = Instant::now();
500 stream.write_all(&request_bytes).await?;
501 stream.flush().await?;
502
503 let mut buffer = Vec::new();
504 let mut chunk = [0_u8; 4096];
505 let mut download_start: Option<Instant> = None;
506
507 loop {
508 let bytes_read = stream.read(&mut chunk).await?;
509 if bytes_read == 0 {
510 break;
511 }
512
513 if download_start.is_none() {
514 measurement.record_ttfb_ms(ttfb_start.elapsed().as_millis() as u64);
515 download_start = Some(Instant::now());
516 }
517
518 buffer.extend_from_slice(&chunk[..bytes_read]);
519 }
520
521 if let Some(start) = download_start {
522 measurement.record_download_ms(start.elapsed().as_millis() as u64);
523 }
524
525 parse_upstream_response(&buffer)
526}
527
528async fn exchange_over_h2<S>(
536 stream: S,
537 host: &str,
538 port: u16,
539 request_target: &str,
540 request: &IncomingRequest,
541 measurement: &mut ProxyMeasurement,
542 pool: &ConnectionPool,
543) -> std::io::Result<UpstreamResponse>
544where
545 S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
546{
547 use http_body_util::Full;
548 use hyper::body::Bytes;
549 use hyper::client::conn::http2;
550 use hyper_util::rt::{TokioExecutor, TokioIo};
551
552 let io = TokioIo::new(stream);
553 let (sender, connection) = http2::handshake::<_, _, Full<Bytes>>(TokioExecutor::new(), io)
554 .await
555 .map_err(|error| std::io::Error::other(error))?;
556
557 tokio::spawn(async move {
564 let _ = connection.await;
565 });
566
567 pool.store_h2(host, port, sender.clone()).await;
570
571 send_over_h2(sender, host, request_target, request, measurement).await
572}
573
574async fn send_over_h2(
578 mut sender: crate::engine::pool::PooledH2Sender,
579 host: &str,
580 request_target: &str,
581 request: &IncomingRequest,
582 measurement: &mut ProxyMeasurement,
583) -> std::io::Result<UpstreamResponse> {
584 use http::Request as HttpRequest;
585 use http_body_util::{BodyExt, Full};
586 use hyper::body::Bytes;
587
588 let uri: http::Uri = format!("https://{host}{request_target}")
593 .parse()
594 .map_err(|error: http::uri::InvalidUri| std::io::Error::other(error))?;
595
596 let method = http::Method::from_bytes(request.method.as_bytes())
597 .map_err(|error| std::io::Error::other(error))?;
598
599 let mut builder = HttpRequest::builder().method(method).uri(uri);
600
601 for (name, value) in &request.headers {
602 if header_is_hop_by_hop_or_h2_incompatible(name) {
603 continue;
604 }
605 builder = builder.header(name.as_str(), value.as_str());
606 }
607
608 let body = Full::new(Bytes::from(request.body.clone()));
609 let req = builder
610 .body(body)
611 .map_err(|error| std::io::Error::other(error))?;
612
613 let ttfb_start = Instant::now();
614 let response = sender
615 .send_request(req)
616 .await
617 .map_err(|error| std::io::Error::other(error))?;
618 measurement.record_ttfb_ms(ttfb_start.elapsed().as_millis() as u64);
619
620 let status_code = response.status().as_u16();
621 let status_text = response
622 .status()
623 .canonical_reason()
624 .unwrap_or("")
625 .to_string();
626
627 let mut response_headers: Vec<(String, String)> = Vec::new();
628 let mut redirect_location: Option<String> = None;
629 for (name, value) in response.headers() {
630 let value_str = match value.to_str() {
631 Ok(text) => text.to_string(),
632 Err(_) => continue,
633 };
634 if name.as_str().eq_ignore_ascii_case("location") {
635 redirect_location = Some(value_str.clone());
636 }
637 response_headers.push((name.as_str().to_string(), value_str));
638 }
639
640 let download_start = Instant::now();
641 let body_bytes = response
642 .into_body()
643 .collect()
644 .await
645 .map_err(|error| std::io::Error::other(error))?
646 .to_bytes();
647 measurement.record_download_ms(download_start.elapsed().as_millis() as u64);
648
649 Ok(UpstreamResponse {
650 status_code,
651 status_text,
652 protocol_version: "HTTP/2".to_string(),
653 response_headers,
654 redirect_location,
655 body: body_bytes.to_vec(),
656 })
657}
658
659fn header_is_hop_by_hop_or_h2_incompatible(name: &str) -> bool {
664 matches!(
665 name.to_ascii_lowercase().as_str(),
666 "host"
667 | "connection"
668 | "proxy-connection"
669 | "keep-alive"
670 | "transfer-encoding"
671 | "te"
672 | "trailer"
673 | "upgrade"
674 | "content-length"
675 )
676}
677
678fn serialize_request(
679 method: &str,
680 request_target: &str,
681 host: &str,
682 headers: &[(String, String)],
683 body: &[u8],
684) -> Vec<u8> {
685 let mut out = format!("{method} {request_target} HTTP/1.1\r\n").into_bytes();
686 out.extend_from_slice(format!("Host: {host}\r\n").as_bytes());
687 out.extend_from_slice(b"Connection: close\r\n");
688
689 for (name, value) in headers {
690 if name.eq_ignore_ascii_case("host")
691 || name.eq_ignore_ascii_case("connection")
692 || name.eq_ignore_ascii_case("proxy-connection")
693 || name.eq_ignore_ascii_case("content-length")
694 {
695 continue;
696 }
697 out.extend_from_slice(name.as_bytes());
698 out.extend_from_slice(b": ");
699 out.extend_from_slice(value.as_bytes());
700 out.extend_from_slice(b"\r\n");
701 }
702
703 if !body.is_empty() {
704 out.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
705 }
706
707 out.extend_from_slice(b"\r\n");
708 out.extend_from_slice(body);
709 out
710}
711
712fn parse_upstream_response(buffer: &[u8]) -> std::io::Result<UpstreamResponse> {
713 let header_end = find_header_end(buffer).ok_or_else(|| {
714 std::io::Error::new(std::io::ErrorKind::InvalidData, "missing response headers")
715 })?;
716
717 let header_bytes = &buffer[..header_end];
718 let body_bytes = &buffer[header_end + 4..];
719
720 let header_text = std::str::from_utf8(header_bytes)
721 .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "response headers not utf8"))?;
722
723 let mut lines = header_text.split("\r\n");
724 let status_line = lines
725 .next()
726 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "missing status line"))?;
727
728 let mut parts = status_line.splitn(3, ' ');
729 let protocol_version = parts.next().unwrap_or("HTTP/1.1").to_string();
730 let status_code = parts
731 .next()
732 .and_then(|value| value.parse::<u16>().ok())
733 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "missing status code"))?;
734 let status_text = parts.next().unwrap_or("").trim().to_string();
735
736 let mut response_headers: Vec<(String, String)> = Vec::new();
737 let mut is_chunked = false;
738 let mut redirect_location: Option<String> = None;
739
740 for line in lines {
741 let Some((name, value)) = line.split_once(':') else {
742 continue;
743 };
744 let name = name.trim();
745 let value = value.trim();
746 if name.eq_ignore_ascii_case("transfer-encoding")
747 && value.to_ascii_lowercase().contains("chunked")
748 {
749 is_chunked = true;
750 }
751 if name.eq_ignore_ascii_case("location") {
752 redirect_location = Some(value.to_string());
753 }
754 response_headers.push((name.to_string(), value.to_string()));
755 }
756
757 let body = if is_chunked {
758 decode_chunked(body_bytes)?
759 } else {
760 body_bytes.to_vec()
761 };
762
763 Ok(UpstreamResponse {
764 status_code,
765 status_text,
766 protocol_version,
767 response_headers,
768 redirect_location,
769 body,
770 })
771}
772
773fn decode_chunked(input: &[u8]) -> std::io::Result<Vec<u8>> {
774 let mut out = Vec::with_capacity(input.len());
775 let mut cursor = 0usize;
776
777 loop {
778 let line_end = match find_crlf(&input[cursor..]) {
779 Some(pos) => cursor + pos,
780 None => {
781 return Err(std::io::Error::new(
782 std::io::ErrorKind::InvalidData,
783 "unterminated chunk size line",
784 ));
785 }
786 };
787
788 let size_line = std::str::from_utf8(&input[cursor..line_end])
789 .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "chunk size not utf8"))?;
790 let size_hex = size_line.split(';').next().unwrap_or("").trim();
791 let chunk_size = usize::from_str_radix(size_hex, 16)
792 .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid chunk size"))?;
793
794 cursor = line_end + 2;
795
796 if chunk_size == 0 {
797 break;
798 }
799
800 if cursor + chunk_size > input.len() {
801 return Err(std::io::Error::new(
802 std::io::ErrorKind::InvalidData,
803 "chunk body exceeds buffer",
804 ));
805 }
806
807 out.extend_from_slice(&input[cursor..cursor + chunk_size]);
808 cursor += chunk_size;
809
810 if input.get(cursor..cursor + 2) != Some(b"\r\n") {
811 return Err(std::io::Error::new(
812 std::io::ErrorKind::InvalidData,
813 "missing CRLF after chunk",
814 ));
815 }
816 cursor += 2;
817 }
818
819 Ok(out)
820}
821
822fn find_crlf(buffer: &[u8]) -> Option<usize> {
823 buffer.windows(2).position(|window| window == b"\r\n")
824}
825
826fn build_tls_config() -> ClientConfig {
827 let roots = RootCertStore::from_iter(TLS_SERVER_ROOTS.iter().cloned());
828 let mut config = ClientConfig::builder()
829 .with_root_certificates(roots)
830 .with_no_client_auth();
831 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
832 config
833}
834
835fn build_tls_connector() -> TlsConnector {
836 TlsConnector::from(Arc::new(build_tls_config()))
837}
838
839fn finish_report(measurement: &ProxyMeasurement, total_start: Instant) -> TimingReport {
840 let mut report = measurement.finish_report();
841 report.total_ms = total_start.elapsed().as_millis() as u64;
842 report
843}
844
845async fn read_http_request(stream: &mut TcpStream) -> std::io::Result<Option<IncomingRequest>> {
846 let mut buffer = Vec::new();
847 let mut chunk = [0_u8; 2048];
848
849 loop {
850 let bytes_read = stream.read(&mut chunk).await?;
851 if bytes_read == 0 {
852 if buffer.is_empty() {
853 return Ok(None);
854 }
855 break;
856 }
857
858 buffer.extend_from_slice(&chunk[..bytes_read]);
859
860 if let Some(header_end) = find_header_end(&buffer) {
861 let content_length = parse_content_length(&buffer[..header_end]).unwrap_or(0);
862 let total_length = header_end + 4 + content_length;
863
864 while buffer.len() < total_length {
865 let bytes_read = stream.read(&mut chunk).await?;
866 if bytes_read == 0 {
867 break;
868 }
869 buffer.extend_from_slice(&chunk[..bytes_read]);
870 }
871
872 return parse_http_request(&buffer).map(Some);
873 }
874 }
875
876 parse_http_request(&buffer).map(Some)
877}
878
879fn parse_http_request(buffer: &[u8]) -> std::io::Result<IncomingRequest> {
880 let header_end = find_header_end(buffer)
881 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "missing request headers"))?;
882 let header_bytes = &buffer[..header_end];
883 let body = buffer[header_end + 4..].to_vec();
884 let header_text = std::str::from_utf8(header_bytes)
885 .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "request headers not utf8"))?;
886 let mut lines = header_text.split("\r\n");
887 let request_line = lines
888 .next()
889 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "missing request line"))?;
890 let mut parts = request_line.split_whitespace();
891 let method = parts
892 .next()
893 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "missing method"))?;
894 let path = parts
895 .next()
896 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "missing path"))?;
897 let version = parts.next().unwrap_or("HTTP/1.1");
898
899 let headers = lines
900 .filter_map(|line| {
901 let (name, value) = line.split_once(':')?;
902 Some((name.trim().to_string(), value.trim().to_string()))
903 })
904 .collect::<Vec<_>>();
905
906 Ok(IncomingRequest {
907 method: method.to_string(),
908 path: normalize_request_path(path),
909 version: version.to_string(),
910 headers,
911 body,
912 })
913}
914
915fn normalize_request_path(path: &str) -> String {
916 if let Ok(url) = Url::parse(path) {
917 let mut normalized = url.path().to_string();
918 if normalized.is_empty() {
919 normalized.push('/');
920 }
921 if let Some(query) = url.query() {
922 normalized.push('?');
923 normalized.push_str(query);
924 }
925 normalized
926 } else if path.is_empty() {
927 "/".to_string()
928 } else {
929 path.to_string()
930 }
931}
932
933fn build_upstream_url(upstream: &Url, request_path: &str) -> std::result::Result<Url, url::ParseError> {
934 let request_path = normalize_request_path(request_path);
935 let (path_only, query) = match request_path.split_once('?') {
936 Some((path, query)) => (path, Some(query)),
937 None => (request_path.as_str(), None),
938 };
939 let request_without_root = path_only.trim_start_matches('/');
940 let base_without_trailing = upstream.path().trim_end_matches('/');
941
942 let combined_path = if base_without_trailing.is_empty() || base_without_trailing == "/" {
943 format!("/{}", request_without_root)
944 } else if request_without_root.is_empty() {
945 base_without_trailing.to_string()
946 } else {
947 format!("{base_without_trailing}/{request_without_root}")
948 };
949
950 let mut url = upstream.clone();
951 url.set_path(if combined_path.is_empty() { "/" } else { &combined_path });
952 url.set_query(query);
953
954 Ok(url)
955}
956
957fn build_request_target(url: &Url) -> String {
958 let mut target = url.path().to_string();
959 if target.is_empty() {
960 target.push('/');
961 }
962
963 if let Some(query) = url.query() {
964 target.push('?');
965 target.push_str(query);
966 }
967
968 target
969}
970
971fn build_raw_response(
972 status_code: u16,
973 status_text: &str,
974 headers: &[(String, String)],
975 body: &[u8],
976) -> Vec<u8> {
977 let mut response = format!("HTTP/1.1 {status_code} {status_text}\r\n").into_bytes();
978
979 for (name, value) in headers {
980 if name.eq_ignore_ascii_case("connection")
981 || name.eq_ignore_ascii_case("transfer-encoding")
982 || name.eq_ignore_ascii_case("content-length")
983 {
984 continue;
985 }
986
987 response.extend_from_slice(name.as_bytes());
988 response.extend_from_slice(b": ");
989 response.extend_from_slice(value.as_bytes());
990 response.extend_from_slice(b"\r\n");
991 }
992
993 response
994 .extend_from_slice(format!("Content-Length: {}\r\nConnection: close\r\n\r\n", body.len()).as_bytes());
995 response.extend_from_slice(body);
996 response
997}
998
999fn success_exchange(
1000 target_url: &Url,
1001 request: &IncomingRequest,
1002 report: TimingReport,
1003 request_headers: Vec<(String, String)>,
1004 response_headers: Vec<(String, String)>,
1005 protocol_version: Option<String>,
1006 redirect_location: Option<String>,
1007 tls: Option<TlsReport>,
1008 metadata: ResponseMetadata,
1009) -> CapturedExchange {
1010 CapturedExchange {
1011 target: target_url.to_string(),
1012 request_host: target_url.host_str().unwrap_or("n/a").to_string(),
1013 request_method: request.method.clone(),
1014 request_path: normalize_request_path(&request.path),
1015 request_headers,
1016 response_headers,
1017 protocol_version,
1018 redirect_location,
1019 response_status_code: metadata.status_code,
1020 response_status_text: metadata.status_text,
1021 response_bytes: metadata.bytes,
1022 report,
1023 tls,
1024 error_message: None,
1025 created_at_ms: current_time_ms().unwrap_or_default(),
1026 connection_reuse: "new".to_string(),
1027 }
1028}
1029
1030fn error_exchange(
1031 upstream: &Url,
1032 request: &IncomingRequest,
1033 report: TimingReport,
1034 tls: Option<TlsReport>,
1035 error_message: String,
1036) -> CapturedExchange {
1037 let body = error_message.clone();
1038 CapturedExchange {
1039 target: upstream.to_string(),
1040 request_host: upstream.host_str().unwrap_or("n/a").to_string(),
1041 request_method: request.method.clone(),
1042 request_path: normalize_request_path(&request.path),
1043 request_headers: request.headers.clone(),
1044 response_headers: Vec::new(),
1045 protocol_version: None,
1046 redirect_location: None,
1047 response_status_code: Some(502),
1048 response_status_text: Some("Bad Gateway".to_string()),
1049 response_bytes: body.len() as u64,
1050 report,
1051 tls,
1052 error_message: Some(error_message),
1053 created_at_ms: current_time_ms().unwrap_or_default(),
1054 connection_reuse: "new".to_string(),
1055 }
1056}
1057
1058fn tls_report_for_failure(is_https: bool, error_message: String) -> Option<TlsReport> {
1059 if is_https {
1060 Some(report_for_https_failure(error_message))
1061 } else {
1062 Some(report_for_http())
1063 }
1064}
1065
1066fn bad_gateway_response(message: &str) -> Vec<u8> {
1067 let body = message.as_bytes();
1068 format!(
1069 "HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1070 body.len(),
1071 message
1072 )
1073 .into_bytes()
1074}
1075
1076fn parse_content_length(header_bytes: &[u8]) -> Option<usize> {
1077 let header_text = std::str::from_utf8(header_bytes).ok()?;
1078 header_text
1079 .lines()
1080 .find_map(|line| {
1081 let (name, value) = line.split_once(':')?;
1082 if name.eq_ignore_ascii_case("content-length") {
1083 value.trim().parse::<usize>().ok()
1084 } else {
1085 None
1086 }
1087 })
1088}
1089
1090fn find_header_end(buffer: &[u8]) -> Option<usize> {
1091 buffer.windows(4).position(|window| window == b"\r\n\r\n")
1092}
1093
1094fn current_time_ms() -> Result<i64> {
1095 let now = SystemTime::now().duration_since(UNIX_EPOCH)?;
1096 Ok(now.as_millis() as i64)
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101 use super::{
1102 build_raw_response, build_tls_config, build_upstream_url, decode_chunked,
1103 normalize_request_path, parse_http_request, parse_upstream_response, serialize_request,
1104 };
1105 use url::Url;
1106
1107 #[test]
1108 fn tls_config_advertises_h2_then_http1_alpn() {
1109 let config = build_tls_config();
1110 assert_eq!(
1111 config.alpn_protocols,
1112 vec![b"h2".to_vec(), b"http/1.1".to_vec()],
1113 "capture must advertise h2 first so servers pick it (and we then reject cleanly) \
1114 instead of silently downgrading to http/1.1 and reporting wrong numbers",
1115 );
1116 }
1117
1118 #[test]
1119 fn normalize_request_path_extracts_path_from_absolute_url() {
1120 assert_eq!(
1121 normalize_request_path("https://example.com/api/health?full=1"),
1122 "/api/health?full=1"
1123 );
1124 }
1125
1126 #[test]
1127 fn build_upstream_url_preserves_base_prefix() {
1128 let upstream = Url::parse("https://example.com/api").expect("url should parse");
1129
1130 assert_eq!(
1131 build_upstream_url(&upstream, "/health").expect("url should build").as_str(),
1132 "https://example.com/api/health"
1133 );
1134 }
1135
1136 #[test]
1137 fn build_upstream_url_preserves_query_without_encoding_it_into_path() {
1138 let upstream = Url::parse("http://127.0.0.1:8000").expect("url should parse");
1139
1140 assert_eq!(
1141 build_upstream_url(&upstream, "/mlb/run-conversion?timeframe=season")
1142 .expect("url should build")
1143 .as_str(),
1144 "http://127.0.0.1:8000/mlb/run-conversion?timeframe=season"
1145 );
1146 }
1147
1148 #[test]
1149 fn parse_http_request_reads_method_path_and_body() {
1150 let request = b"POST /users HTTP/1.1\r\nHost: localhost\r\nContent-Length: 7\r\n\r\nhello=1";
1151 let parsed = parse_http_request(request).expect("request should parse");
1152
1153 assert_eq!(parsed.method, "POST");
1154 assert_eq!(parsed.path, "/users");
1155 assert_eq!(parsed.version, "HTTP/1.1");
1156 assert_eq!(parsed.body, b"hello=1");
1157 }
1158
1159 #[test]
1160 fn build_raw_response_preserves_content_encoding_and_strips_hop_headers() {
1161 let headers = vec![
1162 ("Content-Type".to_string(), "text/html; charset=utf-8".to_string()),
1163 ("Content-Encoding".to_string(), "gzip".to_string()),
1164 ("Transfer-Encoding".to_string(), "chunked".to_string()),
1165 ("Connection".to_string(), "keep-alive".to_string()),
1166 ];
1167
1168 let response = build_raw_response(200, "OK", &headers, b"abc");
1169 let response_text = String::from_utf8_lossy(&response).to_ascii_lowercase();
1170
1171 assert!(response_text.contains("content-encoding: gzip\r\n"));
1172 assert!(response_text.contains("content-type: text/html; charset=utf-8\r\n"));
1173 assert!(response_text.contains("content-length: 3\r\n"));
1174 assert!(!response_text.contains("transfer-encoding:"));
1175 assert!(response_text.ends_with("\r\n\r\nabc"));
1176 }
1177
1178 #[test]
1179 fn serialize_request_forces_close_and_strips_hop_headers() {
1180 let bytes = serialize_request(
1181 "POST",
1182 "/api/users",
1183 "example.com",
1184 &[
1185 ("Content-Type".to_string(), "application/json".to_string()),
1186 ("Host".to_string(), "old-host".to_string()),
1187 ("Connection".to_string(), "keep-alive".to_string()),
1188 ("Content-Length".to_string(), "999".to_string()),
1189 ],
1190 b"{}",
1191 );
1192
1193 let text = String::from_utf8_lossy(&bytes);
1194 assert!(text.starts_with("POST /api/users HTTP/1.1\r\n"));
1195 assert!(text.contains("Host: example.com\r\n"));
1196 assert!(text.contains("Connection: close\r\n"));
1197 assert!(text.contains("Content-Length: 2\r\n"));
1198 assert!(!text.to_ascii_lowercase().contains("host: old-host"));
1199 assert!(text.ends_with("\r\n\r\n{}"));
1200 }
1201
1202 #[test]
1203 fn parse_upstream_response_reads_status_and_body() {
1204 let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello world";
1205 let response = parse_upstream_response(raw).expect("should parse");
1206
1207 assert_eq!(response.status_code, 200);
1208 assert_eq!(response.status_text, "OK");
1209 assert_eq!(response.protocol_version, "HTTP/1.1");
1210 assert_eq!(response.body, b"hello world");
1211 assert!(response
1212 .response_headers
1213 .iter()
1214 .any(|(name, value)| name.eq_ignore_ascii_case("content-type") && value == "text/plain"));
1215 }
1216
1217 #[test]
1218 fn parse_upstream_response_decodes_chunked_body() {
1219 let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
1220 let response = parse_upstream_response(raw).expect("should parse");
1221
1222 assert_eq!(response.body, b"hello world");
1223 }
1224
1225 #[test]
1226 fn parse_upstream_response_captures_redirect_location() {
1227 let raw = b"HTTP/1.1 302 Found\r\nLocation: /next\r\nContent-Length: 0\r\n\r\n";
1228 let response = parse_upstream_response(raw).expect("should parse");
1229
1230 assert_eq!(response.status_code, 302);
1231 assert_eq!(response.redirect_location.as_deref(), Some("/next"));
1232 }
1233
1234 #[test]
1235 fn decode_chunked_handles_extension_after_size() {
1236 let raw = b"3;name=value\r\nabc\r\n0\r\n\r\n";
1237 assert_eq!(decode_chunked(raw).expect("should decode"), b"abc");
1238 }
1239
1240 #[test]
1241 fn decode_chunked_rejects_missing_crlf() {
1242 let raw = b"3\r\nabcXX0\r\n\r\n";
1243 assert!(decode_chunked(raw).is_err());
1244 }
1245
1246 #[test]
1247 fn h2_header_filter_strips_incompatible_and_hop_by_hop_headers() {
1248 use super::header_is_hop_by_hop_or_h2_incompatible as filter;
1249
1250 assert!(filter("connection"));
1252 assert!(filter("Connection"));
1253 assert!(filter("keep-alive"));
1254 assert!(filter("proxy-connection"));
1255 assert!(filter("transfer-encoding"));
1256 assert!(filter("upgrade"));
1257 assert!(filter("TE"));
1258
1259 assert!(filter("host"));
1261
1262 assert!(filter("content-length"));
1264
1265 assert!(!filter("content-type"));
1267 assert!(!filter("accept"));
1268 assert!(!filter("authorization"));
1269 assert!(!filter("user-agent"));
1270 assert!(!filter("x-custom-header"));
1271 }
1272}