147 lines
6.1 KiB
Rust
147 lines
6.1 KiB
Rust
//! Compatibility fixes for JSON-RPC HTTP requests.
|
|
//!
|
|
//! These fixes are applied at the HTTP level, before the RPC request is parsed.
|
|
|
|
use futures::TryStreamExt;
|
|
use hyper::{body::Bytes, Body};
|
|
|
|
use jsonrpc_http_server::RequestMiddleware;
|
|
|
|
/// HTTP [`RequestMiddleware`] with compatibility workarounds.
|
|
///
|
|
/// This middleware makes the following changes to HTTP requests:
|
|
///
|
|
/// ## Remove `jsonrpc` field in JSON RPC 1.0
|
|
///
|
|
/// Removes "jsonrpc: 1.0" fields from requests,
|
|
/// because the "jsonrpc" field was only added in JSON-RPC 2.0.
|
|
///
|
|
/// <http://www.simple-is-better.org/rpc/#differences-between-1-0-and-2-0>
|
|
///
|
|
/// ## Add missing `content-type` HTTP header
|
|
///
|
|
/// Some RPC clients don't include a `content-type` HTTP header.
|
|
/// But unlike web browsers, [`jsonrpc_http_server`] does not do content sniffing.
|
|
///
|
|
/// If there is no `content-type` header, we assume the content is JSON,
|
|
/// and let the parser error if we are incorrect.
|
|
///
|
|
/// This enables compatibility with `zcash-cli`.
|
|
///
|
|
/// ## Security
|
|
///
|
|
/// Any user-specified data in RPC requests is hex or base58check encoded.
|
|
/// We assume lightwalletd validates data encodings before sending it on to Zebra.
|
|
/// So any fixes Zebra performs won't change user-specified data.
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct FixHttpRequestMiddleware;
|
|
|
|
impl RequestMiddleware for FixHttpRequestMiddleware {
|
|
fn on_request(
|
|
&self,
|
|
mut request: hyper::Request<hyper::Body>,
|
|
) -> jsonrpc_http_server::RequestMiddlewareAction {
|
|
tracing::trace!(?request, "original HTTP request");
|
|
|
|
// Fix the request headers if needed and we can do so.
|
|
FixHttpRequestMiddleware::insert_or_replace_content_type_header(request.headers_mut());
|
|
|
|
// Fix the request body
|
|
let request = request.map(|body| {
|
|
let body = body.map_ok(|data| {
|
|
// To simplify data handling, we assume that any search strings won't be split
|
|
// across multiple `Bytes` data buffers.
|
|
//
|
|
// To simplify error handling, Zebra only supports valid UTF-8 requests,
|
|
// and uses lossy UTF-8 conversion.
|
|
//
|
|
// JSON-RPC requires all requests to be valid UTF-8.
|
|
// The lower layers should reject invalid requests with lossy changes.
|
|
// But if they accept some lossy changes, that's ok,
|
|
// because the request was non-standard anyway.
|
|
//
|
|
// We're not concerned about performance here, so we just clone the Cow<str>
|
|
let data = String::from_utf8_lossy(data.as_ref()).to_string();
|
|
|
|
// Fix up the request.
|
|
let data = Self::remove_json_1_fields(data);
|
|
|
|
Bytes::from(data)
|
|
});
|
|
|
|
Body::wrap_stream(body)
|
|
});
|
|
|
|
tracing::trace!(?request, "modified HTTP request");
|
|
|
|
jsonrpc_http_server::RequestMiddlewareAction::Proceed {
|
|
// TODO: disable this security check if we see errors from lightwalletd.
|
|
should_continue_on_invalid_cors: false,
|
|
request,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FixHttpRequestMiddleware {
|
|
/// Remove any "jsonrpc: 1.0" fields in `data`, and return the resulting string.
|
|
pub fn remove_json_1_fields(data: String) -> String {
|
|
// Replace "jsonrpc = 1.0":
|
|
// - at the start or middle of a list, and
|
|
// - at the end of a list;
|
|
// with no spaces (lightwalletd format), and spaces after separators (example format).
|
|
//
|
|
// TODO: if we see errors from lightwalletd, make this replacement more accurate:
|
|
// - use a partial JSON fragment parser
|
|
// - combine the whole request into a single buffer, and use a JSON parser
|
|
// - use a regular expression
|
|
//
|
|
// We could also just handle the exact lightwalletd format,
|
|
// by replacing `{"jsonrpc":"1.0",` with `{`.
|
|
data.replace("\"jsonrpc\":\"1.0\",", "")
|
|
.replace("\"jsonrpc\": \"1.0\",", "")
|
|
.replace(",\"jsonrpc\":\"1.0\"", "")
|
|
.replace(", \"jsonrpc\": \"1.0\"", "")
|
|
}
|
|
|
|
/// Insert or replace client supplied `content-type` HTTP header to `application/json` in the following cases:
|
|
///
|
|
/// - no `content-type` supplied.
|
|
/// - supplied `content-type` start with `text/plain`, for example:
|
|
/// - `text/plain`
|
|
/// - `text/plain;`
|
|
/// - `text/plain; charset=utf-8`
|
|
///
|
|
/// `application/json` is the only `content-type` accepted by the Zebra rpc endpoint:
|
|
///
|
|
/// <https://github.com/paritytech/jsonrpc/blob/38af3c9439aa75481805edf6c05c6622a5ab1e70/http/src/handler.rs#L582-L584>
|
|
///
|
|
/// # Security
|
|
///
|
|
/// - `content-type` headers exist so that applications know they are speaking the correct protocol with the correct format.
|
|
/// We can be a bit flexible, but there are some types (such as binary) we shouldn't allow.
|
|
/// In particular, the "application/x-www-form-urlencoded" header should be rejected, so browser forms can't be used to attack
|
|
/// a local RPC port. See "The Role of Routers in the CSRF Attack" in
|
|
/// <https://www.invicti.com/blog/web-security/importance-content-type-header-http-requests/>
|
|
/// - Checking all the headers is secure, but only because hyper has custom code that just reads the first content-type header.
|
|
/// <https://github.com/hyperium/headers/blob/f01cc90cf8d601a716856bc9d29f47df92b779e4/src/common/content_type.rs#L102-L108>
|
|
pub fn insert_or_replace_content_type_header(headers: &mut hyper::header::HeaderMap) {
|
|
if !headers.contains_key(hyper::header::CONTENT_TYPE)
|
|
|| headers
|
|
.get(hyper::header::CONTENT_TYPE)
|
|
.filter(|value| {
|
|
value
|
|
.to_str()
|
|
.ok()
|
|
.unwrap_or_default()
|
|
.starts_with("text/plain")
|
|
})
|
|
.is_some()
|
|
{
|
|
headers.insert(
|
|
hyper::header::CONTENT_TYPE,
|
|
hyper::header::HeaderValue::from_static("application/json"),
|
|
);
|
|
}
|
|
}
|
|
}
|