1
#![cfg_attr(docsrs, feature(doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![allow(clippy::cognitive_complexity)] // See arti#2556
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![deny(clippy::print_stderr)]
28
#![deny(clippy::print_stdout)]
29
#![warn(clippy::rc_buffer)]
30
#![deny(clippy::ref_option_ref)]
31
#![warn(clippy::semicolon_if_nothing_returned)]
32
#![warn(clippy::trait_duplication_in_bounds)]
33
#![deny(clippy::unchecked_time_subtraction)]
34
#![deny(clippy::unnecessary_wraps)]
35
#![warn(clippy::unseparated_literal_suffix)]
36
#![deny(clippy::unwrap_used)]
37
#![deny(clippy::mod_module_files)]
38
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39
#![allow(clippy::uninlined_format_args)]
40
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43
#![allow(clippy::needless_lifetimes)] // See arti#1765
44
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45
#![allow(clippy::collapsible_if)] // See arti#2342
46
#![deny(clippy::unused_async)]
47
#![deny(clippy::string_slice)] // See arti#2571
48
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
49
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
50

            
51
pub mod dispatch;
52
mod err;
53
mod method;
54
mod obj;
55

            
56
use std::{collections::HashSet, convert::Infallible, sync::Arc};
57

            
58
pub use dispatch::{DispatchTable, InvokeError, UpdateSink};
59
pub use err::{RpcError, RpcErrorKind};
60
pub use method::{
61
    DeserMethod, DynMethod, Method, NoUpdates, RpcMethod, check_method_names, is_method_name,
62
    iter_method_names,
63
};
64
pub use obj::{Object, ObjectArcExt, ObjectId};
65

            
66
#[cfg(feature = "describe-methods")]
67
pub use dispatch::description::RpcDispatchInformation;
68

            
69
#[cfg(feature = "describe-methods")]
70
#[doc(hidden)]
71
pub use dispatch::description::DelegationNote;
72

            
73
#[doc(hidden)]
74
pub use obj::cast::CastTable;
75
#[doc(hidden)]
76
pub use {
77
    derive_deftly, dispatch::RpcResult, downcast_rs, erased_serde, futures, inventory,
78
    method::MethodInfo_, paste, tor_async_utils, tor_error::internal, typetag,
79
};
80

            
81
/// Templates for use with [`derive_deftly`]
82
pub mod templates {
83
    pub use crate::method::derive_deftly_template_DynMethod;
84
    pub use crate::obj::derive_deftly_template_Object;
85
}
86

            
87
/// An error returned from [`ContextExt::lookup`].
88
#[derive(Debug, Clone, thiserror::Error)]
89
#[non_exhaustive]
90
pub enum LookupError {
91
    /// The specified object does not (currently) exist,
92
    /// or the user does not have permission to access it.
93
    #[error("No visible object with ID {0:?}")]
94
    NoObject(ObjectId),
95

            
96
    /// The specified object exists, but does not have the
97
    /// expected type.
98
    #[error("Unexpected type on object with ID {0:?}")]
99
    WrongType(ObjectId),
100

            
101
    /// The object once existed, but this is a weak reference,
102
    /// and it has expired.
103
    #[error("Weak reference {0:?} is no longer present")]
104
    Expired(ObjectId),
105
}
106

            
107
impl LookupError {
108
    /// Return the RpcErrorKind for this lookup error.
109
    fn rpc_error_kind(&self) -> RpcErrorKind {
110
        use LookupError as E;
111
        use RpcErrorKind as EK;
112

            
113
        match self {
114
            E::NoObject(_) => EK::ObjectNotFound,
115
            E::WrongType(_) => EK::InvalidRequest,
116
            E::Expired(_) => EK::WeakReferenceExpired,
117
        }
118
    }
119
}
120

            
121
impl From<LookupError> for RpcError {
122
    fn from(err: LookupError) -> Self {
123
        RpcError::new(err.to_string(), err.rpc_error_kind())
124
    }
125
}
126

            
127
/// A trait describing the context in which an RPC method is executed.
128
pub trait Context: Send + Sync {
129
    /// Look up an object by identity within this context.
130
    fn lookup_object(&self, id: &ObjectId) -> Result<Arc<dyn Object>, LookupError>;
131

            
132
    /// Create an owning reference to `object` within this context.
133
    ///
134
    /// Return an ObjectId for this object.
135
    fn register_owned(&self, object: Arc<dyn Object>) -> ObjectId;
136

            
137
    /// Create a non-owning weak referene to `object` within this context.
138
    ///
139
    /// Return an ObjectId for this object.
140
    fn register_weak(&self, object: &Arc<dyn Object>) -> ObjectId;
141

            
142
    /// Drop a reference to the object called `object` within this context.
143
    ///
144
    /// This will return an error if `object` does not exist.
145
    fn release(&self, object: &ObjectId) -> Result<(), LookupError>;
146

            
147
    /// Return a dispatch table that can be used to invoke other RPC methods.
148
    fn dispatch_table(&self) -> &Arc<std::sync::RwLock<DispatchTable>>;
149
}
150

            
151
/// An error caused while trying to send an update to a method.
152
///
153
/// These errors should be impossible in our current implementation, since they
154
/// can only happen if the `mpsc::Receiver` is closed—which can only happen
155
/// when the session loop drops it, which only happens when the session loop has
156
/// stopped polling its `FuturesUnordered` full of RPC request futures. Thus, any
157
/// `send` that would encounter this error should be in a future that is never
158
/// polled under circumstances when the error could happen.
159
///
160
/// Still, programming errors are real, so we are handling this rather than
161
/// declaring it a panic or something.
162
#[derive(Debug, Clone, thiserror::Error)]
163
#[non_exhaustive]
164
pub enum SendUpdateError {
165
    /// The request was cancelled, or the connection was closed.
166
    #[error("Unable to send on MPSC connection")]
167
    ConnectionClosed,
168
}
169

            
170
impl tor_error::HasKind for SendUpdateError {
171
    fn kind(&self) -> tor_error::ErrorKind {
172
        tor_error::ErrorKind::Internal
173
    }
174
}
175

            
176
impl From<Infallible> for SendUpdateError {
177
    fn from(_: Infallible) -> Self {
178
        unreachable!()
179
    }
180
}
181
impl From<futures::channel::mpsc::SendError> for SendUpdateError {
182
    fn from(_: futures::channel::mpsc::SendError) -> Self {
183
        SendUpdateError::ConnectionClosed
184
    }
185
}
186

            
187
/// Extension trait for [`Context`].
188
///
189
/// This is a separate trait so that `Context` can be object-safe.
190
pub trait ContextExt: Context {
191
    /// Look up an object of a given type, and downcast it.
192
    ///
193
    /// Return an error if the object can't be found, or has the wrong type.
194
    fn lookup<T: Object>(&self, id: &ObjectId) -> Result<Arc<T>, LookupError> {
195
        self.lookup_object(id)?
196
            .downcast_arc()
197
            .map_err(|_| LookupError::WrongType(id.clone()))
198
    }
199
}
200

            
201
impl<T: Context> ContextExt for T {}
202

            
203
/// Try to find an appropriate function for calling a given RPC method on a
204
/// given ObjectId.
205
///
206
/// On success, return a Future.
207
///
208
/// Differs from using `DispatchTable::invoke()` in that it drops its lock
209
/// on the dispatch table before invoking the method.
210
42
pub fn invoke_rpc_method(
211
42
    ctx: Arc<dyn Context>,
212
42
    obj_id: &ObjectId,
213
42
    method: Box<dyn DynMethod>,
214
42
    sink: dispatch::BoxedUpdateSink,
215
42
) -> Result<dispatch::RpcResultFuture, InvokeError> {
216
42
    match method.invoke_without_dispatch(Arc::clone(&ctx), obj_id) {
217
42
        Err(InvokeError::NoDispatchBypass) => {
218
42
            // fall through
219
42
        }
220
        other => return other,
221
    }
222

            
223
42
    let obj = ctx.lookup_object(obj_id).map_err(InvokeError::NoObject)?;
224

            
225
42
    let (obj, invocable) = ctx
226
42
        .dispatch_table()
227
42
        .read()
228
42
        .expect("poisoned lock")
229
42
        .resolve_rpc_invoker(obj, method.as_ref())?;
230

            
231
34
    invocable.invoke(obj, method, ctx, sink)
232
42
}
233

            
234
/// Invoke the given `method` on `obj` within `ctx`, and return its
235
/// actual result type.
236
///
237
/// Unlike `invoke_rpc_method`, this method does not return a type-erased result,
238
/// and does not require that the result can be serialized as an RPC object.
239
///
240
/// Differs from using `DispatchTable::invoke_special()` in that it drops its lock
241
/// on the dispatch table before invoking the method.
242
6
pub async fn invoke_special_method<M: Method>(
243
6
    ctx: Arc<dyn Context>,
244
6
    obj: Arc<dyn Object>,
245
6
    method: Box<M>,
246
6
) -> Result<Box<M::Output>, InvokeError> {
247
6
    let (obj, invocable) = ctx
248
6
        .dispatch_table()
249
6
        .read()
250
6
        .expect("poisoned lock")
251
6
        .resolve_special_invoker::<M>(obj)?;
252

            
253
6
    invocable
254
6
        .invoke_special(obj, method, ctx)?
255
6
        .await
256
6
        .downcast()
257
6
        .map_err(|_| InvokeError::Bug(tor_error::internal!("Downcast to wrong type")))
258
6
}
259

            
260
/// A serializable empty object.
261
///
262
/// Used when we need to declare that a method returns nothing.
263
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
264
#[non_exhaustive]
265
pub struct Nil {}
266
/// An instance of rpc::Nil.
267
pub const NIL: Nil = Nil {};
268

            
269
/// Common return type for RPC methods that return a single object ID
270
/// and nothing else.
271
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, derive_more::From)]
272
pub struct SingleIdResponse {
273
    /// The ID of the object that we're returning.
274
    id: ObjectId,
275
}
276

            
277
/// Error representing an "invalid" RPC identifier.
278
#[derive(Clone, Debug, thiserror::Error)]
279
#[non_exhaustive]
280
#[cfg_attr(test, derive(Eq, PartialEq))]
281
pub enum InvalidRpcIdentifier {
282
    /// The method doesn't have a ':' to demarcate its namespace.
283
    #[error("Identifier has no namespace separator")]
284
    NoNamespace,
285

            
286
    /// The method's namespace is not one we recognize.
287
    #[error("Identifier has unrecognized namespace")]
288
    UnrecognizedNamespace,
289

            
290
    /// The method's name is not in snake_case.
291
    #[error("Identifier name has unexpected format")]
292
    BadIdName,
293
}
294

            
295
/// Check whether `method` is an expected and well-formed RPC identifier.
296
///
297
/// If `recognized_namespaces` is provided, only identifiers within those
298
/// namespaces are accepted; otherwise, all namespaces are accepted.
299
///
300
/// (Examples of RPC identifiers are method names.)
301
1064
pub(crate) fn is_valid_rpc_identifier(
302
1064
    recognized_namespaces: Option<&HashSet<&str>>,
303
1064
    method: &str,
304
1064
) -> Result<(), InvalidRpcIdentifier> {
305
    /// Return true if name is in acceptable format.
306
1060
    fn name_ok(n: &str) -> bool {
307
1060
        let mut chars = n.chars();
308
1060
        let Some(first) = chars.next() else {
309
2
            return false;
310
        };
311
1058
        first.is_ascii_lowercase()
312
14915
            && chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
313
1060
    }
314
1064
    let (scope, name) = method
315
1064
        .split_once(':')
316
1064
        .ok_or(InvalidRpcIdentifier::NoNamespace)?;
317

            
318
1062
    if let Some(recognized_namespaces) = recognized_namespaces {
319
1060
        if !(scope.starts_with("x-") || recognized_namespaces.contains(scope)) {
320
2
            return Err(InvalidRpcIdentifier::UnrecognizedNamespace);
321
1058
        }
322
2
    }
323
1060
    if !name_ok(name) {
324
8
        return Err(InvalidRpcIdentifier::BadIdName);
325
1052
    }
326

            
327
1052
    Ok(())
328
1064
}
329

            
330
#[cfg(test)]
331
mod test {
332
    // @@ begin test lint list maintained by maint/add_warning @@
333
    #![allow(clippy::bool_assert_comparison)]
334
    #![allow(clippy::clone_on_copy)]
335
    #![allow(clippy::dbg_macro)]
336
    #![allow(clippy::mixed_attributes_style)]
337
    #![allow(clippy::print_stderr)]
338
    #![allow(clippy::print_stdout)]
339
    #![allow(clippy::single_char_pattern)]
340
    #![allow(clippy::unwrap_used)]
341
    #![allow(clippy::unchecked_time_subtraction)]
342
    #![allow(clippy::useless_vec)]
343
    #![allow(clippy::needless_pass_by_value)]
344
    #![allow(clippy::string_slice)] // See arti#2571
345
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
346

            
347
    use futures::SinkExt as _;
348
    use futures_await_test::async_test;
349

            
350
    use super::*;
351
    use crate::dispatch::test::{Ctx, GetKids, Swan};
352

            
353
    #[async_test]
354
    async fn invoke() {
355
        let ctx = Arc::new(Ctx::from(DispatchTable::from_inventory()));
356
        let discard = || Box::pin(futures::sink::drain().sink_err_into());
357
        let id = ctx.register_owned(Arc::new(Swan));
358

            
359
        let r = invoke_rpc_method(ctx.clone(), &id, Box::new(GetKids), discard())
360
            .unwrap()
361
            .await
362
            .unwrap();
363
        assert_eq!(serde_json::to_string(&r).unwrap(), r#"{"v":"cygnets"}"#);
364

            
365
        let r = invoke_special_method(ctx, Arc::new(Swan), Box::new(GetKids))
366
            .await
367
            .unwrap()
368
            .unwrap();
369
        assert_eq!(r.v, "cygnets");
370
    }
371

            
372
    #[test]
373
    fn valid_method_names() {
374
        let namespaces: HashSet<_> = ["arti", "wombat"].into_iter().collect();
375

            
376
        for name in [
377
            "arti:clone",
378
            "arti:clone7",
379
            "arti:clone_now",
380
            "wombat:knish",
381
            "x-foo:bar",
382
        ] {
383
            assert!(is_valid_rpc_identifier(Some(&namespaces), name).is_ok());
384
        }
385
    }
386

            
387
    #[test]
388
    fn invalid_method_names() {
389
        let namespaces: HashSet<_> = ["arti", "wombat"].into_iter().collect();
390
        use InvalidRpcIdentifier as E;
391

            
392
        for (name, expect_err) in [
393
            ("arti-foo:clone", E::UnrecognizedNamespace),
394
            ("fred", E::NoNamespace),
395
            ("arti:", E::BadIdName),
396
            ("arti:7clone", E::BadIdName),
397
            ("arti:CLONE", E::BadIdName),
398
            ("arti:clone-now", E::BadIdName),
399
        ] {
400
            assert_eq!(
401
                is_valid_rpc_identifier(Some(&namespaces), name),
402
                Err(expect_err)
403
            );
404
        }
405
    }
406
}