1
//! Declare RPC functionality on for the `arti-client` crate.
2

            
3
use derive_deftly::Deftly;
4
use dyn_clone::DynClone;
5
use futures::{SinkExt as _, StreamExt as _};
6
use serde::{Deserialize, Serialize};
7
use std::{net::IpAddr, sync::Arc};
8
use tor_proto::stream::DataStream;
9

            
10
use tor_rpcbase as rpc;
11
use tor_rtcompat::Runtime;
12

            
13
use crate::{StreamPrefs, TorAddr, TorClient};
14

            
15
impl<R: Runtime> TorClient<R> {
16
    /// Ensure that every RPC method is registered for this instantiation of TorClient.
17
    ///
18
    /// We can't use [`rpc::static_rpc_invoke_fn`] for these, since TorClient is
19
    /// parameterized.
20
    pub fn rpc_methods() -> Vec<rpc::dispatch::InvokerEnt> {
21
        rpc::invoker_ent_list![
22
            get_client_status::<R>,
23
            watch_client_status::<R>,
24
            isolated_client::<R>,
25
            @special client_connect_with_prefs::<R>,
26
            @special client_resolve_with_prefs::<R>,
27
            @special client_resolve_ptr_with_prefs::<R>,
28
        ]
29
    }
30
}
31

            
32
/// Return current bootstrap and health information for a client.
33
#[derive(Deftly, Debug, Serialize, Deserialize)]
34
#[derive_deftly(rpc::DynMethod)]
35
#[deftly(rpc(method_name = "arti:get_client_status"))]
36
struct GetClientStatus {}
37

            
38
impl rpc::RpcMethod for GetClientStatus {
39
    type Output = ClientStatusInfo;
40
    type Update = rpc::NoUpdates;
41
}
42

            
43
/// Run forever, delivering updates about a client's bootstrap and health information.
44
///
45
/// (This method can return updates that have no visible changes.)
46
#[derive(Deftly, Debug, Serialize, Deserialize)]
47
#[derive_deftly(rpc::DynMethod)]
48
#[deftly(rpc(method_name = "arti:watch_client_status"))]
49
struct WatchClientStatus {}
50

            
51
impl rpc::RpcMethod for WatchClientStatus {
52
    type Output = rpc::Nil; // TODO: Possibly there should be an rpc::Never for methods that don't return.
53
    type Update = ClientStatusInfo;
54
}
55

            
56
/// Reported bootstrap and health information for a client.
57
///
58
/// Note that all `TorClient`s on a session share the same underlying bootstrap status:
59
/// if you check the status for one, you don't need to check the others.
60
#[derive(Serialize, Deserialize)]
61
struct ClientStatusInfo {
62
    /// True if the client is ready for traffic.
63
    ready: bool,
64
    /// Approximate estimate of how close the client is to being ready for traffic.
65
    ///
66
    /// This value is a rough approximation; its exact implementation may change over
67
    /// arti versions.  It is not guaranteed to be monotonic.
68
    fraction: f32,
69
    /// If present, a description of possible problem(s) that may be stopping
70
    /// the client from using the Tor network.
71
    blocked: Option<String>,
72
}
73

            
74
impl From<crate::status::BootstrapStatus> for ClientStatusInfo {
75
    fn from(s: crate::status::BootstrapStatus) -> Self {
76
        let ready = s.ready_for_traffic();
77
        let fraction = s.as_frac();
78
        let blocked = s.blocked().map(|b| b.to_string());
79
        Self {
80
            ready,
81
            fraction,
82
            blocked,
83
        }
84
    }
85
}
86

            
87
// NOTE: These functions could be defined as methods on TorClient<R>.
88
// I'm defining them like this to make it more clear that they are never
89
// invoked as client.method(), but only via the RPC system.
90
// We can revisit this later if we want.
91

            
92
// TODO RPC: Once we have one or two more get/watch combinations,
93
// we should look into some facility for automatically declaring them,
94
// so that their behavior stays uniform.
95
//
96
// See https://gitlab.torproject.org/tpo/core/arti/-/issues/1384#note_3023659
97

            
98
/// Invocable function to run [`GetClientStatus`] on a [`TorClient`].
99
async fn get_client_status<R: Runtime>(
100
    client: Arc<TorClient<R>>,
101
    _method: Box<GetClientStatus>,
102
    _ctx: Arc<dyn rpc::Context>,
103
) -> Result<ClientStatusInfo, rpc::RpcError> {
104
    Ok(client.bootstrap_status().into())
105
}
106

            
107
/// Invocable function to run [`WatchClientStatus`] on a [`TorClient`].
108
async fn watch_client_status<R: Runtime>(
109
    client: Arc<TorClient<R>>,
110
    _method: Box<WatchClientStatus>,
111
    _ctx: Arc<dyn rpc::Context>,
112
    mut updates: rpc::UpdateSink<ClientStatusInfo>,
113
) -> Result<rpc::Nil, rpc::RpcError> {
114
    let mut events = client.bootstrap_events();
115

            
116
    // Send the _current_ status, no matter what.
117
    // (We do this after constructing er)
118
    updates.send(client.bootstrap_status().into()).await?;
119

            
120
    // Send additional updates whenever the status changes.
121
    while let Some(status) = events.next().await {
122
        updates.send(status.into()).await?;
123
    }
124

            
125
    // This can only happen if the client exits.
126
    Ok(rpc::NIL)
127
}
128

            
129
/// Create a new isolated client instance.
130
///
131
/// Returned ObjectID is a handle for a new `TorClient`,
132
/// which is isolated from other `TorClients`:
133
/// any streams created with the new `TorClient` will not share circuits
134
/// with streams created with any other `TorClient`.
135
#[derive(Deftly, Debug, Serialize, Deserialize)]
136
#[derive_deftly(rpc::DynMethod)]
137
#[deftly(rpc(method_name = "arti:new_isolated_client"))]
138
#[non_exhaustive]
139
pub struct IsolatedClient {}
140

            
141
impl rpc::RpcMethod for IsolatedClient {
142
    type Output = rpc::SingleIdResponse;
143
    type Update = rpc::NoUpdates;
144
}
145

            
146
/// RPC method implementation: return a new isolated client based on a given client.
147
async fn isolated_client<R: Runtime>(
148
    client: Arc<TorClient<R>>,
149
    _method: Box<IsolatedClient>,
150
    ctx: Arc<dyn rpc::Context>,
151
) -> Result<rpc::SingleIdResponse, rpc::RpcError> {
152
    let new_client = Arc::new(client.isolated_client());
153
    let client_id = ctx.register_owned(new_client);
154
    Ok(rpc::SingleIdResponse::from(client_id))
155
}
156

            
157
/// Type-erased error returned by ClientConnectionTarget.
158
//
159
// TODO RPC: It would be handy if this implemented HasErrorHint, but HasErrorHint is sealed.
160
// Perhaps we could go and solve our problem by implementing HasErrorHint on dyn StdError?
161
pub trait ClientConnectionError:
162
    std::error::Error + tor_error::HasKind + DynClone + Send + Sync + seal::Sealed
163
{
164
}
165
impl<E> seal::Sealed for E where E: std::error::Error + tor_error::HasKind + DynClone + Send + Sync {}
166
impl<E> ClientConnectionError for E where
167
    E: std::error::Error + tor_error::HasKind + DynClone + Send + Sync + seal::Sealed
168
{
169
}
170
impl std::error::Error for Box<dyn ClientConnectionError> {
171
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172
        self.as_ref().source()
173
    }
174
}
175
impl tor_error::HasKind for Box<dyn ClientConnectionError> {
176
    fn kind(&self) -> tor_error::ErrorKind {
177
        self.as_ref().kind()
178
    }
179
}
180
dyn_clone::clone_trait_object!(ClientConnectionError);
181

            
182
/// module to seal the ClientConnectionError trait.
183
mod seal {
184
    /// hidden trait to seal the ClientConnectionError trait.
185
    #[allow(unreachable_pub)]
186
    pub trait Sealed {}
187
}
188

            
189
/// Type alias for a Result return by ClientConnectionTarget
190
pub type ClientConnectionResult<T> = Result<T, Box<dyn ClientConnectionError>>;
191

            
192
/// RPC special method: make a connection to a chosen address and preferences.
193
///
194
/// This method has no method name, and is not invoked by an RPC session
195
/// directly.  Instead, it is invoked in response to a SOCKS request.
196
/// It receives its target from the SOCKS `DEST` field.
197
/// The isolation information in its `SrreamPrefs`, if any, is taken from
198
/// the SOCKS username/password.
199
/// Other information in the `StreamPrefs` is inferred
200
/// from the SOCKS port configuration in the usual way.
201
///
202
/// When this method returns successfully,
203
/// the proxy code sends a SOCKS reply indicating success,
204
/// and links the returned `DataStream` with the application's incoming socket,
205
/// copying data back and forth.
206
///
207
/// If instead this method returns an error,
208
/// the error is either used to generate a SOCKS error code,
209
///
210
/// Note 1: in the future, this method will likely be used to integrate RPC data streams
211
/// with other proxy types other than SOCKS.
212
/// When this happens, we will specify how those proxy types
213
/// will provide `target` and `prefs`.
214
///
215
/// Note 2: This has to be a special method, because
216
/// it needs to return a DataStream, which can't be serialized.
217
///
218
/// > TODO RPC: The above documentation still isn't quite specific enough,
219
/// > and a lot of it belongs in socks.rs where it could explain how a SOCKS request
220
/// > is interpreted and converted into a ConnectWithPrefs call.
221
/// > See <https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/2373#note_3071833>
222
/// > for discussion.
223
#[derive(Deftly, Debug)]
224
#[derive_deftly(rpc::DynMethod)]
225
#[deftly(rpc(no_method_name))]
226
#[allow(clippy::exhaustive_structs)]
227
pub struct ConnectWithPrefs {
228
    /// The target address
229
    pub target: TorAddr,
230
    /// The stream preferences implied by the SOCKS connect request.
231
    pub prefs: StreamPrefs,
232
}
233
impl rpc::Method for ConnectWithPrefs {
234
    // TODO RPC: I am not sure that this is the error type we truly want.
235
    type Output = Result<DataStream, Box<dyn ClientConnectionError>>;
236
    type Update = rpc::NoUpdates;
237
}
238

            
239
/// RPC special method: lookup an address with a chosen address and preferences.
240
///
241
/// This method has no method name, and is not invoked by an RPC connection
242
/// directly.  Instead, it is invoked in response to a SOCKS request.
243
//
244
// TODO RPC: We _could_ give this a method name so that it can be invoked as an RPC method, and
245
// maybe we should.  First, however, we would need to make `StreamPrefs` an RPC-visible serializable
246
// type, or replace it with an equivalent.
247
#[derive(Deftly, Debug)]
248
#[derive_deftly(rpc::DynMethod)]
249
#[deftly(rpc(no_method_name))]
250
#[allow(clippy::exhaustive_structs)]
251
pub struct ResolveWithPrefs {
252
    /// The hostname to resolve.
253
    pub hostname: String,
254
    /// The stream preferences implied by the SOCKS resolve request.
255
    pub prefs: StreamPrefs,
256
}
257
impl rpc::Method for ResolveWithPrefs {
258
    // TODO RPC: I am not sure that this is the error type we truly want.
259
    type Output = Result<Vec<IpAddr>, Box<dyn ClientConnectionError>>;
260
    type Update = rpc::NoUpdates;
261
}
262

            
263
/// RPC special method: reverse-lookup an address with a chosen address and preferences.
264
///
265
/// This method has no method name, and is not invoked by an RPC connection
266
/// directly.  Instead, it is invoked in response to a SOCKS request.
267
//
268
// TODO RPC: We _could_ give this a method name so that it can be invoked as an RPC method, and
269
// maybe we should.  First, however, we would need to make `StreamPrefs` an RPC-visible serializable
270
// type, or replace it with an equivalent.
271
#[derive(Deftly, Debug)]
272
#[derive_deftly(rpc::DynMethod)]
273
#[deftly(rpc(no_method_name))]
274
#[allow(clippy::exhaustive_structs)]
275
pub struct ResolvePtrWithPrefs {
276
    /// The address to resolve.
277
    pub addr: IpAddr,
278
    /// The stream preferences implied by the SOCKS resolve request.
279
    pub prefs: StreamPrefs,
280
}
281
impl rpc::Method for ResolvePtrWithPrefs {
282
    // TODO RPC: I am not sure that this is the error type we truly want.
283
    type Output = Result<Vec<String>, Box<dyn ClientConnectionError>>;
284
    type Update = rpc::NoUpdates;
285
}
286

            
287
/// RPC method implementation: start a connection on a `TorClient`.
288
async fn client_connect_with_prefs<R: Runtime>(
289
    client: Arc<TorClient<R>>,
290
    method: Box<ConnectWithPrefs>,
291
    _ctx: Arc<dyn rpc::Context>,
292
) -> Result<DataStream, Box<dyn ClientConnectionError>> {
293
    TorClient::connect_with_prefs(client.as_ref(), &method.target, &method.prefs)
294
        .await
295
        .map_err(|e| Box::new(e) as _)
296
}
297

            
298
/// RPC method implementation: perform a remote DNS lookup using a `TorClient`.
299
async fn client_resolve_with_prefs<R: Runtime>(
300
    client: Arc<TorClient<R>>,
301
    method: Box<ResolveWithPrefs>,
302
    _ctx: Arc<dyn rpc::Context>,
303
) -> Result<Vec<IpAddr>, Box<dyn ClientConnectionError>> {
304
    TorClient::resolve_with_prefs(client.as_ref(), &method.hostname, &method.prefs)
305
        .await
306
        .map_err(|e| Box::new(e) as _)
307
}
308

            
309
/// RPC method implementation: perform a remote DNS reverse lookup using a `TorClient`.
310
async fn client_resolve_ptr_with_prefs<R: Runtime>(
311
    client: Arc<TorClient<R>>,
312
    method: Box<ResolvePtrWithPrefs>,
313
    _ctx: Arc<dyn rpc::Context>,
314
) -> Result<Vec<String>, Box<dyn ClientConnectionError>> {
315
    TorClient::resolve_ptr_with_prefs(client.as_ref(), method.addr, &method.prefs)
316
        .await
317
        .map_err(|e| Box::new(e) as _)
318
}