From 7ee665a8223d0d426c3cddcdfb2b716cc10ac9d8 Mon Sep 17 00:00:00 2001 From: carneiro-cw <156914855+carneiro-cw@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:26:11 -0300 Subject: [PATCH] fix: implement a temporary proxy layer for get requests (#1940) --- Cargo.lock | 57 ++--- Cargo.toml | 5 +- e2e/test/automine/e2e-json-rpc.test.ts | 6 + src/eth/primitives/mod.rs | 1 + src/eth/rpc/mod.rs | 1 + src/eth/rpc/proxy_get_request.rs | 214 +++++++++++++++++++ src/eth/rpc/rpc_server.rs | 10 +- src/eth/storage/permanent/rocks/types/mod.rs | 2 +- 8 files changed, 262 insertions(+), 34 deletions(-) create mode 100644 src/eth/rpc/proxy_get_request.rs diff --git a/Cargo.lock b/Cargo.lock index 0dfa87339..35c483e10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -617,9 +617,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] @@ -1936,9 +1936,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -1977,7 +1977,7 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.2", "hyper-util", "log", "rustls 0.23.12", @@ -2007,7 +2007,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.2", "hyper-util", "native-tls", "tokio", @@ -2026,7 +2026,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.2", "pin-project-lite", "socket2", "tokio", @@ -2235,9 +2235,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f01f48e04e0d7da72280ab787c9943695699c9b32b99158ece105e8ad0afea" +checksum = "c5c71d8c1a731cc4227c2f698d377e7848ca12c8a48866fc5e6951c43a4db843" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -2251,9 +2251,9 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d80eccbd47a7b9f1e67663fd846928e941cb49c65236e297dd11c9ea3c5e3387" +checksum = "548125b159ba1314104f5bb5f38519e03a41862786aa3925cf349aae9cdd546e" dependencies = [ "base64 0.22.1", "futures-channel", @@ -2276,9 +2276,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c2709a32915d816a6e8f625bf72cf74523ebe5d8829f895d6b041b1d3137818" +checksum = "f2882f6f8acb9fdaec7cefc4fd607119a9bd709831df7d7672a1d3b644628280" dependencies = [ "async-trait", "bytes", @@ -2303,14 +2303,14 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc54db939002b030e794fbfc9d5a925aa2854889c5a2f0352b0bffa54681707e" +checksum = "b3638bc4617f96675973253b3a45006933bde93c2fd8a6170b33c777cc389e5b" dependencies = [ "async-trait", "base64 0.22.1", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.2", "hyper-rustls 0.27.2", "hyper-util", "jsonrpsee-core", @@ -2328,15 +2328,15 @@ dependencies = [ [[package]] name = "jsonrpsee-server" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e30110d0f2d7866c8cc6c86483bdab2eb9f4d2f0e20db55518b2bca84651ba8e" +checksum = "82ad8ddc14be1d4290cd68046e7d1d37acd408efed6d3ca08aefcc3ad6da069c" dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.2", "hyper-util", "jsonrpsee-core", "jsonrpsee-types", @@ -2355,9 +2355,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca331cd7b3fe95b33432825c2d4c9f5a43963e207fdc01ae67f9fd80ab0930f" +checksum = "a178c60086f24cc35bb82f57c651d0d25d99c4742b4d335de04e97fa1f08a8a1" dependencies = [ "http 1.1.0", "serde", @@ -2367,9 +2367,9 @@ dependencies = [ [[package]] name = "jsonrpsee-wasm-client" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c603d97578071dc44d79d3cfaf0775437638fd5adc33c6b622dfe4fa2ec812d" +checksum = "1a01cd500915d24ab28ca17527e23901ef1be6d659a2322451e1045532516c25" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -2378,9 +2378,9 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755ca3da1c67671f1fae01cd1a47f41dfb2233a8f19a643e587ab0a663942044" +checksum = "0fe322e0896d0955a3ebdd5bf813571c53fea29edd713bc315b76620b327e86d" dependencies = [ "http 1.1.0", "jsonrpsee-client-transport", @@ -2589,7 +2589,7 @@ checksum = "26eb45aff37b45cff885538e1dcbd6c2b462c04fe84ce0155ea469f325672c98" dependencies = [ "base64 0.22.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.2", "hyper-tls", "hyper-util", "indexmap 2.2.6", @@ -3672,7 +3672,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.2", "hyper-tls", "hyper-util", "ipnet", @@ -4900,6 +4900,7 @@ dependencies = [ "anyhow", "async-trait", "bincode", + "bytes", "cfg-if", "chrono", "clap", @@ -4924,6 +4925,8 @@ dependencies = [ "hex-literal", "hex_fmt", "http 1.1.0", + "http-body 1.0.1", + "http-body-util", "humantime", "indexmap 2.2.6", "indicatif", diff --git a/Cargo.toml b/Cargo.toml index 5f480cf46..5f50687b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,12 +83,15 @@ rlp = "=0.5.2" triehash = "=0.8.4" # network -jsonrpsee = { version = "=0.24.6", features = ["server", "client"] } +jsonrpsee = { version = "=0.24.7", features = ["server", "client"] } reqwest = { version = "=0.12.4", features = ["json"] } tonic = "=0.11.0" tower = "=0.4.13" tower-http = { version = "=0.5.2", features = ["cors"] } http = "=1.1.0" +http-body = "=1.0.1" +http-body-util = "=0.1.2" +bytes = "=1.9.0" # observability console-subscriber = "=0.2.0" diff --git a/e2e/test/automine/e2e-json-rpc.test.ts b/e2e/test/automine/e2e-json-rpc.test.ts index 956752c82..23afb5d56 100644 --- a/e2e/test/automine/e2e-json-rpc.test.ts +++ b/e2e/test/automine/e2e-json-rpc.test.ts @@ -60,6 +60,10 @@ describe("JSON-RPC", () => { const error = await sendAndGetError("eth_blockNumber"); expect(error.code).eq(1003); + // GET request to health endpoint should fail when unknown clients are disallowed + const healthResponseErr = await fetch("http://localhost:3000/health"); + expect(healthResponseErr.status).eq(500); + // Requests with client identification should succeed const validHeaders = { "x-app": "test-client", @@ -76,6 +80,8 @@ describe("JSON-RPC", () => { // URL parameters should also work const validUrlParams = ["app=test-client", "client=test-client"]; + const healthResponse = await fetch(`http://localhost:3000/health?app=test-client`); + expect(healthResponse.status).eq(200); for (const param of validUrlParams) { const providerWithParam = new JsonRpcProvider(`http://localhost:3000?${param}`); const blockNumber = await providerWithParam.getBlockNumber(); diff --git a/src/eth/primitives/mod.rs b/src/eth/primitives/mod.rs index 4fdba33c7..36d431a4b 100644 --- a/src/eth/primitives/mod.rs +++ b/src/eth/primitives/mod.rs @@ -94,6 +94,7 @@ pub use slot::Slot; pub use slot_index::SlotIndex; pub use slot_value::SlotValue; pub use stratus_error::ConsensusError; +pub use stratus_error::ErrorCode; pub use stratus_error::ImporterError; pub use stratus_error::RpcError; pub use stratus_error::StateError; diff --git a/src/eth/rpc/mod.rs b/src/eth/rpc/mod.rs index cc2c6a824..71865f69f 100644 --- a/src/eth/rpc/mod.rs +++ b/src/eth/rpc/mod.rs @@ -1,5 +1,6 @@ //! Ethereum JSON-RPC server. +pub mod proxy_get_request; mod rpc_client_app; mod rpc_config; mod rpc_context; diff --git a/src/eth/rpc/proxy_get_request.rs b/src/eth/rpc/proxy_get_request.rs new file mode 100644 index 000000000..e1d37a8bf --- /dev/null +++ b/src/eth/rpc/proxy_get_request.rs @@ -0,0 +1,214 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Middleware that proxies requests at a specified URI to internal +//! RPC method calls. Temporary "fork" of jsonrpsee while #1512 is not merged + +use std::future::Future; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; + +use bytes::Bytes; +use http::header::ACCEPT; +use http::header::CONTENT_TYPE; +use http::HeaderValue; +use http::Method; +use http::Uri; +use http_body_util::BodyExt; +use jsonrpsee::core::BoxError; +use jsonrpsee::server::http::response::internal_error; +use jsonrpsee::server::http::response::ok_response; +use jsonrpsee::server::HttpBody; +use jsonrpsee::server::HttpRequest; +use jsonrpsee::server::HttpResponse; +use jsonrpsee::types::Id; +use jsonrpsee::types::RequestSer; +use tower::Layer; +use tower::Service; + +/// Error that occur if the specified path doesn't start with `/` +#[derive(Debug, thiserror::Error)] +#[error("ProxyGetRequestLayer path must start with `/`, got `{0}`")] +pub struct InvalidPath(String); + +/// Layer that applies [`ProxyGetRequest`] which proxies the `GET /path` requests to +/// specific RPC method calls and that strips the response. +/// +/// See [`ProxyGetRequest`] for more details. +#[derive(Debug, Clone)] +pub struct ProxyGetRequestTempLayer { + path: String, + method: String, +} + +impl ProxyGetRequestTempLayer { + /// Creates a new [`ProxyGetRequestLayer`]. + /// + /// See [`ProxyGetRequest`] for more details. + pub fn new(path: impl Into, method: impl Into) -> Result { + let path = path.into(); + if !path.starts_with('/') { + return Err(InvalidPath(path)); + } + + Ok(Self { path, method: method.into() }) + } +} +impl Layer for ProxyGetRequestTempLayer { + type Service = ProxyGetRequestTemp; + + #[allow(clippy::expect_used)] + fn layer(&self, inner: S) -> Self::Service { + ProxyGetRequestTemp::new(inner, &self.path, &self.method).expect("Path already validated in ProxyGetRequestLayer; qed") + } +} + +/// Proxy `GET /path` requests to the specified RPC method calls. +/// +/// # Request +/// +/// The `GET /path` requests are modified into valid `POST` requests for +/// calling the RPC method. This middleware adds appropriate headers to the +/// request, and completely modifies the request `BODY`. +/// +/// # Response +/// +/// The response of the RPC method is stripped down to contain only the method's +/// response, removing any RPC 2.0 spec logic regarding the response' body. +#[derive(Debug, Clone)] +pub struct ProxyGetRequestTemp { + inner: S, + path: Arc, + method: Arc, +} + +impl ProxyGetRequestTemp { + /// Creates a new [`ProxyGetRequest`]. + /// + /// The request `GET /path` is redirected to the provided method. + /// Fails if the path does not start with `/`. + pub fn new(inner: S, path: &str, method: &str) -> Result { + if !path.starts_with('/') { + return Err(InvalidPath(path.to_string())); + } + + Ok(Self { + inner, + path: Arc::from(path), + method: Arc::from(method), + }) + } +} + +impl Service> for ProxyGetRequestTemp +where + S: Service, + S::Response: 'static, + S::Error: Into + 'static, + S::Future: Send + 'static, + B: http_body::Body + Send + 'static, + B::Data: Send, + B::Error: Into, +{ + type Response = S::Response; + type Error = BoxError; + type Future = Pin> + Send + 'static>>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(Into::into) + } + + #[allow(clippy::expect_used)] + fn call(&mut self, mut req: HttpRequest) -> Self::Future { + let modify = self.path.as_ref() == req.uri().path() && req.method() == Method::GET; + + // Proxy the request to the appropriate method call. + let req = if modify { + // RPC methods are accessed with `POST`. + *req.method_mut() = Method::POST; + // Precautionary remove the URI. + *req.uri_mut() = if let Some(query) = req.uri().query() { + Uri::from_str(&format!("/?{}", query)).expect("Valid uri; qed") + } else { + Uri::from_static("/") + }; + + // Requests must have the following headers: + req.headers_mut().insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + req.headers_mut().insert(ACCEPT, HeaderValue::from_static("application/json")); + + // Adjust the body to reflect the method call. + let bytes = serde_json::to_vec(&RequestSer::borrowed(&Id::Number(0), &self.method, None)).expect("Valid request; qed"); + + let body = HttpBody::from(bytes); + + req.map(|_| body) + } else { + req.map(HttpBody::new) + }; + + // Call the inner service and get a future that resolves to the response. + let fut = self.inner.call(req); + + // Adjust the response if needed. + let res_fut = async move { + let res = fut.await.map_err(|err| err.into())?; + + // Nothing to modify: return the response as is. + if !modify { + return Ok(res); + } + + let mut body = http_body_util::BodyStream::new(res.into_body()); + let mut bytes = Vec::new(); + + while let Some(frame) = body.frame().await { + let data = frame?.into_data().map_err(|e| format!("{e:?}"))?; + bytes.extend(data); + } + + #[derive(serde::Deserialize, Debug)] + struct RpcPayload<'a> { + #[serde(borrow)] + result: &'a serde_json::value::RawValue, + } + + let response = if let Ok(payload) = serde_json::from_slice::(&bytes) { + ok_response(payload.result.to_string()) + } else { + internal_error() + }; + + Ok(response) + }; + + Box::pin(res_fut) + } +} diff --git a/src/eth/rpc/rpc_server.rs b/src/eth/rpc/rpc_server.rs index 20d7ebc49..6af000aaf 100644 --- a/src/eth/rpc/rpc_server.rs +++ b/src/eth/rpc/rpc_server.rs @@ -11,7 +11,6 @@ use ethereum_types::U256; use futures::join; use http::Method; use itertools::Itertools; -use jsonrpsee::server::middleware::http::ProxyGetRequestLayer; use jsonrpsee::server::RandomStringIdProvider; use jsonrpsee::server::RpcModule; use jsonrpsee::server::RpcServiceBuilder; @@ -63,6 +62,7 @@ use crate::eth::primitives::TransactionInput; use crate::eth::rpc::next_rpc_param; use crate::eth::rpc::next_rpc_param_or_default; use crate::eth::rpc::parse_rpc_rlp; +use crate::eth::rpc::proxy_get_request::ProxyGetRequestTempLayer; use crate::eth::rpc::rpc_parser::RpcExtensionsExt; use crate::eth::rpc::RpcContext; use crate::eth::rpc::RpcHttpMiddleware; @@ -138,10 +138,10 @@ pub async fn serve_rpc( let http_middleware = tower::ServiceBuilder::new() .layer(cors) .layer_fn(RpcHttpMiddleware::new) - .layer(ProxyGetRequestLayer::new("/health", "stratus_health").unwrap()) - .layer(ProxyGetRequestLayer::new("/version", "stratus_version").unwrap()) - .layer(ProxyGetRequestLayer::new("/config", "stratus_config").unwrap()) - .layer(ProxyGetRequestLayer::new("/state", "stratus_state").unwrap()); + .layer(ProxyGetRequestTempLayer::new("/health", "stratus_health").unwrap()) + .layer(ProxyGetRequestTempLayer::new("/version", "stratus_version").unwrap()) + .layer(ProxyGetRequestTempLayer::new("/config", "stratus_config").unwrap()) + .layer(ProxyGetRequestTempLayer::new("/state", "stratus_state").unwrap()); // serve module let server = Server::builder() diff --git a/src/eth/storage/permanent/rocks/types/mod.rs b/src/eth/storage/permanent/rocks/types/mod.rs index 851175590..23115940d 100644 --- a/src/eth/storage/permanent/rocks/types/mod.rs +++ b/src/eth/storage/permanent/rocks/types/mod.rs @@ -37,7 +37,6 @@ pub use unix_time::UnixTimeRocksdb; #[cfg(test)] mod tests { use block_header::BlockHeaderRocksdb; - use bytes::BytesRocksdb; use chain_id::ChainIdRocksdb; use difficulty::DifficultyRocksdb; use execution::ExecutionRocksdb; @@ -53,6 +52,7 @@ mod tests { use unix_time::UnixTimeRocksdb; use wei::WeiRocksdb; + use self::bytes::BytesRocksdb; use super::log::LogRocksdb; use super::*; use crate::gen_test_bincode;