1use std::net::SocketAddr;
74use std::sync::Arc;
75
76use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
77use tokio::net::{TcpListener, TcpStream};
78
79use crate::error::{Error, ErrorKind};
80use crate::session::proxy::{PROXY_AUTH_HEADER, PROXY_PORT_HEADER, ProxyAuth};
81
82pub const DEFAULT_EXCHANGE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
89
90#[derive(Clone, Debug)]
97pub enum ForwardEvent {
98 Listening { local: SocketAddr, guest_port: u16 },
101 Forwarded { status: u16, upgraded: bool },
103 Refused { status: u16, explanation: String },
109 ConnectionError { detail: String },
111}
112
113pub fn refusal_explanation(status: u16, guest_port: u16) -> Option<String> {
118 match status {
119 403 => Some(format!(
120 "the endpoint proxy refused the request for port {guest_port} (403 Access to port \
121 denied): the minted token's scope does not cover it. Nothing was wrong with the \
122 guest — the credential never authorized this port."
123 )),
124 502 => Some(format!(
125 "the endpoint proxy reached the VM and found nothing listening on port \
126 {guest_port} (502). The credential is correct and the scope covers the port, so \
127 this is a dead server in the guest rather than an auth problem."
128 )),
129 401 => Some(
130 "the endpoint proxy rejected the credential (401). On a long-lived tunnel this \
131 usually means a minted token expired without being refreshed; the mint count is \
132 the observable that says whether a refresh happened."
133 .to_string(),
134 ),
135 _ => None,
136 }
137}
138
139pub async fn splice<A, B>(a: &mut A, b: &mut B) -> Result<(u64, u64), Error>
145where
146 A: AsyncRead + AsyncWrite + Unpin + ?Sized,
147 B: AsyncRead + AsyncWrite + Unpin + ?Sized,
148{
149 tokio::io::copy_bidirectional(a, b).await.map_err(|err| {
150 Error::new(
151 ErrorKind::Unexpected,
152 format!("the upgraded connection ended abnormally: {err}"),
153 )
154 })
155}
156
157#[derive(Clone, Debug)]
163pub struct ForwardSpec {
164 pub bind: SocketAddr,
167 pub guest_port: u16,
169 pub endpoint: String,
171 pub exchange_timeout: std::time::Duration,
173}
174
175impl ForwardSpec {
176 pub fn new(bind: SocketAddr, guest_port: u16, endpoint: impl Into<String>) -> Self {
178 Self {
179 bind,
180 guest_port,
181 endpoint: endpoint.into(),
182 exchange_timeout: DEFAULT_EXCHANGE_TIMEOUT,
183 }
184 }
185}
186
187pub fn upstream_url(endpoint: &str, target: &str) -> String {
193 let base = endpoint.trim_end_matches('/');
194 let base = if base.starts_with("https://") || base.starts_with("http://") {
195 base.to_string()
196 } else {
197 format!("https://{base}")
198 };
199 if target.starts_with('/') {
200 format!("{base}{target}")
201 } else {
202 format!("{base}/{target}")
203 }
204}
205
206pub const STRIPPED_REQUEST_HEADERS: [&str; 3] = [PROXY_AUTH_HEADER, PROXY_PORT_HEADER, "host"];
214
215pub fn forwards_request_header(name: &str) -> bool {
217 !STRIPPED_REQUEST_HEADERS
218 .iter()
219 .any(|stripped| stripped.eq_ignore_ascii_case(name))
220}
221
222pub struct ForwardClient {
229 inner: reqwest::Client,
230}
231
232impl ForwardClient {
233 pub fn new() -> Result<Self, Error> {
235 reqwest::Client::builder()
236 .build()
237 .map(|inner| Self { inner })
238 .map_err(|err| {
239 Error::new(
240 ErrorKind::Unexpected,
241 format!("could not build the forwarder's HTTP client: {err}"),
242 )
243 })
244 }
245}
246
247impl std::fmt::Debug for ForwardClient {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("ForwardClient").finish_non_exhaustive()
250 }
251}
252
253pub async fn bind(spec: &ForwardSpec) -> Result<TcpListener, Error> {
259 TcpListener::bind(spec.bind).await.map_err(|err| {
260 Error::new(
261 ErrorKind::InvalidArg,
262 format!(
263 "could not bind {}: {err}. Another process is probably already listening \
264 there; pass a different local port.",
265 spec.bind
266 ),
267 )
268 })
269}
270
271pub async fn hop_headers(
276 auth: &Arc<ProxyAuth>,
277 guest_port: u16,
278) -> Result<Vec<(String, String)>, Error> {
279 auth.headers_for_port(guest_port).await
280}
281
282pub async fn accept(listener: &TcpListener) -> Result<(TcpStream, SocketAddr), Error> {
288 listener.accept().await.map_err(|err| {
289 Error::new(
290 ErrorKind::Unexpected,
291 format!("the local listener stopped accepting: {err}"),
292 )
293 })
294}
295
296pub fn is_upgrade(status: u16) -> bool {
303 status == 101
304}
305
306pub trait LocalStream: AsyncRead + AsyncWrite + Unpin + Send {}
313impl<T: AsyncRead + AsyncWrite + Unpin + Send> LocalStream for T {}
314
315pub async fn serve_connection<S, F>(
331 mut local: S,
332 spec: &ForwardSpec,
333 auth: &Arc<ProxyAuth>,
334 client: &ForwardClient,
335 mut on_event: F,
336) -> Result<(), Error>
337where
338 S: LocalStream,
339 F: FnMut(ForwardEvent),
340{
341 let head = match read_request_head(&mut local).await? {
342 Some(head) => head,
343 None => return Ok(()),
346 };
347
348 let minted = hop_headers(auth, spec.guest_port).await?;
351
352 let url = upstream_url(&spec.endpoint, &head.target);
353 let method = reqwest::Method::from_bytes(head.method.as_bytes()).map_err(|err| {
354 Error::new(
355 ErrorKind::InvalidArg,
356 format!(
357 "the local client sent an unusable method {:?}: {err}",
358 head.method
359 ),
360 )
361 })?;
362
363 let mut request = client
364 .inner
365 .request(method, &url)
366 .timeout(spec.exchange_timeout);
367 for (name, value) in head
368 .headers
369 .iter()
370 .filter(|(name, _)| forwards_request_header(name))
371 {
372 request = request.header(name, value);
373 }
374 for (name, value) in &minted {
375 request = request.header(name, value);
376 }
377 if !head.body.is_empty() {
378 request = request.body(head.body.clone());
379 }
380
381 let response = request.send().await.map_err(|err| {
382 Error::new(
383 ErrorKind::Unexpected,
384 format!(
385 "forwarding {} {} to the endpoint failed: {err}",
386 head.method, head.target
387 ),
388 )
389 })?;
390 let status = response.status().as_u16();
391
392 if let Some(explanation) = refusal_explanation(status, spec.guest_port) {
393 on_event(ForwardEvent::Refused {
394 status,
395 explanation: explanation.clone(),
396 });
397 write_local_error(&mut local, status, &explanation).await?;
398 return Ok(());
399 }
400
401 if is_upgrade(status) {
402 write_response_head(&mut local, status, response.headers()).await?;
406 let mut upstream = response.upgrade().await.map_err(|err| {
407 Error::new(
408 ErrorKind::Unexpected,
409 format!("the endpoint answered 101 but the connection did not upgrade: {err}"),
410 )
411 })?;
412 on_event(ForwardEvent::Forwarded {
413 status,
414 upgraded: true,
415 });
416 splice(&mut local, &mut upstream).await?;
417 return Ok(());
418 }
419
420 let headers = response.headers().clone();
421 let body = response.bytes().await.map_err(|err| {
422 Error::new(
423 ErrorKind::Unexpected,
424 format!("reading the guest's response body failed: {err}"),
425 )
426 })?;
427 write_response(&mut local, status, &headers, &body).await?;
428 on_event(ForwardEvent::Forwarded {
429 status,
430 upgraded: false,
431 });
432 Ok(())
433}
434
435#[derive(Clone, Debug)]
442struct RequestHead {
443 method: String,
444 target: String,
445 headers: Vec<(String, String)>,
446 body: Vec<u8>,
447}
448
449async fn read_request_head<S>(local: &mut S) -> Result<Option<RequestHead>, Error>
454where
455 S: AsyncRead + Unpin + ?Sized,
456{
457 use tokio::io::AsyncReadExt as _;
458
459 let mut buffer = Vec::new();
460 let mut byte = [0_u8; 1];
461 loop {
465 match local.read(&mut byte).await {
466 Ok(0) if buffer.is_empty() => return Ok(None),
467 Ok(0) => {
468 return Err(Error::new(
469 ErrorKind::Protocol,
470 "the local client closed the connection mid-request".to_string(),
471 ));
472 }
473 Ok(_) => {
474 buffer.push(byte[0]);
475 if buffer.len() > MAX_REQUEST_HEAD_BYTES {
476 return Err(Error::new(
477 ErrorKind::InvalidArg,
478 format!(
479 "the local client's request head exceeded {MAX_REQUEST_HEAD_BYTES} \
480 bytes without terminating"
481 ),
482 ));
483 }
484 if buffer.ends_with(b"\r\n\r\n") {
485 break;
486 }
487 }
488 Err(err) => {
489 return Err(Error::new(
490 ErrorKind::Unexpected,
491 format!("reading the local client's request failed: {err}"),
492 ));
493 }
494 }
495 }
496
497 let text = String::from_utf8(buffer).map_err(|err| {
498 Error::new(
499 ErrorKind::Protocol,
500 format!("the local client's request head is not utf-8: {err}"),
501 )
502 })?;
503 let mut lines = text.split("\r\n");
504 let request_line = lines.next().unwrap_or_default();
505 let mut parts = request_line.split_whitespace();
506 let method = parts.next().unwrap_or_default().to_string();
507 let target = parts.next().unwrap_or_default().to_string();
508 if method.is_empty() || target.is_empty() {
509 return Err(Error::new(
510 ErrorKind::Protocol,
511 format!("the local client sent an unparseable request line {request_line:?}"),
512 ));
513 }
514
515 let mut headers = Vec::new();
516 for line in lines {
517 if line.is_empty() {
518 continue;
519 }
520 let Some((name, value)) = line.split_once(':') else {
521 return Err(Error::new(
522 ErrorKind::Protocol,
523 format!("the local client sent an unparseable header line {line:?}"),
524 ));
525 };
526 headers.push((name.trim().to_string(), value.trim().to_string()));
527 }
528
529 if headers.iter().any(|(name, value)| {
530 name.eq_ignore_ascii_case("transfer-encoding") && value.contains("chunked")
531 }) {
532 return Err(Error::new(
533 ErrorKind::InvalidArg,
534 "the local client sent a chunked request body, which this forwarder does not \
535 re-frame. Send a content-length body, or report the client that needs chunked \
536 uploads so the forwarder can grow a streaming path."
537 .to_string(),
538 ));
539 }
540
541 let length: usize = headers
542 .iter()
543 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
544 .and_then(|(_, value)| value.parse().ok())
545 .unwrap_or(0);
546
547 let mut body = vec![0_u8; length];
548 if length > 0 {
549 local.read_exact(&mut body).await.map_err(|err| {
550 Error::new(
551 ErrorKind::Protocol,
552 format!("the local client announced {length} body bytes and sent fewer: {err}"),
553 )
554 })?;
555 }
556
557 Ok(Some(RequestHead {
558 method,
559 target,
560 headers,
561 body,
562 }))
563}
564
565const MAX_REQUEST_HEAD_BYTES: usize = 64 * 1024;
568
569const RECOMPUTED_RESPONSE_HEADERS: [&str; 2] = ["content-length", "transfer-encoding"];
575
576async fn write_response_head<W>(
578 local: &mut W,
579 status: u16,
580 headers: &reqwest::header::HeaderMap,
581) -> Result<(), Error>
582where
583 W: AsyncWrite + Unpin + ?Sized,
584{
585 let mut head = format!("HTTP/1.1 {status} {}\r\n", reason_phrase(status));
586 for (name, value) in headers {
587 if let Ok(value) = value.to_str() {
588 head.push_str(&format!("{}: {}\r\n", name.as_str(), value));
589 }
590 }
591 head.push_str("\r\n");
592 local.write_all(head.as_bytes()).await.map_err(|err| {
593 Error::new(
594 ErrorKind::Unexpected,
595 format!("could not write the response head to the local client: {err}"),
596 )
597 })?;
598 local.flush().await.map_err(|err| {
599 Error::new(
600 ErrorKind::Unexpected,
601 format!("could not flush the response head: {err}"),
602 )
603 })
604}
605
606async fn write_response<W>(
608 local: &mut W,
609 status: u16,
610 headers: &reqwest::header::HeaderMap,
611 body: &[u8],
612) -> Result<(), Error>
613where
614 W: AsyncWrite + Unpin + ?Sized,
615{
616 let mut head = format!("HTTP/1.1 {status} {}\r\n", reason_phrase(status));
617 for (name, value) in headers {
618 if RECOMPUTED_RESPONSE_HEADERS
619 .iter()
620 .any(|skip| skip.eq_ignore_ascii_case(name.as_str()))
621 {
622 continue;
623 }
624 if let Ok(value) = value.to_str() {
625 head.push_str(&format!("{}: {}\r\n", name.as_str(), value));
626 }
627 }
628 head.push_str(&format!("content-length: {}\r\n\r\n", body.len()));
629
630 local.write_all(head.as_bytes()).await.map_err(|err| {
631 Error::new(
632 ErrorKind::Unexpected,
633 format!("could not write the response to the local client: {err}"),
634 )
635 })?;
636 local.write_all(body).await.map_err(|err| {
637 Error::new(
638 ErrorKind::Unexpected,
639 format!("could not write the response body to the local client: {err}"),
640 )
641 })?;
642 local.flush().await.map_err(|err| {
643 Error::new(
644 ErrorKind::Unexpected,
645 format!("could not flush the response: {err}"),
646 )
647 })
648}
649
650fn reason_phrase(status: u16) -> &'static str {
655 match status {
656 101 => "Switching Protocols",
657 200 => "OK",
658 403 => "Forbidden",
659 404 => "Not Found",
660 500 => "Internal Server Error",
661 502 => "Bad Gateway",
662 504 => "Gateway Timeout",
663 _ => "Status",
664 }
665}
666
667pub async fn write_local_error<W>(stream: &mut W, status: u16, body: &str) -> Result<(), Error>
673where
674 W: AsyncWrite + Unpin + ?Sized,
675{
676 let reason = match status {
677 403 => "Forbidden",
678 502 => "Bad Gateway",
679 504 => "Gateway Timeout",
680 _ => "Bad Gateway",
681 };
682 let response = format!(
683 "HTTP/1.1 {status} {reason}\r\ncontent-type: text/plain; charset=utf-8\r\n\
684 content-length: {}\r\nconnection: close\r\n\r\n{body}",
685 body.len()
686 );
687 stream.write_all(response.as_bytes()).await.map_err(|err| {
688 Error::new(
689 ErrorKind::Unexpected,
690 format!("could not answer the local client: {err}"),
691 )
692 })?;
693 stream.flush().await.map_err(|err| {
694 Error::new(
695 ErrorKind::Unexpected,
696 format!("could not flush the local client's response: {err}"),
697 )
698 })
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704 use crate::session::proxy::DEFAULT_AGENT_PORT;
705 use crate::session::proxy::testing::{CountingMinter, ManualClock};
706
707 fn auth_pair() -> (Arc<ProxyAuth>, Arc<CountingMinter>) {
710 let minter = Arc::new(CountingMinter::default());
711 let auth = ProxyAuth::with_refresh_after(
712 minter.clone(),
713 DEFAULT_AGENT_PORT,
714 std::time::Duration::from_secs(30 * 60),
715 Arc::new(ManualClock::default()),
716 )
717 .expect("the default refresh interval is below the ceiling");
718 (Arc::new(auth), minter)
719 }
720
721 async fn upstream(response: &'static str) -> (String, tokio::task::JoinHandle<String>) {
728 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
729 let addr = listener.local_addr().expect("bound");
730 let handle = tokio::spawn(async move {
731 let (mut socket, _) = listener.accept().await.expect("one connection");
732 let head = read_request_head(&mut socket)
733 .await
734 .expect("a readable request")
735 .expect("a request arrived");
736 socket
737 .write_all(response.as_bytes())
738 .await
739 .expect("the canned response is written");
740 socket.flush().await.expect("flushed");
741 let headers = head
742 .headers
743 .iter()
744 .map(|(name, value)| format!("{}: {value}", name.to_ascii_lowercase()))
745 .collect::<Vec<_>>()
746 .join("\n");
747 format!("{} {}\n{headers}", head.method, head.target)
748 });
749 (format!("http://{addr}"), handle)
750 }
751
752 async fn exchange(
755 request: &str,
756 endpoint: String,
757 guest_port: u16,
758 auth: &Arc<ProxyAuth>,
759 ) -> (String, Vec<ForwardEvent>) {
760 let (mut client, server) = tokio::io::duplex(64 * 1024);
761 let spec = ForwardSpec::new(
762 "127.0.0.1:0".parse().expect("an addr"),
763 guest_port,
764 endpoint,
765 );
766 let http = ForwardClient::new().expect("a client");
767
768 let auth = auth.clone();
769 let served = tokio::spawn(async move {
770 let mut events = Vec::new();
771 let result =
772 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
773 (result, events)
774 });
775
776 client
777 .write_all(request.as_bytes())
778 .await
779 .expect("the request is written");
780 client.flush().await.expect("flushed");
781
782 let (result, events) = served.await.expect("the task joins");
783 result.expect("the exchange completes");
784
785 let mut seen = Vec::new();
786 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut seen)
787 .await
788 .expect("the client reads the response");
789 (String::from_utf8_lossy(&seen).to_string(), events)
790 }
791
792 #[tokio::test]
793 async fn an_ordinary_get_reaches_the_guest_carrying_both_minted_headers() {
794 let (endpoint, upstream_saw) =
797 upstream("HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello").await;
798 let (auth, minter) = auth_pair();
799
800 let (seen, events) = exchange(
801 "GET /index.html HTTP/1.1\r\nhost: localhost:8080\r\nuser-agent: probe\r\n\r\n",
802 endpoint,
803 8080,
804 &auth,
805 )
806 .await;
807
808 let request = upstream_saw.await.expect("the upstream task joins");
809 assert!(request.starts_with("GET /index.html"), "{request}");
810 assert!(
811 request.contains(&format!(
812 "{}: jwe-0",
813 PROXY_AUTH_HEADER.to_ascii_lowercase()
814 )),
815 "the minted auth header did not reach upstream: {request}"
816 );
817 assert!(
818 request.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
819 "the port header did not name the guest port: {request}"
820 );
821 assert!(request.contains("user-agent: probe"), "{request}");
822
823 assert!(seen.starts_with("HTTP/1.1 200 OK\r\n"), "{seen}");
824 assert!(seen.ends_with("hello"), "{seen}");
825 assert!(
826 matches!(
827 events.as_slice(),
828 [ForwardEvent::Forwarded {
829 status: 200,
830 upgraded: false
831 }]
832 ),
833 "{events:?}"
834 );
835
836 let requested = minter.requested();
839 assert!(
840 requested.iter().any(|ports| ports.contains(&8080)),
841 "the mint never asked for port 8080: {requested:?}"
842 );
843 }
844
845 #[tokio::test]
846 async fn the_caller_cannot_smuggle_its_own_proxy_headers_upstream() {
847 let (endpoint, upstream_saw) =
851 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
852 let (auth, _minter) = auth_pair();
853
854 let request = format!(
855 "GET / HTTP/1.1\r\nhost: local\r\n{PROXY_AUTH_HEADER}: forged-jwe\r\n\
856 {PROXY_PORT_HEADER}: 1\r\n\r\n"
857 );
858 let (_seen, _events) = exchange(&request, endpoint, 8080, &auth).await;
859
860 let saw = upstream_saw.await.expect("the upstream task joins");
861 assert!(
862 !saw.contains("forged-jwe"),
863 "a caller's forged credential was forwarded: {saw}"
864 );
865 assert!(
866 saw.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
867 "the minted port header should be the only one: {saw}"
868 );
869 assert!(
870 !saw.contains("host: local"),
871 "the local host header must not override the endpoint's: {saw}"
872 );
873 }
874
875 #[tokio::test]
876 async fn a_scope_refusal_is_answered_with_the_sentence_that_names_it() {
877 let (endpoint, upstream_saw) =
880 upstream("HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\n\r\n").await;
881 let (auth, _minter) = auth_pair();
882
883 let (seen, events) = exchange(
884 "GET / HTTP/1.1\r\nhost: local\r\n\r\n",
885 endpoint,
886 8080,
887 &auth,
888 )
889 .await;
890 let _ = upstream_saw.await;
891
892 assert!(seen.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{seen}");
893 assert!(seen.contains("scope does not cover"), "{seen}");
894 match events.as_slice() {
895 [
896 ForwardEvent::Refused {
897 status: 403,
898 explanation,
899 },
900 ] => {
901 assert!(explanation.contains("8080"), "{explanation}");
902 }
903 other => panic!("expected one 403 refusal, got {other:?}"),
904 }
905 }
906
907 #[tokio::test]
908 async fn a_dead_guest_server_is_reported_as_502_rather_than_as_an_auth_problem() {
909 let (endpoint, upstream_saw) =
910 upstream("HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\n\r\n").await;
911 let (auth, _minter) = auth_pair();
912
913 let (seen, events) = exchange(
914 "GET / HTTP/1.1\r\nhost: local\r\n\r\n",
915 endpoint,
916 5432,
917 &auth,
918 )
919 .await;
920 let _ = upstream_saw.await;
921
922 assert!(seen.contains("nothing listening on port 5432"), "{seen}");
923 assert!(
924 matches!(
925 events.as_slice(),
926 [ForwardEvent::Refused { status: 502, .. }]
927 ),
928 "{events:?}"
929 );
930 }
931
932 #[tokio::test]
933 async fn the_guests_own_404_is_relayed_and_not_reinterpreted() {
934 let (endpoint, upstream_saw) =
937 upstream("HTTP/1.1 404 Not Found\r\ncontent-length: 9\r\n\r\nno-route!").await;
938 let (auth, _minter) = auth_pair();
939
940 let (seen, events) = exchange(
941 "GET /nope HTTP/1.1\r\nhost: local\r\n\r\n",
942 endpoint,
943 8080,
944 &auth,
945 )
946 .await;
947 let _ = upstream_saw.await;
948
949 assert!(seen.starts_with("HTTP/1.1 404 Not Found\r\n"), "{seen}");
950 assert!(seen.ends_with("no-route!"), "{seen}");
951 assert!(
952 matches!(
953 events.as_slice(),
954 [ForwardEvent::Forwarded { status: 404, .. }]
955 ),
956 "{events:?}"
957 );
958 }
959
960 #[tokio::test]
961 async fn a_post_body_is_forwarded_and_the_response_length_is_re_derived() {
962 let (endpoint, upstream_saw) = upstream(
965 "HTTP/1.1 200 OK\r\ncontent-length: 3\r\ntransfer-encoding: identity\r\n\r\nack",
966 )
967 .await;
968 let (auth, _minter) = auth_pair();
969
970 let (seen, _events) = exchange(
971 "POST /submit HTTP/1.1\r\nhost: local\r\ncontent-length: 7\r\n\r\npayload",
972 endpoint,
973 8080,
974 &auth,
975 )
976 .await;
977 let saw = upstream_saw.await.expect("the upstream task joins");
978
979 assert!(saw.starts_with("POST /submit"), "{saw}");
980 assert!(saw.contains("content-length: 7"), "{saw}");
981 assert_eq!(
982 seen.matches("content-length:").count(),
983 1,
984 "exactly one framing header should be written: {seen}"
985 );
986 assert!(seen.contains("content-length: 3\r\n"), "{seen}");
987 assert!(
988 !seen.to_ascii_lowercase().contains("transfer-encoding"),
989 "the upstream framing header must not be copied: {seen}"
990 );
991 assert!(seen.ends_with("ack"), "{seen}");
992 }
993
994 #[tokio::test]
995 async fn a_101_relays_the_handshake_and_then_stops_parsing() {
996 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1000 let addr = listener.local_addr().expect("bound");
1001 let guest = tokio::spawn(async move {
1002 let (mut socket, _) = listener.accept().await.expect("one connection");
1003 let _ = read_request_head(&mut socket)
1004 .await
1005 .expect("a request")
1006 .expect("present");
1007 socket
1008 .write_all(
1009 b"HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\n\
1010 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1011 )
1012 .await
1013 .expect("the handshake is written");
1014 socket.flush().await.expect("flushed");
1015 let mut frame = [0_u8; 5];
1017 tokio::io::AsyncReadExt::read_exact(&mut socket, &mut frame)
1018 .await
1019 .expect("the client's bytes arrive");
1020 socket
1021 .write_all(b"PONG")
1022 .await
1023 .expect("the answer is written");
1024 socket.flush().await.expect("flushed");
1025 frame.to_vec()
1026 });
1027
1028 let (auth, _minter) = auth_pair();
1029 let (mut client, server) = tokio::io::duplex(64 * 1024);
1030 let spec = ForwardSpec::new(
1031 "127.0.0.1:0".parse().expect("an addr"),
1032 8080,
1033 format!("http://{addr}"),
1034 );
1035 let http = ForwardClient::new().expect("a client");
1036 let served = tokio::spawn(async move {
1037 let mut events = Vec::new();
1038 let result =
1039 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
1040 (result, events)
1041 });
1042
1043 client
1044 .write_all(
1045 b"GET /socket HTTP/1.1\r\nhost: local\r\nupgrade: websocket\r\n\
1046 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1047 )
1048 .await
1049 .expect("the handshake request is written");
1050 client.flush().await.expect("flushed");
1051
1052 let mut head = Vec::new();
1055 let mut byte = [0_u8; 1];
1056 while !head.ends_with(b"\r\n\r\n") {
1057 tokio::io::AsyncReadExt::read_exact(&mut client, &mut byte)
1058 .await
1059 .expect("the handshake response arrives");
1060 head.push(byte[0]);
1061 }
1062 let head = String::from_utf8_lossy(&head).to_string();
1063 assert!(
1064 head.starts_with("HTTP/1.1 101 Switching Protocols\r\n"),
1065 "{head}"
1066 );
1067 assert!(
1068 head.to_ascii_lowercase()
1069 .contains("sec-websocket-protocol: my-app-protocol"),
1070 "the guest's negotiated subprotocol must reach the client: {head}"
1071 );
1072
1073 client
1074 .write_all(b"PING!")
1075 .await
1076 .expect("a frame is written");
1077 client.flush().await.expect("flushed");
1078 let mut answer = [0_u8; 4];
1079 tokio::io::AsyncReadExt::read_exact(&mut client, &mut answer)
1080 .await
1081 .expect("the guest's answer arrives");
1082 assert_eq!(&answer, b"PONG");
1083
1084 drop(client);
1085 let guest_saw = guest.await.expect("the guest task joins");
1086 assert_eq!(
1087 &guest_saw, b"PING!",
1088 "the client's bytes reached the guest unframed"
1089 );
1090
1091 let (result, events) = served.await.expect("the task joins");
1092 result.expect("the splice ends cleanly");
1093 assert!(
1094 events.iter().any(|event| matches!(
1095 event,
1096 ForwardEvent::Forwarded {
1097 status: 101,
1098 upgraded: true
1099 }
1100 )),
1101 "{events:?}"
1102 );
1103 }
1104
1105 #[tokio::test]
1106 async fn every_exchange_mints_through_the_cache_rather_than_capturing_headers_once() {
1107 let (auth, minter) = auth_pair();
1111
1112 for _ in 0..2 {
1113 let (endpoint, upstream_saw) =
1114 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
1115 let (_seen, _events) = exchange(
1116 "GET / HTTP/1.1\r\nhost: local\r\n\r\n",
1117 endpoint,
1118 8080,
1119 &auth,
1120 )
1121 .await;
1122 let saw = upstream_saw.await.expect("the upstream task joins");
1123 assert!(
1124 saw.contains(&PROXY_AUTH_HEADER.to_ascii_lowercase()),
1125 "every hop carries the minted credential: {saw}"
1126 );
1127 }
1128
1129 assert_eq!(
1133 auth.mint_count(),
1134 1,
1135 "the second hop should reuse the cached token rather than re-mint"
1136 );
1137 assert_eq!(minter.requested().len(), 1);
1138 }
1139
1140 #[tokio::test]
1141 async fn a_chunked_request_body_is_refused_by_name_rather_than_truncated() {
1142 let (mut client, mut server) = tokio::io::duplex(4096);
1145 client
1146 .write_all(b"POST /up HTTP/1.1\r\nhost: local\r\ntransfer-encoding: chunked\r\n\r\n")
1147 .await
1148 .expect("written");
1149 client.flush().await.expect("flushed");
1150
1151 let error = read_request_head(&mut server)
1152 .await
1153 .expect_err("chunked is refused");
1154 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1155 assert!(error.to_string().contains("chunked"), "{error}");
1156 }
1157
1158 #[tokio::test]
1159 async fn a_connection_that_closes_before_sending_anything_is_not_an_error() {
1160 let (client, mut server) = tokio::io::duplex(4096);
1163 drop(client);
1164 let head = read_request_head(&mut server)
1165 .await
1166 .expect("an empty connection is fine");
1167 assert!(head.is_none());
1168 }
1169
1170 #[tokio::test]
1171 async fn an_unterminated_request_head_is_capped_rather_than_buffered_without_bound() {
1172 let (mut client, mut server) = tokio::io::duplex(1024 * 1024);
1173 let writer = tokio::spawn(async move {
1174 let filler = vec![b'x'; MAX_REQUEST_HEAD_BYTES + 1024];
1176 let _ = client.write_all(b"GET / HTTP/1.1\r\nx: ").await;
1177 let _ = client.write_all(&filler).await;
1178 let _ = client.flush().await;
1179 });
1180
1181 let error = read_request_head(&mut server)
1182 .await
1183 .expect_err("the cap fires");
1184 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1185 assert!(error.to_string().contains("without terminating"), "{error}");
1186 writer.abort();
1187 }
1188
1189 #[test]
1190 fn only_a_101_counts_as_an_upgrade() {
1191 assert!(is_upgrade(101));
1195 assert!(!is_upgrade(200));
1196 assert!(!is_upgrade(426));
1197 }
1198
1199 #[test]
1200 fn a_bare_endpoint_host_is_read_as_https() {
1201 assert_eq!(
1204 upstream_url("vm-abc.example.aws", "/index.html"),
1205 "https://vm-abc.example.aws/index.html"
1206 );
1207 }
1208
1209 #[test]
1210 fn an_explicit_scheme_is_left_alone() {
1211 assert_eq!(
1212 upstream_url("https://vm-abc.example.aws/", "/a"),
1213 "https://vm-abc.example.aws/a"
1214 );
1215 assert_eq!(
1216 upstream_url("http://127.0.0.1:9000", "/a"),
1217 "http://127.0.0.1:9000/a"
1218 );
1219 }
1220
1221 #[test]
1222 fn a_target_without_a_leading_slash_still_forms_one_path() {
1223 assert_eq!(
1224 upstream_url("vm.example", "index.html"),
1225 "https://vm.example/index.html"
1226 );
1227 }
1228
1229 #[test]
1230 fn the_two_proxy_headers_are_never_copied_from_the_caller() {
1231 assert!(!forwards_request_header("x-aws-proxy-auth"));
1234 assert!(!forwards_request_header("X-AWS-PROXY-AUTH"));
1235 assert!(!forwards_request_header("x-aws-proxy-port"));
1236 assert!(!forwards_request_header("Host"));
1237 assert!(forwards_request_header("user-agent"));
1238 assert!(forwards_request_header("sec-websocket-protocol"));
1239 }
1240
1241 #[test]
1242 fn the_refusal_wording_separates_a_scope_mistake_from_a_dead_server() {
1243 let scope = refusal_explanation(403, 8080).expect("403 explains itself");
1246 assert!(scope.contains("scope does not cover"), "{scope}");
1247 assert!(scope.contains("8080"), "{scope}");
1248
1249 let dead = refusal_explanation(502, 8080).expect("502 explains itself");
1250 assert!(dead.contains("nothing listening"), "{dead}");
1251 assert!(dead.contains("8080"), "{dead}");
1252
1253 assert!(
1254 refusal_explanation(401, 8080).is_some(),
1255 "401 is worth naming"
1256 );
1257 assert!(
1258 refusal_explanation(200, 8080).is_none(),
1259 "a success explains nothing"
1260 );
1261 assert!(
1262 refusal_explanation(404, 8080).is_none(),
1263 "the guest's own 404 is not ours"
1264 );
1265 }
1266
1267 #[tokio::test]
1268 async fn a_bind_collision_names_the_address_and_suggests_another_port() {
1269 let held = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1270 let addr = held.local_addr().expect("a bound address");
1271
1272 let spec = ForwardSpec::new(addr, 8080, "vm.example");
1273 let error = bind(&spec).await.expect_err("the address is taken");
1274 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1275 assert!(error.to_string().contains(&addr.to_string()), "{error}");
1276 assert!(
1277 error.to_string().contains("different local port"),
1278 "{error}"
1279 );
1280 }
1281
1282 #[tokio::test]
1283 async fn a_local_error_response_is_a_readable_http_message() {
1284 let (mut client, mut server) = tokio::io::duplex(4096);
1287 let body = refusal_explanation(403, 8080).expect("wording");
1288 write_local_error(&mut server, 403, &body)
1289 .await
1290 .expect("the response is written");
1291 drop(server);
1292
1293 let mut got = Vec::new();
1294 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut got)
1295 .await
1296 .expect("the client reads it");
1297 let text = String::from_utf8(got).expect("utf-8");
1298 assert!(text.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{text}");
1299 assert!(text.contains("content-length: "), "{text}");
1300 assert!(text.ends_with(&body), "{text}");
1301 }
1302
1303 #[tokio::test]
1304 async fn splice_copies_both_directions_and_reports_the_byte_counts() {
1305 let (mut left_near, mut left_far) = tokio::io::duplex(4096);
1306 let (mut right_near, mut right_far) = tokio::io::duplex(4096);
1307
1308 let pump = tokio::spawn(async move { splice(&mut left_far, &mut right_near).await });
1309
1310 left_near.write_all(b"to-guest").await.expect("write");
1311 left_near.shutdown().await.expect("half close");
1312 right_far.write_all(b"from-guest!").await.expect("write");
1313 right_far.shutdown().await.expect("half close");
1314
1315 let mut to_guest = Vec::new();
1316 tokio::io::AsyncReadExt::read_to_end(&mut right_far, &mut to_guest)
1317 .await
1318 .expect("read");
1319 let mut from_guest = Vec::new();
1320 tokio::io::AsyncReadExt::read_to_end(&mut left_near, &mut from_guest)
1321 .await
1322 .expect("read");
1323
1324 let (a_to_b, b_to_a) = pump.await.expect("the task joins").expect("a clean splice");
1325 assert_eq!(to_guest, b"to-guest");
1326 assert_eq!(from_guest, b"from-guest!");
1327 assert_eq!(a_to_b, 8);
1328 assert_eq!(b_to_a, 11);
1329 }
1330}