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