1
1
//! High-level layer for making http(s) requests the Tor network as a client.
2
//!
3
//! This can be used by applications which embed Arti,
4
//! and could also be used as an example of how to build on top of [`arti_client`].
5
//!
6
//! There is an example program [`hyper.rs`] which uses `arti-hyper`
7
//! to connect to Tor and make a single HTTP\[S] request.
8
//!
9
//! [`hyper.rs`]: <https://gitlab.torproject.org/tpo/core/arti/-/blob/main/crates/arti-hyper/examples/hyper.rs>
10

            
11
#![deny(missing_docs)]
12
#![warn(noop_method_call)]
13
#![deny(unreachable_pub)]
14
#![warn(clippy::all)]
15
#![deny(clippy::await_holding_lock)]
16
#![deny(clippy::cargo_common_metadata)]
17
#![deny(clippy::cast_lossless)]
18
#![deny(clippy::checked_conversions)]
19
#![warn(clippy::cognitive_complexity)]
20
#![deny(clippy::debug_assert_with_mut_call)]
21
#![deny(clippy::exhaustive_enums)]
22
#![deny(clippy::exhaustive_structs)]
23
#![deny(clippy::expl_impl_clone_on_copy)]
24
#![deny(clippy::fallible_impl_from)]
25
#![deny(clippy::implicit_clone)]
26
#![deny(clippy::large_stack_arrays)]
27
#![warn(clippy::manual_ok_or)]
28
#![deny(clippy::missing_docs_in_private_items)]
29
#![deny(clippy::missing_panics_doc)]
30
#![warn(clippy::needless_borrow)]
31
#![warn(clippy::needless_pass_by_value)]
32
#![warn(clippy::option_option)]
33
#![warn(clippy::rc_buffer)]
34
#![deny(clippy::ref_option_ref)]
35
#![warn(clippy::semicolon_if_nothing_returned)]
36
#![warn(clippy::trait_duplication_in_bounds)]
37
#![deny(clippy::unnecessary_wraps)]
38
#![warn(clippy::unseparated_literal_suffix)]
39
#![deny(clippy::unwrap_used)]
40

            
41
use std::future::Future;
42
use std::io::Error;
43
use std::pin::Pin;
44
use std::sync::Arc;
45
use std::task::{Context, Poll};
46

            
47
use arti_client::{DataStream, IntoTorAddr, TorClient};
48
use educe::Educe;
49
use hyper::client::connect::{Connected, Connection};
50
use hyper::http::uri::Scheme;
51
use hyper::http::Uri;
52
use hyper::service::Service;
53
use pin_project::pin_project;
54
use thiserror::Error;
55
use tls_api::TlsConnector as TlsConn; // This is different from tor_rtompat::TlsConnector
56
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
57
use tor_rtcompat::Runtime;
58

            
59
/// Error making or using http connection
60
///
61
/// This error ends up being passed to hyper and bundled up into a [`hyper::Error`]
62
#[derive(Error, Clone, Debug)]
63
#[non_exhaustive]
64
pub enum ConnectionError {
65
    /// Unsupported URI scheme
66
    #[error("unsupported URI scheme in {uri:?}")]
67
    UnsupportedUriScheme {
68
        /// URI
69
        uri: Uri,
70
    },
71

            
72
    /// Missing hostname
73
    #[error("Missing hostname in {uri:?}")]
74
    MissingHostname {
75
        /// URI
76
        uri: Uri,
77
    },
78

            
79
    /// Tor connection failed
80
    #[error("Tor connection failed")]
81
    Arti(#[from] arti_client::Error),
82

            
83
    /// TLS connection failed
84
    #[error("TLS connection failed")]
85
    TLS(#[source] Arc<anyhow::Error>),
86
}
87

            
88
/// We implement this for form's sake
89
impl tor_error::HasKind for ConnectionError {
90
    #[rustfmt::skip]
91
    fn kind(&self) -> tor_error::ErrorKind {
92
        use ConnectionError as CE;
93
        use tor_error::ErrorKind as EK;
94
        match self {
95
            CE::UnsupportedUriScheme{..} => EK::NotImplemented,
96
            CE::MissingHostname{..}      => EK::BadApiUsage,
97
            CE::Arti(e)                  => e.kind(),
98
            CE::TLS(_)                   => EK::RemoteProtocolFailed,
99
        }
100
    }
101
}
102

            
103
/// **Main entrypoint**: `hyper` connector to make HTTP\[S] connections via Tor, using Arti.
104
///
105
/// An `ArtiHttpConnector` combines an Arti Tor client, and a TLS implementation,
106
/// in a form that can be provided to hyper
107
/// (e.g. to [`hyper::client::Builder`]'s `build` method)
108
/// so that hyper can speak HTTP and HTTPS to origin servers via Tor.
109
///
110
/// TC is the TLS to used *across* Tor to connect to the origin server.
111
/// For example, it could be a [`tls_api_native_tls::TlsConnector`].
112
/// This is a different Rust type to the TLS used *by* Tor to connect to relays etc.
113
/// It might even be a different underlying TLS implementation
114
/// (although that is usually not a particularly good idea).
115
#[derive(Educe)]
116
#[educe(Clone)] // #[derive(Debug)] infers an unwanted bound TC: Clone
117
pub struct ArtiHttpConnector<R: Runtime, TC: TlsConn> {
118
    /// The client
119
    client: TorClient<R>,
120

            
121
    /// TLS for using across Tor.
122
    tls_conn: Arc<TC>,
123
}
124

            
125
// #[derive(Clone)] infers a TC: Clone bound
126

            
127
impl<R: Runtime, TC: TlsConn> ArtiHttpConnector<R, TC> {
128
    /// Make a new `ArtiHttpConnector` using an Arti `TorClient` object.
129
    pub fn new(client: TorClient<R>, tls_conn: TC) -> Self {
130
        let tls_conn = tls_conn.into();
131
        Self { client, tls_conn }
132
    }
133
}
134

            
135
/// Wrapper type that makes an Arti `DataStream` implement necessary traits to be used as
136
/// a `hyper` connection object (mainly `Connection`).
137
///
138
/// This might represent a bare HTTP connection across Tor,
139
/// or it might represent an HTTPS connection through Tor to an origin server,
140
/// `TC::TlsStream` as the TLS layer.
141
///
142
/// An `ArtiHttpConnection` is constructed by hyper's use of the [`ArtiHttpConnector`]
143
/// implementation of [`hyper::service::Service`],
144
/// and then used by hyper as the transport for hyper's HTTP implementation.
145
#[pin_project]
146
pub struct ArtiHttpConnection<TC: TlsConn> {
147
    /// The stream
148
    #[pin]
149
    inner: MaybeHttpsStream<TC>,
150
}
151

            
152
/// The actual actual stream; might be TLS, might not
153
#[pin_project(project = MaybeHttpsStreamProj)]
154
enum MaybeHttpsStream<TC: TlsConn> {
155
    /// http
156
    Http(Pin<Box<DataStream>>), // Tc:TlsStream is generally boxed; box this one too
157

            
158
    /// https
159
    Https(#[pin] TC::TlsStream),
160
}
161

            
162
impl<TC: TlsConn> Connection for ArtiHttpConnection<TC> {
163
    fn connected(&self) -> Connected {
164
        Connected::new()
165
    }
166
}
167

            
168
// These trait implementations just defer to the inner `DataStream`; the wrapper type is just
169
// there to implement the `Connection` trait.
170
impl<TC: TlsConn> AsyncRead for ArtiHttpConnection<TC> {
171
    fn poll_read(
172
        self: Pin<&mut Self>,
173
        cx: &mut Context<'_>,
174
        buf: &mut ReadBuf<'_>,
175
    ) -> Poll<Result<(), std::io::Error>> {
176
        match self.project().inner.project() {
177
            MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf),
178
            MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf),
179
        }
180
    }
181
}
182

            
183
impl<TC: TlsConn> AsyncWrite for ArtiHttpConnection<TC> {
184
    fn poll_write(
185
        self: Pin<&mut Self>,
186
        cx: &mut Context<'_>,
187
        buf: &[u8],
188
    ) -> Poll<Result<usize, Error>> {
189
        match self.project().inner.project() {
190
            MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf),
191
            MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf),
192
        }
193
    }
194

            
195
    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
196
        match self.project().inner.project() {
197
            MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx),
198
            MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx),
199
        }
200
    }
201

            
202
    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
203
        match self.project().inner.project() {
204
            MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx),
205
            MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx),
206
        }
207
    }
208
}
209

            
210
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
211
/// Are we doing TLS?
212
enum UseTls {
213
    /// No
214
    Bare,
215

            
216
    /// Yes
217
    Tls,
218
}
219

            
220
/// Convert uri to http\[s\] host and port, and whether to do tls
221
fn uri_to_host_port_tls(uri: Uri) -> Result<(String, u16, UseTls), ConnectionError> {
222
    let use_tls = {
223
        // Scheme doesn't derive PartialEq so can't be matched on
224
        let scheme = uri.scheme();
225
        if scheme == Some(&Scheme::HTTP) {
226
            UseTls::Bare
227
        } else if scheme == Some(&Scheme::HTTPS) {
228
            UseTls::Tls
229
        } else {
230
            return Err(ConnectionError::UnsupportedUriScheme { uri });
231
        }
232
    };
233
    let host = match uri.host() {
234
        Some(h) => h,
235
        _ => return Err(ConnectionError::MissingHostname { uri }),
236
    };
237
    let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls {
238
        UseTls::Tls => 443,
239
        UseTls::Bare => 80,
240
    });
241

            
242
    Ok((host.to_owned(), port, use_tls))
243
}
244

            
245
impl<R: Runtime, TC: TlsConn> Service<Uri> for ArtiHttpConnector<R, TC> {
246
    type Response = ArtiHttpConnection<TC>;
247
    type Error = ConnectionError;
248
    #[allow(clippy::type_complexity)]
249
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
250

            
251
    fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
252
        Poll::Ready(Ok(()))
253
    }
254

            
255
    fn call(&mut self, req: Uri) -> Self::Future {
256
        // `TorClient` objects can be cloned cheaply (the cloned objects refer to the same
257
        // underlying handles required to make Tor connections internally).
258
        // We use this to avoid the returned future having to borrow `self`.
259
        let client = self.client.clone();
260
        let tls_conn = self.tls_conn.clone();
261
        Box::pin(async move {
262
            // Extract the host and port to connect to from the URI.
263
            let (host, port, use_tls) = uri_to_host_port_tls(req)?;
264
            // Initiate a new Tor connection, producing a `DataStream` if successful.
265
            let addr = (&host as &str, port)
266
                .into_tor_addr()
267
                .map_err(arti_client::Error::from)?;
268
            let ds = client.connect(addr).await?;
269

            
270
            let inner = match use_tls {
271
                UseTls::Tls => {
272
                    let conn = tls_conn
273
                        .connect_impl_tls_stream(&host, ds)
274
                        .await
275
                        .map_err(|e| ConnectionError::TLS(e.into()))?;
276
                    MaybeHttpsStream::Https(conn)
277
                }
278
                UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()),
279
            };
280

            
281
            Ok(ArtiHttpConnection { inner })
282
        })
283
    }
284
}