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
497async fn serve_path<S>(port: u16, mut tls: S, handlers: &BTreeMap<String, ServeTarget>)
506where
507 S: AsyncRead + AsyncWrite + Unpin,
508{
509 let Some((buf, _end)) = read_http_head(&mut tls).await else {
510 tracing::debug!(%port, "serve path: incomplete/oversized request head; dropping conn");
511 return;
512 };
513 let Some(path) = request_path(&buf) else {
514 write_http_status(port, tls, "400 Bad Request").await;
515 return;
516 };
517
518 let matched = handlers
520 .iter()
521 .filter(|(prefix, _)| path.starts_with(prefix.as_str()))
522 .max_by_key(|(prefix, _)| prefix.len())
523 .map(|(_, target)| target);
524
525 let Some(target) = matched else {
526 write_http_status(port, tls, "404 Not Found").await;
527 return;
528 };
529
530 match target {
531 ServeTarget::Proxy { to } => proxy_to_backend_with_prefix(port, tls, to, &buf).await,
535 ServeTarget::Text { body } => write_text(port, tls, body).await,
536 ServeTarget::Redirect { to, status } => serve_redirect(port, tls, to, *status).await,
537 _ => {
541 tracing::warn!(%port, "serve path: unsupported nested target; dropping conn");
542 write_http_status(port, tls, "404 Not Found").await;
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 fn proxy(to: &str) -> ServeTarget {
552 ServeTarget::Proxy { to: to.into() }
553 }
554
555 #[test]
556 fn cap_is_bounded() {
557 assert_eq!(MAX_SERVE_CONNS_PER_PORT, 256);
558 }
559
560 #[test]
561 fn reconcile_adds_new_ports() {
562 let current = BTreeMap::new();
563 let mut next = BTreeMap::new();
564 next.insert(443u16, ServeTarget::Accept);
565 next.insert(8443u16, proxy("127.0.0.1:8080"));
566 let (add, remove) = pure_reconcile(¤t, &next);
567 assert_eq!(add, BTreeSet::from([443, 8443]));
568 assert!(remove.is_empty());
569 }
570
571 #[test]
572 fn reconcile_removes_dropped_ports() {
573 let mut current = BTreeMap::new();
574 current.insert(443u16, ServeTarget::Accept);
575 current.insert(8443u16, proxy("127.0.0.1:8080"));
576 let mut next = BTreeMap::new();
577 next.insert(443u16, ServeTarget::Accept);
578 let (add, remove) = pure_reconcile(¤t, &next);
579 assert!(add.is_empty());
580 assert_eq!(remove, BTreeSet::from([8443]));
581 }
582
583 #[test]
584 fn reconcile_changed_port_is_remove_and_add() {
585 let mut current = BTreeMap::new();
587 current.insert(443u16, proxy("127.0.0.1:8080"));
588 let mut next = BTreeMap::new();
589 next.insert(443u16, proxy("127.0.0.1:9090"));
590 let (add, remove) = pure_reconcile(¤t, &next);
591 assert_eq!(add, BTreeSet::from([443]));
592 assert_eq!(remove, BTreeSet::from([443]));
593 }
594
595 #[test]
596 fn reconcile_unchanged_port_is_noop() {
597 let mut current = BTreeMap::new();
598 current.insert(443u16, ServeTarget::Accept);
599 let next = current.clone();
600 let (add, remove) = pure_reconcile(¤t, &next);
601 assert!(add.is_empty());
602 assert!(remove.is_empty());
603 }
604
605 #[test]
606 fn terminates_tls_matches_dispatch_arm() {
607 assert!(ServeTarget::Accept.terminates_tls());
610 assert!(proxy("127.0.0.1:8080").terminates_tls());
611 assert!(ServeTarget::Text { body: "ok".into() }.terminates_tls());
612 assert!(
613 ServeTarget::Redirect {
614 to: "/elsewhere".into(),
615 status: 302,
616 }
617 .terminates_tls()
618 );
619 let mut handlers = BTreeMap::new();
620 handlers.insert("/".to_string(), proxy("127.0.0.1:8080"));
621 assert!(ServeTarget::Path { handlers }.terminates_tls());
622 assert!(
623 !ServeTarget::TcpForward {
624 to: "127.0.0.1:5000".into()
625 }
626 .terminates_tls()
627 );
628 }
629
630 #[test]
631 fn find_header_end_shared_with_peerapi_doh() {
632 assert_eq!(
636 crate::peerapi_doh::find_header_end(b"GET / HTTP/1.1\r\n\r\n"),
637 Some(18)
638 );
639 assert_eq!(
640 crate::peerapi_doh::find_header_end(b"GET / HTTP/1.1\r\n"),
641 None
642 );
643 }
644
645 #[test]
646 fn request_path_strips_query() {
647 assert_eq!(
648 request_path(b"GET /api/v1?x=1 HTTP/1.1\r\nHost: h\r\n\r\n").as_deref(),
649 Some("/api/v1")
650 );
651 assert_eq!(
652 request_path(b"GET / HTTP/1.1\r\n\r\n").as_deref(),
653 Some("/")
654 );
655 assert_eq!(request_path(b"not a request").as_deref(), None);
656 }
657
658 #[test]
659 fn request_path_none_on_malformed_request_line() {
660 assert_eq!(request_path(b"GARBAGE\r\n\r\n").as_deref(), None);
662 assert_eq!(request_path(b"").as_deref(), None);
664 }
665
666 #[test]
667 fn longest_prefix_wins() {
668 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
670 handlers.insert("/".to_string(), proxy("127.0.0.1:1"));
671 handlers.insert("/api".to_string(), proxy("127.0.0.1:2"));
672 handlers.insert("/api/v2".to_string(), proxy("127.0.0.1:3"));
673
674 let pick = |path: &str| -> Option<&ServeTarget> {
675 handlers
676 .iter()
677 .filter(|(prefix, _)| path.starts_with(prefix.as_str()))
678 .max_by_key(|(prefix, _)| prefix.len())
679 .map(|(_, target)| target)
680 };
681
682 assert_eq!(pick("/api/v2/x"), Some(&proxy("127.0.0.1:3")));
683 assert_eq!(pick("/api/v1"), Some(&proxy("127.0.0.1:2")));
684 assert_eq!(pick("/other"), Some(&proxy("127.0.0.1:1")));
685 }
686
687 #[test]
688 fn redirect_reason_known_statuses() {
689 assert_eq!(redirect_reason(301), "Moved Permanently");
690 assert_eq!(redirect_reason(308), "Permanent Redirect");
691 assert_eq!(redirect_reason(399), "Redirect");
692 }
693
694 use tokio::io::{AsyncReadExt, AsyncWriteExt};
695
696 async fn drain_to_string(mut client: tokio::io::DuplexStream) -> String {
699 let mut out = Vec::new();
700 drop(client.read_to_end(&mut out).await);
701 String::from_utf8(out).expect("server emitted valid utf8")
702 }
703
704 #[tokio::test]
705 async fn serve_redirect_emits_exact_response() {
706 let (client, server) = tokio::io::duplex(4096);
707 let t = tokio::spawn(async move {
708 serve_redirect(443, server, "/elsewhere", 302).await;
709 });
710 let got = drain_to_string(client).await;
711 t.await.unwrap();
712 assert_eq!(
713 got,
714 "HTTP/1.1 302 Found\r\nLocation: /elsewhere\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
715 );
716 }
717
718 #[tokio::test]
719 async fn write_http_status_emits_status_line() {
720 let (client, server) = tokio::io::duplex(4096);
721 let t = tokio::spawn(async move {
722 write_http_status(443, server, "404 Not Found").await;
723 });
724 let got = drain_to_string(client).await;
725 t.await.unwrap();
726 assert_eq!(
727 got,
728 "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
729 );
730
731 let (client, server) = tokio::io::duplex(4096);
732 let t = tokio::spawn(async move {
733 write_http_status(443, server, "400 Bad Request").await;
734 });
735 let got = drain_to_string(client).await;
736 t.await.unwrap();
737 assert_eq!(
738 got,
739 "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
740 );
741 }
742
743 #[tokio::test]
744 async fn read_http_head_reads_terminated_head() {
745 let (mut client, mut server) = tokio::io::duplex(4096);
746 client
747 .write_all(b"GET /api HTTP/1.1\r\nHost: h\r\n\r\nBODY")
748 .await
749 .unwrap();
750 drop(client);
751 let (buf, end) = read_http_head(&mut server).await.expect("complete head");
752 assert_eq!(&buf[..end], b"GET /api HTTP/1.1\r\nHost: h\r\n\r\n");
754 assert_eq!(&buf[end..], b"BODY");
755 }
756
757 #[tokio::test]
758 async fn read_http_head_none_on_early_eof() {
759 let (mut client, mut server) = tokio::io::duplex(4096);
760 client.write_all(b"GET / HTTP/1.1\r\n").await.unwrap();
761 drop(client); assert!(read_http_head(&mut server).await.is_none());
763 }
764
765 #[tokio::test]
766 async fn read_http_head_none_on_oversized_head() {
767 let (mut client, mut server) = tokio::io::duplex(64 * 1024);
768 let oversized = vec![b'a'; MAX_HTTP_HEAD + 1024];
770 client.write_all(&oversized).await.unwrap();
771 drop(client);
772 assert!(read_http_head(&mut server).await.is_none());
773 }
774
775 #[tokio::test]
776 async fn read_http_head_never_exceeds_max_head() {
777 let (mut client, mut server) = tokio::io::duplex(MAX_HTTP_HEAD + 16);
779 let mut head = vec![b'a'; MAX_HTTP_HEAD - 4];
780 head.extend_from_slice(b"\r\n\r\n");
781 assert_eq!(head.len(), MAX_HTTP_HEAD);
782 client.write_all(&head).await.unwrap();
783 drop(client);
784 let (buf, end) = read_http_head(&mut server).await.expect("head at bound");
785 assert_eq!(end, MAX_HTTP_HEAD);
786 assert!(buf.len() <= MAX_HTTP_HEAD);
787 }
788
789 #[tokio::test]
790 async fn proxy_with_prefix_writes_prefix_before_bidi_copy() {
791 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
795 let backend_addr = listener.local_addr().unwrap();
796
797 let prefix = b"GET /api HTTP/1.1\r\nHost: h\r\n\r\n";
798 let body = b"trailing-body-bytes";
799 let backend = tokio::spawn(async move {
800 let (mut sock, _) = listener.accept().await.unwrap();
801 let mut head = vec![0u8; prefix.len()];
802 sock.read_exact(&mut head).await.unwrap();
803 let mut rest = vec![0u8; body.len()];
804 sock.read_exact(&mut rest).await.unwrap();
805 (head, rest)
806 });
807
808 let (mut client, server) = tokio::io::duplex(4096);
810 let to = backend_addr.to_string();
811 let proxy_task = tokio::spawn(async move {
812 proxy_to_backend_with_prefix(443, server, &to, prefix).await;
813 });
814
815 client.write_all(body).await.unwrap();
817 drop(client);
818
819 let (head, rest) = backend.await.unwrap();
820 proxy_task.await.unwrap();
821 assert_eq!(
822 head, prefix,
823 "prefix (consumed head) replayed to backend first"
824 );
825 assert_eq!(rest, body, "remaining stream spliced after the prefix");
826 }
827
828 #[tokio::test]
829 async fn serve_path_proxy_replays_consumed_head_to_backend() {
830 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
833 let backend_addr = listener.local_addr().unwrap();
834 let request = b"GET /api/v2/x HTTP/1.1\r\nHost: h\r\n\r\n";
835 let backend = tokio::spawn(async move {
836 let (mut sock, _) = listener.accept().await.unwrap();
837 let mut head = vec![0u8; request.len()];
838 sock.read_exact(&mut head).await.unwrap();
839 head
840 });
841
842 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
843 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);
847 let path_task = tokio::spawn(async move {
848 serve_path(443, server, &handlers).await;
849 });
850 client.write_all(request).await.unwrap();
851 drop(client);
852
853 let head = backend.await.unwrap();
854 path_task.await.unwrap();
855 assert_eq!(
856 head, request,
857 "serve_path routed to the longest-prefix Proxy and replayed the consumed head"
858 );
859 }
860
861 #[tokio::test]
862 async fn serve_path_text_target_emits_body() {
863 let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
865 handlers.insert(
866 "/".to_string(),
867 ServeTarget::Text {
868 body: "root".into(),
869 },
870 );
871 handlers.insert(
872 "/hello".to_string(),
873 ServeTarget::Text {
874 body: "hello-body".into(),
875 },
876 );
877
878 let (mut client, server) = tokio::io::duplex(4096);
879 let t = tokio::spawn(async move {
880 serve_path(443, server, &handlers).await;
881 });
882 client
883 .write_all(b"GET /hello/world HTTP/1.1\r\nHost: h\r\n\r\n")
884 .await
885 .unwrap();
886 let got = drain_to_string(client).await;
889 t.await.unwrap();
890 assert_eq!(got, "hello-body");
891 }
892
893 }