From 71db0b4f9e1d3793ae35963ad0581ff9a6220791 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 10 Dec 2024 10:10:31 -0500 Subject: [PATCH] feat(katana-rpc): rpc server builder (#2788) --- Cargo.lock | 4 + crates/dojo/test-utils/src/sequencer.rs | 2 +- crates/katana/cli/Cargo.toml | 1 + crates/katana/cli/src/args.rs | 20 +++ crates/katana/cli/src/options.rs | 16 ++- crates/katana/cli/src/utils.rs | 30 ++++- crates/katana/node/src/config/rpc.rs | 6 +- crates/katana/node/src/lib.rs | 89 ++++---------- crates/katana/rpc/rpc/Cargo.toml | 4 + crates/katana/rpc/rpc/src/cors.rs | 154 ++++++++++++++++++++++++ crates/katana/rpc/rpc/src/health.rs | 29 +++++ crates/katana/rpc/rpc/src/lib.rs | 138 ++++++++++++++++++++- 12 files changed, 420 insertions(+), 73 deletions(-) create mode 100644 crates/katana/rpc/rpc/src/cors.rs create mode 100644 crates/katana/rpc/rpc/src/health.rs diff --git a/Cargo.lock b/Cargo.lock index 26028e75d9..4713f01c86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8413,6 +8413,7 @@ dependencies = [ "katana-core", "katana-node", "katana-primitives", + "katana-rpc", "katana-slot-controller", "serde", "serde_json", @@ -8720,6 +8721,7 @@ dependencies = [ "dojo-test-utils", "dojo-utils", "futures", + "http 0.2.12", "indexmap 2.5.0", "jsonrpsee 0.16.3", "katana-cairo", @@ -8744,6 +8746,8 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tower 0.4.13", + "tower-http 0.4.4", "tracing", "url", ] diff --git a/crates/dojo/test-utils/src/sequencer.rs b/crates/dojo/test-utils/src/sequencer.rs index 43e154e072..e2fd3d5ad9 100644 --- a/crates/dojo/test-utils/src/sequencer.rs +++ b/crates/dojo/test-utils/src/sequencer.rs @@ -118,7 +118,7 @@ pub fn get_default_test_config(sequencing: SequencingConfig) -> Config { chain.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS; let rpc = RpcConfig { - cors_origins: None, + cors_origins: Vec::new(), port: 0, addr: DEFAULT_RPC_ADDR, max_connections: DEFAULT_RPC_MAX_CONNECTIONS, diff --git a/crates/katana/cli/Cargo.toml b/crates/katana/cli/Cargo.toml index 62c3ea284e..be65483377 100644 --- a/crates/katana/cli/Cargo.toml +++ b/crates/katana/cli/Cargo.toml @@ -10,6 +10,7 @@ dojo-utils.workspace = true katana-core.workspace = true katana-node.workspace = true katana-primitives.workspace = true +katana-rpc.workspace = true katana-slot-controller = { workspace = true, optional = true } alloy-primitives.workspace = true diff --git a/crates/katana/cli/src/args.rs b/crates/katana/cli/src/args.rs index 2fe54e7c3f..5962f61844 100644 --- a/crates/katana/cli/src/args.rs +++ b/crates/katana/cli/src/args.rs @@ -397,6 +397,7 @@ mod test { }; use katana_primitives::chain::ChainId; use katana_primitives::{address, felt, ContractAddress, Felt}; + use katana_rpc::cors::HeaderValue; use super::*; @@ -615,4 +616,23 @@ chain_id.Named = "Mainnet" assert_eq!(config.chain.genesis.gas_prices.strk, 8888); assert_eq!(config.chain.id, ChainId::Id(Felt::from_str("0x123").unwrap())); } + + #[test] + #[cfg(feature = "server")] + fn parse_cors_origins() { + let config = NodeArgs::parse_from([ + "katana", + "--http.cors_origins", + "*,http://localhost:3000,https://example.com", + ]) + .config() + .unwrap(); + + let cors_origins = config.rpc.cors_origins; + + assert_eq!(cors_origins.len(), 3); + assert!(cors_origins.contains(&HeaderValue::from_static("*"))); + assert!(cors_origins.contains(&HeaderValue::from_static("http://localhost:3000"))); + assert!(cors_origins.contains(&HeaderValue::from_static("https://example.com"))); + } } diff --git a/crates/katana/cli/src/options.rs b/crates/katana/cli/src/options.rs index 64dc2ad8a7..c6ba47b83b 100644 --- a/crates/katana/cli/src/options.rs +++ b/crates/katana/cli/src/options.rs @@ -20,10 +20,14 @@ use katana_node::config::rpc::{ use katana_primitives::block::BlockHashOrNumber; use katana_primitives::chain::ChainId; use katana_primitives::genesis::Genesis; +use katana_rpc::cors::HeaderValue; use serde::{Deserialize, Serialize}; use url::Url; -use crate::utils::{parse_block_hash_or_number, parse_genesis, LogFormat}; +use crate::utils::{ + deserialize_cors_origins, parse_block_hash_or_number, parse_genesis, serialize_cors_origins, + LogFormat, +}; const DEFAULT_DEV_SEED: &str = "0"; const DEFAULT_DEV_ACCOUNTS: u16 = 10; @@ -85,8 +89,12 @@ pub struct ServerOptions { /// Comma separated list of domains from which to accept cross origin requests. #[arg(long = "http.cors_origins")] #[arg(value_delimiter = ',')] - #[serde(default)] - pub http_cors_origins: Option>, + #[serde( + default, + serialize_with = "serialize_cors_origins", + deserialize_with = "deserialize_cors_origins" + )] + pub http_cors_origins: Vec, /// Maximum number of concurrent connections allowed. #[arg(long = "rpc.max-connections", value_name = "COUNT")] @@ -108,7 +116,7 @@ impl Default for ServerOptions { http_addr: DEFAULT_RPC_ADDR, http_port: DEFAULT_RPC_PORT, max_connections: DEFAULT_RPC_MAX_CONNECTIONS, - http_cors_origins: None, + http_cors_origins: Vec::new(), max_event_page_size: DEFAULT_RPC_MAX_EVENT_PAGE_SIZE, } } diff --git a/crates/katana/cli/src/utils.rs b/crates/katana/cli/src/utils.rs index 28cc0ffddc..eb8457166c 100644 --- a/crates/katana/cli/src/utils.rs +++ b/crates/katana/cli/src/utils.rs @@ -15,7 +15,8 @@ use katana_primitives::genesis::constant::{ }; use katana_primitives::genesis::json::GenesisJson; use katana_primitives::genesis::Genesis; -use serde::{Deserialize, Serialize}; +use katana_rpc::cors::HeaderValue; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tracing::info; use crate::args::LOG_TARGET; @@ -191,6 +192,33 @@ PREFUNDED ACCOUNTS } } +pub fn serialize_cors_origins(values: &[HeaderValue], serializer: S) -> Result +where + S: Serializer, +{ + let string = values + .iter() + .map(|v| v.to_str()) + .collect::, _>>() + .map_err(serde::ser::Error::custom)? + .join(","); + + serializer.serialize_str(&string) +} + +pub fn deserialize_cors_origins<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + String::deserialize(deserializer)? + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(HeaderValue::from_str) + .collect::, _>>() + .map_err(serde::de::Error::custom) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/katana/node/src/config/rpc.rs b/crates/katana/node/src/config/rpc.rs index bda2b90d8b..e816506d8b 100644 --- a/crates/katana/node/src/config/rpc.rs +++ b/crates/katana/node/src/config/rpc.rs @@ -1,6 +1,8 @@ use std::collections::HashSet; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use katana_rpc::cors::HeaderValue; + /// The default maximum number of concurrent RPC connections. pub const DEFAULT_RPC_MAX_CONNECTIONS: u32 = 100; pub const DEFAULT_RPC_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); @@ -28,7 +30,7 @@ pub struct RpcConfig { pub max_connections: u32, pub apis: HashSet, pub max_event_page_size: Option, - pub cors_origins: Option>, + pub cors_origins: Vec, } impl RpcConfig { @@ -41,7 +43,7 @@ impl RpcConfig { impl Default for RpcConfig { fn default() -> Self { Self { - cors_origins: None, + cors_origins: Vec::new(), addr: DEFAULT_RPC_ADDR, port: DEFAULT_RPC_PORT, max_connections: DEFAULT_RPC_MAX_CONNECTIONS, diff --git a/crates/katana/node/src/lib.rs b/crates/katana/node/src/lib.rs index e49d3643a1..f876bcfb05 100644 --- a/crates/katana/node/src/lib.rs +++ b/crates/katana/node/src/lib.rs @@ -8,18 +8,14 @@ pub mod exit; pub mod version; use std::future::IntoFuture; -use std::net::SocketAddr; use std::sync::Arc; -use std::time::Duration; use anyhow::Result; use config::rpc::{ApiKind, RpcConfig}; use config::Config; use dojo_metrics::exporters::prometheus::PrometheusRecorder; use dojo_metrics::{Report, Server as MetricsServer}; -use hyper::{Method, Uri}; -use jsonrpsee::server::middleware::proxy_get_request::ProxyGetRequestLayer; -use jsonrpsee::server::{AllowHosts, ServerBuilder, ServerHandle}; +use hyper::Method; use jsonrpsee::RpcModule; use katana_core::backend::gas_oracle::L1GasOracle; use katana_core::backend::storage::Blockchain; @@ -37,19 +33,19 @@ use katana_pool::ordering::FiFo; use katana_pool::TxPool; use katana_primitives::block::GasPrices; use katana_primitives::env::{CfgEnv, FeeTokenAddressses}; +use katana_rpc::cors::Cors; use katana_rpc::dev::DevApi; -use katana_rpc::metrics::RpcServerMetrics; use katana_rpc::saya::SayaApi; use katana_rpc::starknet::forking::ForkedClient; use katana_rpc::starknet::{StarknetApi, StarknetApiConfig}; use katana_rpc::torii::ToriiApi; +use katana_rpc::{RpcServer, RpcServerHandle}; use katana_rpc_api::dev::DevApiServer; use katana_rpc_api::saya::SayaApiServer; use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, StarknetWriteApiServer}; use katana_rpc_api::torii::ToriiApiServer; use katana_stage::Sequencing; use katana_tasks::TaskManager; -use tower_http::cors::{AllowOrigin, CorsLayer}; use tracing::info; use crate::exit::NodeStoppedFuture; @@ -59,7 +55,7 @@ use crate::exit::NodeStoppedFuture; pub struct LaunchedNode { pub node: Node, /// Handle to the rpc server. - pub rpc: RpcServer, + pub rpc: RpcServerHandle, } impl LaunchedNode { @@ -261,16 +257,21 @@ pub async fn build(mut config: Config) -> Result { pub async fn spawn( node_components: (TxPool, Arc>, BlockProducer, Option), config: RpcConfig, -) -> Result { +) -> Result { let (pool, backend, block_producer, forked_client) = node_components; - let mut methods = RpcModule::new(()); - methods.register_method("health", |_, _| Ok(serde_json::json!({ "health": true })))?; + let mut modules = RpcModule::new(()); + + let cors = Cors::new() + .allow_origins(config.cors_origins.clone()) + // Allow `POST` when accessing the resource + .allow_methods([Method::POST, Method::GET]) + .allow_headers([hyper::header::CONTENT_TYPE, "argent-client".parse().unwrap(), "argent-version".parse().unwrap()]); if config.apis.contains(&ApiKind::Starknet) { let cfg = StarknetApiConfig { max_event_page_size: config.max_event_page_size }; - let server = if let Some(client) = forked_client { + let api = if let Some(client) = forked_client { StarknetApi::new_forked( backend.clone(), pool.clone(), @@ -282,68 +283,28 @@ pub async fn spawn( StarknetApi::new(backend.clone(), pool.clone(), Some(block_producer.clone()), cfg) }; - methods.merge(StarknetApiServer::into_rpc(server.clone()))?; - methods.merge(StarknetWriteApiServer::into_rpc(server.clone()))?; - methods.merge(StarknetTraceApiServer::into_rpc(server))?; + modules.merge(StarknetApiServer::into_rpc(api.clone()))?; + modules.merge(StarknetWriteApiServer::into_rpc(api.clone()))?; + modules.merge(StarknetTraceApiServer::into_rpc(api))?; } if config.apis.contains(&ApiKind::Dev) { - methods.merge(DevApi::new(backend.clone(), block_producer.clone()).into_rpc())?; + let api = DevApi::new(backend.clone(), block_producer.clone()); + modules.merge(api.into_rpc())?; } if config.apis.contains(&ApiKind::Torii) { - methods.merge( - ToriiApi::new(backend.clone(), pool.clone(), block_producer.clone()).into_rpc(), - )?; + let api = ToriiApi::new(backend.clone(), pool.clone(), block_producer.clone()); + modules.merge(api.into_rpc())?; } if config.apis.contains(&ApiKind::Saya) { - methods.merge(SayaApi::new(backend.clone(), block_producer.clone()).into_rpc())?; + let api = SayaApi::new(backend.clone(), block_producer.clone()); + modules.merge(api.into_rpc())?; } - let cors = CorsLayer::new() - // Allow `POST` when accessing the resource - .allow_methods([Method::POST, Method::GET]) - .allow_headers([hyper::header::CONTENT_TYPE, "argent-client".parse().unwrap(), "argent-version".parse().unwrap()]); - - let cors = - config.cors_origins.clone().map(|allowed_origins| match allowed_origins.as_slice() { - [origin] if origin == "*" => cors.allow_origin(AllowOrigin::mirror_request()), - origins => cors.allow_origin( - origins - .iter() - .map(|o| { - let _ = o.parse::().expect("Invalid URI"); - - o.parse().expect("Invalid origin") - }) - .collect::>(), - ), - }); - - let middleware = tower::ServiceBuilder::new() - .option_layer(cors) - .layer(ProxyGetRequestLayer::new("/", "health")?) - .timeout(Duration::from_secs(20)); - - let server = ServerBuilder::new() - .set_logger(RpcServerMetrics::new(&methods)) - .set_host_filtering(AllowHosts::Any) - .set_middleware(middleware) - .max_connections(config.max_connections) - .build(config.socket_addr()) - .await?; - - let addr = server.local_addr()?; - let handle = server.start(methods)?; - - info!(target: "rpc", %addr, "RPC server started."); - - Ok(RpcServer { handle, addr }) -} + let server = RpcServer::new().metrics().health_check().cors(cors).module(modules); + let handle = server.start(config.socket_addr()).await?; -#[derive(Debug)] -pub struct RpcServer { - pub addr: SocketAddr, - pub handle: ServerHandle, + Ok(handle) } diff --git a/crates/katana/rpc/rpc/Cargo.toml b/crates/katana/rpc/rpc/Cargo.toml index 6ffc5703ac..0c09844955 100644 --- a/crates/katana/rpc/rpc/Cargo.toml +++ b/crates/katana/rpc/rpc/Cargo.toml @@ -10,6 +10,7 @@ version.workspace = true anyhow.workspace = true dojo-metrics.workspace = true futures.workspace = true +http.workspace = true jsonrpsee = { workspace = true, features = [ "server" ] } katana-core.workspace = true katana-executor.workspace = true @@ -21,9 +22,12 @@ katana-rpc-types.workspace = true katana-rpc-types-builder.workspace = true katana-tasks.workspace = true metrics.workspace = true +serde_json.workspace = true starknet.workspace = true thiserror.workspace = true tokio.workspace = true +tower.workspace = true +tower-http.workspace = true tracing.workspace = true url.workspace = true diff --git a/crates/katana/rpc/rpc/src/cors.rs b/crates/katana/rpc/rpc/src/cors.rs new file mode 100644 index 0000000000..a31453bef8 --- /dev/null +++ b/crates/katana/rpc/rpc/src/cors.rs @@ -0,0 +1,154 @@ +pub use http::HeaderValue; +use tower::Layer; +use tower_http::cors::{self, Any}; +pub use tower_http::cors::{AllowHeaders, AllowMethods}; + +/// Layer that applies the [`Cors`] middleware which adds headers for [CORS][mdn]. +/// +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +/// [`Cors`]: cors::Cors +#[derive(Debug, Clone, Default)] +pub struct Cors(cors::CorsLayer); + +impl Cors { + pub fn new() -> Self { + Self::default() + } + + /// A permissive configuration: + /// + /// - All request headers allowed. + /// - All methods allowed. + /// - All origins allowed. + /// - All headers exposed. + pub fn permissive() -> Self { + Self(cors::CorsLayer::permissive()) + } + + /// A very permissive configuration: + /// + /// - **Credentials allowed.** + /// - The method received in `Access-Control-Request-Method` is sent back as an allowed method. + /// - The origin of the preflight request is sent back as an allowed origin. + /// - The header names received in `Access-Control-Request-Headers` are sent back as allowed + /// headers. + /// - No headers are currently exposed, but this may change in the future. + pub fn very_permissive() -> Self { + Self(cors::CorsLayer::very_permissive()) + } + + pub fn allow_origins(self, origins: impl Into) -> Self { + Self(self.0.allow_origin(origins.into())) + } + + pub fn allow_methods(self, methods: impl Into) -> Self { + Self(self.0.allow_methods(methods)) + } + + pub fn allow_headers(self, headers: impl Into) -> Self { + Self(self.0.allow_headers(headers)) + } +} + +impl Layer for Cors { + type Service = cors::Cors; + + fn layer(&self, inner: S) -> Self::Service { + self.0.layer(inner) + } +} + +const WILDCARD: HeaderValue = HeaderValue::from_static("*"); + +/// Holds configuration for how to set the [`Access-Control-Allow-Origin`][mdn] header. +/// +/// This is just a lightweight wrapper of [`cors::AllowOrigin`] that doesn't fail when a wildcard, +/// `*`, is passed to [`cors::AllowOrigin::list`]. See [`cors::AllowOrigin`] for more details. +/// +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin +#[derive(Debug, Clone, Default)] +pub struct AllowOrigins(cors::AllowOrigin); + +impl AllowOrigins { + /// Allow any origin by sending a wildcard (`*`). + pub fn any() -> Self { + Self(cors::AllowOrigin::any()) + } + + /// Set a single allowed origin. + pub fn exact(origin: HeaderValue) -> Self { + Self(cors::AllowOrigin::exact(origin)) + } + + /// Allow any origin, by mirroring the request origin. + pub fn mirror_request() -> Self { + Self(cors::AllowOrigin::mirror_request()) + } + + /// Set multiple allowed origins. + /// + /// This will not return an error if a wildcard, `*`, is in the list. + pub fn list(origins: I) -> Self + where + I: IntoIterator, + { + let origins = origins.into_iter().collect::>(); + if origins.iter().any(|o| o == WILDCARD) { + Self(cors::AllowOrigin::any()) + } else { + Self(cors::AllowOrigin::list(origins)) + } + } +} + +impl From for AllowOrigins { + fn from(value: cors::AllowOrigin) -> Self { + Self(value) + } +} + +impl From for cors::AllowOrigin { + fn from(value: AllowOrigins) -> Self { + value.0 + } +} + +impl From for AllowOrigins { + fn from(_: Any) -> Self { + Self(cors::AllowOrigin::any()) + } +} + +impl From for AllowOrigins { + fn from(val: HeaderValue) -> Self { + Self(cors::AllowOrigin::exact(val)) + } +} + +impl From<[HeaderValue; N]> for AllowOrigins { + fn from(arr: [HeaderValue; N]) -> Self { + Self::list(arr) + } +} + +impl From> for AllowOrigins { + fn from(vec: Vec) -> Self { + Self::list(vec) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wildcard_in_list() { + let origins = vec![ + HeaderValue::from_static("http://example.com"), + HeaderValue::from_static("*"), + HeaderValue::from_static("http://other.com"), + ]; + + let _ = AllowOrigins::list(origins); + } +} diff --git a/crates/katana/rpc/rpc/src/health.rs b/crates/katana/rpc/rpc/src/health.rs new file mode 100644 index 0000000000..3299e56d90 --- /dev/null +++ b/crates/katana/rpc/rpc/src/health.rs @@ -0,0 +1,29 @@ +use jsonrpsee::core::server::rpc_module::Methods; +use jsonrpsee::server::middleware::proxy_get_request::ProxyGetRequestLayer; +use jsonrpsee::RpcModule; +use serde_json::json; + +/// Simple health check endpoint. +#[derive(Debug)] +pub struct HealthCheck; + +impl HealthCheck { + const METHOD: &'static str = "health"; + const PROXY_PATH: &'static str = "/"; + + pub(crate) fn proxy() -> ProxyGetRequestLayer { + Self::proxy_with_path(Self::PROXY_PATH) + } + + fn proxy_with_path(path: impl Into) -> ProxyGetRequestLayer { + ProxyGetRequestLayer::new(path, Self::METHOD).expect("path starts with /") + } +} + +impl From for Methods { + fn from(_: HealthCheck) -> Self { + let mut module = RpcModule::new(()); + module.register_method(HealthCheck::METHOD, |_, _| Ok(json!({ "health": true }))).unwrap(); + module.into() + } +} diff --git a/crates/katana/rpc/rpc/src/lib.rs b/crates/katana/rpc/rpc/src/lib.rs index b24f787512..5a1230b39b 100644 --- a/crates/katana/rpc/rpc/src/lib.rs +++ b/crates/katana/rpc/rpc/src/lib.rs @@ -3,10 +3,146 @@ #![allow(clippy::blocks_in_conditions)] #![cfg_attr(not(test), warn(unused_crate_dependencies))] +use std::net::SocketAddr; +use std::time::Duration; + +use jsonrpsee::server::{AllowHosts, ServerBuilder, ServerHandle}; +use jsonrpsee::RpcModule; +use tower::ServiceBuilder; +use tracing::info; + +pub mod cors; pub mod dev; +pub mod health; pub mod metrics; pub mod saya; pub mod starknet; pub mod torii; - mod utils; + +use cors::Cors; +use health::HealthCheck; +use metrics::RpcServerMetrics; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Jsonrpsee(#[from] jsonrpsee::core::Error), + + #[error("RPC server has already been stopped")] + AlreadyStopped, +} + +#[derive(Debug)] +pub struct RpcServerHandle { + pub addr: SocketAddr, + pub handle: ServerHandle, +} + +impl RpcServerHandle { + pub fn stop(&self) -> Result<(), Error> { + self.handle.stop().map_err(|_| Error::AlreadyStopped) + } + + /// Wait until the server has stopped. + pub async fn stopped(self) { + self.handle.stopped().await + } +} + +#[derive(Debug)] +pub struct RpcServer { + metrics: bool, + cors: Option, + health_check: bool, + module: RpcModule<()>, + max_connections: u32, +} + +impl RpcServer { + pub fn new() -> Self { + Self { + cors: None, + metrics: false, + health_check: false, + max_connections: 100, + module: RpcModule::new(()), + } + } + + /// Collect metrics about the RPC server. + /// + /// See top level module of [`crate::metrics`] to see what metrics are collected. + pub fn metrics(mut self) -> Self { + self.metrics = true; + self + } + + /// Enables health checking endpoint via HTTP `GET /health` + pub fn health_check(mut self) -> Self { + self.health_check = true; + self + } + + pub fn cors(mut self, cors: Cors) -> Self { + self.cors = Some(cors); + self + } + + pub fn module(mut self, module: RpcModule<()>) -> Self { + self.module = module; + self + } + + pub async fn start(&self, addr: SocketAddr) -> Result { + let mut modules = self.module.clone(); + + let health_check_proxy = if self.health_check { + modules.merge(HealthCheck)?; + Some(HealthCheck::proxy()) + } else { + None + }; + + let middleware = ServiceBuilder::new() + .option_layer(self.cors.clone()) + .option_layer(health_check_proxy) + .timeout(Duration::from_secs(20)); + + let builder = ServerBuilder::new() + .set_middleware(middleware) + .set_host_filtering(AllowHosts::Any) + .max_connections(self.max_connections); + + let handle = if self.metrics { + let logger = RpcServerMetrics::new(&modules); + let server = builder.set_logger(logger).build(addr).await?; + + let addr = server.local_addr()?; + let handle = server.start(modules)?; + + RpcServerHandle { addr, handle } + } else { + let server = builder.build(addr).await?; + + let addr = server.local_addr()?; + let handle = server.start(modules)?; + + RpcServerHandle { addr, handle } + }; + + // The socket address that we log out must be from the RPC handle, in the case that the + // `addr` passed to this method has port number 0. As the 0 port will be resolved to + // a free port during the call to `ServerBuilder::build(addr)`. + + info!(target: "rpc", addr = %handle.addr, "RPC server started."); + + Ok(handle) + } +} + +impl Default for RpcServer { + fn default() -> Self { + Self::new() + } +}