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