feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "pezsc-rpc-server"
|
||||
version = "11.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Bizinikiwi RPC servers."
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
dyn-clone = { workspace = true }
|
||||
forwarded-header-value = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
governor = { workspace = true }
|
||||
http = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hyper = { workspace = true }
|
||||
ip_network = { workspace = true }
|
||||
jsonrpsee = { features = ["server"], workspace = true }
|
||||
log = { workspace = true, default-features = true }
|
||||
prometheus-endpoint = { workspace = true, default-features = true }
|
||||
pezsc-rpc-api = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true, default-features = true }
|
||||
tokio = { features = [
|
||||
"parking_lot",
|
||||
], workspace = true, default-features = true }
|
||||
tower = { workspace = true, features = ["util"] }
|
||||
tower-http = { workspace = true, features = ["cors"] }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = ["pezsc-rpc-api/runtime-benchmarks"]
|
||||
@@ -0,0 +1,3 @@
|
||||
Bizinikiwi RPC servers.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,310 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Bizinikiwi RPC servers.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod middleware;
|
||||
pub mod utils;
|
||||
|
||||
use std::{error::Error as StdError, net::SocketAddr, time::Duration};
|
||||
|
||||
use jsonrpsee::{
|
||||
core::BoxError,
|
||||
server::{
|
||||
serve_with_graceful_shutdown, stop_channel, ws, PingConfig, ServerHandle, StopHandle,
|
||||
},
|
||||
Methods, RpcModule,
|
||||
};
|
||||
use tower::Service;
|
||||
use utils::{
|
||||
build_rpc_api, deny_unsafe, format_listen_addrs, get_proxy_ip, ListenAddrError, RpcSettings,
|
||||
};
|
||||
|
||||
pub use ip_network::IpNetwork;
|
||||
pub use jsonrpsee::{
|
||||
core::id_providers::{RandomIntegerIdProvider, RandomStringIdProvider},
|
||||
server::{middleware::rpc::RpcServiceBuilder, BatchRequestConfig},
|
||||
};
|
||||
pub use middleware::{Metrics, MiddlewareLayer, NodeHealthProxyLayer, RpcMetrics};
|
||||
pub use utils::{RpcEndpoint, RpcMethods};
|
||||
|
||||
const MEGABYTE: u32 = 1024 * 1024;
|
||||
|
||||
/// Type to encapsulate the server handle and listening address.
|
||||
pub struct Server {
|
||||
/// Handle to the rpc server
|
||||
handle: ServerHandle,
|
||||
/// Listening address of the server
|
||||
listen_addrs: Vec<SocketAddr>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Creates a new Server.
|
||||
pub fn new(handle: ServerHandle, listen_addrs: Vec<SocketAddr>) -> Server {
|
||||
Server { handle, listen_addrs }
|
||||
}
|
||||
|
||||
/// Returns the `jsonrpsee::server::ServerHandle` for this Server. Can be used to stop the
|
||||
/// server.
|
||||
pub fn handle(&self) -> &ServerHandle {
|
||||
&self.handle
|
||||
}
|
||||
|
||||
/// The listen address for the running RPC service.
|
||||
pub fn listen_addrs(&self) -> &[SocketAddr] {
|
||||
&self.listen_addrs
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
// This doesn't not wait for the server to be stopped but fires the signal.
|
||||
let _ = self.handle.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for providing subscription IDs that can be cloned.
|
||||
pub trait SubscriptionIdProvider:
|
||||
jsonrpsee::core::traits::IdProvider + dyn_clone::DynClone
|
||||
{
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(SubscriptionIdProvider);
|
||||
|
||||
/// RPC server configuration.
|
||||
#[derive(Debug)]
|
||||
pub struct Config<M: Send + Sync + 'static> {
|
||||
/// RPC interfaces to start.
|
||||
pub endpoints: Vec<RpcEndpoint>,
|
||||
/// Metrics.
|
||||
pub metrics: Option<RpcMetrics>,
|
||||
/// RPC API.
|
||||
pub rpc_api: RpcModule<M>,
|
||||
/// Subscription ID provider.
|
||||
pub id_provider: Option<Box<dyn SubscriptionIdProvider>>,
|
||||
/// Tokio runtime handle.
|
||||
pub tokio_handle: tokio::runtime::Handle,
|
||||
/// RPC logger capacity (default: 1024).
|
||||
pub request_logger_limit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PerConnection {
|
||||
methods: Methods,
|
||||
stop_handle: StopHandle,
|
||||
metrics: Option<RpcMetrics>,
|
||||
tokio_handle: tokio::runtime::Handle,
|
||||
}
|
||||
|
||||
/// Start RPC server listening on given address.
|
||||
pub async fn start_server<M>(config: Config<M>) -> Result<Server, Box<dyn StdError + Send + Sync>>
|
||||
where
|
||||
M: Send + Sync,
|
||||
{
|
||||
let Config { endpoints, metrics, tokio_handle, rpc_api, id_provider, request_logger_limit } =
|
||||
config;
|
||||
|
||||
let (stop_handle, server_handle) = stop_channel();
|
||||
let cfg = PerConnection {
|
||||
methods: build_rpc_api(rpc_api).into(),
|
||||
metrics,
|
||||
tokio_handle: tokio_handle.clone(),
|
||||
stop_handle,
|
||||
};
|
||||
|
||||
let mut local_addrs = Vec::new();
|
||||
|
||||
for endpoint in endpoints {
|
||||
let allowed_to_fail = endpoint.is_optional;
|
||||
let local_addr = endpoint.listen_addr;
|
||||
|
||||
let mut listener = match endpoint.bind().await {
|
||||
Ok(l) => l,
|
||||
Err(e) if allowed_to_fail => {
|
||||
log::debug!(target: "rpc", "JSON-RPC server failed to bind optional address: {:?}, error: {:?}", local_addr, e);
|
||||
continue;
|
||||
},
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let local_addr = listener.local_addr();
|
||||
local_addrs.push(local_addr);
|
||||
let cfg = cfg.clone();
|
||||
|
||||
let RpcSettings {
|
||||
batch_config,
|
||||
max_connections,
|
||||
max_payload_in_mb,
|
||||
max_payload_out_mb,
|
||||
max_buffer_capacity_per_connection,
|
||||
max_subscriptions_per_connection,
|
||||
rpc_methods,
|
||||
rate_limit_trust_proxy_headers,
|
||||
rate_limit_whitelisted_ips,
|
||||
host_filter,
|
||||
cors,
|
||||
rate_limit,
|
||||
} = listener.rpc_settings();
|
||||
|
||||
let http_middleware = tower::ServiceBuilder::new()
|
||||
.option_layer(host_filter)
|
||||
// Proxy `GET /health, /health/readiness` requests to the internal
|
||||
// `system_health` method.
|
||||
.layer(NodeHealthProxyLayer::default())
|
||||
.layer(cors);
|
||||
|
||||
let mut builder = jsonrpsee::server::Server::builder()
|
||||
.max_request_body_size(max_payload_in_mb.saturating_mul(MEGABYTE))
|
||||
.max_response_body_size(max_payload_out_mb.saturating_mul(MEGABYTE))
|
||||
.max_connections(max_connections)
|
||||
.max_subscriptions_per_connection(max_subscriptions_per_connection)
|
||||
.enable_ws_ping(
|
||||
PingConfig::new()
|
||||
.ping_interval(Duration::from_secs(30))
|
||||
.inactive_limit(Duration::from_secs(60))
|
||||
.max_failures(3),
|
||||
)
|
||||
.set_http_middleware(http_middleware)
|
||||
.set_message_buffer_capacity(max_buffer_capacity_per_connection)
|
||||
.set_batch_request_config(batch_config)
|
||||
.custom_tokio_runtime(cfg.tokio_handle.clone());
|
||||
|
||||
if let Some(provider) = id_provider.clone() {
|
||||
builder = builder.set_id_provider(provider);
|
||||
} else {
|
||||
builder = builder.set_id_provider(RandomStringIdProvider::new(16));
|
||||
};
|
||||
|
||||
let service_builder = builder.to_service_builder();
|
||||
let deny_unsafe = deny_unsafe(&local_addr, &rpc_methods);
|
||||
|
||||
tokio_handle.spawn(async move {
|
||||
loop {
|
||||
let (sock, remote_addr) = tokio::select! {
|
||||
res = listener.accept() => {
|
||||
match res {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::debug!(target: "rpc", "Failed to accept connection: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = cfg.stop_handle.clone().shutdown() => break,
|
||||
};
|
||||
|
||||
let ip = remote_addr.ip();
|
||||
let cfg2 = cfg.clone();
|
||||
let service_builder2 = service_builder.clone();
|
||||
let rate_limit_whitelisted_ips2 = rate_limit_whitelisted_ips.clone();
|
||||
|
||||
let svc =
|
||||
tower::service_fn(move |mut req: http::Request<hyper::body::Incoming>| {
|
||||
req.extensions_mut().insert(deny_unsafe);
|
||||
|
||||
let PerConnection { methods, metrics, tokio_handle, stop_handle } =
|
||||
cfg2.clone();
|
||||
let service_builder = service_builder2.clone();
|
||||
|
||||
let proxy_ip =
|
||||
if rate_limit_trust_proxy_headers { get_proxy_ip(&req) } else { None };
|
||||
|
||||
let rate_limit_cfg = if rate_limit_whitelisted_ips2
|
||||
.iter()
|
||||
.any(|ips| ips.contains(proxy_ip.unwrap_or(ip)))
|
||||
{
|
||||
log::debug!(target: "rpc", "ip={ip}, proxy_ip={:?} is trusted, disabling rate-limit", proxy_ip);
|
||||
None
|
||||
} else {
|
||||
if !rate_limit_whitelisted_ips2.is_empty() {
|
||||
log::debug!(target: "rpc", "ip={ip}, proxy_ip={:?} is not trusted, rate-limit enabled", proxy_ip);
|
||||
}
|
||||
rate_limit
|
||||
};
|
||||
|
||||
let is_websocket = ws::is_upgrade_request(&req);
|
||||
let transport_label = if is_websocket { "ws" } else { "http" };
|
||||
|
||||
let middleware_layer = match (metrics, rate_limit_cfg) {
|
||||
(None, None) => None,
|
||||
(Some(metrics), None) => Some(
|
||||
MiddlewareLayer::new()
|
||||
.with_metrics(Metrics::new(metrics, transport_label)),
|
||||
),
|
||||
(None, Some(rate_limit)) =>
|
||||
Some(MiddlewareLayer::new().with_rate_limit_per_minute(rate_limit)),
|
||||
(Some(metrics), Some(rate_limit)) => Some(
|
||||
MiddlewareLayer::new()
|
||||
.with_metrics(Metrics::new(metrics, transport_label))
|
||||
.with_rate_limit_per_minute(rate_limit),
|
||||
),
|
||||
};
|
||||
|
||||
let rpc_middleware = RpcServiceBuilder::new()
|
||||
.rpc_logger(request_logger_limit)
|
||||
.option_layer(middleware_layer.clone());
|
||||
let mut svc = service_builder
|
||||
.set_rpc_middleware(rpc_middleware)
|
||||
.build(methods, stop_handle);
|
||||
|
||||
async move {
|
||||
if is_websocket {
|
||||
let on_disconnect = svc.on_session_closed();
|
||||
|
||||
// Spawn a task to handle when the connection is closed.
|
||||
tokio_handle.spawn(async move {
|
||||
let now = std::time::Instant::now();
|
||||
middleware_layer.as_ref().map(|m| m.ws_connect());
|
||||
on_disconnect.await;
|
||||
middleware_layer.as_ref().map(|m| m.ws_disconnect(now));
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/rust-lang/rust/issues/102211 the error type can't be inferred
|
||||
// to be `Box<dyn std::error::Error + Send + Sync>` so we need to
|
||||
// convert it to a concrete type as workaround.
|
||||
svc.call(req).await.map_err(|e| BoxError::from(e))
|
||||
}
|
||||
});
|
||||
|
||||
cfg.tokio_handle.spawn(serve_with_graceful_shutdown(
|
||||
sock,
|
||||
svc,
|
||||
cfg.stop_handle.clone().shutdown(),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if local_addrs.is_empty() {
|
||||
return Err(Box::new(ListenAddrError));
|
||||
}
|
||||
|
||||
// The previous logging format was before
|
||||
// `Running JSON-RPC server: addr=127.0.0.1:9944, allowed origins=["*"]`
|
||||
//
|
||||
// The new format is `Running JSON-RPC server: addr=<addr1, addr2, .. addr_n>`
|
||||
// with the exception that for a single address it will be `Running JSON-RPC server: addr=addr,`
|
||||
// with a trailing comma.
|
||||
//
|
||||
// This is to make it work with old scripts/utils that parse the logs.
|
||||
log::info!("Running JSON-RPC server: addr={}", format_listen_addrs(&local_addrs));
|
||||
|
||||
Ok(Server::new(server_handle, local_addrs))
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! RPC middleware to collect prometheus metrics on RPC calls.
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use jsonrpsee::{types::Request, MethodResponse};
|
||||
use prometheus_endpoint::{
|
||||
register, Counter, CounterVec, HistogramOpts, HistogramVec, Opts, PrometheusError, Registry,
|
||||
U64,
|
||||
};
|
||||
|
||||
/// Histogram time buckets in microseconds.
|
||||
const HISTOGRAM_BUCKETS: [f64; 11] = [
|
||||
5.0,
|
||||
25.0,
|
||||
100.0,
|
||||
500.0,
|
||||
1_000.0,
|
||||
2_500.0,
|
||||
10_000.0,
|
||||
25_000.0,
|
||||
100_000.0,
|
||||
1_000_000.0,
|
||||
10_000_000.0,
|
||||
];
|
||||
|
||||
/// Metrics for RPC middleware storing information about the number of requests started/completed,
|
||||
/// calls started/completed and their timings.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RpcMetrics {
|
||||
/// Histogram over RPC execution times.
|
||||
calls_time: HistogramVec,
|
||||
/// Number of calls started.
|
||||
calls_started: CounterVec<U64>,
|
||||
/// Number of calls completed.
|
||||
calls_finished: CounterVec<U64>,
|
||||
/// Number of Websocket sessions opened.
|
||||
ws_sessions_opened: Option<Counter<U64>>,
|
||||
/// Number of Websocket sessions closed.
|
||||
ws_sessions_closed: Option<Counter<U64>>,
|
||||
/// Histogram over RPC websocket sessions.
|
||||
ws_sessions_time: HistogramVec,
|
||||
}
|
||||
|
||||
impl RpcMetrics {
|
||||
/// Create an instance of metrics
|
||||
pub fn new(metrics_registry: Option<&Registry>) -> Result<Option<Self>, PrometheusError> {
|
||||
if let Some(metrics_registry) = metrics_registry {
|
||||
Ok(Some(Self {
|
||||
calls_time: register(
|
||||
HistogramVec::new(
|
||||
HistogramOpts::new(
|
||||
"bizinikiwi_rpc_calls_time",
|
||||
"Total time [μs] of processed RPC calls",
|
||||
)
|
||||
.buckets(HISTOGRAM_BUCKETS.to_vec()),
|
||||
&["protocol", "method", "is_rate_limited"],
|
||||
)?,
|
||||
metrics_registry,
|
||||
)?,
|
||||
calls_started: register(
|
||||
CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_rpc_calls_started",
|
||||
"Number of received RPC calls (unique un-batched requests)",
|
||||
),
|
||||
&["protocol", "method"],
|
||||
)?,
|
||||
metrics_registry,
|
||||
)?,
|
||||
calls_finished: register(
|
||||
CounterVec::new(
|
||||
Opts::new(
|
||||
"bizinikiwi_rpc_calls_finished",
|
||||
"Number of processed RPC calls (unique un-batched requests)",
|
||||
),
|
||||
&["protocol", "method", "is_error", "is_rate_limited"],
|
||||
)?,
|
||||
metrics_registry,
|
||||
)?,
|
||||
ws_sessions_opened: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_rpc_sessions_opened",
|
||||
"Number of persistent RPC sessions opened",
|
||||
)?,
|
||||
metrics_registry,
|
||||
)?
|
||||
.into(),
|
||||
ws_sessions_closed: register(
|
||||
Counter::new(
|
||||
"bizinikiwi_rpc_sessions_closed",
|
||||
"Number of persistent RPC sessions closed",
|
||||
)?,
|
||||
metrics_registry,
|
||||
)?
|
||||
.into(),
|
||||
ws_sessions_time: register(
|
||||
HistogramVec::new(
|
||||
HistogramOpts::new(
|
||||
"bizinikiwi_rpc_sessions_time",
|
||||
"Total time [s] for each websocket session",
|
||||
)
|
||||
.buckets(HISTOGRAM_BUCKETS.to_vec()),
|
||||
&["protocol"],
|
||||
)?,
|
||||
metrics_registry,
|
||||
)?,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ws_connect(&self) {
|
||||
self.ws_sessions_opened.as_ref().map(|counter| counter.inc());
|
||||
}
|
||||
|
||||
pub(crate) fn ws_disconnect(&self, now: Instant) {
|
||||
let micros = now.elapsed().as_secs();
|
||||
|
||||
self.ws_sessions_closed.as_ref().map(|counter| counter.inc());
|
||||
self.ws_sessions_time.with_label_values(&["ws"]).observe(micros as _);
|
||||
}
|
||||
|
||||
pub(crate) fn on_call(&self, req: &Request, transport_label: &'static str) {
|
||||
log::trace!(
|
||||
target: "rpc_metrics",
|
||||
"[{transport_label}] on_call name={} params={:?}",
|
||||
req.method_name(),
|
||||
req.params(),
|
||||
);
|
||||
|
||||
self.calls_started
|
||||
.with_label_values(&[transport_label, req.method_name()])
|
||||
.inc();
|
||||
}
|
||||
|
||||
pub(crate) fn on_response(
|
||||
&self,
|
||||
req: &Request,
|
||||
rp: &MethodResponse,
|
||||
is_rate_limited: bool,
|
||||
transport_label: &'static str,
|
||||
now: Instant,
|
||||
) {
|
||||
log::trace!(target: "rpc_metrics", "[{transport_label}] on_response started_at={:?}", now);
|
||||
log::trace!(target: "rpc_metrics::extra", "[{transport_label}] result={}", rp.as_result());
|
||||
|
||||
let micros = now.elapsed().as_micros();
|
||||
log::debug!(
|
||||
target: "rpc_metrics",
|
||||
"[{transport_label}] {} call took {} μs",
|
||||
req.method_name(),
|
||||
micros,
|
||||
);
|
||||
self.calls_time
|
||||
.with_label_values(&[
|
||||
transport_label,
|
||||
req.method_name(),
|
||||
if is_rate_limited { "true" } else { "false" },
|
||||
])
|
||||
.observe(micros as _);
|
||||
self.calls_finished
|
||||
.with_label_values(&[
|
||||
transport_label,
|
||||
req.method_name(),
|
||||
// the label "is_error", so `success` should be regarded as false
|
||||
// and vice-versa to be registered correctly.
|
||||
if rp.is_success() { "false" } else { "true" },
|
||||
if is_rate_limited { "true" } else { "false" },
|
||||
])
|
||||
.inc();
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics with transport label.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Metrics {
|
||||
pub(crate) inner: RpcMetrics,
|
||||
pub(crate) transport_label: &'static str,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
/// Create a new [`Metrics`].
|
||||
pub fn new(metrics: RpcMetrics, transport_label: &'static str) -> Self {
|
||||
Self { inner: metrics, transport_label }
|
||||
}
|
||||
|
||||
pub(crate) fn ws_connect(&self) {
|
||||
self.inner.ws_connect();
|
||||
}
|
||||
|
||||
pub(crate) fn ws_disconnect(&self, now: Instant) {
|
||||
self.inner.ws_disconnect(now)
|
||||
}
|
||||
|
||||
pub(crate) fn on_call(&self, req: &Request) {
|
||||
self.inner.on_call(req, self.transport_label)
|
||||
}
|
||||
|
||||
pub(crate) fn on_response(
|
||||
&self,
|
||||
req: &Request,
|
||||
rp: &MethodResponse,
|
||||
is_rate_limited: bool,
|
||||
now: Instant,
|
||||
) {
|
||||
self.inner.on_response(req, rp, is_rate_limited, self.transport_label, now)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! JSON-RPC specific middleware.
|
||||
|
||||
use std::{
|
||||
num::NonZeroU32,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use governor::{clock::Clock, Jitter};
|
||||
use jsonrpsee::{
|
||||
server::middleware::rpc::RpcServiceT,
|
||||
types::{ErrorObject, Id, Request},
|
||||
MethodResponse,
|
||||
};
|
||||
|
||||
mod metrics;
|
||||
mod node_health;
|
||||
mod rate_limit;
|
||||
|
||||
pub use metrics::*;
|
||||
pub use node_health::*;
|
||||
pub use rate_limit::*;
|
||||
|
||||
const MAX_JITTER: Duration = Duration::from_millis(50);
|
||||
const MAX_RETRIES: usize = 10;
|
||||
|
||||
/// JSON-RPC middleware layer.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MiddlewareLayer {
|
||||
rate_limit: Option<RateLimit>,
|
||||
metrics: Option<Metrics>,
|
||||
}
|
||||
|
||||
impl MiddlewareLayer {
|
||||
/// Create an empty MiddlewareLayer.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Enable new rate limit middleware enforced per minute.
|
||||
pub fn with_rate_limit_per_minute(self, n: NonZeroU32) -> Self {
|
||||
Self { rate_limit: Some(RateLimit::per_minute(n)), metrics: self.metrics }
|
||||
}
|
||||
|
||||
/// Enable metrics middleware.
|
||||
pub fn with_metrics(self, metrics: Metrics) -> Self {
|
||||
Self { rate_limit: self.rate_limit, metrics: Some(metrics) }
|
||||
}
|
||||
|
||||
/// Register a new websocket connection.
|
||||
pub fn ws_connect(&self) {
|
||||
self.metrics.as_ref().map(|m| m.ws_connect());
|
||||
}
|
||||
|
||||
/// Register that a websocket connection was closed.
|
||||
pub fn ws_disconnect(&self, now: Instant) {
|
||||
self.metrics.as_ref().map(|m| m.ws_disconnect(now));
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> tower::Layer<S> for MiddlewareLayer {
|
||||
type Service = Middleware<S>;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
Middleware { service, rate_limit: self.rate_limit.clone(), metrics: self.metrics.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
/// JSON-RPC middleware that handles metrics
|
||||
/// and rate-limiting.
|
||||
///
|
||||
/// These are part of the same middleware
|
||||
/// because the metrics needs to know whether
|
||||
/// a call was rate-limited or not because
|
||||
/// it will impact the roundtrip for a call.
|
||||
pub struct Middleware<S> {
|
||||
service: S,
|
||||
rate_limit: Option<RateLimit>,
|
||||
metrics: Option<Metrics>,
|
||||
}
|
||||
|
||||
impl<'a, S> RpcServiceT<'a> for Middleware<S>
|
||||
where
|
||||
S: Send + Sync + RpcServiceT<'a> + Clone + 'static,
|
||||
{
|
||||
type Future = BoxFuture<'a, MethodResponse>;
|
||||
|
||||
fn call(&self, req: Request<'a>) -> Self::Future {
|
||||
let now = Instant::now();
|
||||
|
||||
self.metrics.as_ref().map(|m| m.on_call(&req));
|
||||
|
||||
let service = self.service.clone();
|
||||
let rate_limit = self.rate_limit.clone();
|
||||
let metrics = self.metrics.clone();
|
||||
|
||||
async move {
|
||||
let mut is_rate_limited = false;
|
||||
|
||||
if let Some(limit) = rate_limit.as_ref() {
|
||||
let mut attempts = 0;
|
||||
let jitter = Jitter::up_to(MAX_JITTER);
|
||||
|
||||
loop {
|
||||
if attempts >= MAX_RETRIES {
|
||||
return reject_too_many_calls(req.id);
|
||||
}
|
||||
|
||||
if let Err(rejected) = limit.inner.check() {
|
||||
tokio::time::sleep(jitter + rejected.wait_time_from(limit.clock.now()))
|
||||
.await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
is_rate_limited = true;
|
||||
attempts += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let rp = service.call(req.clone()).await;
|
||||
metrics.as_ref().map(|m| m.on_response(&req, &rp, is_rate_limited, now));
|
||||
|
||||
rp
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_too_many_calls(id: Id) -> MethodResponse {
|
||||
MethodResponse::error(id, ErrorObject::owned(-32999, "RPC rate limit exceeded", None::<()>))
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Middleware for handling `/health` and `/health/readiness` endpoints.
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures::future::FutureExt;
|
||||
use http::{HeaderValue, Method, StatusCode, Uri};
|
||||
use jsonrpsee::{
|
||||
server::{HttpBody, HttpRequest, HttpResponse},
|
||||
types::{Response as RpcResponse, ResponseSuccess as RpcResponseSuccess},
|
||||
};
|
||||
use tower::Service;
|
||||
|
||||
const RPC_SYSTEM_HEALTH_CALL: &str = r#"{"jsonrpc":"2.0","method":"system_health","id":0}"#;
|
||||
const HEADER_VALUE_JSON: HeaderValue = HeaderValue::from_static("application/json; charset=utf-8");
|
||||
|
||||
/// Layer that applies [`NodeHealthProxy`] which
|
||||
/// proxies `/health` and `/health/readiness` endpoints.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NodeHealthProxyLayer;
|
||||
|
||||
impl<S> tower::Layer<S> for NodeHealthProxyLayer {
|
||||
type Service = NodeHealthProxy<S>;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
NodeHealthProxy::new(service)
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware that proxies `/health` and `/health/readiness` endpoints.
|
||||
pub struct NodeHealthProxy<S>(S);
|
||||
|
||||
impl<S> NodeHealthProxy<S> {
|
||||
/// Creates a new [`NodeHealthProxy`].
|
||||
pub fn new(service: S) -> Self {
|
||||
Self(service)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> tower::Service<http::Request<hyper::body::Incoming>> for NodeHealthProxy<S>
|
||||
where
|
||||
S: Service<HttpRequest, Response = HttpResponse>,
|
||||
S::Response: 'static,
|
||||
S::Error: Into<Box<dyn Error + Send + Sync>> + 'static,
|
||||
S::Future: Send + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = Box<dyn Error + Send + Sync + 'static>;
|
||||
type Future =
|
||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.0.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: http::Request<hyper::body::Incoming>) -> Self::Future {
|
||||
let mut req = req.map(|body| HttpBody::new(body));
|
||||
let maybe_intercept = InterceptRequest::from_http(&req);
|
||||
|
||||
// Modify the request and proxy it to `system_health`
|
||||
if let InterceptRequest::Health | InterceptRequest::Readiness = maybe_intercept {
|
||||
// RPC methods are accessed with `POST`.
|
||||
*req.method_mut() = Method::POST;
|
||||
// Precautionary remove the URI.
|
||||
*req.uri_mut() = Uri::from_static("/");
|
||||
|
||||
// Requests must have the following headers:
|
||||
req.headers_mut().insert(http::header::CONTENT_TYPE, HEADER_VALUE_JSON);
|
||||
req.headers_mut().insert(http::header::ACCEPT, HEADER_VALUE_JSON);
|
||||
|
||||
// Adjust the body to reflect the method call.
|
||||
req = req.map(|_| HttpBody::from(RPC_SYSTEM_HEALTH_CALL));
|
||||
}
|
||||
|
||||
// Call the inner service and get a future that resolves to the response.
|
||||
let fut = self.0.call(req);
|
||||
|
||||
async move {
|
||||
Ok(match maybe_intercept {
|
||||
InterceptRequest::Deny =>
|
||||
http_response(StatusCode::METHOD_NOT_ALLOWED, HttpBody::empty()),
|
||||
InterceptRequest::No => fut.await.map_err(|err| err.into())?,
|
||||
InterceptRequest::Health => {
|
||||
let res = fut.await.map_err(|err| err.into())?;
|
||||
if let Ok(health) = parse_rpc_response(res.into_body()).await {
|
||||
http_ok_response(serde_json::to_string(&health)?)
|
||||
} else {
|
||||
http_internal_error()
|
||||
}
|
||||
},
|
||||
InterceptRequest::Readiness => {
|
||||
let res = fut.await.map_err(|err| err.into())?;
|
||||
match parse_rpc_response(res.into_body()).await {
|
||||
Ok(health)
|
||||
if (!health.is_syncing && health.peers > 0) ||
|
||||
!health.should_have_peers =>
|
||||
http_ok_response(HttpBody::empty()),
|
||||
_ => http_internal_error(),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: This is duplicated here to avoid dependency to the `RPC API`.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Health {
|
||||
/// Number of connected peers
|
||||
pub peers: usize,
|
||||
/// Is the node syncing
|
||||
pub is_syncing: bool,
|
||||
/// Should this node have any peers
|
||||
///
|
||||
/// Might be false for local chains or when running without discovery.
|
||||
pub should_have_peers: bool,
|
||||
}
|
||||
|
||||
fn http_ok_response<S: Into<HttpBody>>(body: S) -> HttpResponse {
|
||||
http_response(StatusCode::OK, body)
|
||||
}
|
||||
|
||||
fn http_response<S: Into<HttpBody>>(status_code: StatusCode, body: S) -> HttpResponse {
|
||||
HttpResponse::builder()
|
||||
.status(status_code)
|
||||
.header(http::header::CONTENT_TYPE, HEADER_VALUE_JSON)
|
||||
.body(body.into())
|
||||
.expect("Header is valid; qed")
|
||||
}
|
||||
|
||||
fn http_internal_error() -> HttpResponse {
|
||||
http_response(hyper::StatusCode::INTERNAL_SERVER_ERROR, HttpBody::empty())
|
||||
}
|
||||
|
||||
async fn parse_rpc_response(
|
||||
body: HttpBody,
|
||||
) -> Result<Health, Box<dyn Error + Send + Sync + 'static>> {
|
||||
use http_body_util::BodyExt;
|
||||
|
||||
let bytes = body.collect().await?.to_bytes();
|
||||
|
||||
let raw_rp = serde_json::from_slice::<RpcResponse<Health>>(&bytes)?;
|
||||
let rp = RpcResponseSuccess::<Health>::try_from(raw_rp)?;
|
||||
|
||||
Ok(rp.result)
|
||||
}
|
||||
|
||||
/// Whether the request should be treated as ordinary RPC call or be modified.
|
||||
enum InterceptRequest {
|
||||
/// Proxy `/health` to `system_health`.
|
||||
Health,
|
||||
/// Checks if node has at least one peer and is not doing major syncing.
|
||||
///
|
||||
/// Returns HTTP status code 200 on success otherwise HTTP status code 500 is returned.
|
||||
Readiness,
|
||||
/// Treat as a ordinary RPC call and don't modify the request or response.
|
||||
No,
|
||||
/// Deny health or readiness calls that is not HTTP GET request.
|
||||
///
|
||||
/// Returns HTTP status code 405.
|
||||
Deny,
|
||||
}
|
||||
|
||||
impl InterceptRequest {
|
||||
fn from_http(req: &HttpRequest) -> InterceptRequest {
|
||||
match req.uri().path() {
|
||||
"/health" =>
|
||||
if req.method() == http::Method::GET {
|
||||
InterceptRequest::Health
|
||||
} else {
|
||||
InterceptRequest::Deny
|
||||
},
|
||||
"/health/readiness" =>
|
||||
if req.method() == http::Method::GET {
|
||||
InterceptRequest::Readiness
|
||||
} else {
|
||||
InterceptRequest::Deny
|
||||
},
|
||||
// Forward all other requests to the RPC server.
|
||||
_ => InterceptRequest::No,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! RPC rate limit.
|
||||
|
||||
use governor::{
|
||||
clock::{DefaultClock, QuantaClock},
|
||||
middleware::NoOpMiddleware,
|
||||
state::{InMemoryState, NotKeyed},
|
||||
Quota,
|
||||
};
|
||||
use std::{num::NonZeroU32, sync::Arc};
|
||||
|
||||
type RateLimitInner = governor::RateLimiter<NotKeyed, InMemoryState, DefaultClock, NoOpMiddleware>;
|
||||
|
||||
/// Rate limit.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RateLimit {
|
||||
pub(crate) inner: Arc<RateLimitInner>,
|
||||
pub(crate) clock: QuantaClock,
|
||||
}
|
||||
|
||||
impl RateLimit {
|
||||
/// Create a new `RateLimit` per minute.
|
||||
pub fn per_minute(n: NonZeroU32) -> Self {
|
||||
let clock = QuantaClock::default();
|
||||
Self {
|
||||
inner: Arc::new(RateLimitInner::direct_with_clock(Quota::per_minute(n), &clock)),
|
||||
clock,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Bizinikiwi RPC server utils.
|
||||
|
||||
use crate::BatchRequestConfig;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
net::{IpAddr, SocketAddr},
|
||||
num::NonZeroU32,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use forwarded_header_value::ForwardedHeaderValue;
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use ip_network::IpNetwork;
|
||||
use jsonrpsee::{server::middleware::http::HostFilterLayer, RpcModule};
|
||||
use pezsc_rpc_api::DenyUnsafe;
|
||||
use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||
|
||||
const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
|
||||
const X_REAL_IP: HeaderName = HeaderName::from_static("x-real-ip");
|
||||
const FORWARDED: HeaderName = HeaderName::from_static("forwarded");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ListenAddrError;
|
||||
|
||||
impl std::error::Error for ListenAddrError {}
|
||||
|
||||
impl std::fmt::Display for ListenAddrError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "No listen address was successfully bound")
|
||||
}
|
||||
}
|
||||
|
||||
/// Available RPC methods.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum RpcMethods {
|
||||
/// Allow only a safe subset of RPC methods.
|
||||
Safe,
|
||||
/// Expose every RPC method (even potentially unsafe ones).
|
||||
Unsafe,
|
||||
/// Automatically determine the RPC methods based on the connection.
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl Default for RpcMethods {
|
||||
fn default() -> Self {
|
||||
RpcMethods::Auto
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RpcMethods {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"safe" => Ok(RpcMethods::Safe),
|
||||
"unsafe" => Ok(RpcMethods::Unsafe),
|
||||
"auto" => Ok(RpcMethods::Auto),
|
||||
invalid => Err(format!("Invalid rpc methods {invalid}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RpcSettings {
|
||||
pub(crate) batch_config: BatchRequestConfig,
|
||||
pub(crate) max_connections: u32,
|
||||
pub(crate) max_payload_in_mb: u32,
|
||||
pub(crate) max_payload_out_mb: u32,
|
||||
pub(crate) max_subscriptions_per_connection: u32,
|
||||
pub(crate) max_buffer_capacity_per_connection: u32,
|
||||
pub(crate) rpc_methods: RpcMethods,
|
||||
pub(crate) rate_limit: Option<NonZeroU32>,
|
||||
pub(crate) rate_limit_trust_proxy_headers: bool,
|
||||
pub(crate) rate_limit_whitelisted_ips: Vec<IpNetwork>,
|
||||
pub(crate) cors: CorsLayer,
|
||||
pub(crate) host_filter: Option<HostFilterLayer>,
|
||||
}
|
||||
|
||||
/// Represent a single RPC endpoint with its configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RpcEndpoint {
|
||||
/// Listen address.
|
||||
pub listen_addr: SocketAddr,
|
||||
/// Batch request configuration.
|
||||
pub batch_config: BatchRequestConfig,
|
||||
/// Maximum number of connections.
|
||||
pub max_connections: u32,
|
||||
/// Maximum inbound payload size in MB.
|
||||
pub max_payload_in_mb: u32,
|
||||
/// Maximum outbound payload size in MB.
|
||||
pub max_payload_out_mb: u32,
|
||||
/// Maximum number of subscriptions per connection.
|
||||
pub max_subscriptions_per_connection: u32,
|
||||
/// Maximum buffer capacity per connection.
|
||||
pub max_buffer_capacity_per_connection: u32,
|
||||
/// Rate limit per minute.
|
||||
pub rate_limit: Option<NonZeroU32>,
|
||||
/// Whether to trust proxy headers for rate limiting.
|
||||
pub rate_limit_trust_proxy_headers: bool,
|
||||
/// Whitelisted IPs for rate limiting.
|
||||
pub rate_limit_whitelisted_ips: Vec<IpNetwork>,
|
||||
/// CORS.
|
||||
pub cors: Option<Vec<String>>,
|
||||
/// RPC methods to expose.
|
||||
pub rpc_methods: RpcMethods,
|
||||
/// Whether it's an optional listening address i.e, it's ignored if it fails to bind.
|
||||
/// For example bizinikiwi tries to bind both ipv4 and ipv6 addresses but some platforms
|
||||
/// may not support ipv6.
|
||||
pub is_optional: bool,
|
||||
/// Whether to retry with a random port if the provided port is already in use.
|
||||
pub retry_random_port: bool,
|
||||
}
|
||||
|
||||
impl RpcEndpoint {
|
||||
/// Binds to the listen address.
|
||||
pub(crate) async fn bind(self) -> Result<Listener, Box<dyn StdError + Send + Sync>> {
|
||||
let listener = match tokio::net::TcpListener::bind(self.listen_addr).await {
|
||||
Ok(listener) => listener,
|
||||
Err(_) if self.retry_random_port => {
|
||||
let mut addr = self.listen_addr;
|
||||
addr.set_port(0);
|
||||
|
||||
tokio::net::TcpListener::bind(addr).await?
|
||||
},
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let local_addr = listener.local_addr()?;
|
||||
let host_filter = host_filtering(self.cors.is_some(), local_addr);
|
||||
let cors = try_into_cors(self.cors)?;
|
||||
|
||||
Ok(Listener {
|
||||
listener,
|
||||
local_addr,
|
||||
cfg: RpcSettings {
|
||||
batch_config: self.batch_config,
|
||||
max_connections: self.max_connections,
|
||||
max_payload_in_mb: self.max_payload_in_mb,
|
||||
max_payload_out_mb: self.max_payload_out_mb,
|
||||
max_subscriptions_per_connection: self.max_subscriptions_per_connection,
|
||||
max_buffer_capacity_per_connection: self.max_buffer_capacity_per_connection,
|
||||
rpc_methods: self.rpc_methods,
|
||||
rate_limit: self.rate_limit,
|
||||
rate_limit_trust_proxy_headers: self.rate_limit_trust_proxy_headers,
|
||||
rate_limit_whitelisted_ips: self.rate_limit_whitelisted_ips,
|
||||
host_filter,
|
||||
cors,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// TCP socket server with RPC settings.
|
||||
pub(crate) struct Listener {
|
||||
listener: tokio::net::TcpListener,
|
||||
local_addr: SocketAddr,
|
||||
cfg: RpcSettings,
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
/// Accepts a new connection.
|
||||
pub(crate) async fn accept(&mut self) -> std::io::Result<(tokio::net::TcpStream, SocketAddr)> {
|
||||
let (sock, remote_addr) = self.listener.accept().await?;
|
||||
Ok((sock, remote_addr))
|
||||
}
|
||||
|
||||
/// Returns the local address the listener is bound to.
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.local_addr
|
||||
}
|
||||
|
||||
pub fn rpc_settings(&self) -> RpcSettings {
|
||||
self.cfg.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn host_filtering(enabled: bool, addr: SocketAddr) -> Option<HostFilterLayer> {
|
||||
if enabled {
|
||||
// NOTE: The listening addresses are whitelisted by default.
|
||||
|
||||
let hosts = [
|
||||
format!("localhost:{}", addr.port()),
|
||||
format!("127.0.0.1:{}", addr.port()),
|
||||
format!("[::1]:{}", addr.port()),
|
||||
];
|
||||
|
||||
Some(HostFilterLayer::new(hosts).expect("Valid hosts; qed"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_rpc_api<M: Send + Sync + 'static>(mut rpc_api: RpcModule<M>) -> RpcModule<M> {
|
||||
let mut available_methods = rpc_api.method_names().collect::<Vec<_>>();
|
||||
// The "rpc_methods" is defined below and we want it to be part of the reported methods.
|
||||
available_methods.push("rpc_methods");
|
||||
available_methods.sort();
|
||||
|
||||
rpc_api
|
||||
.register_method("rpc_methods", move |_, _, _| {
|
||||
serde_json::json!({
|
||||
"methods": available_methods,
|
||||
})
|
||||
})
|
||||
.expect("infallible all other methods have their own address space; qed");
|
||||
|
||||
rpc_api
|
||||
}
|
||||
|
||||
pub(crate) fn try_into_cors(
|
||||
maybe_cors: Option<Vec<String>>,
|
||||
) -> Result<CorsLayer, Box<dyn StdError + Send + Sync>> {
|
||||
if let Some(cors) = maybe_cors {
|
||||
let mut list = Vec::new();
|
||||
|
||||
for origin in cors {
|
||||
list.push(HeaderValue::from_str(&origin)?)
|
||||
}
|
||||
|
||||
Ok(CorsLayer::new().allow_origin(AllowOrigin::list(list)))
|
||||
} else {
|
||||
// allow all cors
|
||||
Ok(CorsLayer::permissive())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the IP addr from the HTTP request.
|
||||
///
|
||||
/// It is extracted in the following order:
|
||||
/// 1. `Forwarded` header.
|
||||
/// 2. `X-Forwarded-For` header.
|
||||
/// 3. `X-Real-Ip`.
|
||||
pub(crate) fn get_proxy_ip<B>(req: &http::Request<B>) -> Option<IpAddr> {
|
||||
if let Some(ip) = req
|
||||
.headers()
|
||||
.get(&FORWARDED)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| ForwardedHeaderValue::from_forwarded(v).ok())
|
||||
.and_then(|v| v.remotest_forwarded_for_ip())
|
||||
{
|
||||
return Some(ip);
|
||||
}
|
||||
|
||||
if let Some(ip) = req
|
||||
.headers()
|
||||
.get(&X_FORWARDED_FOR)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| ForwardedHeaderValue::from_x_forwarded_for(v).ok())
|
||||
.and_then(|v| v.remotest_forwarded_for_ip())
|
||||
{
|
||||
return Some(ip);
|
||||
}
|
||||
|
||||
if let Some(ip) = req
|
||||
.headers()
|
||||
.get(&X_REAL_IP)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| IpAddr::from_str(v).ok())
|
||||
{
|
||||
return Some(ip);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the `deny_unsafe` setting based on the address and the RPC methods exposed by the interface.
|
||||
pub fn deny_unsafe(addr: &SocketAddr, methods: &RpcMethods) -> DenyUnsafe {
|
||||
match (addr.ip().is_loopback(), methods) {
|
||||
(_, RpcMethods::Unsafe) | (true, RpcMethods::Auto) => DenyUnsafe::No,
|
||||
_ => DenyUnsafe::Yes,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_listen_addrs(addr: &[SocketAddr]) -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
let mut it = addr.iter().peekable();
|
||||
|
||||
while let Some(addr) = it.next() {
|
||||
s.push_str(&addr.to_string());
|
||||
|
||||
if it.peek().is_some() {
|
||||
s.push(',');
|
||||
}
|
||||
}
|
||||
|
||||
if addr.len() == 1 {
|
||||
s.push(',');
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hyper::header::HeaderValue;
|
||||
use jsonrpsee::server::{HttpBody, HttpRequest};
|
||||
|
||||
fn request() -> http::Request<HttpBody> {
|
||||
HttpRequest::builder().body(HttpBody::empty()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_works() {
|
||||
let req = request();
|
||||
let host = get_proxy_ip(&req);
|
||||
assert!(host.is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_from_x_real_ip() {
|
||||
let mut req = request();
|
||||
|
||||
req.headers_mut().insert(&X_REAL_IP, HeaderValue::from_static("127.0.0.1"));
|
||||
let ip = get_proxy_ip(&req);
|
||||
assert_eq!(Some(IpAddr::from_str("127.0.0.1").unwrap()), ip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_from_forwarded_works() {
|
||||
let mut req = request();
|
||||
|
||||
req.headers_mut().insert(
|
||||
&FORWARDED,
|
||||
HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43;host=example.com"),
|
||||
);
|
||||
let ip = get_proxy_ip(&req);
|
||||
assert_eq!(Some(IpAddr::from_str("192.0.2.60").unwrap()), ip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_from_forwarded_multiple() {
|
||||
let mut req = request();
|
||||
|
||||
req.headers_mut().append(&FORWARDED, HeaderValue::from_static("for=127.0.0.1"));
|
||||
req.headers_mut().append(&FORWARDED, HeaderValue::from_static("for=192.0.2.60"));
|
||||
req.headers_mut().append(&FORWARDED, HeaderValue::from_static("for=192.0.2.61"));
|
||||
let ip = get_proxy_ip(&req);
|
||||
assert_eq!(Some(IpAddr::from_str("127.0.0.1").unwrap()), ip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_from_x_forwarded_works() {
|
||||
let mut req = request();
|
||||
|
||||
req.headers_mut()
|
||||
.insert(&X_FORWARDED_FOR, HeaderValue::from_static("127.0.0.1,192.0.2.60,0.0.0.1"));
|
||||
let ip = get_proxy_ip(&req);
|
||||
assert_eq!(Some(IpAddr::from_str("127.0.0.1").unwrap()), ip);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user