test: add basic auth server tests (#2278)

This commit is contained in:
Matthias Seitz
2023-04-17 03:43:08 +02:00
committed by GitHub
parent ca70d7337c
commit a0bfb654cd
13 changed files with 215 additions and 32 deletions

3
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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<Client>,
) -> Result<ServerHandle, RpcError>
) -> Result<AuthServerHandle, RpcError>
where
Client: BlockProvider
+ HeaderProvider

View File

@@ -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"

View File

@@ -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<Client, Pool, Network, Tasks, EngineApi>(
engine_api: EngineApi,
socket_addr: SocketAddr,
secret: JwtSecret,
) -> Result<ServerHandle, RpcError>
) -> Result<AuthServerHandle, RpcError>
where
Client: BlockProvider
+ HeaderProvider
@@ -52,7 +59,7 @@ pub async fn launch_with_eth_api<Client, Pool, Network, EngineApi>(
engine_api: EngineApi,
socket_addr: SocketAddr,
secret: JwtSecret,
) -> Result<ServerHandle, RpcError>
) -> Result<AuthServerHandle, RpcError>
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<ServerHandle, RpcError> {
pub async fn start(self, module: AuthRpcModule) -> Result<AuthServerHandle, RpcError> {
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<EngineApi>(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<ServerHandle, RpcError> {
pub async fn start_server(
self,
config: AuthServerConfig,
) -> Result<AuthServerHandle, RpcError> {
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")
}
}

View File

@@ -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<SocketAddr>,

View File

@@ -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<C>(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
}

View File

@@ -1,3 +1,4 @@
mod auth;
mod http;
mod serde;
mod startup;

View File

@@ -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<RpcModuleSelection>) -> RpcServerHandle {
let builder = test_rpc_builder();

View File

@@ -143,12 +143,24 @@ impl JwtSecret {
JwtSecret::from_hex(secret).unwrap()
}
#[cfg(test)]
pub(crate) fn encode(&self, claims: &Claims) -> Result<String, Box<dyn std::error::Error>> {
/// 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<String, jsonwebtoken::errors::Error> {
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<u64>,
pub iat: u64,
/// Expiration, if any
pub exp: Option<u64>,
}
impl Claims {

View File

@@ -68,9 +68,8 @@ fn err_response(err: JwtError) -> Response<hyper::Body> {
#[cfg(test)]
mod tests {
use http::{header, HeaderMap};
use crate::layers::jwt_validator::get_bearer;
use http::{header, HeaderMap};
#[test]
fn auth_header_available() {

View File

@@ -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

View File

@@ -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;