From a0bfb654cda91149f28e68c193b4e18e148c6519 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 17 Apr 2023 03:43:08 +0200 Subject: [PATCH] test: add basic auth server tests (#2278) --- Cargo.lock | 3 +- bin/reth/Cargo.toml | 1 - bin/reth/src/args/rpc_server_args.rs | 13 ++- crates/rpc/rpc-builder/Cargo.toml | 2 + crates/rpc/rpc-builder/src/auth.rs | 122 +++++++++++++++++++-- crates/rpc/rpc-builder/src/lib.rs | 2 +- crates/rpc/rpc-builder/tests/it/auth.rs | 44 ++++++++ crates/rpc/rpc-builder/tests/it/main.rs | 1 + crates/rpc/rpc-builder/tests/it/utils.rs | 27 ++++- crates/rpc/rpc/src/layers/jwt_secret.rs | 25 ++++- crates/rpc/rpc/src/layers/jwt_validator.rs | 3 +- crates/rpc/rpc/src/layers/mod.rs | 2 +- crates/rpc/rpc/src/lib.rs | 2 +- 13 files changed, 215 insertions(+), 32 deletions(-) create mode 100644 crates/rpc/rpc-builder/tests/it/auth.rs diff --git a/Cargo.lock b/Cargo.lock index 9403be2a4e..ed4de751dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4459,7 +4459,6 @@ dependencies = [ "hex", "human_bytes", "hyper", - "jsonrpsee", "metrics", "metrics-exporter-prometheus", "metrics-util", @@ -5167,9 +5166,11 @@ version = "0.1.0" dependencies = [ "hyper", "jsonrpsee", + "reth-beacon-consensus", "reth-interfaces", "reth-ipc", "reth-network-api", + "reth-payload-builder", "reth-primitives", "reth-provider", "reth-rpc", diff --git a/bin/reth/Cargo.toml b/bin/reth/Cargo.toml index 51d5c4978d..9e6ff29775 100644 --- a/bin/reth/Cargo.toml +++ b/bin/reth/Cargo.toml @@ -75,7 +75,6 @@ pin-project = "1.0" # http/rpc hyper = "0.14.25" -jsonrpsee = { version = "0.16", features = ["server"] } # misc eyre = "0.6.8" diff --git a/bin/reth/src/args/rpc_server_args.rs b/bin/reth/src/args/rpc_server_args.rs index e3996e26e2..98c18e192a 100644 --- a/bin/reth/src/args/rpc_server_args.rs +++ b/bin/reth/src/args/rpc_server_args.rs @@ -3,16 +3,17 @@ use crate::dirs::{JwtSecretPath, PlatformPath}; use clap::Args; use futures::FutureExt; -use jsonrpsee::server::ServerHandle; use reth_network_api::{NetworkInfo, Peers}; use reth_provider::{ BlockProvider, CanonStateSubscriptions, EvmEnvProvider, HeaderProvider, StateProviderFactory, }; use reth_rpc::{JwtError, JwtSecret}; use reth_rpc_builder::{ - auth::AuthServerConfig, constants, error::RpcError, IpcServerBuilder, RethRpcModule, - RpcModuleBuilder, RpcModuleSelection, RpcServerConfig, RpcServerHandle, ServerBuilder, - TransportRpcModuleConfig, + auth::{AuthServerConfig, AuthServerHandle}, + constants, + error::RpcError, + IpcServerBuilder, RethRpcModule, RpcModuleBuilder, RpcModuleSelection, RpcServerConfig, + RpcServerHandle, ServerBuilder, TransportRpcModuleConfig, }; use reth_rpc_engine_api::{EngineApi, EngineApiServer}; use reth_tasks::TaskSpawner; @@ -129,7 +130,7 @@ impl RpcServerArgs { executor: Tasks, events: Events, engine_api: Engine, - ) -> Result<(RpcServerHandle, ServerHandle), RpcError> + ) -> Result<(RpcServerHandle, AuthServerHandle), RpcError> where Client: BlockProvider + HeaderProvider @@ -212,7 +213,7 @@ impl RpcServerArgs { network: Network, executor: Tasks, engine_api: EngineApi, - ) -> Result + ) -> Result where Client: BlockProvider + HeaderProvider diff --git a/crates/rpc/rpc-builder/Cargo.toml b/crates/rpc/rpc-builder/Cargo.toml index 3d5ac8bbea..dfce673cab 100644 --- a/crates/rpc/rpc-builder/Cargo.toml +++ b/crates/rpc/rpc-builder/Cargo.toml @@ -39,6 +39,8 @@ reth-transaction-pool = { path = "../../transaction-pool", features = ["test-uti reth-provider = { path = "../../storage/provider", features = ["test-utils"] } reth-network-api = { path = "../../net/network-api", features = ["test-utils"] } reth-interfaces = { path = "../../interfaces", features = ["test-utils"] } +reth-beacon-consensus = { path = "../../consensus/beacon" } +reth-payload-builder = { path = "../../payload/builder", features = ["test-utils"] } tokio = { version = "1", features = ["rt", "rt-multi-thread"] } serde_json = "1.0.94" diff --git a/crates/rpc/rpc-builder/src/auth.rs b/crates/rpc/rpc-builder/src/auth.rs index 8f4238b173..74e2c2dc9e 100644 --- a/crates/rpc/rpc-builder/src/auth.rs +++ b/crates/rpc/rpc-builder/src/auth.rs @@ -2,17 +2,24 @@ use crate::{ constants, error::{RpcError, ServerKind}, }; +use hyper::header::AUTHORIZATION; pub use jsonrpsee::server::ServerBuilder; -use jsonrpsee::server::{RpcModule, ServerHandle}; +use jsonrpsee::{ + http_client::HeaderMap, + server::{RpcModule, ServerHandle}, +}; use reth_network_api::{NetworkInfo, Peers}; use reth_provider::{BlockProvider, EvmEnvProvider, HeaderProvider, StateProviderFactory}; use reth_rpc::{ - eth::cache::EthStateCache, AuthLayer, EthApi, EthFilter, JwtAuthValidator, JwtSecret, + eth::cache::EthStateCache, AuthLayer, Claims, EthApi, EthFilter, JwtAuthValidator, JwtSecret, }; use reth_rpc_api::{servers::*, EngineApiServer}; use reth_tasks::TaskSpawner; use reth_transaction_pool::TransactionPool; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; /// Configure and launch a _standalone_ auth server with `engine` and a _new_ `eth` namespace. #[allow(clippy::too_many_arguments)] @@ -24,7 +31,7 @@ pub async fn launch( engine_api: EngineApi, socket_addr: SocketAddr, secret: JwtSecret, -) -> Result +) -> Result where Client: BlockProvider + HeaderProvider @@ -52,7 +59,7 @@ pub async fn launch_with_eth_api( engine_api: EngineApi, socket_addr: SocketAddr, secret: JwtSecret, -) -> Result +) -> Result where Client: BlockProvider + HeaderProvider @@ -73,7 +80,7 @@ where // Create auth middleware. let middleware = - tower::ServiceBuilder::new().layer(AuthLayer::new(JwtAuthValidator::new(secret))); + tower::ServiceBuilder::new().layer(AuthLayer::new(JwtAuthValidator::new(secret.clone()))); // By default, both http and ws are enabled. let server = ServerBuilder::new() @@ -82,7 +89,10 @@ where .await .map_err(|err| RpcError::from_jsonrpsee_error(err, ServerKind::Auth(socket_addr)))?; - Ok(server.start(module)?) + let local_addr = server.local_addr()?; + + let handle = server.start(module)?; + Ok(AuthServerHandle { handle, local_addr, secret }) } /// Server configuration for the auth server. @@ -103,12 +113,12 @@ impl AuthServerConfig { } /// Convenience function to start a server in one step. - pub async fn start(self, module: AuthRpcModule) -> Result { + pub async fn start(self, module: AuthRpcModule) -> Result { let Self { socket_addr, secret } = self; // Create auth middleware. - let middleware = - tower::ServiceBuilder::new().layer(AuthLayer::new(JwtAuthValidator::new(secret))); + let middleware = tower::ServiceBuilder::new() + .layer(AuthLayer::new(JwtAuthValidator::new(secret.clone()))); // By default, both http and ws are enabled. let server = @@ -116,7 +126,10 @@ impl AuthServerConfig { |err| RpcError::from_jsonrpsee_error(err, ServerKind::Auth(socket_addr)), )?; - Ok(server.start(module.inner)?) + let local_addr = server.local_addr()?; + + let handle = server.start(module.inner)?; + Ok(AuthServerHandle { handle, local_addr, secret }) } } @@ -172,8 +185,93 @@ pub struct AuthRpcModule { // === impl TransportRpcModules === impl AuthRpcModule { + /// Create a new `AuthRpcModule` with the given `engine_api`. + pub fn new(engine: EngineApi) -> Self + where + EngineApi: EngineApiServer, + { + let mut module = RpcModule::new(()); + module.merge(engine.into_rpc()).expect("No conflicting methods"); + Self { inner: module } + } + + /// Get a reference to the inner `RpcModule`. + pub fn module_mut(&mut self) -> &mut RpcModule<()> { + &mut self.inner + } + /// Convenience function for starting a server - pub async fn start_server(self, config: AuthServerConfig) -> Result { + pub async fn start_server( + self, + config: AuthServerConfig, + ) -> Result { config.start(self).await } } + +/// A handle to the spawned auth server. +/// +/// When this type is dropped or [AuthServerHandle::stop] has been called the server will be +/// stopped. +#[derive(Clone, Debug)] +#[must_use = "Server stops if dropped"] +pub struct AuthServerHandle { + local_addr: SocketAddr, + handle: ServerHandle, + secret: JwtSecret, +} + +// === impl AuthServerHandle === + +impl AuthServerHandle { + /// Returns the [`SocketAddr`] of the http server if started. + pub fn local_addr(&self) -> SocketAddr { + self.local_addr + } + + /// Tell the server to stop without waiting for the server to stop. + pub fn stop(self) -> Result<(), RpcError> { + Ok(self.handle.stop()?) + } + + /// Returns the url to the http server + pub fn http_url(&self) -> String { + format!("http://{}", self.local_addr) + } + + /// Returns the url to the ws server + pub fn ws_url(&self) -> String { + format!("ws://{}", self.local_addr) + } + + fn bearer(&self) -> String { + format!( + "Bearer {}", + self.secret + .encode(&Claims { + iat: (SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + + Duration::from_secs(60)) + .as_secs(), + exp: None, + }) + .unwrap() + ) + } + + /// Returns a http client connected to the server. + pub fn http_client(&self) -> jsonrpsee::http_client::HttpClient { + jsonrpsee::http_client::HttpClientBuilder::default() + .set_headers(HeaderMap::from_iter([(AUTHORIZATION, self.bearer().parse().unwrap())])) + .build(self.http_url()) + .expect("Failed to create http client") + } + + /// Returns a ws client connected to the server. + pub async fn ws_client(&self) -> jsonrpsee::ws_client::WsClient { + jsonrpsee::ws_client::WsClientBuilder::default() + .set_headers(HeaderMap::from_iter([(AUTHORIZATION, self.bearer().parse().unwrap())])) + .build(self.ws_url()) + .await + .expect("Failed to create ws client") + } +} diff --git a/crates/rpc/rpc-builder/src/lib.rs b/crates/rpc/rpc-builder/src/lib.rs index 51eda8a381..c7d2651ee5 100644 --- a/crates/rpc/rpc-builder/src/lib.rs +++ b/crates/rpc/rpc-builder/src/lib.rs @@ -1255,7 +1255,7 @@ impl fmt::Debug for RpcServer { /// /// When this type is dropped or [RpcServerHandle::stop] has been called the server will be stopped. #[derive(Clone)] -#[must_use = "Server stop if dropped"] +#[must_use = "Server stops if dropped"] pub struct RpcServerHandle { /// The address of the http/ws server http_local_addr: Option, diff --git a/crates/rpc/rpc-builder/tests/it/auth.rs b/crates/rpc/rpc-builder/tests/it/auth.rs new file mode 100644 index 0000000000..3e8063d73b --- /dev/null +++ b/crates/rpc/rpc-builder/tests/it/auth.rs @@ -0,0 +1,44 @@ +//! Auth server tests + +use crate::utils::launch_auth; +use jsonrpsee::core::client::{ClientT, SubscriptionClientT}; +use reth_primitives::Block; +use reth_rpc::JwtSecret; +use reth_rpc_api::clients::EngineApiClient; +use reth_rpc_types::engine::{ForkchoiceState, PayloadId, TransitionConfiguration}; + +#[allow(unused_must_use)] +async fn test_basic_engine_calls(client: &C) +where + C: ClientT + SubscriptionClientT + Sync, +{ + let block = Block::default().seal_slow(); + EngineApiClient::new_payload_v1(client, block.clone().into()).await; + EngineApiClient::new_payload_v2(client, block.into()).await; + EngineApiClient::fork_choice_updated_v1(client, ForkchoiceState::default(), None).await; + EngineApiClient::get_payload_v1(client, PayloadId::new([0, 0, 0, 0, 0, 0, 0, 0])).await; + EngineApiClient::get_payload_v2(client, PayloadId::new([0, 0, 0, 0, 0, 0, 0, 0])).await; + EngineApiClient::get_payload_bodies_by_hash_v1(client, vec![]).await; + EngineApiClient::get_payload_bodies_by_range_v1(client, 0u64.into(), 1u64.into()).await; + EngineApiClient::exchange_transition_configuration(client, TransitionConfiguration::default()) + .await; + EngineApiClient::exchange_capabilities(client, vec![]).await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_auth_endpoints_http() { + reth_tracing::init_test_tracing(); + let secret = JwtSecret::random(); + let handle = launch_auth(secret).await; + let client = handle.http_client(); + test_basic_engine_calls(&client).await +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_auth_endpoints_ws() { + reth_tracing::init_test_tracing(); + let secret = JwtSecret::random(); + let handle = launch_auth(secret).await; + let client = handle.ws_client().await; + test_basic_engine_calls(&client).await +} diff --git a/crates/rpc/rpc-builder/tests/it/main.rs b/crates/rpc/rpc-builder/tests/it/main.rs index 58a8a1fa3a..6f9290d0f9 100644 --- a/crates/rpc/rpc-builder/tests/it/main.rs +++ b/crates/rpc/rpc-builder/tests/it/main.rs @@ -1,3 +1,4 @@ +mod auth; mod http; mod serde; mod startup; diff --git a/crates/rpc/rpc-builder/tests/it/utils.rs b/crates/rpc/rpc-builder/tests/it/utils.rs index e2efd31d54..52b0610ed7 100644 --- a/crates/rpc/rpc-builder/tests/it/utils.rs +++ b/crates/rpc/rpc-builder/tests/it/utils.rs @@ -1,18 +1,43 @@ +use reth_beacon_consensus::BeaconConsensusEngineHandle; use reth_network_api::test_utils::NoopNetwork; +use reth_payload_builder::test_utils::spawn_test_payload_service; +use reth_primitives::MAINNET; use reth_provider::test_utils::{NoopProvider, TestCanonStateSubscriptions}; +use reth_rpc::JwtSecret; use reth_rpc_builder::{ + auth::{AuthRpcModule, AuthServerConfig, AuthServerHandle}, RpcModuleBuilder, RpcModuleSelection, RpcServerConfig, RpcServerHandle, TransportRpcModuleConfig, }; +use reth_rpc_engine_api::EngineApi; use reth_tasks::TokioTaskExecutor; use reth_transaction_pool::test_utils::{testing_pool, TestPool}; -use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::{ + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + sync::Arc, +}; +use tokio::sync::mpsc::unbounded_channel; /// Localhost with port 0 so a free port is used. pub fn test_address() -> SocketAddr { SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)) } +/// Launches a new server for the auth module +pub async fn launch_auth(secret: JwtSecret) -> AuthServerHandle { + let config = AuthServerConfig::builder(secret).socket_addr(test_address()).build(); + let (tx, _rx) = unbounded_channel(); + let beacon_engine_handle = BeaconConsensusEngineHandle::new(tx); + let engine_api = EngineApi::new( + NoopProvider::default(), + Arc::new(MAINNET.clone()), + beacon_engine_handle, + spawn_test_payload_service().into(), + ); + let module = AuthRpcModule::new(engine_api); + module.start_server(config).await.unwrap() +} + /// Launches a new server with http only with the given modules pub async fn launch_http(modules: impl Into) -> RpcServerHandle { let builder = test_rpc_builder(); diff --git a/crates/rpc/rpc/src/layers/jwt_secret.rs b/crates/rpc/rpc/src/layers/jwt_secret.rs index 2ceca80a3f..608f90be43 100644 --- a/crates/rpc/rpc/src/layers/jwt_secret.rs +++ b/crates/rpc/rpc/src/layers/jwt_secret.rs @@ -143,12 +143,24 @@ impl JwtSecret { JwtSecret::from_hex(secret).unwrap() } - #[cfg(test)] - pub(crate) fn encode(&self, claims: &Claims) -> Result> { + /// Encode the header and claims given and sign the payload using the algorithm from the header + /// and the key. + /// + /// ```rust + /// use reth_rpc::{Claims, JwtSecret}; + /// + /// let my_claims = Claims { + /// iat: 0, + /// exp: None + /// }; + /// let secret = JwtSecret::random(); + /// let token = secret.encode(&my_claims).unwrap(); + /// ``` + pub fn encode(&self, claims: &Claims) -> Result { let bytes = &self.0; let key = jsonwebtoken::EncodingKey::from_secret(bytes); let algo = jsonwebtoken::Header::new(Algorithm::HS256); - Ok(jsonwebtoken::encode(&algo, claims, &key)?) + jsonwebtoken::encode(&algo, claims, &key) } } @@ -160,14 +172,15 @@ impl JwtSecret { /// The Engine API spec requires that just the `iat` (issued-at) claim is provided. /// It ignores claims that are optional or additional for this specification. #[derive(Debug, Serialize, Deserialize)] -pub(crate) struct Claims { +pub struct Claims { /// The "iat" value MUST be a number containing a NumericDate value. /// According to the RFC A NumericDate represents the number of seconds since /// the UNIX_EPOCH. /// - [`RFC-7519 - Spec`](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6) /// - [`RFC-7519 - Notations`](https://www.rfc-editor.org/rfc/rfc7519#section-2) - pub(crate) iat: u64, - pub(crate) exp: Option, + pub iat: u64, + /// Expiration, if any + pub exp: Option, } impl Claims { diff --git a/crates/rpc/rpc/src/layers/jwt_validator.rs b/crates/rpc/rpc/src/layers/jwt_validator.rs index 27d6321da3..3bec565242 100644 --- a/crates/rpc/rpc/src/layers/jwt_validator.rs +++ b/crates/rpc/rpc/src/layers/jwt_validator.rs @@ -68,9 +68,8 @@ fn err_response(err: JwtError) -> Response { #[cfg(test)] mod tests { - use http::{header, HeaderMap}; - use crate::layers::jwt_validator::get_bearer; + use http::{header, HeaderMap}; #[test] fn auth_header_available() { diff --git a/crates/rpc/rpc/src/layers/mod.rs b/crates/rpc/rpc/src/layers/mod.rs index f464e6f5dc..b8a3cf2e47 100644 --- a/crates/rpc/rpc/src/layers/mod.rs +++ b/crates/rpc/rpc/src/layers/mod.rs @@ -4,7 +4,7 @@ mod auth_layer; mod jwt_secret; mod jwt_validator; pub use auth_layer::AuthLayer; -pub use jwt_secret::{JwtError, JwtSecret}; +pub use jwt_secret::{Claims, JwtError, JwtSecret}; pub use jwt_validator::JwtAuthValidator; /// General purpose trait to validate Http Authorization diff --git a/crates/rpc/rpc/src/lib.rs b/crates/rpc/rpc/src/lib.rs index 8be6e3af43..7e5fedda70 100644 --- a/crates/rpc/rpc/src/lib.rs +++ b/crates/rpc/rpc/src/lib.rs @@ -26,7 +26,7 @@ pub use call_guard::TracingCallGuard; pub use debug::DebugApi; pub use engine::EngineApi; pub use eth::{EthApi, EthApiSpec, EthFilter, EthPubSub, EthSubscriptionIdProvider}; -pub use layers::{AuthLayer, AuthValidator, JwtAuthValidator, JwtError, JwtSecret}; +pub use layers::{AuthLayer, AuthValidator, Claims, JwtAuthValidator, JwtError, JwtSecret}; pub use net::NetApi; pub use trace::TraceApi; pub use web3::Web3Api;