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:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
+310
View File
@@ -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,
}
}
}
+369
View File
@@ -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);
}
}