1use super::{ListenerIsolation, ProxyContext};
8use anyhow::{Context as _, anyhow};
9use arti_client::{StreamPrefs, TorAddr};
10use futures::{AsyncRead, AsyncWrite, io::BufReader};
11use http::{Method, StatusCode, response::Builder as ResponseBuilder};
12use hyper::{Response, server::conn::http1::Builder as ServerBuilder, service::service_fn};
13use safelog::{Sensitive as Sv, sensitive as sv};
14use tor_error::{ErrorKind, ErrorReport as _, HasKind, into_internal, warn_report};
15use tor_rtcompat::Runtime;
16use tor_rtcompat::SpawnExt as _;
17use tracing::{instrument, warn};
18
19use hyper_futures_io::FuturesIoCompat;
20
21#[cfg(feature = "rpc")]
22use {crate::rpc::conntarget::ConnTarget, tor_rpcbase as rpc};
23
24cfg_if::cfg_if! {
25 if #[cfg(feature="rpc")] {
26 type ClientError = Box<dyn arti_client::rpc::ClientConnectionError>;
28 } else {
29 type ClientError = arti_client::Error;
31 }
32}
33
34type Request = hyper::Request<hyper::body::Incoming>;
36
37type Body = String;
43
44#[derive(Clone, Debug, Eq, PartialEq)]
46pub(super) struct Isolation {
47 proxy_auth: Option<ProxyAuthorization>,
49 x_tor_isolation: Option<String>,
51 tor_isolation: Option<String>,
53}
54
55impl Isolation {
56 pub(super) fn is_empty(&self) -> bool {
58 let Isolation {
59 proxy_auth,
60 x_tor_isolation,
61 tor_isolation,
62 } = self;
63 proxy_auth.as_ref().is_none_or(ProxyAuthorization::is_empty)
64 && x_tor_isolation.as_ref().is_none_or(String::is_empty)
65 && tor_isolation.as_ref().is_none_or(String::is_empty)
66 }
67}
68
69mod hdr {
71 pub(super) use http::header::{CONTENT_TYPE, HOST, PROXY_AUTHORIZATION, SERVER, VIA};
72
73 pub(super) const TOR_FAMILY_PREFERENCE: &str = "Tor-Family-Preference";
75
76 pub(super) const TOR_RPC_TARGET: &str = "Tor-RPC-Target";
78
79 pub(super) const X_TOR_STREAM_ISOLATION: &str = "X-Tor-Stream-Isolation";
82
83 pub(super) const TOR_STREAM_ISOLATION: &str = "Tor-Stream-Isolation";
85
86 pub(super) const TOR_CAPABILITIES: &str = "Tor-Capabilities";
88
89 pub(super) const TOR_REQUEST_FAILED: &str = "Tor-Request-Failed";
91
92 pub(super) const ALL_REQUEST_HEADERS: &[&str] = &[
97 TOR_FAMILY_PREFERENCE,
98 TOR_RPC_TARGET,
99 X_TOR_STREAM_ISOLATION,
100 TOR_STREAM_ISOLATION,
101 "Proxy-Authorization",
103 ];
104
105 pub(super) fn uniq_utf8(
109 map: &http::HeaderMap,
110 name: impl http::header::AsHeaderName,
111 ) -> Result<Option<&str>, super::HttpConnectError> {
112 let mut iter = map.get_all(name).iter();
113 let val = match iter.next() {
114 Some(v) => v,
115 None => return Ok(None),
116 };
117 match iter.next() {
118 Some(_) => Err(super::HttpConnectError::DuplicateHeader),
119 None => val
120 .to_str()
121 .map(Some)
122 .map_err(|_| super::HttpConnectError::HeaderNotUtf8),
123 }
124 }
125}
126
127#[instrument(skip_all, level = "trace")]
134pub(super) async fn handle_http_conn<R, S>(
135 context: super::ProxyContext<R>,
136 stream: BufReader<S>,
137 isolation_info: ListenerIsolation,
138) -> crate::Result<()>
139where
140 R: Runtime,
141 S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
142{
143 ServerBuilder::new()
146 .half_close(false)
147 .keep_alive(true)
148 .max_headers(256)
149 .max_buf_size(16 * 1024)
150 .title_case_headers(true)
151 .auto_date_header(false) .serve_connection(
153 FuturesIoCompat(stream),
154 service_fn(|request| handle_http_request::<R, S>(request, &context, isolation_info)),
155 )
156 .with_upgrades()
157 .await?;
158
159 Ok(())
160}
161
162async fn handle_http_request<R, S>(
166 request: Request,
167 context: &ProxyContext<R>,
168 listener_isolation: ListenerIsolation,
169) -> Result<Response<Body>, anyhow::Error>
170where
171 R: Runtime,
172 S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
173{
174 if request.method() != Method::CONNECT {
182 match hdr::uniq_utf8(request.headers(), hdr::HOST) {
183 Err(e) => return Err(e).context("Host header invalid. Rejecting request."),
184 Ok(Some(host)) if !host_is_localhost(host) => {
185 return Err(anyhow!(
186 "Host header {host:?} was not localhost. Rejecting request."
187 ));
188 }
189 Ok(_) => {}
190 }
191 }
192
193 match *request.method() {
194 Method::OPTIONS => handle_options_request(&request),
195 Method::CONNECT => {
196 handle_connect_request::<R, S>(request, context, listener_isolation).await
197 }
198 _ => Ok(ResponseBuilder::new()
199 .status(StatusCode::NOT_IMPLEMENTED)
200 .err(
201 request.method(),
202 format!("{} is not supported", request.method()),
203 )?),
204 }
205}
206
207fn handle_options_request(request: &Request) -> Result<Response<Body>, anyhow::Error> {
209 use hyper::body::Body as _;
210
211 let target = request.uri().to_string();
212 match target.as_str() {
213 "*" => {}
214 s if TorAddr::from(s).is_ok() => {}
215 _ => {
216 return Ok(ResponseBuilder::new()
217 .status(StatusCode::BAD_REQUEST)
218 .err(&Method::OPTIONS, "Target was not a valid address")?);
219 }
220 }
221 if request.headers().contains_key(hdr::CONTENT_TYPE) {
222 return Ok(ResponseBuilder::new()
225 .status(StatusCode::BAD_REQUEST)
226 .err(&Method::OPTIONS, "Unexpected Content-Type on OPTIONS")?);
227
228 }
231 if !request.body().is_end_stream() {
232 return Ok(ResponseBuilder::new()
233 .status(StatusCode::BAD_REQUEST)
234 .err(&Method::OPTIONS, "Unexpected body on OPTIONS request")?);
235 }
236
237 Ok(ResponseBuilder::new()
238 .header("Allow", "OPTIONS, CONNECT")
239 .status(StatusCode::OK)
240 .ok(&Method::OPTIONS)?)
241}
242
243async fn handle_connect_request<R, S>(
245 request: Request,
246 context: &ProxyContext<R>,
247 listener_isolation: ListenerIsolation,
248) -> anyhow::Result<Response<Body>>
249where
250 R: Runtime,
251 S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
252{
253 match handle_connect_request_impl::<R, S>(request, context, listener_isolation).await {
254 Ok(response) => Ok(response),
255 Err(e) => Ok(e.try_into_response()?),
256 }
257}
258
259async fn handle_connect_request_impl<R, S>(
264 request: Request,
265 context: &ProxyContext<R>,
266 listener_isolation: ListenerIsolation,
267) -> Result<Response<Body>, HttpConnectError>
268where
269 R: Runtime,
270 S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
271{
272 let target = request.uri().to_string();
273 let tor_addr =
274 TorAddr::from(&target).map_err(|e| HttpConnectError::InvalidStreamTarget(sv(target), e))?;
275
276 let mut stream_prefs = StreamPrefs::default();
277 set_family_preference(&mut stream_prefs, &tor_addr, request.headers())?;
278
279 set_isolation(&mut stream_prefs, request.headers(), listener_isolation)?;
280
281 let client = find_conn_target(
282 context,
283 hdr::uniq_utf8(request.headers(), hdr::TOR_RPC_TARGET)?,
284 )?;
285
286 let tor_stream = client
288 .connect_with_prefs(&tor_addr, &stream_prefs)
289 .await
290 .map_err(|e| HttpConnectError::ConnectFailed(sv(tor_addr), e))?;
291
292 context
296 .tor_client
297 .runtime()
298 .spawn(async move {
299 match transfer::<S>(request, tor_stream).await {
300 Ok(()) => {}
301 Err(e) => {
302 warn_report!(e, "Error while launching transfer");
303 }
304 }
305 })
306 .map_err(into_internal!("Unable to spawn transfer task"))?;
307
308 ResponseBuilder::new()
309 .status(StatusCode::OK)
310 .ok(&Method::CONNECT)
311}
312
313fn set_family_preference(
315 prefs: &mut StreamPrefs,
316 addr: &TorAddr,
317 headers: &http::HeaderMap,
318) -> Result<(), HttpConnectError> {
319 if let Some(val) = hdr::uniq_utf8(headers, hdr::TOR_FAMILY_PREFERENCE)? {
320 match val.trim() {
321 "ipv4-preferred" => prefs.ipv4_preferred(),
322 "ipv6-preferred" => prefs.ipv6_preferred(),
323 "ipv4-only" => prefs.ipv4_only(),
324 "ipv6-only" => prefs.ipv6_only(),
325 _ => return Err(HttpConnectError::InvalidFamilyPreference),
326 };
327 } else if let Some(ip) = addr.as_ip_address() {
328 if ip.is_ipv4() {
332 prefs.ipv4_only();
333 } else {
334 prefs.ipv6_only();
335 }
336 }
337
338 Ok(())
339}
340
341fn set_isolation(
343 prefs: &mut StreamPrefs,
344 headers: &http::HeaderMap,
345 listener_isolation: ListenerIsolation,
346) -> Result<(), HttpConnectError> {
347 let proxy_auth =
348 hdr::uniq_utf8(headers, hdr::PROXY_AUTHORIZATION)?.map(ProxyAuthorization::from_header);
349 let x_tor_isolation = hdr::uniq_utf8(headers, hdr::X_TOR_STREAM_ISOLATION)?.map(str::to_owned);
350 let tor_isolation = hdr::uniq_utf8(headers, hdr::TOR_STREAM_ISOLATION)?.map(str::to_owned);
351
352 let isolation = super::ProvidedIsolation::Http(Isolation {
353 proxy_auth,
354 x_tor_isolation,
355 tor_isolation,
356 });
357
358 let isolation = super::StreamIsolationKey(listener_isolation, isolation);
359 prefs.set_isolation(isolation);
360
361 Ok(())
362}
363
364#[derive(Debug, Clone, Eq, PartialEq)]
366pub(super) enum ProxyAuthorization {
367 Legacy(String),
369 Modern(Vec<u8>),
371}
372
373impl ProxyAuthorization {
374 fn from_header(value: &str) -> Self {
378 if let Some(result) = Self::modern_from_header(value) {
379 result
380 } else {
381 warn!(
382 "{} header in obsolete format. If you want isolation, use {}, \
383 or {} with Basic authentication and username 'tor-iso'",
384 hdr::PROXY_AUTHORIZATION,
385 hdr::X_TOR_STREAM_ISOLATION,
386 hdr::PROXY_AUTHORIZATION
387 );
388 Self::Legacy(value.to_owned())
389 }
390 }
391
392 fn modern_from_header(value: &str) -> Option<Self> {
394 use base64ct::Encoding as _;
395 let value = value.trim_ascii();
396 let (kind, value) = value.split_once(' ')?;
397 if kind != "Basic" {
398 return None;
399 }
400 let value = value.trim_ascii();
401 let decoded = base64ct::Base64::decode_vec(value).ok()?;
403 if decoded.starts_with(b"tor-iso:") {
404 Some(ProxyAuthorization::Modern(decoded))
405 } else {
406 None
407 }
408 }
409
410 fn is_empty(&self) -> bool {
412 match self {
413 ProxyAuthorization::Legacy(s) => s.is_empty(),
414 ProxyAuthorization::Modern(v) => v.is_empty(),
415 }
416 }
417}
418
419#[cfg(feature = "rpc")]
421fn find_conn_target<R: Runtime>(
422 context: &ProxyContext<R>,
423 rpc_target: Option<&str>,
424) -> Result<ConnTarget<R>, HttpConnectError> {
425 let Some(target_id) = rpc_target else {
426 return Ok(ConnTarget::Client(Box::new(context.tor_client.clone())));
427 };
428
429 let Some(rpc_mgr) = &context.rpc_mgr else {
430 return Err(HttpConnectError::NoRpcSupport);
431 };
432
433 let (context, object) = rpc_mgr
434 .lookup_object(&rpc::ObjectId::from(target_id))
435 .map_err(|_| HttpConnectError::RpcObjectNotFound)?;
436
437 Ok(ConnTarget::Rpc { object, context })
438}
439
440#[cfg(not(feature = "rpc"))]
444fn find_conn_target<R: Runtime>(
445 context: &ProxyContext<R>,
446 rpc_target: Option<&str>,
447) -> Result<arti_client::TorClient<R>, HttpConnectError> {
448 if rpc_target.is_some() {
449 Err(HttpConnectError::NoRpcSupport)
450 } else {
451 Ok(context.tor_client.clone())
452 }
453}
454
455trait RespBldExt {
457 fn ok(self, method: &Method) -> anyhow::Result<Response<Body>, HttpConnectError>;
459
460 fn err(
462 self,
463 method: &Method,
464 message: impl Into<String>,
465 ) -> Result<Response<Body>, HttpConnectError>;
466}
467
468impl RespBldExt for ResponseBuilder {
469 fn ok(self, method: &Method) -> Result<Response<Body>, HttpConnectError> {
470 let bld = add_common_headers(self, method);
471 Ok(bld
472 .body("".into())
473 .map_err(into_internal!("Formatting HTTP response"))?)
474 }
475
476 fn err(
477 self,
478 method: &Method,
479 message: impl Into<String>,
480 ) -> Result<Response<Body>, HttpConnectError> {
481 let bld = add_common_headers(self, method).header(hdr::CONTENT_TYPE, "text/plain");
482 Ok(bld
483 .body(message.into())
484 .map_err(into_internal!("Formatting HTTP response"))?)
485 }
486}
487
488fn capabilities() -> &'static str {
490 use std::sync::LazyLock;
491 static CAPS: LazyLock<String> = LazyLock::new(|| {
492 let mut caps = hdr::ALL_REQUEST_HEADERS.to_vec();
493 caps.sort();
494 caps.join(" ")
495 });
496
497 CAPS.as_str()
498}
499
500fn add_common_headers(mut bld: ResponseBuilder, method: &Method) -> ResponseBuilder {
502 bld = bld.header(hdr::TOR_CAPABILITIES, capabilities());
503 if let (Some(software), Some(version)) = (
504 option_env!("CARGO_PKG_NAME"),
505 option_env!("CARGO_PKG_VERSION"),
506 ) {
507 if method == Method::CONNECT {
508 bld = bld.header(
509 hdr::VIA,
510 format!("tor/1.0 tor-network ({software} {version})"),
511 );
512 } else {
513 bld = bld.header(hdr::SERVER, format!("tor/1.0 ({software} {version})"));
514 }
515 }
516 bld
517}
518
519#[derive(Clone, Debug, thiserror::Error)]
522enum HttpConnectError {
523 #[error("Invalid target address {0:?}")]
525 InvalidStreamTarget(Sv<String>, #[source] arti_client::TorAddrError),
526
527 #[error("Duplicate HTTP header found.")]
531 DuplicateHeader,
532
533 #[error("HTTP header value was not in UTF-8")]
537 HeaderNotUtf8,
538
539 #[error("Unrecognized value for {}", hdr::TOR_FAMILY_PREFERENCE)]
541 InvalidFamilyPreference,
542
543 #[error(
545 "Found {} header, but we are running without RPC support",
546 hdr::TOR_RPC_TARGET
547 )]
548 NoRpcSupport,
549
550 #[error("RPC target object not found")]
552 RpcObjectNotFound,
553
554 #[error("Unable to connect to {0}")]
556 ConnectFailed(Sv<TorAddr>, #[source] ClientError),
557
558 #[error("Internal error while handling request")]
560 Internal(#[from] tor_error::Bug),
561}
562
563impl HasKind for HttpConnectError {
564 fn kind(&self) -> ErrorKind {
565 use ErrorKind as EK;
566 use HttpConnectError as HCE;
567 match self {
568 HCE::InvalidStreamTarget(_, _)
569 | HCE::DuplicateHeader
570 | HCE::HeaderNotUtf8
571 | HCE::InvalidFamilyPreference
572 | HCE::RpcObjectNotFound => EK::LocalProtocolViolation,
573 HCE::NoRpcSupport => EK::FeatureDisabled,
574 HCE::ConnectFailed(_, e) => e.kind(),
575 HCE::Internal(e) => e.kind(),
576 }
577 }
578}
579
580impl HttpConnectError {
581 fn status_code(&self) -> StatusCode {
583 use HttpConnectError as HCE; use StatusCode as SC;
585 if let Some(end_reason) = self.remote_end_reason() {
586 return end_reason_to_http_status(end_reason);
587 }
588 match self {
589 HCE::InvalidStreamTarget(_, _)
590 | HCE::DuplicateHeader
591 | HCE::HeaderNotUtf8
592 | HCE::InvalidFamilyPreference
593 | HCE::RpcObjectNotFound
594 | HCE::NoRpcSupport => SC::BAD_REQUEST,
595 HCE::ConnectFailed(_, e) => e.kind().http_status_code(),
596 HCE::Internal(e) => e.kind().http_status_code(),
597 }
598 }
599
600 fn try_into_response(self) -> Result<Response<Body>, HttpConnectError> {
602 let error_kind = self.kind();
603 let end_reason = self.remote_end_reason();
604 let status_code = self.status_code();
605 let mut request_failed = format!("arti/{error_kind:?}");
606 if let Some(end_reason) = end_reason {
607 request_failed.push_str(&format!(" end/{end_reason}"));
608 }
609
610 ResponseBuilder::new()
611 .status(status_code)
612 .header(hdr::TOR_REQUEST_FAILED, request_failed)
613 .err(&Method::CONNECT, self.report().to_string())
614 }
615
616 fn remote_end_reason(&self) -> Option<tor_cell::relaycell::msg::EndReason> {
623 use tor_proto::Error::EndReceived;
624 if let Some(EndReceived(reason)) = super::extract_proto_err(self) {
625 Some(*reason)
626 } else {
627 None
628 }
629 }
630}
631
632fn end_reason_to_http_status(end_reason: tor_cell::relaycell::msg::EndReason) -> StatusCode {
642 use StatusCode as S;
643 use tor_cell::relaycell::msg::EndReason as R;
644 match end_reason {
645 R::CONNECTREFUSED => S::FORBIDDEN, R::MISC | R::NOTDIRECTORY => S::INTERNAL_SERVER_ERROR,
649
650 R::DESTROY | R::DONE | R::HIBERNATING | R::INTERNAL | R::RESOURCELIMIT | R::TORPROTOCOL => {
652 S::BAD_GATEWAY
653 }
654 R::CONNRESET | R::EXITPOLICY | R::NOROUTE | R::RESOLVEFAILED => S::SERVICE_UNAVAILABLE,
656
657 R::TIMEOUT => S::GATEWAY_TIMEOUT,
659
660 _ => S::INTERNAL_SERVER_ERROR, }
663}
664
665fn deconstruct_upgrade<S>(upgraded: hyper::upgrade::Upgraded) -> Result<BufReader<S>, anyhow::Error>
667where
668 S: AsyncRead + AsyncWrite + Unpin + 'static,
669{
670 let parts: hyper::upgrade::Parts<FuturesIoCompat<BufReader<S>>> = upgraded
671 .downcast()
672 .map_err(|_| anyhow!("downcast failed!"))?;
673 let hyper::upgrade::Parts { io, read_buf, .. } = parts;
674 if !read_buf.is_empty() {
675 return Err(anyhow!(
678 "Extraneous data on hyper buffer after upgrade to proxy mode"
679 ));
680 }
681 let io: BufReader<S> = io.0;
682 Ok(io)
683}
684
685async fn transfer<S>(request: Request, tor_stream: arti_client::DataStream) -> anyhow::Result<()>
688where
689 S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
690{
691 let upgraded = hyper::upgrade::on(request)
692 .await
693 .context("Unable to upgrade connection")?;
694 let app_stream: BufReader<S> = deconstruct_upgrade(upgraded)?;
695 let tor_stream = BufReader::with_capacity(super::APP_STREAM_BUF_LEN, tor_stream);
696
697 let _ = futures_copy::copy_buf_bidirectional(
700 app_stream,
701 tor_stream,
702 futures_copy::eof::Close,
703 futures_copy::eof::Close,
704 )
705 .await?;
706
707 Ok(())
708}
709
710fn host_is_localhost(host: &str) -> bool {
712 if let Ok(addr) = host.parse::<std::net::SocketAddr>() {
713 addr.ip().is_loopback()
714 } else if let Ok(ip) = host.parse::<std::net::IpAddr>() {
715 ip.is_loopback()
716 } else if let Some((addr, port)) = host.split_once(':') {
717 port.parse::<std::num::NonZeroU16>().is_ok() && addr.eq_ignore_ascii_case("localhost")
718 } else {
719 host.eq_ignore_ascii_case("localhost")
720 }
721}
722
723mod hyper_futures_io {
728 use pin_project::pin_project;
729 use std::{
730 io,
731 pin::Pin,
732 task::{Context, Poll, ready},
733 };
734
735 use hyper::rt::ReadBufCursor;
736
737 #[derive(Debug)]
739 #[pin_project]
740 pub(super) struct FuturesIoCompat<T>(#[pin] pub(super) T);
741
742 impl<T> hyper::rt::Read for FuturesIoCompat<T>
743 where
744 T: futures::io::AsyncBufRead,
746 {
747 fn poll_read(
748 self: Pin<&mut Self>,
749 cx: &mut Context<'_>,
750 mut buf: ReadBufCursor<'_>,
751 ) -> Poll<Result<(), io::Error>> {
752 let mut this = self.project();
753
754 let available: &[u8] = ready!(this.0.as_mut().poll_fill_buf(cx))?;
755 let n_available = available.len();
756
757 if !available.is_empty() {
758 buf.put_slice(available);
759 this.0.consume(n_available);
760 }
761
762 Poll::Ready(Ok(()))
764 }
765 }
766
767 impl<T> hyper::rt::Write for FuturesIoCompat<T>
768 where
769 T: futures::io::AsyncWrite,
770 {
771 fn poll_write(
772 self: Pin<&mut Self>,
773 cx: &mut Context<'_>,
774 buf: &[u8],
775 ) -> Poll<Result<usize, std::io::Error>> {
776 self.project().0.poll_write(cx, buf)
777 }
778
779 fn poll_flush(
780 self: Pin<&mut Self>,
781 cx: &mut Context<'_>,
782 ) -> Poll<Result<(), std::io::Error>> {
783 self.project().0.poll_flush(cx)
784 }
785
786 fn poll_shutdown(
787 self: Pin<&mut Self>,
788 cx: &mut Context<'_>,
789 ) -> Poll<Result<(), std::io::Error>> {
790 self.project().0.poll_close(cx)
791 }
792 }
793}
794
795#[cfg(test)]
796mod test {
797 #![allow(clippy::bool_assert_comparison)]
799 #![allow(clippy::clone_on_copy)]
800 #![allow(clippy::dbg_macro)]
801 #![allow(clippy::mixed_attributes_style)]
802 #![allow(clippy::print_stderr)]
803 #![allow(clippy::print_stdout)]
804 #![allow(clippy::single_char_pattern)]
805 #![allow(clippy::unwrap_used)]
806 #![allow(clippy::unchecked_time_subtraction)]
807 #![allow(clippy::useless_vec)]
808 #![allow(clippy::needless_pass_by_value)]
809 use arti_client::{BootstrapBehavior, TorClient, config::TorClientConfigBuilder};
812 use futures::{AsyncReadExt as _, AsyncWriteExt as _};
813 use tor_rtmock::{MockRuntime, io::stream_pair};
814
815 use super::*;
816
817 #[test]
819 fn headermap_casei() {
820 use http::header::{HeaderMap, HeaderValue};
821 let mut hm = HeaderMap::new();
822 hm.append(
823 "my-head-is-a-house-for",
824 HeaderValue::from_str("a-secret").unwrap(),
825 );
826 assert_eq!(
827 hm.get("My-Head-Is-A-House-For").unwrap().as_bytes(),
828 b"a-secret"
829 );
830 assert_eq!(
831 hm.get("MY-HEAD-IS-A-HOUSE-FOR").unwrap().as_bytes(),
832 b"a-secret"
833 );
834 }
835
836 #[test]
837 fn host_header_localhost() {
838 assert_eq!(host_is_localhost("localhost"), true);
839 assert_eq!(host_is_localhost("localhost:9999"), true);
840 assert_eq!(host_is_localhost("localHOSt:9999"), true);
841 assert_eq!(host_is_localhost("127.0.0.1:9999"), true);
842 assert_eq!(host_is_localhost("[::1]:9999"), true);
843 assert_eq!(host_is_localhost("127.1.2.3:1234"), true);
844 assert_eq!(host_is_localhost("127.0.0.1"), true);
845 assert_eq!(host_is_localhost("::1"), true);
846
847 assert_eq!(host_is_localhost("[::1]"), false); assert_eq!(host_is_localhost("www.torproject.org"), false);
849 assert_eq!(host_is_localhost("www.torproject.org:1234"), false);
850 assert_eq!(host_is_localhost("localhost:0"), false);
851 assert_eq!(host_is_localhost("localhost:999999"), false);
852 assert_eq!(host_is_localhost("plocalhost:1234"), false);
853 assert_eq!(host_is_localhost("[::0]:1234"), false);
854 assert_eq!(host_is_localhost("192.0.2.55:1234"), false);
855 assert_eq!(host_is_localhost("3fff::1"), false);
856 assert_eq!(host_is_localhost("[3fff::1]:1234"), false);
857 }
858
859 fn interactive_test_setup(
860 rt: &MockRuntime,
861 ) -> anyhow::Result<(
862 tor_rtmock::io::LocalStream,
863 impl Future<Output = anyhow::Result<()>>,
864 tempfile::TempDir,
865 )> {
866 let (s1, s2) = stream_pair();
867 let s1: BufReader<_> = BufReader::new(s1);
868
869 let iso: ListenerIsolation = (7, "127.0.0.1".parse().unwrap());
870 let dir = tempfile::TempDir::new().unwrap();
871 let cfg = TorClientConfigBuilder::from_directories(
872 dir.as_ref().join("state"),
873 dir.as_ref().join("cache"),
874 )
875 .build()
876 .unwrap();
877 let tor_client = TorClient::with_runtime(rt.clone())
878 .config(cfg)
879 .bootstrap_behavior(BootstrapBehavior::Manual)
880 .create_unbootstrapped()?;
881 let context: ProxyContext<_> = ProxyContext {
882 tor_client,
883 #[cfg(feature = "rpc")]
884 rpc_mgr: None,
885 protocols: crate::proxy::ListenProtocols::SocksAndHttpConnect,
886 };
887 let handle = rt.spawn_join("HTTP Handler", handle_http_conn(context, s1, iso));
888 Ok((s2, handle, dir))
889 }
890
891 #[test]
892 fn successful_options_test() -> anyhow::Result<()> {
893 MockRuntime::try_test_with_various(async |rt| -> anyhow::Result<()> {
898 let (mut s, join, _dir) = interactive_test_setup(&rt)?;
899
900 s.write_all(b"OPTIONS * HTTP/1.0\r\nHost: localhost\r\n\r\n")
901 .await?;
902 let mut buf = Vec::new();
903 let _n_read = s.read_to_end(&mut buf).await?;
904 let () = join.await?;
905
906 let reply = std::str::from_utf8(&buf)?;
907 assert!(dbg!(reply).starts_with("HTTP/1.0 200 OK\r\n"));
908
909 Ok(())
910 })
911 }
912
913 #[test]
914 fn invalid_host_test() -> anyhow::Result<()> {
915 MockRuntime::try_test_with_various(async |rt| -> anyhow::Result<()> {
918 let (mut s, join, _dir) = interactive_test_setup(&rt)?;
919
920 s.write_all(b"OPTIONS * HTTP/1.0\r\nHost: csrf.example.com\r\n\r\n")
921 .await?;
922 let mut buf = Vec::new();
923 let n_read = s.read_to_end(&mut buf).await?;
924 let http_outcome = join.await;
925
926 assert_eq!(n_read, 0);
927 assert!(buf.is_empty());
928 assert!(http_outcome.is_err());
929
930 let error_msg = http_outcome.unwrap_err().source().unwrap().to_string();
931 assert_eq!(
932 error_msg,
933 r#"Host header "csrf.example.com" was not localhost. Rejecting request."#
934 );
935
936 Ok(())
937 })
938 }
939}