aboutsummaryrefslogtreecommitdiff
path: root/crates/arti-rpcserver/src/msgs.rs
blob: 4c73c5c515861114cd0c93306b129974a252813a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
//! Message types used in the Arti's RPC protocol.
//
// TODO: This could become a more zero-copy-friendly with some effort, but it's
// not really sure if it's needed.

mod invalid;
use serde::{Deserialize, Serialize};
use tor_rpcbase as rpc;

/// An identifier for a Request within the context of a Session.
///
/// Multiple inflight requests can share the same `RequestId`,
/// but doing so may make Arti's responses ambiguous.
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum RequestId {
    /// A client-provided string.
    //
    // (We use Box<str> to save a word here, since these don't have to be
    // mutable ever.)
    Str(Box<str>),
    /// A client-provided integer.
    ///
    /// [I-JSON] says that we don't have to handle any integer that can't be
    /// represented as an `f64`, but we do anyway.  This won't confuse clients,
    /// since we won't send them any integer that they didn't send us first.
    ///
    /// [I-JSON]: https://www.rfc-editor.org/rfc/rfc7493
    Int(i64),
}

/// Metadata associated with a single Request.
//
// NOTE: When adding new fields to this type, make sure that `Default` gives
// the correct value for an absent metadata.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub(crate) struct ReqMeta {
    /// If true, the client will accept intermediate Updates other than the
    /// final Request or Response.
    pub(crate) updates: bool,
}

/// A single Request received from an RPC client.
#[derive(Debug, Deserialize)]
pub(crate) struct Request {
    /// The client's identifier for this request.
    ///
    /// We'll use this to link all responses to this request.
    pub(crate) id: RequestId,
    /// The object to receive this request.
    pub(crate) obj: rpc::ObjectId,
    /// Any metadata to explain how this request is handled.
    #[serde(default)]
    pub(crate) meta: ReqMeta,
    /// The method to actually execute.
    ///
    /// Using "flatten" here will make it expand to "method" and "params".
    ///
    /// TODO RPC: Note that our spec says that "params" can be omitted, but I
    /// don't think we support that right now.
    #[serde(flatten)]
    pub(crate) method: Box<dyn rpc::DynMethod>,
}

/// A request that may or may not be valid.
///
/// If it invalid, it contains information that can be used to construct an error.
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub(crate) enum FlexibleRequest {
    /// A valid request.
    Valid(Request),
    /// An invalid request.
    Invalid(invalid::InvalidRequest),
}

/// A Response to send to an RPC client.
#[derive(Debug, Serialize)]
pub(crate) struct BoxedResponse {
    /// An ID for the request that we're responding to.
    ///
    /// This is always present on a response to every valid request; it is also
    /// present on responses to invalid requests if we could discern what their
    /// `id` field was. We only omit it when the request id was indeterminate.
    /// If we do that, we close the connection immediately afterwards.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) id: Option<RequestId>,
    /// The body  that we're sending.
    #[serde(flatten)]
    pub(crate) body: ResponseBody,
}

impl BoxedResponse {
    /// Construct a BoxedResponse from an error that can be converted into an
    /// RpcError.
    pub(crate) fn from_error<E>(id: Option<RequestId>, error: E) -> Self
    where
        E: Into<rpc::RpcError>,
    {
        let error: rpc::RpcError = error.into();
        let body = ResponseBody::Error(Box::new(error));
        Self { id, body }
    }
}

/// The body of a response for an RPC client.
#[derive(Serialize)]
pub(crate) enum ResponseBody {
    /// The request has failed; no more responses will be sent in reply to it.
    #[serde(rename = "error")]
    Error(Box<rpc::RpcError>),
    /// The request has succeeded; no more responses will be sent in reply to
    /// it.
    ///
    /// Note that in the spec, this is called a "result": we don't propagate
    /// that terminology into Rust, where `Result` has a different meaning.
    #[serde(rename = "result")]
    Success(Box<dyn erased_serde::Serialize + Send>),
    /// The request included the `updates` flag to increment that incremental
    /// progress information is acceptable.
    #[serde(rename = "update")]
    Update(Box<dyn erased_serde::Serialize + Send>),
}

impl ResponseBody {
    /// Return true if this body type indicates that no future responses will be
    /// sent for this request.
    pub(crate) fn is_final(&self) -> bool {
        match self {
            ResponseBody::Error(_) | ResponseBody::Success(_) => true,
            ResponseBody::Update(_) => false,
        }
    }
}

impl From<rpc::RpcError> for ResponseBody {
    fn from(inp: rpc::RpcError) -> ResponseBody {
        ResponseBody::Error(Box::new(inp))
    }
}

impl std::fmt::Debug for ResponseBody {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // We use serde_json to format the output for debugging, since that's all we care about at this point.
        let json = |x| match serde_json::to_string(x) {
            Ok(s) => s,
            Err(e) => format!("«could not serialize: {}»", e),
        };
        match self {
            Self::Error(arg0) => f.debug_tuple("Error").field(arg0).finish(),
            Self::Update(arg0) => f.debug_tuple("Update").field(&json(arg0)).finish(),
            Self::Success(arg0) => f.debug_tuple("Success").field(&json(arg0)).finish(),
        }
    }
}

#[cfg(test)]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;

    /// Assert that two arguments have the same output from `std::fmt::Debug`.
    ///
    /// This can be handy for testing for some notion of equality on objects
    /// that implement `Debug` but not `PartialEq`.
    macro_rules! assert_dbg_eq {
        ($a:expr, $b:expr) => {
            assert_eq!(format!("{:?}", $a), format!("{:?}", $b));
        };
    }

    // TODO RPC: note that the existence of this method type can potentially
    // leak into our real RPC engine when we're compiled with `test` enabled!
    // We should consider how bad this is, and maybe use a real method instead.
    #[derive(Debug, serde::Deserialize)]
    struct DummyMethod {
        #[serde(default)]
        #[allow(dead_code)]
        stuff: u64,
    }

    impl rpc::Method for DummyMethod {
        type Output = DummyResponse;
        type Update = rpc::NoUpdates;
    }

    tor_rpcbase::decl_method! {"x-test:dummy" => DummyMethod}

    #[derive(Serialize)]
    struct DummyResponse {
        hello: i64,
        world: String,
    }

    #[test]
    fn valid_requests() {
        let parse_request = |s| match serde_json::from_str::<FlexibleRequest>(s) {
            Ok(FlexibleRequest::Valid(req)) => req,
            _ => panic!(),
        };

        let r =
            parse_request(r#"{"id": 7, "obj": "hello", "method": "x-test:dummy", "params": {} }"#);
        assert_dbg_eq!(
            r,
            Request {
                id: RequestId::Int(7),
                obj: rpc::ObjectId::from("hello"),
                meta: ReqMeta::default(),
                method: Box::new(DummyMethod { stuff: 0 })
            }
        );
    }

    #[test]
    fn invalid_requests() {
        use crate::err::RequestParseError as RPE;
        fn parsing_error(s: &str) -> RPE {
            match serde_json::from_str::<FlexibleRequest>(s) {
                Ok(FlexibleRequest::Invalid(req)) => req.error(),
                x => panic!("Didn't expect {:?}", x),
            }
        }

        macro_rules! expect_err {
            ($p:pat, $e:expr) => {
                let err = parsing_error($e);
                assert!(matches!(err, $p), "Unexpected error type {:?}", err);
            };
        }

        expect_err!(
            RPE::IdMissing,
            r#"{ "obj": "hello", "method": "x-test:dummy", "params": {} }"#
        );
        expect_err!(
            RPE::IdType,
            r#"{ "id": {}, "obj": "hello", "method": "x-test:dummy", "params": {} }"#
        );
        expect_err!(
            RPE::ObjMissing,
            r#"{ "id": 3, "method": "x-test:dummy", "params": {} }"#
        );
        expect_err!(
            RPE::ObjType,
            r#"{ "id": 3, "obj": 9, "method": "x-test:dummy", "params": {} }"#
        );
        expect_err!(
            RPE::MethodMissing,
            r#"{ "id": 3, "obj": "hello",  "params": {} }"#
        );
        expect_err!(
            RPE::MethodType,
            r#"{ "id": 3, "obj": "hello", "method": [], "params": {} }"#
        );
        expect_err!(
            RPE::MetaType,
            r#"{ "id": 3, "obj": "hello", "meta": 7, "method": "x-test:dummy", "params": {} }"#
        );
        expect_err!(
            RPE::MetaType,
            r#"{ "id": 3, "obj": "hello", "meta": { "updates": 3}, "method": "x-test:dummy", "params": {} }"#
        );
        expect_err!(
            RPE::MethodUnrecognized,
            r#"{ "id": 3, "obj": "hello", "method": "arti:this-is-not-a-method", "params": {} }"#
        );
        expect_err!(
            RPE::MissingParams,
            r#"{ "id": 3, "obj": "hello", "method": "x-test:dummy" }"#
        );
        expect_err!(
            RPE::ParamType,
            r#"{ "id": 3, "obj": "hello", "method": "x-test:dummy", "params": 7 }"#
        );
    }

    #[test]
    fn fmt_replies() {
        let resp = BoxedResponse {
            id: Some(RequestId::Int(7)),
            body: ResponseBody::Success(Box::new(DummyResponse {
                hello: 99,
                world: "foo".into(),
            })),
        };
        let s = serde_json::to_string(&resp).unwrap();
        // NOTE: This is a bit fragile for a test, since nothing in serde or
        // serde_json guarantees that the fields will be serialized in this
        // exact order.
        assert_eq!(s, r#"{"id":7,"result":{"hello":99,"world":"foo"}}"#);

        let resp = BoxedResponse {
            id: None,
            body: ResponseBody::Error(Box::new(rpc::RpcError::from(
                crate::err::RequestParseError::IdMissing,
            ))),
        };
        let s = serde_json::to_string(&resp).unwrap();
        // NOTE: as above.
        assert_eq!(
            s,
            r#"{"error":{"message":"Request did not have any `id` field.","code":-32600,"kinds":["arti:RpcInvalidRequest"],"data":"IdMissing"}}"#
        );
    }
}