Skip to main content

zebra_rpc/server/
http_request_compatibility.rs

1//! Compatibility fixes for JSON-RPC HTTP requests.
2//!
3//! These fixes are applied at the HTTP level, before the RPC request is parsed.
4
5use std::future::Future;
6
7use std::pin::Pin;
8
9use futures::{future, FutureExt};
10use http_body_util::BodyExt;
11use hyper::header;
12use jsonrpsee::{
13    core::BoxError,
14    server::{HttpBody, HttpRequest, HttpResponse},
15};
16use jsonrpsee_types::ErrorObject;
17use serde::{Deserialize, Serialize};
18use tower::Service;
19
20use super::cookie::Cookie;
21
22use base64::{engine::general_purpose::STANDARD, Engine as _};
23
24/// HTTP [`HttpRequestMiddleware`] with compatibility workarounds.
25///
26/// This middleware makes the following changes to HTTP requests:
27///
28/// ### Remove `jsonrpc` field in JSON RPC 1.0
29///
30/// Removes "jsonrpc: 1.0" fields from requests,
31/// because the "jsonrpc" field was only added in JSON-RPC 2.0.
32///
33/// <http://www.simple-is-better.org/rpc/#differences-between-1-0-and-2-0>
34///
35/// ### Add missing `content-type` HTTP header
36///
37/// Some RPC clients don't include a `content-type` HTTP header.
38/// But unlike web browsers, [`jsonrpsee`] does not do content sniffing.
39///
40/// If there is no `content-type` header, we assume the content is JSON,
41/// and let the parser error if we are incorrect.
42///
43/// ### Authenticate incoming requests
44///
45/// If the cookie-based RPC authentication is enabled, check that the incoming request contains the
46/// authentication cookie.
47///
48/// This enables compatibility with `zcash-cli`.
49///
50/// ## Security
51///
52/// Any user-specified data in RPC requests is hex or base58check encoded.
53/// We assume lightwalletd validates data encodings before sending it on to Zebra.
54/// So any fixes Zebra performs won't change user-specified data.
55#[derive(Clone, Debug)]
56pub struct HttpRequestMiddleware<S> {
57    service: S,
58    cookie: Option<Cookie>,
59}
60
61impl<S> HttpRequestMiddleware<S> {
62    /// Create a new `HttpRequestMiddleware` with the given service and cookie.
63    pub fn new(service: S, cookie: Option<Cookie>) -> Self {
64        Self { service, cookie }
65    }
66
67    /// Check if the request is authenticated.
68    pub fn check_credentials(&self, headers: &header::HeaderMap) -> bool {
69        self.cookie.as_ref().is_none_or(|internal_cookie| {
70            headers
71                .get(header::AUTHORIZATION)
72                .and_then(|auth_header| auth_header.to_str().ok())
73                .and_then(|auth_header| auth_header.split_whitespace().nth(1))
74                .and_then(|encoded| STANDARD.decode(encoded).ok())
75                .and_then(|decoded| String::from_utf8(decoded).ok())
76                .and_then(|request_cookie| request_cookie.split(':').nth(1).map(String::from))
77                .is_some_and(|passwd| internal_cookie.authenticate(passwd))
78        })
79    }
80
81    /// Insert or replace client supplied `content-type` HTTP header to `application/json` in the following cases:
82    ///
83    /// - no `content-type` supplied.
84    /// - supplied `content-type` start with `text/plain`, for example:
85    ///   - `text/plain`
86    ///   - `text/plain;`
87    ///   - `text/plain; charset=utf-8`
88    ///
89    /// `application/json` is the only `content-type` accepted by the Zebra rpc endpoint:
90    ///
91    /// <https://github.com/paritytech/jsonrpc/blob/38af3c9439aa75481805edf6c05c6622a5ab1e70/http/src/handler.rs#L582-L584>
92    ///
93    /// # Security
94    ///
95    /// - `content-type` headers exist so that applications know they are speaking the correct protocol with the correct format.
96    ///   We can be a bit flexible, but there are some types (such as binary) we shouldn't allow.
97    ///   In particular, the "application/x-www-form-urlencoded" header should be rejected, so browser forms can't be used to attack
98    ///   a local RPC port. See "The Role of Routers in the CSRF Attack" in
99    ///   <https://www.invicti.com/blog/web-security/importance-content-type-header-http-requests/>
100    /// - Checking all the headers is secure, but only because hyper has custom code that just reads the first content-type header.
101    ///   <https://github.com/hyperium/headers/blob/f01cc90cf8d601a716856bc9d29f47df92b779e4/src/common/content_type.rs#L102-L108>
102    pub fn insert_or_replace_content_type_header(headers: &mut header::HeaderMap) {
103        if !headers.contains_key(header::CONTENT_TYPE)
104            || headers
105                .get(header::CONTENT_TYPE)
106                .filter(|value| {
107                    value
108                        .to_str()
109                        .ok()
110                        .unwrap_or_default()
111                        .starts_with("text/plain")
112                })
113                .is_some()
114        {
115            headers.insert(
116                header::CONTENT_TYPE,
117                header::HeaderValue::from_static("application/json"),
118            );
119        }
120    }
121
122    /// Maps whatever JSON-RPC version the client is using to JSON-RPC 2.0.
123    async fn request_to_json_rpc_2(
124        request: HttpRequest<HttpBody>,
125    ) -> Result<(JsonRpcVersion, HttpRequest<HttpBody>), BoxError> {
126        let (parts, body) = request.into_parts();
127        let bytes = body.collect().await?.to_bytes();
128        let (version, bytes) =
129            if let Ok(request) = serde_json::from_slice::<'_, JsonRpcRequest>(bytes.as_ref()) {
130                let version = request.version();
131                if matches!(version, JsonRpcVersion::Unknown) {
132                    (version, bytes)
133                } else {
134                    (
135                        version,
136                        serde_json::to_vec(&request.into_2()).expect("valid").into(),
137                    )
138                }
139            } else {
140                (JsonRpcVersion::Unknown, bytes)
141            };
142        Ok((
143            version,
144            HttpRequest::from_parts(parts, HttpBody::from(bytes.as_ref().to_vec())),
145        ))
146    }
147    /// Maps JSON-2.0 to whatever JSON-RPC version the client is using.
148    async fn response_from_json_rpc_2(
149        version: JsonRpcVersion,
150        response: HttpResponse<HttpBody>,
151    ) -> Result<HttpResponse<HttpBody>, BoxError> {
152        let (parts, body) = response.into_parts();
153        let bytes = body.collect().await?.to_bytes();
154        let bytes =
155            if let Ok(response) = serde_json::from_slice::<'_, JsonRpcResponse>(bytes.as_ref()) {
156                serde_json::to_vec(&response.into_version(version))
157                    .expect("valid")
158                    .into()
159            } else {
160                bytes
161            };
162        Ok(HttpResponse::from_parts(
163            parts,
164            HttpBody::from(bytes.as_ref().to_vec()),
165        ))
166    }
167}
168
169/// Implement the Layer for HttpRequestMiddleware to allow injecting the cookie
170#[derive(Clone)]
171pub struct HttpRequestMiddlewareLayer {
172    cookie: Option<Cookie>,
173}
174
175impl HttpRequestMiddlewareLayer {
176    /// Create a new `HttpRequestMiddlewareLayer` with the given cookie.
177    pub fn new(cookie: Option<Cookie>) -> Self {
178        Self { cookie }
179    }
180}
181
182impl<S> tower::Layer<S> for HttpRequestMiddlewareLayer {
183    type Service = HttpRequestMiddleware<S>;
184
185    fn layer(&self, service: S) -> Self::Service {
186        HttpRequestMiddleware::new(service, self.cookie.clone())
187    }
188}
189
190/// A trait for updating an object, consuming it and returning the updated version.
191pub trait With<T> {
192    /// Updates `self` with an instance of type `T` and returns the updated version of `self`.
193    fn with(self, _: T) -> Self;
194}
195
196impl<S> With<Cookie> for HttpRequestMiddleware<S> {
197    fn with(mut self, cookie: Cookie) -> Self {
198        self.cookie = Some(cookie);
199        self
200    }
201}
202
203impl<S> Service<HttpRequest<HttpBody>> for HttpRequestMiddleware<S>
204where
205    S: Service<HttpRequest, Response = HttpResponse> + std::clone::Clone + Send + 'static,
206    S::Error: Into<BoxError> + 'static,
207    S::Future: Send + 'static,
208{
209    type Response = S::Response;
210    type Error = BoxError;
211    type Future =
212        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
213
214    fn poll_ready(
215        &mut self,
216        cx: &mut std::task::Context<'_>,
217    ) -> std::task::Poll<Result<(), Self::Error>> {
218        self.service.poll_ready(cx).map_err(Into::into)
219    }
220
221    fn call(&mut self, mut request: HttpRequest<HttpBody>) -> Self::Future {
222        // Check if the request is authenticated
223        if !self.check_credentials(request.headers_mut()) {
224            let error = ErrorObject::borrowed(401, "unauthenticated method", None);
225            // TODO: Error object is not being returned to the user but an empty response.
226            return future::err(BoxError::from(error)).boxed();
227        }
228
229        // Fix the request headers.
230        Self::insert_or_replace_content_type_header(request.headers_mut());
231
232        let mut service = self.service.clone();
233
234        async move {
235            let (version, request) = Self::request_to_json_rpc_2(request).await?;
236            let response = service.call(request).await.map_err(Into::into)?;
237            Self::response_from_json_rpc_2(version, response).await
238        }
239        .boxed()
240    }
241}
242
243#[derive(Clone, Copy, Debug)]
244enum JsonRpcVersion {
245    /// bitcoind used a mishmash of 1.0, 1.1, and 2.0 for its JSON-RPC.
246    Bitcoind,
247    /// lightwalletd uses the above mishmash, but also breaks spec to include a
248    /// `"jsonrpc": "1.0"` key.
249    Lightwalletd,
250    /// The client is indicating strict 2.0 handling.
251    TwoPointZero,
252    /// On parse errors we don't modify anything, and let the `jsonrpsee` crate handle it.
253    Unknown,
254}
255
256/// A version-agnostic JSON-RPC request.
257#[derive(Debug, Deserialize, Serialize)]
258struct JsonRpcRequest {
259    #[serde(skip_serializing_if = "Option::is_none")]
260    jsonrpc: Option<String>,
261    method: String,
262    #[serde(skip_serializing_if = "Option::is_none")]
263    params: Option<serde_json::Value>,
264    #[serde(skip_serializing_if = "Option::is_none")]
265    id: Option<serde_json::Value>,
266}
267
268impl JsonRpcRequest {
269    fn version(&self) -> JsonRpcVersion {
270        match (self.jsonrpc.as_deref(), &self.params, &self.id) {
271            (
272                Some("2.0"),
273                _,
274                None
275                | Some(
276                    serde_json::Value::Null
277                    | serde_json::Value::String(_)
278                    | serde_json::Value::Number(_),
279                ),
280            ) => JsonRpcVersion::TwoPointZero,
281            (Some("1.0"), Some(_), Some(_)) => JsonRpcVersion::Lightwalletd,
282            (None, Some(_), Some(_)) => JsonRpcVersion::Bitcoind,
283            _ => JsonRpcVersion::Unknown,
284        }
285    }
286
287    fn into_2(mut self) -> Self {
288        self.jsonrpc = Some("2.0".into());
289        self
290    }
291}
292/// A version-agnostic JSON-RPC response.
293#[derive(Debug, Deserialize, Serialize)]
294struct JsonRpcResponse {
295    #[serde(skip_serializing_if = "Option::is_none")]
296    jsonrpc: Option<String>,
297    id: serde_json::Value,
298    #[serde(skip_serializing_if = "Option::is_none")]
299    result: Option<Box<serde_json::value::RawValue>>,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    error: Option<serde_json::Value>,
302}
303
304impl JsonRpcResponse {
305    fn into_version(mut self, version: JsonRpcVersion) -> Self {
306        match version {
307            JsonRpcVersion::Bitcoind => {
308                self.jsonrpc = None;
309                self.result = self
310                    .result
311                    .or_else(|| serde_json::value::to_raw_value(&()).ok());
312                self.error = self.error.or(Some(serde_json::Value::Null));
313            }
314            JsonRpcVersion::Lightwalletd => {
315                self.jsonrpc = Some("1.0".into());
316                self.result = self
317                    .result
318                    .or_else(|| serde_json::value::to_raw_value(&()).ok());
319                self.error = self.error.or(Some(serde_json::Value::Null));
320            }
321            JsonRpcVersion::TwoPointZero => {
322                // `jsonrpsee` should be returning valid JSON-RPC 2.0 responses. However,
323                // a valid result of `null` can be parsed into `None` by this parser, so
324                // we map the result explicitly to `Null` when there is no error.
325                assert_eq!(self.jsonrpc.as_deref(), Some("2.0"));
326                if self.error.is_none() {
327                    self.result = self
328                        .result
329                        .or_else(|| serde_json::value::to_raw_value(&()).ok());
330                } else {
331                    assert!(self.result.is_none());
332                }
333            }
334            JsonRpcVersion::Unknown => (),
335        }
336        self
337    }
338}