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