fix(compatibility): Replace or add RPC content type header when applicable (#6885)
* ignore client supplied content-type header and use json always * rename method * add one more check to test * Add missing proptest-impl dependency from zebrad to zebra-rpc * change to replace only specific content type * remove cargo mods * refactor `insert_or_replace_content_type_header` * add security comments * allow variants of text/plain ocntent_type --------- Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
17bd7884ea
commit
219d472270
|
@ -43,6 +43,44 @@ impl RpcRequestClient {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds rpc request with a variable `content-type`.
|
||||||
|
pub async fn call_with_content_type(
|
||||||
|
&self,
|
||||||
|
method: impl AsRef<str>,
|
||||||
|
params: impl AsRef<str>,
|
||||||
|
content_type: String,
|
||||||
|
) -> reqwest::Result<reqwest::Response> {
|
||||||
|
let method = method.as_ref();
|
||||||
|
let params = params.as_ref();
|
||||||
|
|
||||||
|
self.client
|
||||||
|
.post(format!("http://{}", &self.rpc_address))
|
||||||
|
.body(format!(
|
||||||
|
r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
|
||||||
|
))
|
||||||
|
.header("Content-Type", content_type)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds rpc request with no content type.
|
||||||
|
pub async fn call_with_no_content_type(
|
||||||
|
&self,
|
||||||
|
method: impl AsRef<str>,
|
||||||
|
params: impl AsRef<str>,
|
||||||
|
) -> reqwest::Result<reqwest::Response> {
|
||||||
|
let method = method.as_ref();
|
||||||
|
let params = params.as_ref();
|
||||||
|
|
||||||
|
self.client
|
||||||
|
.post(format!("http://{}", &self.rpc_address))
|
||||||
|
.body(format!(
|
||||||
|
r#"{{"jsonrpc": "2.0", "method": "{method}", "params": {params}, "id":123 }}"#
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds rpc request and gets text from response
|
/// Builds rpc request and gets text from response
|
||||||
pub async fn text_from_call(
|
pub async fn text_from_call(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -43,8 +43,8 @@ impl RequestMiddleware for FixHttpRequestMiddleware {
|
||||||
) -> jsonrpc_http_server::RequestMiddlewareAction {
|
) -> jsonrpc_http_server::RequestMiddlewareAction {
|
||||||
tracing::trace!(?request, "original HTTP request");
|
tracing::trace!(?request, "original HTTP request");
|
||||||
|
|
||||||
// Fix the request headers
|
// Fix the request headers if needed and we can do so.
|
||||||
FixHttpRequestMiddleware::add_missing_content_type_header(request.headers_mut());
|
FixHttpRequestMiddleware::insert_or_replace_content_type_header(request.headers_mut());
|
||||||
|
|
||||||
// Fix the request body
|
// Fix the request body
|
||||||
let request = request.map(|body| {
|
let request = request.map(|body| {
|
||||||
|
@ -103,11 +103,44 @@ impl FixHttpRequestMiddleware {
|
||||||
.replace(", \"jsonrpc\": \"1.0\"", "")
|
.replace(", \"jsonrpc\": \"1.0\"", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the `content-type` HTTP header is not present,
|
/// Insert or replace client supplied `content-type` HTTP header to `application/json` in the following cases:
|
||||||
/// add an `application/json` content type header.
|
///
|
||||||
pub fn add_missing_content_type_header(headers: &mut hyper::header::HeaderMap) {
|
/// - no `content-type` supplied.
|
||||||
headers
|
/// - supplied `content-type` start with `text/plain`, for example:
|
||||||
.entry(hyper::header::CONTENT_TYPE)
|
/// - `text/plain`
|
||||||
.or_insert(hyper::header::HeaderValue::from_static("application/json"));
|
/// - `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"),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1467,6 +1467,88 @@ async fn rpc_endpoint(parallel_cpu_threads: bool) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test that the JSON-RPC endpoint responds to requests with different content types.
|
||||||
|
///
|
||||||
|
/// This test ensures that the curl examples of zcashd rpc methods will also work in Zebra.
|
||||||
|
///
|
||||||
|
/// https://zcash.github.io/rpc/getblockchaininfo.html
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rpc_endpoint_client_content_type() -> Result<()> {
|
||||||
|
let _init_guard = zebra_test::init();
|
||||||
|
if zebra_test::net::zebra_skip_network_tests() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a configuration that has RPC listen_addr set
|
||||||
|
// [Note on port conflict](#Note on port conflict)
|
||||||
|
let mut config = random_known_rpc_port_config(true)?;
|
||||||
|
|
||||||
|
let dir = testdir()?.with_config(&mut config)?;
|
||||||
|
let mut child = dir.spawn_child(args!["start"])?;
|
||||||
|
|
||||||
|
// Wait until port is open.
|
||||||
|
child.expect_stdout_line_matches(
|
||||||
|
format!("Opened RPC endpoint at {}", config.rpc.listen_addr.unwrap()).as_str(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Create an http client
|
||||||
|
let client = RpcRequestClient::new(config.rpc.listen_addr.unwrap());
|
||||||
|
|
||||||
|
// Call to `getinfo` RPC method with a no content type.
|
||||||
|
let res = client
|
||||||
|
.call_with_no_content_type("getinfo", "[]".to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Zebra will insert valid `application/json` content type and succeed.
|
||||||
|
assert!(res.status().is_success());
|
||||||
|
|
||||||
|
// Call to `getinfo` RPC method with a `text/plain`.
|
||||||
|
let res = client
|
||||||
|
.call_with_content_type("getinfo", "[]".to_string(), "text/plain".to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Zebra will replace to the valid `application/json` content type and succeed.
|
||||||
|
assert!(res.status().is_success());
|
||||||
|
|
||||||
|
// Call to `getinfo` RPC method with a `text/plain` content type as the zcashd rpc docs.
|
||||||
|
let res = client
|
||||||
|
.call_with_content_type("getinfo", "[]".to_string(), "text/plain;".to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Zebra will replace to the valid `application/json` content type and succeed.
|
||||||
|
assert!(res.status().is_success());
|
||||||
|
|
||||||
|
// Call to `getinfo` RPC method with a `text/plain; other string` content type.
|
||||||
|
let res = client
|
||||||
|
.call_with_content_type(
|
||||||
|
"getinfo",
|
||||||
|
"[]".to_string(),
|
||||||
|
"text/plain; other string".to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Zebra will replace to the valid `application/json` content type and succeed.
|
||||||
|
assert!(res.status().is_success());
|
||||||
|
|
||||||
|
// Call to `getinfo` RPC method with a valid `application/json` content type.
|
||||||
|
let res = client
|
||||||
|
.call_with_content_type("getinfo", "[]".to_string(), "application/json".to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Zebra will not replace valid content type and succeed.
|
||||||
|
assert!(res.status().is_success());
|
||||||
|
|
||||||
|
// Call to `getinfo` RPC method with invalid string as content type.
|
||||||
|
let res = client
|
||||||
|
.call_with_content_type("getinfo", "[]".to_string(), "whatever".to_string())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Zebra will not replace unrecognized content type and fail.
|
||||||
|
assert!(res.status().is_client_error());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Test that Zebra's non-blocking logger works, by creating lots of debug output, but not reading the logs.
|
/// Test that Zebra's non-blocking logger works, by creating lots of debug output, but not reading the logs.
|
||||||
/// Then make sure Zebra drops excess log lines. (Previously, it would block waiting for logs to be read.)
|
/// Then make sure Zebra drops excess log lines. (Previously, it would block waiting for logs to be read.)
|
||||||
///
|
///
|
||||||
|
|
Loading…
Reference in New Issue