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