1use std::net::SocketAddr;
74use std::sync::Arc;
75
76use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
77use tokio::net::{TcpListener, TcpStream};
78
79use microvms_app::error::{Error, ErrorKind};
80use microvms_app::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 fn direct_url(endpoint: &str, guest_port: u16, target: &str) -> Result<String, Error> {
211 let mut url = reqwest::Url::parse(&upstream_url(endpoint, target)).map_err(|err| {
212 Error::new(
213 ErrorKind::InvalidArg,
214 format!("{endpoint} is not an endpoint a request can be addressed to: {err}"),
215 )
216 })?;
217 url.set_port(Some(guest_port)).map_err(|()| {
218 Error::new(
219 ErrorKind::InvalidArg,
220 format!("{endpoint} names no host a port can be set on"),
221 )
222 })?;
223 Ok(url.to_string())
224}
225
226pub const STRIPPED_REQUEST_HEADERS: [&str; 3] = [PROXY_AUTH_HEADER, PROXY_PORT_HEADER, "host"];
234
235pub fn forwards_request_header(name: &str) -> bool {
237 !STRIPPED_REQUEST_HEADERS
238 .iter()
239 .any(|stripped| stripped.eq_ignore_ascii_case(name))
240}
241
242pub struct ForwardClient {
249 inner: reqwest::Client,
250}
251
252impl ForwardClient {
253 pub fn new() -> Result<Self, Error> {
255 reqwest::Client::builder()
256 .build()
257 .map(|inner| Self { inner })
258 .map_err(|err| {
259 Error::new(
260 ErrorKind::Unexpected,
261 format!("could not build the forwarder's HTTP client: {err}"),
262 )
263 })
264 }
265}
266
267impl std::fmt::Debug for ForwardClient {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 f.debug_struct("ForwardClient").finish_non_exhaustive()
270 }
271}
272
273pub async fn bind(spec: &ForwardSpec) -> Result<TcpListener, Error> {
279 TcpListener::bind(spec.bind).await.map_err(|err| {
280 Error::new(
281 ErrorKind::InvalidArg,
282 format!(
283 "could not bind {}: {err}. Another process is probably already listening \
284 there; pass a different local port.",
285 spec.bind
286 ),
287 )
288 })
289}
290
291pub async fn hop_headers(
296 auth: &Arc<ProxyAuth>,
297 guest_port: u16,
298) -> Result<Vec<(String, String)>, Error> {
299 auth.headers_for_port(guest_port).await
300}
301
302pub async fn accept(listener: &TcpListener) -> Result<(TcpStream, SocketAddr), Error> {
307 listener.accept().await.map_err(|err| {
308 Error::new(
309 ErrorKind::Unexpected,
310 format!("the local listener stopped accepting: {err}"),
311 )
312 })
313}
314
315pub fn is_upgrade(status: u16) -> bool {
322 status == 101
323}
324
325pub trait LocalStream: AsyncRead + AsyncWrite + Unpin + Send {}
332impl<T: AsyncRead + AsyncWrite + Unpin + Send> LocalStream for T {}
333
334pub async fn serve_connection<S, F>(
350 local: S,
351 spec: &ForwardSpec,
352 auth: &Arc<ProxyAuth>,
353 client: &ForwardClient,
354 on_event: F,
355) -> Result<(), Error>
356where
357 S: LocalStream,
358 F: FnMut(ForwardEvent),
359{
360 serve_connection_via(local, spec, Some(auth), client, on_event).await
361}
362
363pub(crate) async fn serve_connection_via<S, F>(
370 mut local: S,
371 spec: &ForwardSpec,
372 auth: Option<&Arc<ProxyAuth>>,
373 client: &ForwardClient,
374 mut on_event: F,
375) -> Result<(), Error>
376where
377 S: LocalStream,
378 F: FnMut(ForwardEvent),
379{
380 let head = match read_request_head(&mut local).await? {
381 Some(head) => head,
382 None => return Ok(()),
385 };
386
387 let (minted, url) = match auth {
390 Some(auth) => (
391 hop_headers(auth, spec.guest_port).await?,
392 upstream_url(&spec.endpoint, &head.target),
393 ),
394 None => (
395 Vec::new(),
396 direct_url(&spec.endpoint, spec.guest_port, &head.target)?,
397 ),
398 };
399 let method = reqwest::Method::from_bytes(head.method.as_bytes()).map_err(|err| {
400 Error::new(
401 ErrorKind::InvalidArg,
402 format!(
403 "the local client sent an unusable method {:?}: {err}",
404 head.method
405 ),
406 )
407 })?;
408
409 let mut request = client
410 .inner
411 .request(method, &url)
412 .timeout(spec.exchange_timeout);
413 for (name, value) in head
414 .headers
415 .iter()
416 .filter(|(name, _)| forwards_request_header(name))
417 {
418 request = request.header(name, value);
419 }
420 for (name, value) in &minted {
421 request = request.header(name, value);
422 }
423 if !head.body.is_empty() {
424 request = request.body(head.body.clone());
425 }
426
427 let response = request.send().await.map_err(|err| {
428 Error::new(
429 ErrorKind::Unexpected,
430 format!(
431 "forwarding {} {} to the endpoint failed: {err}",
432 head.method, head.target
433 ),
434 )
435 })?;
436 let status = response.status().as_u16();
437
438 if let Some(explanation) = refusal_explanation(status, spec.guest_port) {
439 on_event(ForwardEvent::Refused {
440 status,
441 explanation: explanation.clone(),
442 });
443 write_local_error(&mut local, status, &explanation).await?;
444 return Ok(());
445 }
446
447 if is_upgrade(status) {
448 write_response_head(&mut local, status, response.headers()).await?;
452 let mut upstream = response.upgrade().await.map_err(|err| {
453 Error::new(
454 ErrorKind::Unexpected,
455 format!("the endpoint answered 101 but the connection did not upgrade: {err}"),
456 )
457 })?;
458 on_event(ForwardEvent::Forwarded {
459 status,
460 upgraded: true,
461 });
462 splice(&mut local, &mut upstream).await?;
463 return Ok(());
464 }
465
466 let headers = response.headers().clone();
467 let body = response.bytes().await.map_err(|err| {
468 Error::new(
469 ErrorKind::Unexpected,
470 format!("reading the guest's response body failed: {err}"),
471 )
472 })?;
473 write_response(&mut local, status, &headers, &body).await?;
474 on_event(ForwardEvent::Forwarded {
475 status,
476 upgraded: false,
477 });
478 Ok(())
479}
480
481#[derive(Clone, Debug)]
488struct RequestHead {
489 method: String,
490 target: String,
491 headers: Vec<(String, String)>,
492 body: Vec<u8>,
493}
494
495async fn read_request_head<S>(local: &mut S) -> Result<Option<RequestHead>, Error>
500where
501 S: AsyncRead + Unpin + ?Sized,
502{
503 use tokio::io::AsyncReadExt as _;
504
505 let mut buffer = Vec::new();
506 let mut byte = [0_u8; 1];
507 loop {
511 match local.read(&mut byte).await {
512 Ok(0) if buffer.is_empty() => return Ok(None),
513 Ok(0) => {
514 return Err(Error::new(
515 ErrorKind::Protocol,
516 "the local client closed the connection mid-request".to_string(),
517 ));
518 }
519 Ok(_) => {
520 buffer.push(byte[0]);
521 if buffer.len() > MAX_REQUEST_HEAD_BYTES {
522 return Err(Error::new(
523 ErrorKind::InvalidArg,
524 format!(
525 "the local client's request head exceeded {MAX_REQUEST_HEAD_BYTES} \
526 bytes without terminating"
527 ),
528 ));
529 }
530 if buffer.ends_with(b"\r\n\r\n") {
531 break;
532 }
533 }
534 Err(err) => {
535 return Err(Error::new(
536 ErrorKind::Unexpected,
537 format!("reading the local client's request failed: {err}"),
538 ));
539 }
540 }
541 }
542
543 let text = String::from_utf8(buffer).map_err(|err| {
544 Error::new(
545 ErrorKind::Protocol,
546 format!("the local client's request head is not utf-8: {err}"),
547 )
548 })?;
549 let mut lines = text.split("\r\n");
550 let request_line = lines.next().unwrap_or_default();
551 let mut parts = request_line.split_whitespace();
552 let method = parts.next().unwrap_or_default().to_string();
553 let target = parts.next().unwrap_or_default().to_string();
554 if method.is_empty() || target.is_empty() {
555 return Err(Error::new(
556 ErrorKind::Protocol,
557 format!("the local client sent an unparseable request line {request_line:?}"),
558 ));
559 }
560
561 let mut headers = Vec::new();
562 for line in lines {
563 if line.is_empty() {
564 continue;
565 }
566 let Some((name, value)) = line.split_once(':') else {
567 return Err(Error::new(
568 ErrorKind::Protocol,
569 format!("the local client sent an unparseable header line {line:?}"),
570 ));
571 };
572 headers.push((name.trim().to_string(), value.trim().to_string()));
573 }
574
575 if headers.iter().any(|(name, value)| {
576 name.eq_ignore_ascii_case("transfer-encoding") && value.contains("chunked")
577 }) {
578 return Err(Error::new(
579 ErrorKind::InvalidArg,
580 "the local client sent a chunked request body, which this forwarder does not \
581 re-frame. Send a content-length body, or report the client that needs chunked \
582 uploads so the forwarder can grow a streaming path."
583 .to_string(),
584 ));
585 }
586
587 let length: usize = headers
588 .iter()
589 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
590 .and_then(|(_, value)| value.parse().ok())
591 .unwrap_or(0);
592
593 let mut body = vec![0_u8; length];
594 if length > 0 {
595 local.read_exact(&mut body).await.map_err(|err| {
596 Error::new(
597 ErrorKind::Protocol,
598 format!("the local client announced {length} body bytes and sent fewer: {err}"),
599 )
600 })?;
601 }
602
603 Ok(Some(RequestHead {
604 method,
605 target,
606 headers,
607 body,
608 }))
609}
610
611const MAX_REQUEST_HEAD_BYTES: usize = 64 * 1024;
614
615const RECOMPUTED_RESPONSE_HEADERS: [&str; 2] = ["content-length", "transfer-encoding"];
621
622async fn write_response_head<W>(
624 local: &mut W,
625 status: u16,
626 headers: &reqwest::header::HeaderMap,
627) -> Result<(), Error>
628where
629 W: AsyncWrite + Unpin + ?Sized,
630{
631 let mut head = format!("HTTP/1.1 {status} {}\r\n", reason_phrase(status));
632 for (name, value) in headers {
633 if let Ok(value) = value.to_str() {
634 head.push_str(&format!("{}: {}\r\n", name.as_str(), value));
635 }
636 }
637 head.push_str("\r\n");
638 local.write_all(head.as_bytes()).await.map_err(|err| {
639 Error::new(
640 ErrorKind::Unexpected,
641 format!("could not write the response head to the local client: {err}"),
642 )
643 })?;
644 local.flush().await.map_err(|err| {
645 Error::new(
646 ErrorKind::Unexpected,
647 format!("could not flush the response head: {err}"),
648 )
649 })
650}
651
652async fn write_response<W>(
654 local: &mut W,
655 status: u16,
656 headers: &reqwest::header::HeaderMap,
657 body: &[u8],
658) -> Result<(), Error>
659where
660 W: AsyncWrite + Unpin + ?Sized,
661{
662 let mut head = format!("HTTP/1.1 {status} {}\r\n", reason_phrase(status));
663 for (name, value) in headers {
664 if RECOMPUTED_RESPONSE_HEADERS
665 .iter()
666 .any(|skip| skip.eq_ignore_ascii_case(name.as_str()))
667 {
668 continue;
669 }
670 if let Ok(value) = value.to_str() {
671 head.push_str(&format!("{}: {}\r\n", name.as_str(), value));
672 }
673 }
674 head.push_str(&format!("content-length: {}\r\n\r\n", body.len()));
675
676 local.write_all(head.as_bytes()).await.map_err(|err| {
677 Error::new(
678 ErrorKind::Unexpected,
679 format!("could not write the response to the local client: {err}"),
680 )
681 })?;
682 local.write_all(body).await.map_err(|err| {
683 Error::new(
684 ErrorKind::Unexpected,
685 format!("could not write the response body to the local client: {err}"),
686 )
687 })?;
688 local.flush().await.map_err(|err| {
689 Error::new(
690 ErrorKind::Unexpected,
691 format!("could not flush the response: {err}"),
692 )
693 })
694}
695
696fn reason_phrase(status: u16) -> &'static str {
701 match status {
702 101 => "Switching Protocols",
703 200 => "OK",
704 403 => "Forbidden",
705 404 => "Not Found",
706 500 => "Internal Server Error",
707 502 => "Bad Gateway",
708 504 => "Gateway Timeout",
709 _ => "Status",
710 }
711}
712
713pub async fn write_local_error<W>(stream: &mut W, status: u16, body: &str) -> Result<(), Error>
719where
720 W: AsyncWrite + Unpin + ?Sized,
721{
722 let reason = match status {
723 403 => "Forbidden",
724 502 => "Bad Gateway",
725 504 => "Gateway Timeout",
726 _ => "Bad Gateway",
727 };
728 let response = format!(
729 "HTTP/1.1 {status} {reason}\r\ncontent-type: text/plain; charset=utf-8\r\n\
730 content-length: {}\r\nconnection: close\r\n\r\n{body}",
731 body.len()
732 );
733 stream.write_all(response.as_bytes()).await.map_err(|err| {
734 Error::new(
735 ErrorKind::Unexpected,
736 format!("could not answer the local client: {err}"),
737 )
738 })?;
739 stream.flush().await.map_err(|err| {
740 Error::new(
741 ErrorKind::Unexpected,
742 format!("could not flush the local client's response: {err}"),
743 )
744 })
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use microvms_app::session::proxy::DEFAULT_AGENT_PORT;
751 use microvms_app::testing::{CountingMinter, TestClock};
752
753 fn auth_pair() -> (Arc<ProxyAuth>, Arc<CountingMinter>) {
756 let minter = Arc::new(CountingMinter::default());
757 let auth = ProxyAuth::with_refresh_after(
758 minter.clone(),
759 DEFAULT_AGENT_PORT,
760 std::time::Duration::from_secs(30 * 60),
761 Arc::new(TestClock::default()),
762 )
763 .expect("the default refresh interval is below the ceiling");
764 (Arc::new(auth), minter)
765 }
766
767 async fn upstream(response: &'static str) -> (String, tokio::task::JoinHandle<String>) {
774 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
775 let addr = listener.local_addr().expect("bound");
776 let handle = tokio::spawn(async move {
777 let (mut socket, _) =
780 tokio::time::timeout(std::time::Duration::from_secs(10), listener.accept())
781 .await
782 .expect("the forwarder reaches the upstream")
783 .expect("one connection");
784 let head = read_request_head(&mut socket)
785 .await
786 .expect("a readable request")
787 .expect("a request arrived");
788 socket
789 .write_all(response.as_bytes())
790 .await
791 .expect("the canned response is written");
792 socket.flush().await.expect("flushed");
793 let headers = head
794 .headers
795 .iter()
796 .map(|(name, value)| format!("{}: {value}", name.to_ascii_lowercase()))
797 .collect::<Vec<_>>()
798 .join("\n");
799 format!("{} {}\n{headers}", head.method, head.target)
800 });
801 (format!("http://{addr}"), handle)
802 }
803
804 async fn exchange(
807 request: &str,
808 endpoint: String,
809 guest_port: u16,
810 auth: &Arc<ProxyAuth>,
811 ) -> (String, Vec<ForwardEvent>) {
812 let (mut client, server) = tokio::io::duplex(64 * 1024);
813 let spec = ForwardSpec::new(
814 "127.0.0.1:0".parse().expect("an addr"),
815 guest_port,
816 endpoint,
817 );
818 let http = ForwardClient::new().expect("a client");
819
820 let auth = auth.clone();
821 let served = tokio::spawn(async move {
822 let mut events = Vec::new();
823 let result =
824 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
825 (result, events)
826 });
827
828 client
829 .write_all(request.as_bytes())
830 .await
831 .expect("the request is written");
832 client.flush().await.expect("flushed");
833
834 let (result, events) = served.await.expect("the task joins");
835 result.expect("the exchange completes");
836
837 let mut seen = Vec::new();
838 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut seen)
839 .await
840 .expect("the client reads the response");
841 (String::from_utf8_lossy(&seen).to_string(), events)
842 }
843
844 #[tokio::test]
845 async fn an_ordinary_get_reaches_the_guest_carrying_both_minted_headers() {
846 let (endpoint, upstream_saw) =
849 upstream("HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello").await;
850 let (auth, minter) = auth_pair();
851
852 let (seen, events) = exchange(
853 "GET /index.html HTTP/1.1\r\nhost: localhost:8080\r\nuser-agent: probe\r\n\r\n",
854 endpoint,
855 8080,
856 &auth,
857 )
858 .await;
859
860 let request = upstream_saw.await.expect("the upstream task joins");
861 assert!(request.starts_with("GET /index.html"), "{request}");
862 assert!(
863 request.contains(&format!(
864 "{}: jwe-0",
865 PROXY_AUTH_HEADER.to_ascii_lowercase()
866 )),
867 "the minted auth header did not reach upstream: {request}"
868 );
869 assert!(
870 request.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
871 "the port header did not name the guest port: {request}"
872 );
873 assert!(request.contains("user-agent: probe"), "{request}");
874
875 assert!(seen.starts_with("HTTP/1.1 200 OK\r\n"), "{seen}");
876 assert!(seen.ends_with("hello"), "{seen}");
877 assert!(
878 matches!(
879 events.as_slice(),
880 [ForwardEvent::Forwarded {
881 status: 200,
882 upgraded: false
883 }]
884 ),
885 "{events:?}"
886 );
887
888 let requested = minter.requested();
891 assert!(
892 requested.iter().any(|ports| ports.contains(&8080)),
893 "the mint never asked for port 8080: {requested:?}"
894 );
895 }
896
897 #[tokio::test]
901 async fn the_caller_cannot_smuggle_its_own_proxy_headers_upstream() {
902 let (endpoint, upstream_saw) =
903 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
904 let (auth, _minter) = auth_pair();
905
906 let request = format!(
907 "GET / HTTP/1.1\r\nhost: local\r\n{PROXY_AUTH_HEADER}: forged-jwe\r\n\
908 {PROXY_PORT_HEADER}: 1\r\n\r\n"
909 );
910 let (_seen, _events) = exchange(&request, endpoint, 8080, &auth).await;
911
912 let saw = upstream_saw.await.expect("the upstream task joins");
913 assert!(
914 !saw.contains("forged-jwe"),
915 "a caller's forged credential was forwarded: {saw}"
916 );
917 assert!(
918 saw.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
919 "the minted port header should be the only one: {saw}"
920 );
921 assert!(
922 !saw.contains("host: local"),
923 "the local host header must not override the endpoint's: {saw}"
924 );
925 }
926
927 #[tokio::test]
928 async fn a_scope_refusal_is_answered_with_the_sentence_that_names_it() {
929 let (endpoint, upstream_saw) =
932 upstream("HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\n\r\n").await;
933 let (auth, _minter) = auth_pair();
934
935 let (seen, events) = exchange(
936 "GET / HTTP/1.1\r\nhost: local\r\n\r\n",
937 endpoint,
938 8080,
939 &auth,
940 )
941 .await;
942 let _ = upstream_saw.await;
943
944 assert!(seen.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{seen}");
945 assert!(seen.contains("scope does not cover"), "{seen}");
946 match events.as_slice() {
947 [
948 ForwardEvent::Refused {
949 status: 403,
950 explanation,
951 },
952 ] => {
953 assert!(explanation.contains("8080"), "{explanation}");
954 }
955 other => panic!("expected one 403 refusal, got {other:?}"),
956 }
957 }
958
959 #[tokio::test]
960 async fn a_dead_guest_server_is_reported_as_502_rather_than_as_an_auth_problem() {
961 let (endpoint, upstream_saw) =
962 upstream("HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\n\r\n").await;
963 let (auth, _minter) = auth_pair();
964
965 let (seen, events) = exchange(
966 "GET / HTTP/1.1\r\nhost: local\r\n\r\n",
967 endpoint,
968 5432,
969 &auth,
970 )
971 .await;
972 let _ = upstream_saw.await;
973
974 assert!(seen.contains("nothing listening on port 5432"), "{seen}");
975 assert!(
976 matches!(
977 events.as_slice(),
978 [ForwardEvent::Refused { status: 502, .. }]
979 ),
980 "{events:?}"
981 );
982 }
983
984 #[tokio::test]
985 async fn the_guests_own_404_is_relayed_and_not_reinterpreted() {
986 let (endpoint, upstream_saw) =
989 upstream("HTTP/1.1 404 Not Found\r\ncontent-length: 9\r\n\r\nno-route!").await;
990 let (auth, _minter) = auth_pair();
991
992 let (seen, events) = exchange(
993 "GET /nope HTTP/1.1\r\nhost: local\r\n\r\n",
994 endpoint,
995 8080,
996 &auth,
997 )
998 .await;
999 let _ = upstream_saw.await;
1000
1001 assert!(seen.starts_with("HTTP/1.1 404 Not Found\r\n"), "{seen}");
1002 assert!(seen.ends_with("no-route!"), "{seen}");
1003 assert!(
1004 matches!(
1005 events.as_slice(),
1006 [ForwardEvent::Forwarded { status: 404, .. }]
1007 ),
1008 "{events:?}"
1009 );
1010 }
1011
1012 #[tokio::test]
1013 async fn a_post_body_is_forwarded_and_the_response_length_is_re_derived() {
1014 let (endpoint, upstream_saw) = upstream(
1017 "HTTP/1.1 200 OK\r\ncontent-length: 3\r\ntransfer-encoding: identity\r\n\r\nack",
1018 )
1019 .await;
1020 let (auth, _minter) = auth_pair();
1021
1022 let (seen, _events) = exchange(
1023 "POST /submit HTTP/1.1\r\nhost: local\r\ncontent-length: 7\r\n\r\npayload",
1024 endpoint,
1025 8080,
1026 &auth,
1027 )
1028 .await;
1029 let saw = upstream_saw.await.expect("the upstream task joins");
1030
1031 assert!(saw.starts_with("POST /submit"), "{saw}");
1032 assert!(saw.contains("content-length: 7"), "{saw}");
1033 assert_eq!(
1034 seen.matches("content-length:").count(),
1035 1,
1036 "exactly one framing header should be written: {seen}"
1037 );
1038 assert!(seen.contains("content-length: 3\r\n"), "{seen}");
1039 assert!(
1040 !seen.to_ascii_lowercase().contains("transfer-encoding"),
1041 "the upstream framing header must not be copied: {seen}"
1042 );
1043 assert!(seen.ends_with("ack"), "{seen}");
1044 }
1045
1046 #[tokio::test]
1047 async fn a_101_relays_the_handshake_and_then_stops_parsing() {
1048 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1052 let addr = listener.local_addr().expect("bound");
1053 let guest = tokio::spawn(async move {
1054 let (mut socket, _) = listener.accept().await.expect("one connection");
1055 let _ = read_request_head(&mut socket)
1056 .await
1057 .expect("a request")
1058 .expect("present");
1059 socket
1060 .write_all(
1061 b"HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\n\
1062 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1063 )
1064 .await
1065 .expect("the handshake is written");
1066 socket.flush().await.expect("flushed");
1067 let mut frame = [0_u8; 5];
1069 tokio::io::AsyncReadExt::read_exact(&mut socket, &mut frame)
1070 .await
1071 .expect("the client's bytes arrive");
1072 socket
1073 .write_all(b"PONG")
1074 .await
1075 .expect("the answer is written");
1076 socket.flush().await.expect("flushed");
1077 frame.to_vec()
1078 });
1079
1080 let (auth, _minter) = auth_pair();
1081 let (mut client, server) = tokio::io::duplex(64 * 1024);
1082 let spec = ForwardSpec::new(
1083 "127.0.0.1:0".parse().expect("an addr"),
1084 8080,
1085 format!("http://{addr}"),
1086 );
1087 let http = ForwardClient::new().expect("a client");
1088 let served = tokio::spawn(async move {
1089 let mut events = Vec::new();
1090 let result =
1091 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
1092 (result, events)
1093 });
1094
1095 client
1096 .write_all(
1097 b"GET /socket HTTP/1.1\r\nhost: local\r\nupgrade: websocket\r\n\
1098 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1099 )
1100 .await
1101 .expect("the handshake request is written");
1102 client.flush().await.expect("flushed");
1103
1104 let mut head = Vec::new();
1107 let mut byte = [0_u8; 1];
1108 while !head.ends_with(b"\r\n\r\n") {
1109 tokio::io::AsyncReadExt::read_exact(&mut client, &mut byte)
1110 .await
1111 .expect("the handshake response arrives");
1112 head.push(byte[0]);
1113 }
1114 let head = String::from_utf8_lossy(&head).to_string();
1115 assert!(
1116 head.starts_with("HTTP/1.1 101 Switching Protocols\r\n"),
1117 "{head}"
1118 );
1119 assert!(
1120 head.to_ascii_lowercase()
1121 .contains("sec-websocket-protocol: my-app-protocol"),
1122 "the guest's negotiated subprotocol must reach the client: {head}"
1123 );
1124
1125 client
1126 .write_all(b"PING!")
1127 .await
1128 .expect("a frame is written");
1129 client.flush().await.expect("flushed");
1130 let mut answer = [0_u8; 4];
1131 tokio::io::AsyncReadExt::read_exact(&mut client, &mut answer)
1132 .await
1133 .expect("the guest's answer arrives");
1134 assert_eq!(&answer, b"PONG");
1135
1136 drop(client);
1137 let guest_saw = guest.await.expect("the guest task joins");
1138 assert_eq!(
1139 &guest_saw, b"PING!",
1140 "the client's bytes reached the guest unframed"
1141 );
1142
1143 let (result, events) = served.await.expect("the task joins");
1144 result.expect("the splice ends cleanly");
1145 assert!(
1146 events.iter().any(|event| matches!(
1147 event,
1148 ForwardEvent::Forwarded {
1149 status: 101,
1150 upgraded: true
1151 }
1152 )),
1153 "{events:?}"
1154 );
1155 }
1156
1157 #[tokio::test]
1158 async fn every_exchange_mints_through_the_cache_rather_than_capturing_headers_once() {
1159 let (auth, minter) = auth_pair();
1163
1164 for _ in 0..2 {
1165 let (endpoint, upstream_saw) =
1166 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
1167 let (_seen, _events) = exchange(
1168 "GET / HTTP/1.1\r\nhost: local\r\n\r\n",
1169 endpoint,
1170 8080,
1171 &auth,
1172 )
1173 .await;
1174 let saw = upstream_saw.await.expect("the upstream task joins");
1175 assert!(
1176 saw.contains(&PROXY_AUTH_HEADER.to_ascii_lowercase()),
1177 "every hop carries the minted credential: {saw}"
1178 );
1179 }
1180
1181 assert_eq!(
1185 auth.mint_count(),
1186 1,
1187 "the second hop should reuse the cached token rather than re-mint"
1188 );
1189 assert_eq!(minter.requested().len(), 1);
1190 }
1191
1192 #[tokio::test]
1193 async fn a_chunked_request_body_is_refused_by_name_rather_than_truncated() {
1194 let (mut client, mut server) = tokio::io::duplex(4096);
1197 client
1198 .write_all(b"POST /up HTTP/1.1\r\nhost: local\r\ntransfer-encoding: chunked\r\n\r\n")
1199 .await
1200 .expect("written");
1201 client.flush().await.expect("flushed");
1202
1203 let error = read_request_head(&mut server)
1204 .await
1205 .expect_err("chunked is refused");
1206 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1207 assert!(error.to_string().contains("chunked"), "{error}");
1208 }
1209
1210 #[tokio::test]
1211 async fn a_connection_that_closes_before_sending_anything_is_not_an_error() {
1212 let (client, mut server) = tokio::io::duplex(4096);
1215 drop(client);
1216 let head = read_request_head(&mut server)
1217 .await
1218 .expect("an empty connection is fine");
1219 assert!(head.is_none());
1220 }
1221
1222 #[tokio::test]
1223 async fn an_unterminated_request_head_is_capped_rather_than_buffered_without_bound() {
1224 let (mut client, mut server) = tokio::io::duplex(1024 * 1024);
1225 let writer = tokio::spawn(async move {
1226 let filler = vec![b'x'; MAX_REQUEST_HEAD_BYTES + 1024];
1228 let _ = client.write_all(b"GET / HTTP/1.1\r\nx: ").await;
1229 let _ = client.write_all(&filler).await;
1230 let _ = client.flush().await;
1231 });
1232
1233 let error = read_request_head(&mut server)
1234 .await
1235 .expect_err("the cap fires");
1236 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1237 assert!(error.to_string().contains("without terminating"), "{error}");
1238 writer.abort();
1239 }
1240
1241 #[test]
1242 fn only_a_101_counts_as_an_upgrade() {
1243 assert!(is_upgrade(101));
1247 assert!(!is_upgrade(200));
1248 assert!(!is_upgrade(426));
1249 }
1250
1251 #[test]
1252 fn a_bare_endpoint_host_is_read_as_https() {
1253 assert_eq!(
1256 upstream_url("vm-abc.example.aws", "/index.html"),
1257 "https://vm-abc.example.aws/index.html"
1258 );
1259 }
1260
1261 #[test]
1262 fn an_explicit_scheme_is_left_alone() {
1263 assert_eq!(
1264 upstream_url("https://vm-abc.example.aws/", "/a"),
1265 "https://vm-abc.example.aws/a"
1266 );
1267 assert_eq!(
1268 upstream_url("http://127.0.0.1:9000", "/a"),
1269 "http://127.0.0.1:9000/a"
1270 );
1271 }
1272
1273 #[test]
1274 fn a_target_without_a_leading_slash_still_forms_one_path() {
1275 assert_eq!(
1276 upstream_url("vm.example", "index.html"),
1277 "https://vm.example/index.html"
1278 );
1279 }
1280
1281 #[test]
1282 fn the_two_proxy_headers_are_never_copied_from_the_caller() {
1283 assert!(!forwards_request_header("x-aws-proxy-auth"));
1286 assert!(!forwards_request_header("X-AWS-PROXY-AUTH"));
1287 assert!(!forwards_request_header("x-aws-proxy-port"));
1288 assert!(!forwards_request_header("Host"));
1289 assert!(forwards_request_header("user-agent"));
1290 assert!(forwards_request_header("sec-websocket-protocol"));
1291 }
1292
1293 #[test]
1294 fn the_refusal_wording_separates_a_scope_mistake_from_a_dead_server() {
1295 let scope = refusal_explanation(403, 8080).expect("403 explains itself");
1298 assert!(scope.contains("scope does not cover"), "{scope}");
1299 assert!(scope.contains("8080"), "{scope}");
1300
1301 let dead = refusal_explanation(502, 8080).expect("502 explains itself");
1302 assert!(dead.contains("nothing listening"), "{dead}");
1303 assert!(dead.contains("8080"), "{dead}");
1304
1305 assert!(
1306 refusal_explanation(401, 8080).is_some(),
1307 "401 is worth naming"
1308 );
1309 assert!(
1310 refusal_explanation(200, 8080).is_none(),
1311 "a success explains nothing"
1312 );
1313 assert!(
1314 refusal_explanation(404, 8080).is_none(),
1315 "the guest's own 404 is not ours"
1316 );
1317 }
1318
1319 #[tokio::test]
1320 async fn a_bind_collision_names_the_address_and_suggests_another_port() {
1321 let held = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1322 let addr = held.local_addr().expect("a bound address");
1323
1324 let spec = ForwardSpec::new(addr, 8080, "vm.example");
1325 let error = bind(&spec).await.expect_err("the address is taken");
1326 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1327 assert!(error.to_string().contains(&addr.to_string()), "{error}");
1328 assert!(
1329 error.to_string().contains("different local port"),
1330 "{error}"
1331 );
1332 }
1333
1334 #[tokio::test]
1335 async fn a_local_error_response_is_a_readable_http_message() {
1336 let (mut client, mut server) = tokio::io::duplex(4096);
1339 let body = refusal_explanation(403, 8080).expect("wording");
1340 write_local_error(&mut server, 403, &body)
1341 .await
1342 .expect("the response is written");
1343 drop(server);
1344
1345 let mut got = Vec::new();
1346 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut got)
1347 .await
1348 .expect("the client reads it");
1349 let text = String::from_utf8(got).expect("utf-8");
1350 assert!(text.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{text}");
1351 assert!(text.contains("content-length: "), "{text}");
1352 assert!(text.ends_with(&body), "{text}");
1353 }
1354
1355 #[tokio::test]
1356 async fn splice_copies_both_directions_and_reports_the_byte_counts() {
1357 let (mut left_near, mut left_far) = tokio::io::duplex(4096);
1358 let (mut right_near, mut right_far) = tokio::io::duplex(4096);
1359
1360 let pump = tokio::spawn(async move { splice(&mut left_far, &mut right_near).await });
1361
1362 left_near.write_all(b"to-guest").await.expect("write");
1363 left_near.shutdown().await.expect("half close");
1364 right_far.write_all(b"from-guest!").await.expect("write");
1365 right_far.shutdown().await.expect("half close");
1366
1367 let mut to_guest = Vec::new();
1368 tokio::io::AsyncReadExt::read_to_end(&mut right_far, &mut to_guest)
1369 .await
1370 .expect("read");
1371 let mut from_guest = Vec::new();
1372 tokio::io::AsyncReadExt::read_to_end(&mut left_near, &mut from_guest)
1373 .await
1374 .expect("read");
1375
1376 let (a_to_b, b_to_a) = pump.await.expect("the task joins").expect("a clean splice");
1377 assert_eq!(to_guest, b"to-guest");
1378 assert_eq!(from_guest, b"from-guest!");
1379 assert_eq!(a_to_b, 8);
1380 assert_eq!(b_to_a, 11);
1381 }
1382}