1use std::{
39 collections::{BTreeMap, BTreeSet},
40 net::{Ipv4Addr, SocketAddr},
41 sync::{Arc, Mutex},
42};
43
44use netstack::{CreateSocket, netcore::Channel, netsock::TcpStream as OverlayStream};
45use tokio::{
46 io::{AsyncRead, AsyncWrite, AsyncWriteExt},
47 sync::{Semaphore, mpsc},
48};
49use ts_control::{ServeState, ServeTarget, tls::TlsAcceptor};
50
51const MAX_SERVE_CONNS_PER_PORT: usize = 256;
57
58pub struct ServeAccepted {
65 pub port: u16,
67 pub stream: Box<dyn AsyncReadWrite>,
69}
70
71pub trait AsyncReadWrite: AsyncRead + AsyncWrite + Send + Unpin {}
73impl<T: AsyncRead + AsyncWrite + Send + Unpin> AsyncReadWrite for T {}
74
75pub type ServeAcceptedReceiver = mpsc::Receiver<ServeAccepted>;
79
80pub struct ResolvedPort {
84 pub target: ServeTarget,
86 pub acceptor: Option<TlsAcceptor>,
88}
89
90struct Inner {
92 state: ServeState,
95 ports: BTreeMap<u16, tokio::task::AbortHandle>,
98}
99
100impl Drop for Inner {
101 fn drop(&mut self) {
102 for h in self.ports.values() {
103 h.abort();
104 }
105 }
106}
107
108pub struct ServeManager {
114 inner: Arc<Mutex<Inner>>,
115 channel: Channel,
116 self_ipv4: Ipv4Addr,
117}
118
119impl ServeManager {
120 pub fn new(channel: Channel, self_ipv4: Ipv4Addr) -> Self {
124 Self {
125 inner: Arc::new(Mutex::new(Inner {
126 state: ServeState::default(),
127 ports: BTreeMap::new(),
128 })),
129 channel,
130 self_ipv4,
131 }
132 }
133
134 pub fn get(&self) -> ServeState {
136 self.inner
137 .lock()
138 .unwrap_or_else(|e| e.into_inner())
139 .state
140 .clone()
141 }
142
143 pub fn set(
155 &self,
156 state: ServeState,
157 resolved: BTreeMap<u16, ResolvedPort>,
158 ) -> ServeAcceptedReceiver {
159 let (accept_tx, accept_rx) = mpsc::channel::<ServeAccepted>(MAX_SERVE_CONNS_PER_PORT);
161
162 let mut new_ports: BTreeMap<u16, tokio::task::AbortHandle> = BTreeMap::new();
163 for (port, rp) in resolved {
164 let channel = self.channel.clone();
165 let self_ipv4 = self.self_ipv4;
166 let accept_tx = accept_tx.clone();
167 let handle = tokio::spawn(async move {
168 if let Err(e) = run_port(channel, self_ipv4, port, rp, accept_tx).await {
169 tracing::warn!(%port, error = %e, "serve listener exited");
170 }
171 })
172 .abort_handle();
173 new_ports.insert(port, handle);
174 }
175
176 let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
179 inner.state = state;
180 let old = std::mem::replace(&mut inner.ports, new_ports);
181 drop(inner);
182 for h in old.values() {
183 h.abort();
184 }
185
186 accept_rx
187 }
188}
189
190#[cfg_attr(not(test), allow(dead_code))]
195fn pure_reconcile(
196 current: &BTreeMap<u16, ServeTarget>,
197 next: &BTreeMap<u16, ServeTarget>,
198) -> (BTreeSet<u16>, BTreeSet<u16>) {
199 let mut to_add = BTreeSet::new();
200 let mut to_remove = BTreeSet::new();
201 for (port, target) in next {
202 match current.get(port) {
203 Some(cur) if cur == target => {}
204 _ => {
205 to_add.insert(*port);
206 }
207 }
208 }
209 for port in current.keys() {
210 match next.get(port) {
211 Some(target) if current.get(port) == Some(target) => {}
212 _ => {
213 to_remove.insert(*port);
214 }
215 }
216 }
217 (to_add, to_remove)
218}
219
220async fn run_port(
223 channel: Channel,
224 self_ipv4: Ipv4Addr,
225 port: u16,
226 rp: ResolvedPort,
227 accept_tx: mpsc::Sender<ServeAccepted>,
228) -> Result<(), netstack::netcore::Error> {
229 let listen_addr = SocketAddr::new(self_ipv4.into(), port);
231 let listener = channel.tcp_listen(listen_addr).await?;
232 tracing::debug!(%port, "serve listener accepting");
233
234 let rp = Arc::new(rp);
235 let inflight = Arc::new(Semaphore::new(MAX_SERVE_CONNS_PER_PORT));
236
237 loop {
238 let Ok(permit) = inflight.clone().acquire_owned().await else {
240 return Ok(());
241 };
242 let overlay = listener.accept().await?;
243
244 let rp = rp.clone();
245 let accept_tx = accept_tx.clone();
246 tokio::spawn(async move {
247 let _permit = permit; dispatch_conn(port, overlay, rp, accept_tx).await;
249 });
250 }
251}
252
253async fn dispatch_conn(
256 port: u16,
257 overlay: OverlayStream,
258 rp: Arc<ResolvedPort>,
259 accept_tx: mpsc::Sender<ServeAccepted>,
260) {
261 match &rp.target {
262 ServeTarget::TcpForward { to } => {
264 forward_to_backend(port, overlay, to).await;
265 }
266 _ => {
268 let Some(acceptor) = rp.acceptor.as_ref() else {
269 tracing::warn!(%port, "serve: missing TLS acceptor for TLS port; dropping conn");
272 return;
273 };
274 let tls = match acceptor.accept(overlay).await {
275 Ok(s) => s,
276 Err(e) => {
277 tracing::debug!(%port, error = %e, "serve: TLS handshake failed; dropping conn");
278 return;
279 }
280 };
281 match &rp.target {
282 ServeTarget::Accept => {
283 let accepted = ServeAccepted {
285 port,
286 stream: Box::new(tls),
287 };
288 if accept_tx.send(accepted).await.is_err() {
289 tracing::debug!(%port, "serve: accept receiver dropped; closing conn");
290 }
291 }
292 ServeTarget::Proxy { to } => {
295 proxy_to_backend(port, tls, to).await;
296 }
297 ServeTarget::Text { body } => {
298 write_text(port, tls, body).await;
299 }
300 ServeTarget::Redirect { to, status } => {
301 serve_redirect(port, tls, to, *status).await;
302 }
303 ServeTarget::Path { handlers } => {
304 serve_path(port, tls, handlers).await;
305 }
306 other => {
311 debug_assert!(
312 !other.terminates_tls(),
313 "TLS-terminating ServeTarget reached fall-through arm"
314 );
315 tracing::warn!(%port, "serve: unhandled ServeTarget on TLS port; dropping conn");
316 }
317 }
318 }
319 }
320}
321
322async fn proxy_to_backend<S>(port: u16, tls: S, to: &str)
330where
331 S: AsyncRead + AsyncWrite + Unpin,
332{
333 proxy_to_backend_with_prefix(port, tls, to, &[]).await;
334}
335
336async fn proxy_to_backend_with_prefix<S>(port: u16, mut tls: S, to: &str, prefix: &[u8])
344where
345 S: AsyncRead + AsyncWrite + Unpin,
346{
347 let mut backend = match tokio::net::TcpStream::connect(to).await {
348 Ok(b) => b,
349 Err(e) => {
350 tracing::debug!(%port, %to, error = %e, "serve proxy: backend dial failed; dropping conn");
351 return;
352 }
353 };
354 if !prefix.is_empty()
355 && let Err(e) = backend.write_all(prefix).await
356 {
357 tracing::debug!(%port, %to, error = %e, "serve proxy: prefix replay failed; dropping conn");
358 return;
359 }
360 if let Err(e) = tokio::io::copy_bidirectional(&mut tls, &mut backend).await {
361 tracing::debug!(%port, %to, error = %e, "serve proxy: splice ended");
362 }
363}
364
365async fn forward_to_backend(port: u16, mut overlay: OverlayStream, to: &str) {
368 let mut backend = match tokio::net::TcpStream::connect(to).await {
369 Ok(b) => b,
370 Err(e) => {
371 tracing::debug!(%port, %to, error = %e, "serve forward: backend dial failed; dropping conn");
372 return;
373 }
374 };
375 if let Err(e) = tokio::io::copy_bidirectional(&mut overlay, &mut backend).await {
376 tracing::debug!(%port, %to, error = %e, "serve forward: splice ended");
377 }
378}
379
380async fn write_text<S>(port: u16, mut tls: S, body: &str)
382where
383 S: AsyncRead + AsyncWrite + Unpin,
384{
385 if let Err(e) = tls.write_all(body.as_bytes()).await {
386 tracing::debug!(%port, error = %e, "serve text: write failed");
387 return;
388 }
389 if let Err(e) = tls.flush().await {
390 tracing::debug!(%port, error = %e, "serve text: flush failed");
391 }
392 drop(tls.shutdown().await);
393}
394
395const MAX_HTTP_HEAD: usize = 8 * 1024;
399
400async fn read_http_head<S>(stream: &mut S) -> Option<(Vec<u8>, usize)>
405where
406 S: AsyncRead + AsyncWrite + Unpin,
407{
408 use tokio::io::AsyncReadExt;
409
410 let mut buf = Vec::with_capacity(1024);
411 let mut tmp = [0u8; 1024];
412 loop {
413 if let Some(end) = crate::peerapi_doh::find_header_end(&buf) {
414 return Some((buf, end));
415 }
416 match stream.read(&mut tmp).await {
417 Ok(0) => return None,
418 Ok(n) => {
419 buf.extend_from_slice(&tmp[..n]);
420 if crate::peerapi_doh::find_header_end(&buf).is_none() && buf.len() >= MAX_HTTP_HEAD
425 {
426 return None;
427 }
428 }
429 Err(_) => return None,
430 }
431 }
432}
433
434fn request_path(buf: &[u8]) -> Option<String> {
438 let mut headers = [httparse::EMPTY_HEADER; 32];
439 let mut req = httparse::Request::new(&mut headers);
440 match req.parse(buf) {
441 Ok(_) => {}
442 Err(_) => return None,
443 }
444 let path = req.path?;
445 let raw = path.split_once('?').map(|(p, _)| p).unwrap_or(path);
446 Some(raw.to_string())
447}
448
449fn redirect_reason(status: u16) -> &'static str {
451 match status {
452 301 => "Moved Permanently",
453 302 => "Found",
454 303 => "See Other",
455 307 => "Temporary Redirect",
456 308 => "Permanent Redirect",
457 _ => "Redirect",
458 }
459}
460
461async fn serve_redirect<S>(port: u16, mut tls: S, to: &str, status: u16)
465where
466 S: AsyncRead + AsyncWrite + Unpin,
467{
468 let head = format!(
469 "HTTP/1.1 {status} {reason}\r\nLocation: {to}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
470 reason = redirect_reason(status),
471 );
472 if let Err(e) = tls.write_all(head.as_bytes()).await {
473 tracing::debug!(%port, error = %e, "serve redirect: write failed");
474 return;
475 }
476 if let Err(e) = tls.flush().await {
477 tracing::debug!(%port, error = %e, "serve redirect: flush failed");
478 }
479 drop(tls.shutdown().await);
480}
481
482async fn write_http_status<S>(port: u16, mut tls: S, status: &str)
485where
486 S: AsyncRead + AsyncWrite + Unpin,
487{
488 let head = format!("HTTP/1.1 {status}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
489 if let Err(e) = tls.write_all(head.as_bytes()).await {
490 tracing::debug!(%port, error = %e, "serve path: status write failed");
491 return;
492 }
493 drop(tls.flush().await);
494 drop(tls.shutdown().await);
495}
496
497fn mount_claims_path(mount: &str, path: &str) -> bool {
518 let base = mount.strip_suffix('/').unwrap_or(mount);
520 if base.is_empty() {
521 return true;
522 }
523 match path.strip_prefix(base) {
524 Some(rest) => rest.is_empty() || rest.starts_with('/'),
527 None => false,
528 }
529}
530
531fn match_path_handler<'h>(
544 handlers: &'h BTreeMap<String, ServeTarget>,
545 path: &str,
546) -> Option<&'h ServeTarget> {
547 handlers
548 .iter()
549 .filter(|(mount, _)| mount_claims_path(mount, path))
550 .max_by_key(|(mount, _)| mount.strip_suffix('/').unwrap_or(mount).len())
551 .map(|(_, target)| target)
552}
553
554async fn serve_path<S>(port: u16, mut tls: S, handlers: &BTreeMap<String, ServeTarget>)
563where
564 S: AsyncRead + AsyncWrite + Unpin,
565{
566 let Some((buf, _end)) = read_http_head(&mut tls).await else {
567 tracing::debug!(%port, "serve path: incomplete/oversized request head; dropping conn");
568 return;
569 };
570 let Some(path) = request_path(&buf) else {
571 write_http_status(port, tls, "400 Bad Request").await;
572 return;
573 };
574
575 let Some(target) = match_path_handler(handlers, &path) else {
576 write_http_status(port, tls, "404 Not Found").await;
577 return;
578 };
579
580 match target {
581 ServeTarget::Proxy { to } => proxy_to_backend_with_prefix(port, tls, to, &buf).await,
585 ServeTarget::Text { body } => write_text(port, tls, body).await,
586 ServeTarget::Redirect { to, status } => serve_redirect(port, tls, to, *status).await,
587 _ => {
591 tracing::warn!(%port, "serve path: unsupported nested target; dropping conn");
592 write_http_status(port, tls, "404 Not Found").await;
593 }
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 fn proxy(to: &str) -> ServeTarget {
602 ServeTarget::Proxy { to: to.into() }
603 }
604
605 #[test]
606 fn cap_is_bounded() {
607 assert_eq!(MAX_SERVE_CONNS_PER_PORT, 256);
608 }
609
610 #[test]
611 fn reconcile_adds_new_ports() {
612 let current = BTreeMap::new();
613 let mut next = BTreeMap::new();
614 next.insert(443u16, ServeTarget::Accept);
615 next.insert(8443u16, proxy("127.0.0.1:8080"));
616 let (add, remove) = pure_reconcile(¤t, &next);
617 assert_eq!(add, BTreeSet::from([443, 8443]));
618 assert!(remove.is_empty());
619 }
620
621 #[test]
622 fn reconcile_removes_dropped_ports() {
623 let mut current = BTreeMap::new();
624 current.insert(443u16, ServeTarget::Accept);
625 current.insert(8443u16, proxy("127.0.0.1:8080"));
626 let mut next = BTreeMap::new();
627 next.insert(443u16, ServeTarget::Accept);
628 let (add, remove) = pure_reconcile(¤t, &next);
629 assert!(add.is_empty());
630 assert_eq!(remove, BTreeSet::from([8443]));
631 }
632
633 #[test]
634 fn reconcile_changed_port_is_remove_and_add() {
635 let mut current = BTreeMap::new();
637 current.insert(443u16, proxy("127.0.0.1:8080"));
638 let mut next = BTreeMap::new();
639 next.insert(443u16, proxy("127.0.0.1:9090"));
640 let (add, remove) = pure_reconcile(¤t, &next);
641 assert_eq!(add, BTreeSet::from([443]));
642 assert_eq!(remove, BTreeSet::from([443]));
643 }
644
645 #[test]
646 fn reconcile_unchanged_port_is_noop() {
647 let mut current = BTreeMap::new();
648 current.insert(443u16, ServeTarget::Accept);
649 let next = current.clone();
650 let (add, remove) = pure_reconcile(¤t, &next);
651 assert!(add.is_empty());
652 assert!(remove.is_empty());
653 }
654
655 #[test]
656 fn terminates_tls_matches_dispatch_arm() {
657 assert!(ServeTarget::Accept.terminates_tls());
660 assert!(proxy("127.0.0.1:8080").terminates_tls());
661 assert!(ServeTarget::Text { body: "ok".into() }.terminates_tls());
662 assert!(
663 ServeTarget::Redirect {
664 to: "/elsewhere".into(),
665 status: 302,
666 }
667 .terminates_tls()
668 );
669 let mut handlers = BTreeMap::new();
670 handlers.insert("/".to_string(), proxy("127.0.0.1:8080"));
671 assert!(ServeTarget::Path { handlers }.terminates_tls());
672 assert!(
673 !ServeTarget::TcpForward {
674 to: "127.0.0.1:5000".into()
675 }
676 .terminates_tls()
677 );
678 }
679
680 #[test]
681 fn find_header_end_shared_with_peerapi_doh() {
682 assert_eq!(
686 crate::peerapi_doh::find_header_end(b"GET / HTTP/1.1\r\n\r\n"),
687 Some(18)
688 );
689 assert_eq!(
690 crate::peerapi_doh::find_header_end(b"GET / HTTP/1.1\r\n"),
691 None
692 );
693 }
694
695 #[test]
696 fn request_path_strips_query() {
697 assert_eq!(
698 request_path(b"GET /api/v1?x=1 HTTP/1.1\r\nHost: h\r\n\r\n").as_deref(),
699 Some("/api/v1")
700 );
701 assert_eq!(
702 request_path(b"GET / HTTP/1.1\r\n\r\n").as_deref(),
703 Some("/")
704 );
705 assert_eq!(request_path(b"not a request").as_deref(), None);
706 }
707
708 #[test]
709 fn request_path_none_on_malformed_request_line() {
710 assert_eq!(request_path(b"GARBAGE\r\n\r\n").as_deref(), None);
712 assert_eq!(request_path(b"").as_deref(), None);
714 }
715
716 fn mux() -> BTreeMap<String, ServeTarget> {
719 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
720 handlers.insert("/".to_string(), proxy("127.0.0.1:1"));
721 handlers.insert("/api".to_string(), proxy("127.0.0.1:2"));
722 handlers.insert("/api/v2".to_string(), proxy("127.0.0.1:3"));
723 handlers
724 }
725
726 #[test]
727 fn longest_matching_mount_wins() {
728 let handlers = mux();
730 assert_eq!(
731 match_path_handler(&handlers, "/api/v2/x"),
732 Some(&proxy("127.0.0.1:3")),
733 "the longest mount claiming the path must win"
734 );
735 assert_eq!(
736 match_path_handler(&handlers, "/api/v1"),
737 Some(&proxy("127.0.0.1:2"))
738 );
739 assert_eq!(
740 match_path_handler(&handlers, "/api"),
741 Some(&proxy("127.0.0.1:2"))
742 );
743 assert_eq!(
744 match_path_handler(&handlers, "/other"),
745 Some(&proxy("127.0.0.1:1"))
746 );
747 }
748
749 #[test]
750 fn mount_does_not_claim_a_longer_first_segment() {
751 let handlers = mux();
755 let api = proxy("127.0.0.1:2");
756 let root = proxy("127.0.0.1:1");
757 for path in ["/apifoo", "/apibar", "/api-internal", "/api_v2", "/apis/x"] {
758 let picked = match_path_handler(&handlers, path);
759 assert_ne!(picked, Some(&api), "{path} must not reach the /api backend");
760 assert_eq!(
761 picked,
762 Some(&root),
763 "{path} must fall through to the / mount"
764 );
765 }
766 let picked = match_path_handler(&handlers, "/api/v20");
768 assert_ne!(
769 picked,
770 Some(&proxy("127.0.0.1:3")),
771 "/api/v20 must not reach the /api/v2 backend"
772 );
773 assert_eq!(picked, Some(&api));
774 }
775
776 #[test]
777 fn mount_claims_itself_and_paths_below_it() {
778 assert!(mount_claims_path("/api", "/api"));
779 assert!(mount_claims_path("/api", "/api/"));
780 assert!(mount_claims_path("/api", "/api/v2/x"));
781 assert!(!mount_claims_path("/api", "/apifoo"));
782 assert!(!mount_claims_path("/api", "/ap"));
783 assert!(!mount_claims_path("/api", "/"));
784 assert!(mount_claims_path("/", "/"));
786 assert!(mount_claims_path("/", "/anything/at/all"));
787 }
788
789 #[test]
790 fn trailing_slash_mount_needs_no_doubled_slash() {
791 assert!(mount_claims_path("/api/", "/api/v2"));
793 assert!(mount_claims_path("/api/", "/api/"));
794 assert!(mount_claims_path("/api/", "/api"));
795 assert!(!mount_claims_path("/api/", "/apifoo"));
796
797 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
798 handlers.insert("/".to_string(), proxy("127.0.0.1:1"));
799 handlers.insert("/api/".to_string(), proxy("127.0.0.1:2"));
800 assert_eq!(
801 match_path_handler(&handlers, "/api/v2"),
802 Some(&proxy("127.0.0.1:2"))
803 );
804 assert_eq!(
805 match_path_handler(&handlers, "/apifoo"),
806 Some(&proxy("127.0.0.1:1")),
807 "/apifoo must fall through to / even when the mount is spelled /api/"
808 );
809 }
810
811 #[test]
812 fn unmatched_path_selects_nothing() {
813 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
815 handlers.insert("/api".to_string(), proxy("127.0.0.1:2"));
816 assert_eq!(match_path_handler(&handlers, "/apifoo"), None);
817 assert_eq!(match_path_handler(&handlers, "/other"), None);
818 assert_eq!(
819 match_path_handler(&handlers, "/api/v2"),
820 Some(&proxy("127.0.0.1:2"))
821 );
822 }
823
824 #[test]
825 fn redirect_reason_known_statuses() {
826 assert_eq!(redirect_reason(301), "Moved Permanently");
827 assert_eq!(redirect_reason(308), "Permanent Redirect");
828 assert_eq!(redirect_reason(399), "Redirect");
829 }
830
831 use tokio::io::{AsyncReadExt, AsyncWriteExt};
832
833 async fn drain_to_string(mut client: tokio::io::DuplexStream) -> String {
836 let mut out = Vec::new();
837 drop(client.read_to_end(&mut out).await);
838 String::from_utf8(out).expect("server emitted valid utf8")
839 }
840
841 #[tokio::test]
842 async fn serve_redirect_emits_exact_response() {
843 let (client, server) = tokio::io::duplex(4096);
844 let t = tokio::spawn(async move {
845 serve_redirect(443, server, "/elsewhere", 302).await;
846 });
847 let got = drain_to_string(client).await;
848 t.await.unwrap();
849 assert_eq!(
850 got,
851 "HTTP/1.1 302 Found\r\nLocation: /elsewhere\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
852 );
853 }
854
855 #[tokio::test]
856 async fn write_http_status_emits_status_line() {
857 let (client, server) = tokio::io::duplex(4096);
858 let t = tokio::spawn(async move {
859 write_http_status(443, server, "404 Not Found").await;
860 });
861 let got = drain_to_string(client).await;
862 t.await.unwrap();
863 assert_eq!(
864 got,
865 "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
866 );
867
868 let (client, server) = tokio::io::duplex(4096);
869 let t = tokio::spawn(async move {
870 write_http_status(443, server, "400 Bad Request").await;
871 });
872 let got = drain_to_string(client).await;
873 t.await.unwrap();
874 assert_eq!(
875 got,
876 "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
877 );
878 }
879
880 #[tokio::test]
881 async fn read_http_head_reads_terminated_head() {
882 let (mut client, mut server) = tokio::io::duplex(4096);
883 client
884 .write_all(b"GET /api HTTP/1.1\r\nHost: h\r\n\r\nBODY")
885 .await
886 .unwrap();
887 drop(client);
888 let (buf, end) = read_http_head(&mut server).await.expect("complete head");
889 assert_eq!(&buf[..end], b"GET /api HTTP/1.1\r\nHost: h\r\n\r\n");
891 assert_eq!(&buf[end..], b"BODY");
892 }
893
894 #[tokio::test]
895 async fn read_http_head_none_on_early_eof() {
896 let (mut client, mut server) = tokio::io::duplex(4096);
897 client.write_all(b"GET / HTTP/1.1\r\n").await.unwrap();
898 drop(client); assert!(read_http_head(&mut server).await.is_none());
900 }
901
902 #[tokio::test]
903 async fn read_http_head_none_on_oversized_head() {
904 let (mut client, mut server) = tokio::io::duplex(64 * 1024);
905 let oversized = vec![b'a'; MAX_HTTP_HEAD + 1024];
907 client.write_all(&oversized).await.unwrap();
908 drop(client);
909 assert!(read_http_head(&mut server).await.is_none());
910 }
911
912 #[tokio::test]
913 async fn read_http_head_never_exceeds_max_head() {
914 let (mut client, mut server) = tokio::io::duplex(MAX_HTTP_HEAD + 16);
916 let mut head = vec![b'a'; MAX_HTTP_HEAD - 4];
917 head.extend_from_slice(b"\r\n\r\n");
918 assert_eq!(head.len(), MAX_HTTP_HEAD);
919 client.write_all(&head).await.unwrap();
920 drop(client);
921 let (buf, end) = read_http_head(&mut server).await.expect("head at bound");
922 assert_eq!(end, MAX_HTTP_HEAD);
923 assert!(buf.len() <= MAX_HTTP_HEAD);
924 }
925
926 #[tokio::test]
927 async fn proxy_with_prefix_writes_prefix_before_bidi_copy() {
928 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
932 let backend_addr = listener.local_addr().unwrap();
933
934 let prefix = b"GET /api HTTP/1.1\r\nHost: h\r\n\r\n";
935 let body = b"trailing-body-bytes";
936 let backend = tokio::spawn(async move {
937 let (mut sock, _) = listener.accept().await.unwrap();
938 let mut head = vec![0u8; prefix.len()];
939 sock.read_exact(&mut head).await.unwrap();
940 let mut rest = vec![0u8; body.len()];
941 sock.read_exact(&mut rest).await.unwrap();
942 (head, rest)
943 });
944
945 let (mut client, server) = tokio::io::duplex(4096);
947 let to = backend_addr.to_string();
948 let proxy_task = tokio::spawn(async move {
949 proxy_to_backend_with_prefix(443, server, &to, prefix).await;
950 });
951
952 client.write_all(body).await.unwrap();
954 drop(client);
955
956 let (head, rest) = backend.await.unwrap();
957 proxy_task.await.unwrap();
958 assert_eq!(
959 head, prefix,
960 "prefix (consumed head) replayed to backend first"
961 );
962 assert_eq!(rest, body, "remaining stream spliced after the prefix");
963 }
964
965 #[tokio::test]
966 async fn serve_path_proxy_replays_consumed_head_to_backend() {
967 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
970 let backend_addr = listener.local_addr().unwrap();
971 let request = b"GET /api/v2/x HTTP/1.1\r\nHost: h\r\n\r\n";
972 let backend = tokio::spawn(async move {
973 let (mut sock, _) = listener.accept().await.unwrap();
974 let mut head = vec![0u8; request.len()];
975 sock.read_exact(&mut head).await.unwrap();
976 head
977 });
978
979 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
980 handlers.insert("/".to_string(), proxy("127.0.0.1:1")); handlers.insert("/api/v2".to_string(), proxy(&backend_addr.to_string())); let (mut client, server) = tokio::io::duplex(4096);
984 let path_task = tokio::spawn(async move {
985 serve_path(443, server, &handlers).await;
986 });
987 client.write_all(request).await.unwrap();
988 drop(client);
989
990 let head = backend.await.unwrap();
991 path_task.await.unwrap();
992 assert_eq!(
993 head, request,
994 "serve_path routed to the longest-prefix Proxy and replayed the consumed head"
995 );
996 }
997
998 #[tokio::test]
999 async fn serve_path_text_target_emits_body() {
1000 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
1002 handlers.insert(
1003 "/".to_string(),
1004 ServeTarget::Text {
1005 body: "root".into(),
1006 },
1007 );
1008 handlers.insert(
1009 "/hello".to_string(),
1010 ServeTarget::Text {
1011 body: "hello-body".into(),
1012 },
1013 );
1014
1015 let (mut client, server) = tokio::io::duplex(4096);
1016 let t = tokio::spawn(async move {
1017 serve_path(443, server, &handlers).await;
1018 });
1019 client
1020 .write_all(b"GET /hello/world HTTP/1.1\r\nHost: h\r\n\r\n")
1021 .await
1022 .unwrap();
1023 let got = drain_to_string(client).await;
1026 t.await.unwrap();
1027 assert_eq!(got, "hello-body");
1028 }
1029
1030 #[tokio::test]
1031 async fn serve_path_does_not_route_a_longer_first_segment_to_the_shorter_mount() {
1032 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
1035 handlers.insert(
1036 "/".to_string(),
1037 ServeTarget::Text {
1038 body: "root".into(),
1039 },
1040 );
1041 handlers.insert(
1042 "/hello".to_string(),
1043 ServeTarget::Text {
1044 body: "hello-body".into(),
1045 },
1046 );
1047
1048 let (mut client, server) = tokio::io::duplex(4096);
1049 let t = tokio::spawn(async move {
1050 serve_path(443, server, &handlers).await;
1051 });
1052 client
1053 .write_all(b"GET /hellofoo HTTP/1.1\r\nHost: h\r\n\r\n")
1054 .await
1055 .unwrap();
1056 let got = drain_to_string(client).await;
1057 t.await.unwrap();
1058 assert_ne!(
1059 got, "hello-body",
1060 "/hellofoo must not reach the /hello mount"
1061 );
1062 assert_eq!(got, "root");
1063 }
1064
1065 }