Allow to expose a subset of unsafe RPCs (#5233)

* sc-cli: Use type-safe constructors for RPC/Prometheus interfaces

* service: Simplify rpc handler creation

Could probably be further simplifies once [this][commit] lands.

[commit]: https://github.com/paritytech/jsonrpc/commit/20485387ed06a48f1a70bf4d609a7cde6cf0accf

* service: Streamline some HTTP & WS server start logic

* client: Introduce a simple RPC policy mechanism

* rpc/system: Check unsafe RPCs

* rpc/offchain: Check unsafe RPCs

* rpc/author: Check unsafe RPCs
This commit is contained in:
Igor Matuszewski
2020-04-20 11:03:58 +02:00
committed by GitHub
parent d05dc090a8
commit 4b1f7d187f
20 changed files with 281 additions and 95 deletions
+6
View File
@@ -279,6 +279,12 @@ macro_rules! substrate_cli_subcommands {
} }
} }
fn unsafe_rpc_expose(&self) -> $crate::Result<bool> {
match self {
$($enum::$variant(cmd) => cmd.unsafe_rpc_expose()),*
}
}
fn rpc_ws_max_connections(&self) -> $crate::Result<::std::option::Option<usize>> { fn rpc_ws_max_connections(&self) -> $crate::Result<::std::option::Option<usize>> {
match self { match self {
$($enum::$variant(cmd) => cmd.rpc_ws_max_connections()),* $($enum::$variant(cmd) => cmd.rpc_ws_max_connections()),*
+42 -41
View File
@@ -27,7 +27,7 @@ use sc_service::{
ChainSpec, Role, ChainSpec, Role,
}; };
use sc_telemetry::TelemetryEndpoints; use sc_telemetry::TelemetryEndpoints;
use std::net::SocketAddr; use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use structopt::{clap::arg_enum, StructOpt}; use structopt::{clap::arg_enum, StructOpt};
arg_enum! { arg_enum! {
@@ -93,6 +93,14 @@ pub struct RunCmd {
#[structopt(long = "unsafe-rpc-external")] #[structopt(long = "unsafe-rpc-external")]
pub unsafe_rpc_external: bool, pub unsafe_rpc_external: bool,
/// Don't deny potentially unsafe RPCs when listening on external interfaces.
///
/// Default is false. This allows exposing RPC methods publicly (same as `--unsafe-{rpc,ws}-external` )
/// but will allow doing so even on validator nodes, which is prohibited by default.
/// Please do this if you know what you're doing.
#[structopt(long = "unsafe-rpc-expose")]
pub unsafe_rpc_expose: bool,
/// Listen to all Websocket interfaces. /// Listen to all Websocket interfaces.
/// ///
/// Default is local. Note: not all RPC methods are safe to be exposed publicly. Use an RPC proxy /// Default is local. Note: not all RPC methods are safe to be exposed publicly. Use an RPC proxy
@@ -361,22 +369,19 @@ impl CliConfiguration for RunCmd {
} }
fn prometheus_config(&self) -> Result<Option<PrometheusConfig>> { fn prometheus_config(&self) -> Result<Option<PrometheusConfig>> {
if self.no_prometheus { Ok(if self.no_prometheus {
Ok(None) None
} else { } else {
let prometheus_interface: &str = if self.prometheus_external { let interface = if self.prometheus_external {
"0.0.0.0" Ipv4Addr::UNSPECIFIED
} else { } else {
"127.0.0.1" Ipv4Addr::LOCALHOST
}; };
Ok(Some(PrometheusConfig::new_with_default_registry( Some(PrometheusConfig::new_with_default_registry(
parse_address( SocketAddr::new(interface.into(), self.prometheus_port.unwrap_or(9615))
&format!("{}:{}", prometheus_interface, 9615), ))
self.prometheus_port, })
)?,
)))
}
} }
fn disable_grandpa(&self) -> Result<bool> { fn disable_grandpa(&self) -> Result<bool> {
@@ -409,23 +414,29 @@ impl CliConfiguration for RunCmd {
} }
fn rpc_http(&self) -> Result<Option<SocketAddr>> { fn rpc_http(&self) -> Result<Option<SocketAddr>> {
let rpc_interface: &str = let interface = rpc_interface(
interface_str(self.rpc_external, self.unsafe_rpc_external, self.validator)?; self.rpc_external,
self.unsafe_rpc_external,
self.unsafe_rpc_expose,
self.validator
)?;
Ok(Some(parse_address( Ok(Some(SocketAddr::new(interface, self.rpc_port.unwrap_or(9933))))
&format!("{}:{}", rpc_interface, 9933),
self.rpc_port,
)?))
} }
fn rpc_ws(&self) -> Result<Option<SocketAddr>> { fn rpc_ws(&self) -> Result<Option<SocketAddr>> {
let ws_interface: &str = let interface = rpc_interface(
interface_str(self.ws_external, self.unsafe_ws_external, self.validator)?; self.ws_external,
self.unsafe_ws_external,
self.unsafe_rpc_expose,
self.validator
)?;
Ok(Some(parse_address( Ok(Some(SocketAddr::new(interface, self.ws_port.unwrap_or(9944))))
&format!("{}:{}", ws_interface, 9944), }
self.ws_port,
)?)) fn unsafe_rpc_expose(&self) -> Result<bool> {
Ok(self.unsafe_rpc_expose)
} }
fn offchain_worker(&self, role: &Role) -> Result<bool> { fn offchain_worker(&self, role: &Role) -> Result<bool> {
@@ -468,23 +479,13 @@ pub fn is_node_name_valid(_name: &str) -> std::result::Result<(), &str> {
Ok(()) Ok(())
} }
fn parse_address(address: &str, port: Option<u16>) -> std::result::Result<SocketAddr, String> { fn rpc_interface(
let mut address: SocketAddr = address
.parse()
.map_err(|_| format!("Invalid address: {}", address))?;
if let Some(port) = port {
address.set_port(port);
}
Ok(address)
}
fn interface_str(
is_external: bool, is_external: bool,
is_unsafe_external: bool, is_unsafe_external: bool,
is_unsafe_rpc_expose: bool,
is_validator: bool, is_validator: bool,
) -> Result<&'static str> { ) -> Result<IpAddr> {
if is_external && is_validator { if is_external && is_validator && !is_unsafe_rpc_expose {
return Err(Error::Input( return Err(Error::Input(
"--rpc-external and --ws-external options shouldn't be \ "--rpc-external and --ws-external options shouldn't be \
used if the node is running as a validator. Use `--unsafe-rpc-external` if you understand \ used if the node is running as a validator. Use `--unsafe-rpc-external` if you understand \
@@ -499,9 +500,9 @@ fn interface_str(
available set of RPC methods." available set of RPC methods."
); );
Ok("0.0.0.0") Ok(Ipv4Addr::UNSPECIFIED.into())
} else { } else {
Ok("127.0.0.1") Ok(Ipv4Addr::LOCALHOST.into())
} }
} }
+8
View File
@@ -249,6 +249,13 @@ pub trait CliConfiguration: Sized {
Ok(Default::default()) Ok(Default::default())
} }
/// Returns `Ok(true) if potentially unsafe RPC is to be exposed.
///
/// By default this is `false`.
fn unsafe_rpc_expose(&self) -> Result<bool> {
Ok(Default::default())
}
/// Get the RPC websockets maximum connections (`None` if unlimited). /// Get the RPC websockets maximum connections (`None` if unlimited).
/// ///
/// By default this is `None`. /// By default this is `None`.
@@ -419,6 +426,7 @@ pub trait CliConfiguration: Sized {
execution_strategies: self.execution_strategies(is_dev)?, execution_strategies: self.execution_strategies(is_dev)?,
rpc_http: self.rpc_http()?, rpc_http: self.rpc_http()?,
rpc_ws: self.rpc_ws()?, rpc_ws: self.rpc_ws()?,
unsafe_rpc_expose: self.unsafe_rpc_expose()?,
rpc_ws_max_connections: self.rpc_ws_max_connections()?, rpc_ws_max_connections: self.rpc_ws_max_connections()?,
rpc_cors: self.rpc_cors(is_dev)?, rpc_cors: self.rpc_cors(is_dev)?,
prometheus_config: self.prometheus_config()?, prometheus_config: self.prometheus_config()?,
@@ -57,6 +57,8 @@ pub enum Error {
/// Invalid session keys encoding. /// Invalid session keys encoding.
#[display(fmt="Session keys are not encoded correctly")] #[display(fmt="Session keys are not encoded correctly")]
InvalidSessionKeys, InvalidSessionKeys,
/// Call to an unsafe RPC was denied.
UnsafeRpcCalled(crate::policy::UnsafeRpcError),
} }
impl std::error::Error for Error { impl std::error::Error for Error {
@@ -65,6 +67,7 @@ impl std::error::Error for Error {
Error::Client(ref err) => Some(&**err), Error::Client(ref err) => Some(&**err),
Error::Pool(ref err) => Some(err), Error::Pool(ref err) => Some(err),
Error::Verification(ref err) => Some(&**err), Error::Verification(ref err) => Some(&**err),
Error::UnsafeRpcCalled(ref err) => Some(err),
_ => None, _ => None,
} }
} }
@@ -152,6 +155,7 @@ impl From<Error> for rpc::Error {
request to insert the key successfully.".into() request to insert the key successfully.".into()
), ),
}, },
Error::UnsafeRpcCalled(e) => e.into(),
e => errors::internal(e), e => errors::internal(e),
} }
} }
+2
View File
@@ -22,11 +22,13 @@
mod errors; mod errors;
mod helpers; mod helpers;
mod policy;
mod subscriptions; mod subscriptions;
pub use jsonrpc_core::IoHandlerExtension as RpcExtension; pub use jsonrpc_core::IoHandlerExtension as RpcExtension;
pub use subscriptions::{Subscriptions, TaskExecutor}; pub use subscriptions::{Subscriptions, TaskExecutor};
pub use helpers::Receiver; pub use helpers::Receiver;
pub use policy::DenyUnsafe;
pub mod author; pub mod author;
pub mod chain; pub mod chain;
@@ -27,11 +27,16 @@ pub enum Error {
/// Unavailable storage kind error. /// Unavailable storage kind error.
#[display(fmt="This storage kind is not available yet.")] #[display(fmt="This storage kind is not available yet.")]
UnavailableStorageKind, UnavailableStorageKind,
/// Call to an unsafe RPC was denied.
UnsafeRpcCalled(crate::policy::UnsafeRpcError),
} }
impl std::error::Error for Error { impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None match self {
Self::UnsafeRpcCalled(err) => Some(err),
_ => None,
}
} }
} }
@@ -46,6 +51,7 @@ impl From<Error> for rpc::Error {
message: "This storage kind is not available yet" .into(), message: "This storage kind is not available yet" .into(),
data: None, data: None,
}, },
Error::UnsafeRpcCalled(e) => e.into(),
} }
} }
} }
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2020 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate 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.
// Substrate 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 Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Policy-related types.
//!
//! Contains a `DenyUnsafe` type that can be used to deny potentially unsafe
//! RPC when accessed externally.
use jsonrpc_core as rpc;
/// Signifies whether a potentially unsafe RPC should be denied.
#[derive(Clone, Copy, Debug)]
pub enum DenyUnsafe {
/// Denies only potentially unsafe RPCs.
Yes,
/// Allows calling every RPCs.
No
}
impl DenyUnsafe {
/// Returns `Ok(())` if the RPCs considered unsafe are safe to call,
/// otherwise returns `Err(UnsafeRpcError)`.
pub fn check_if_safe(self) -> Result<(), UnsafeRpcError> {
match self {
DenyUnsafe::Yes => Err(UnsafeRpcError),
DenyUnsafe::No => Ok(())
}
}
}
/// Signifies whether an RPC considered unsafe is denied to be called externally.
#[derive(Debug)]
pub struct UnsafeRpcError;
impl std::fmt::Display for UnsafeRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RPC call is unsafe to be called externally")
}
}
impl std::error::Error for UnsafeRpcError {}
impl From<UnsafeRpcError> for rpc::Error {
fn from(_: UnsafeRpcError) -> rpc::Error {
rpc::Error::method_not_found()
}
}
+4 -2
View File
@@ -72,14 +72,16 @@ pub trait SystemApi<Hash, Number> {
/// Returns currently connected peers /// Returns currently connected peers
#[rpc(name = "system_peers", returns = "Vec<PeerInfo<Hash, Number>>")] #[rpc(name = "system_peers", returns = "Vec<PeerInfo<Hash, Number>>")]
fn system_peers(&self) -> Receiver<Vec<PeerInfo<Hash, Number>>>; fn system_peers(&self)
-> Compat<BoxFuture<'static, jsonrpc_core::Result<Vec<PeerInfo<Hash, Number>>>>>;
/// Returns current state of the network. /// Returns current state of the network.
/// ///
/// **Warning**: This API is not stable. /// **Warning**: This API is not stable.
// TODO: make this stable and move structs https://github.com/paritytech/substrate/issues/1890 // TODO: make this stable and move structs https://github.com/paritytech/substrate/issues/1890
#[rpc(name = "system_networkState", returns = "jsonrpc_core::Value")] #[rpc(name = "system_networkState", returns = "jsonrpc_core::Value")]
fn system_network_state(&self) -> Receiver<jsonrpc_core::Value>; fn system_network_state(&self)
-> Compat<BoxFuture<'static, jsonrpc_core::Result<jsonrpc_core::Value>>>;
/// Adds a reserved peer. Returns the empty string or an error. The string /// Adds a reserved peer. Returns the empty string or an error. The string
/// parameter should encode a `p2p` multiaddr. /// parameter should encode a `p2p` multiaddr.
+15 -1
View File
@@ -30,7 +30,7 @@ use rpc::futures::{
}; };
use futures::{StreamExt as _, compat::Compat}; use futures::{StreamExt as _, compat::Compat};
use futures::future::{ready, FutureExt, TryFutureExt}; use futures::future::{ready, FutureExt, TryFutureExt};
use sc_rpc_api::Subscriptions; use sc_rpc_api::{DenyUnsafe, Subscriptions};
use jsonrpc_pubsub::{typed::Subscriber, SubscriptionId}; use jsonrpc_pubsub::{typed::Subscriber, SubscriptionId};
use codec::{Encode, Decode}; use codec::{Encode, Decode};
use sp_core::{Bytes, traits::BareCryptoStorePtr}; use sp_core::{Bytes, traits::BareCryptoStorePtr};
@@ -56,6 +56,8 @@ pub struct Author<P, Client> {
subscriptions: Subscriptions, subscriptions: Subscriptions,
/// The key store. /// The key store.
keystore: BareCryptoStorePtr, keystore: BareCryptoStorePtr,
/// Whether to deny unsafe calls
deny_unsafe: DenyUnsafe,
} }
impl<P, Client> Author<P, Client> { impl<P, Client> Author<P, Client> {
@@ -65,12 +67,14 @@ impl<P, Client> Author<P, Client> {
pool: Arc<P>, pool: Arc<P>,
subscriptions: Subscriptions, subscriptions: Subscriptions,
keystore: BareCryptoStorePtr, keystore: BareCryptoStorePtr,
deny_unsafe: DenyUnsafe,
) -> Self { ) -> Self {
Author { Author {
client, client,
pool, pool,
subscriptions, subscriptions,
keystore, keystore,
deny_unsafe,
} }
} }
} }
@@ -97,6 +101,8 @@ impl<P, Client> AuthorApi<TxHash<P>, BlockHash<P>> for Author<P, Client>
suri: String, suri: String,
public: Bytes, public: Bytes,
) -> Result<()> { ) -> Result<()> {
self.deny_unsafe.check_if_safe()?;
let key_type = key_type.as_str().try_into().map_err(|_| Error::BadKeyType)?; let key_type = key_type.as_str().try_into().map_err(|_| Error::BadKeyType)?;
let mut keystore = self.keystore.write(); let mut keystore = self.keystore.write();
keystore.insert_unknown(key_type, &suri, &public[..]) keystore.insert_unknown(key_type, &suri, &public[..])
@@ -105,6 +111,8 @@ impl<P, Client> AuthorApi<TxHash<P>, BlockHash<P>> for Author<P, Client>
} }
fn rotate_keys(&self) -> Result<Bytes> { fn rotate_keys(&self) -> Result<Bytes> {
self.deny_unsafe.check_if_safe()?;
let best_block_hash = self.client.info().best_hash; let best_block_hash = self.client.info().best_hash;
self.client.runtime_api().generate_session_keys( self.client.runtime_api().generate_session_keys(
&generic::BlockId::Hash(best_block_hash), &generic::BlockId::Hash(best_block_hash),
@@ -113,6 +121,8 @@ impl<P, Client> AuthorApi<TxHash<P>, BlockHash<P>> for Author<P, Client>
} }
fn has_session_keys(&self, session_keys: Bytes) -> Result<bool> { fn has_session_keys(&self, session_keys: Bytes) -> Result<bool> {
self.deny_unsafe.check_if_safe()?;
let best_block_hash = self.client.info().best_hash; let best_block_hash = self.client.info().best_hash;
let keys = self.client.runtime_api().decode_session_keys( let keys = self.client.runtime_api().decode_session_keys(
&generic::BlockId::Hash(best_block_hash), &generic::BlockId::Hash(best_block_hash),
@@ -124,6 +134,8 @@ impl<P, Client> AuthorApi<TxHash<P>, BlockHash<P>> for Author<P, Client>
} }
fn has_key(&self, public_key: Bytes, key_type: String) -> Result<bool> { fn has_key(&self, public_key: Bytes, key_type: String) -> Result<bool> {
self.deny_unsafe.check_if_safe()?;
let key_type = key_type.as_str().try_into().map_err(|_| Error::BadKeyType)?; let key_type = key_type.as_str().try_into().map_err(|_| Error::BadKeyType)?;
Ok(self.keystore.read().has_keys(&[(public_key.to_vec(), key_type)])) Ok(self.keystore.read().has_keys(&[(public_key.to_vec(), key_type)]))
} }
@@ -151,6 +163,8 @@ impl<P, Client> AuthorApi<TxHash<P>, BlockHash<P>> for Author<P, Client>
&self, &self,
bytes_or_hash: Vec<hash::ExtrinsicOrHash<TxHash<P>>>, bytes_or_hash: Vec<hash::ExtrinsicOrHash<TxHash<P>>>,
) -> Result<Vec<TxHash<P>>> { ) -> Result<Vec<TxHash<P>>> {
self.deny_unsafe.check_if_safe()?;
let hashes = bytes_or_hash.into_iter() let hashes = bytes_or_hash.into_iter()
.map(|x| match x { .map(|x| match x {
hash::ExtrinsicOrHash::Hash(h) => Ok(h), hash::ExtrinsicOrHash::Hash(h) => Ok(h),
+1
View File
@@ -83,6 +83,7 @@ impl TestSetup {
pool: self.pool.clone(), pool: self.pool.clone(),
subscriptions: Subscriptions::new(Arc::new(self.runtime.executor())), subscriptions: Subscriptions::new(Arc::new(self.runtime.executor())),
keystore: self.keystore.clone(), keystore: self.keystore.clone(),
deny_unsafe: DenyUnsafe::No,
} }
} }
} }
+1 -1
View File
@@ -22,7 +22,7 @@
mod metadata; mod metadata;
pub use sc_rpc_api::Subscriptions; pub use sc_rpc_api::{DenyUnsafe, Subscriptions};
pub use self::metadata::Metadata; pub use self::metadata::Metadata;
pub use rpc::IoHandlerExtension as RpcExtension; pub use rpc::IoHandlerExtension as RpcExtension;
+8 -1
View File
@@ -21,6 +21,7 @@ mod tests;
/// Re-export the API for backward compatibility. /// Re-export the API for backward compatibility.
pub use sc_rpc_api::offchain::*; pub use sc_rpc_api::offchain::*;
use sc_rpc_api::DenyUnsafe;
use self::error::{Error, Result}; use self::error::{Error, Result};
use sp_core::{ use sp_core::{
Bytes, Bytes,
@@ -34,13 +35,15 @@ use std::sync::Arc;
pub struct Offchain<T: OffchainStorage> { pub struct Offchain<T: OffchainStorage> {
/// Offchain storage /// Offchain storage
storage: Arc<RwLock<T>>, storage: Arc<RwLock<T>>,
deny_unsafe: DenyUnsafe,
} }
impl<T: OffchainStorage> Offchain<T> { impl<T: OffchainStorage> Offchain<T> {
/// Create new instance of Offchain API. /// Create new instance of Offchain API.
pub fn new(storage: T) -> Self { pub fn new(storage: T, deny_unsafe: DenyUnsafe) -> Self {
Offchain { Offchain {
storage: Arc::new(RwLock::new(storage)), storage: Arc::new(RwLock::new(storage)),
deny_unsafe,
} }
} }
} }
@@ -48,6 +51,8 @@ impl<T: OffchainStorage> Offchain<T> {
impl<T: OffchainStorage + 'static> OffchainApi for Offchain<T> { impl<T: OffchainStorage + 'static> OffchainApi for Offchain<T> {
/// Set offchain local storage under given key and prefix. /// Set offchain local storage under given key and prefix.
fn set_local_storage(&self, kind: StorageKind, key: Bytes, value: Bytes) -> Result<()> { fn set_local_storage(&self, kind: StorageKind, key: Bytes, value: Bytes) -> Result<()> {
self.deny_unsafe.check_if_safe()?;
let prefix = match kind { let prefix = match kind {
StorageKind::PERSISTENT => sp_offchain::STORAGE_PREFIX, StorageKind::PERSISTENT => sp_offchain::STORAGE_PREFIX,
StorageKind::LOCAL => return Err(Error::UnavailableStorageKind), StorageKind::LOCAL => return Err(Error::UnavailableStorageKind),
@@ -58,6 +63,8 @@ impl<T: OffchainStorage + 'static> OffchainApi for Offchain<T> {
/// Get offchain local storage under given key and prefix. /// Get offchain local storage under given key and prefix.
fn get_local_storage(&self, kind: StorageKind, key: Bytes) -> Result<Option<Bytes>> { fn get_local_storage(&self, kind: StorageKind, key: Bytes) -> Result<Option<Bytes>> {
self.deny_unsafe.check_if_safe()?;
let prefix = match kind { let prefix = match kind {
StorageKind::PERSISTENT => sp_offchain::STORAGE_PREFIX, StorageKind::PERSISTENT => sp_offchain::STORAGE_PREFIX,
StorageKind::LOCAL => return Err(Error::UnavailableStorageKind), StorageKind::LOCAL => return Err(Error::UnavailableStorageKind),
+18 -1
View File
@@ -21,7 +21,7 @@ use sp_core::{Bytes, offchain::storage::InMemOffchainStorage};
#[test] #[test]
fn local_storage_should_work() { fn local_storage_should_work() {
let storage = InMemOffchainStorage::default(); let storage = InMemOffchainStorage::default();
let offchain = Offchain::new(storage); let offchain = Offchain::new(storage, DenyUnsafe::No);
let key = Bytes(b"offchain_storage".to_vec()); let key = Bytes(b"offchain_storage".to_vec());
let value = Bytes(b"offchain_value".to_vec()); let value = Bytes(b"offchain_value".to_vec());
@@ -34,3 +34,20 @@ fn local_storage_should_work() {
Ok(Some(ref v)) if *v == value Ok(Some(ref v)) if *v == value
); );
} }
#[test]
fn offchain_calls_considered_unsafe() {
let storage = InMemOffchainStorage::default();
let offchain = Offchain::new(storage, DenyUnsafe::Yes);
let key = Bytes(b"offchain_storage".to_vec());
let value = Bytes(b"offchain_value".to_vec());
assert_matches!(
offchain.set_local_storage(StorageKind::PERSISTENT, key.clone(), value.clone()),
Err(Error::UnsafeRpcCalled(_))
);
assert_matches!(
offchain.get_local_storage(StorageKind::PERSISTENT, key),
Err(Error::UnsafeRpcCalled(_))
);
}
+34 -5
View File
@@ -21,7 +21,7 @@ mod tests;
use futures::{future::BoxFuture, FutureExt, TryFutureExt}; use futures::{future::BoxFuture, FutureExt, TryFutureExt};
use futures::{channel::oneshot, compat::Compat}; use futures::{channel::oneshot, compat::Compat};
use sc_rpc_api::Receiver; use sc_rpc_api::{DenyUnsafe, Receiver};
use sp_utils::mpsc::TracingUnboundedSender; use sp_utils::mpsc::TracingUnboundedSender;
use sp_runtime::traits::{self, Header as HeaderT}; use sp_runtime::traits::{self, Header as HeaderT};
@@ -31,10 +31,19 @@ pub use sc_rpc_api::system::*;
pub use self::helpers::{SystemInfo, Health, PeerInfo, NodeRole}; pub use self::helpers::{SystemInfo, Health, PeerInfo, NodeRole};
pub use self::gen_client::Client as SystemClient; pub use self::gen_client::Client as SystemClient;
macro_rules! bail_if_unsafe {
($value: expr) => {
if let Err(err) = $value.check_if_safe() {
return async move { Err(err.into()) }.boxed().compat();
}
};
}
/// System API implementation /// System API implementation
pub struct System<B: traits::Block> { pub struct System<B: traits::Block> {
info: SystemInfo, info: SystemInfo,
send_back: TracingUnboundedSender<Request<B>>, send_back: TracingUnboundedSender<Request<B>>,
deny_unsafe: DenyUnsafe,
} }
/// Request to be processed. /// Request to be processed.
@@ -66,10 +75,12 @@ impl<B: traits::Block> System<B> {
pub fn new( pub fn new(
info: SystemInfo, info: SystemInfo,
send_back: TracingUnboundedSender<Request<B>>, send_back: TracingUnboundedSender<Request<B>>,
deny_unsafe: DenyUnsafe,
) -> Self { ) -> Self {
System { System {
info, info,
send_back, send_back,
deny_unsafe,
} }
} }
} }
@@ -113,21 +124,37 @@ impl<B: traits::Block> SystemApi<B::Hash, <B::Header as HeaderT>::Number> for Sy
Receiver(Compat::new(rx)) Receiver(Compat::new(rx))
} }
fn system_peers(&self) -> Receiver<Vec<PeerInfo<B::Hash, <B::Header as HeaderT>::Number>>> { fn system_peers(&self)
-> Compat<BoxFuture<'static, rpc::Result<Vec<PeerInfo<B::Hash, <B::Header as HeaderT>::Number>>>>>
{
bail_if_unsafe!(self.deny_unsafe);
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let _ = self.send_back.unbounded_send(Request::Peers(tx)); let _ = self.send_back.unbounded_send(Request::Peers(tx));
Receiver(Compat::new(rx))
async move {
rx.await.map_err(|_| rpc::Error::internal_error())
}.boxed().compat()
} }
fn system_network_state(&self) -> Receiver<rpc::Value> { fn system_network_state(&self)
-> Compat<BoxFuture<'static, rpc::Result<rpc::Value>>>
{
bail_if_unsafe!(self.deny_unsafe);
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let _ = self.send_back.unbounded_send(Request::NetworkState(tx)); let _ = self.send_back.unbounded_send(Request::NetworkState(tx));
Receiver(Compat::new(rx))
async move {
rx.await.map_err(|_| rpc::Error::internal_error())
}.boxed().compat()
} }
fn system_add_reserved_peer(&self, peer: String) fn system_add_reserved_peer(&self, peer: String)
-> Compat<BoxFuture<'static, std::result::Result<(), rpc::Error>>> -> Compat<BoxFuture<'static, std::result::Result<(), rpc::Error>>>
{ {
bail_if_unsafe!(self.deny_unsafe);
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let _ = self.send_back.unbounded_send(Request::NetworkAddReservedPeer(peer, tx)); let _ = self.send_back.unbounded_send(Request::NetworkAddReservedPeer(peer, tx));
async move { async move {
@@ -142,6 +169,8 @@ impl<B: traits::Block> SystemApi<B::Hash, <B::Header as HeaderT>::Number> for Sy
fn system_remove_reserved_peer(&self, peer: String) fn system_remove_reserved_peer(&self, peer: String)
-> Compat<BoxFuture<'static, std::result::Result<(), rpc::Error>>> -> Compat<BoxFuture<'static, std::result::Result<(), rpc::Error>>>
{ {
bail_if_unsafe!(self.deny_unsafe);
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let _ = self.send_back.unbounded_send(Request::NetworkRemoveReservedPeer(peer, tx)); let _ = self.send_back.unbounded_send(Request::NetworkRemoveReservedPeer(peer, tx));
async move { async move {
+26 -14
View File
@@ -109,13 +109,17 @@ fn api<T: Into<Option<Status>>>(sync: T) -> System<Block> {
future::ready(()) future::ready(())
})) }))
}); });
System::new(SystemInfo { System::new(
impl_name: "testclient".into(), SystemInfo {
impl_version: "0.2.0".into(), impl_name: "testclient".into(),
chain_name: "testchain".into(), impl_version: "0.2.0".into(),
properties: Default::default(), chain_name: "testchain".into(),
chain_type: Default::default(), properties: Default::default(),
}, tx) chain_type: Default::default(),
},
tx,
sc_rpc_api::DenyUnsafe::No
)
} }
fn wait_receiver<T>(rx: Receiver<T>) -> T { fn wait_receiver<T>(rx: Receiver<T>) -> T {
@@ -238,14 +242,19 @@ fn system_local_listen_addresses_works() {
#[test] #[test]
fn system_peers() { fn system_peers() {
let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
let peer_id = PeerId::random(); let peer_id = PeerId::random();
let req = api(Status {
peer_id: peer_id.clone(),
peers: 1,
is_syncing: false,
is_dev: true,
}).system_peers();
let res = runtime.block_on(req).unwrap();
assert_eq!( assert_eq!(
wait_receiver(api(Status { res,
peer_id: peer_id.clone(),
peers: 1,
is_syncing: false,
is_dev: true,
}).system_peers()),
vec![PeerInfo { vec![PeerInfo {
peer_id: peer_id.to_base58(), peer_id: peer_id.to_base58(),
roles: "FULL".into(), roles: "FULL".into(),
@@ -258,7 +267,10 @@ fn system_peers() {
#[test] #[test]
fn system_network_state() { fn system_network_state() {
let res = wait_receiver(api(None).system_network_state()); let mut runtime = tokio::runtime::current_thread::Runtime::new().unwrap();
let req = api(None).system_network_state();
let res = runtime.block_on(req).unwrap();
assert_eq!( assert_eq!(
serde_json::from_value::<sc_network::network_state::NetworkState>(res).unwrap(), serde_json::from_value::<sc_network::network_state::NetworkState>(res).unwrap(),
sc_network::network_state::NetworkState { sc_network::network_state::NetworkState {
+23 -23
View File
@@ -46,6 +46,7 @@ use sp_runtime::traits::{
use sp_api::ProvideRuntimeApi; use sp_api::ProvideRuntimeApi;
use sc_executor::{NativeExecutor, NativeExecutionDispatch}; use sc_executor::{NativeExecutor, NativeExecutionDispatch};
use std::{ use std::{
collections::HashMap,
io::{Read, Write, Seek}, io::{Read, Write, Seek},
marker::PhantomData, sync::Arc, pin::Pin marker::PhantomData, sync::Arc, pin::Pin
}; };
@@ -1001,7 +1002,7 @@ ServiceBuilder<
// RPC // RPC
let (system_rpc_tx, system_rpc_rx) = tracing_unbounded("mpsc_system_rpc"); let (system_rpc_tx, system_rpc_rx) = tracing_unbounded("mpsc_system_rpc");
let gen_handler = || { let gen_handler = |deny_unsafe: sc_rpc::DenyUnsafe| {
use sc_rpc::{chain, state, author, system, offchain}; use sc_rpc::{chain, state, author, system, offchain};
let system_info = sc_rpc::system::SystemInfo { let system_info = sc_rpc::system::SystemInfo {
@@ -1043,32 +1044,31 @@ ServiceBuilder<
transaction_pool.clone(), transaction_pool.clone(),
subscriptions, subscriptions,
keystore.clone(), keystore.clone(),
deny_unsafe,
); );
let system = system::System::new(system_info, system_rpc_tx.clone()); let system = system::System::new(system_info, system_rpc_tx.clone(), deny_unsafe);
match offchain_storage.clone() { let maybe_offchain_rpc = offchain_storage.clone()
Some(storage) => { .map(|storage| {
let offchain = sc_rpc::offchain::Offchain::new(storage); let offchain = sc_rpc::offchain::Offchain::new(storage, deny_unsafe);
sc_rpc_server::rpc_handler(( // FIXME: Use plain Option (don't collect into HashMap) when we upgrade to jsonrpc 14.1
state::StateApi::to_delegate(state), // https://github.com/paritytech/jsonrpc/commit/20485387ed06a48f1a70bf4d609a7cde6cf0accf
chain::ChainApi::to_delegate(chain), let delegate = offchain::OffchainApi::to_delegate(offchain);
offchain::OffchainApi::to_delegate(offchain), delegate.into_iter().collect::<HashMap<_, _>>()
author::AuthorApi::to_delegate(author), }).unwrap_or_default();
system::SystemApi::to_delegate(system),
rpc_extensions.clone(), sc_rpc_server::rpc_handler((
)) state::StateApi::to_delegate(state),
}, chain::ChainApi::to_delegate(chain),
None => sc_rpc_server::rpc_handler(( maybe_offchain_rpc,
state::StateApi::to_delegate(state), author::AuthorApi::to_delegate(author),
chain::ChainApi::to_delegate(chain), system::SystemApi::to_delegate(system),
author::AuthorApi::to_delegate(author), rpc_extensions.clone(),
system::SystemApi::to_delegate(system), ))
rpc_extensions.clone(),
))
}
}; };
let rpc_handlers = gen_handler();
let rpc = start_rpc_servers(&config, gen_handler)?; let rpc = start_rpc_servers(&config, gen_handler)?;
// This is used internally, so don't restrict access to unsafe RPC
let rpc_handlers = gen_handler(sc_rpc::DenyUnsafe::No);
spawn_handle.spawn( spawn_handle.spawn(
"network-worker", "network-worker",
+2
View File
@@ -59,6 +59,8 @@ pub struct Configuration {
pub wasm_method: WasmExecutionMethod, pub wasm_method: WasmExecutionMethod,
/// Execution strategies. /// Execution strategies.
pub execution_strategies: ExecutionStrategies, pub execution_strategies: ExecutionStrategies,
/// Whether potentially unsafe RPC is considered safe to be exposed.
pub unsafe_rpc_expose: bool,
/// RPC over HTTP binding address. `None` if disabled. /// RPC over HTTP binding address. `None` if disabled.
pub rpc_http: Option<SocketAddr>, pub rpc_http: Option<SocketAddr>,
/// RPC over Websockets binding address. `None` if disabled. /// RPC over Websockets binding address. `None` if disabled.
+18 -5
View File
@@ -511,7 +511,7 @@ mod waiting {
/// Starts RPC servers that run in their own thread, and returns an opaque object that keeps them alive. /// Starts RPC servers that run in their own thread, and returns an opaque object that keeps them alive.
#[cfg(not(target_os = "unknown"))] #[cfg(not(target_os = "unknown"))]
fn start_rpc_servers<H: FnMut() -> sc_rpc_server::RpcHandler<sc_rpc::Metadata>>( fn start_rpc_servers<H: FnMut(sc_rpc::DenyUnsafe) -> sc_rpc_server::RpcHandler<sc_rpc::Metadata>>(
config: &Configuration, config: &Configuration,
mut gen_handler: H mut gen_handler: H
) -> Result<Box<dyn std::any::Any + Send + Sync>, error::Error> { ) -> Result<Box<dyn std::any::Any + Send + Sync>, error::Error> {
@@ -533,10 +533,23 @@ fn start_rpc_servers<H: FnMut() -> sc_rpc_server::RpcHandler<sc_rpc::Metadata>>(
}) })
} }
fn deny_unsafe(addr: &Option<SocketAddr>, unsafe_rpc_expose: bool) -> sc_rpc::DenyUnsafe {
let is_exposed_addr = addr.map(|x| x.ip().is_loopback()).unwrap_or(false);
if is_exposed_addr && !unsafe_rpc_expose {
sc_rpc::DenyUnsafe::Yes
} else {
sc_rpc::DenyUnsafe::No
}
}
Ok(Box::new(( Ok(Box::new((
maybe_start_server( maybe_start_server(
config.rpc_http, config.rpc_http,
|address| sc_rpc_server::start_http(address, config.rpc_cors.as_ref(), gen_handler()), |address| sc_rpc_server::start_http(
address,
config.rpc_cors.as_ref(),
gen_handler(deny_unsafe(&config.rpc_http, config.unsafe_rpc_expose)),
),
)?.map(|s| waiting::HttpServer(Some(s))), )?.map(|s| waiting::HttpServer(Some(s))),
maybe_start_server( maybe_start_server(
config.rpc_ws, config.rpc_ws,
@@ -544,15 +557,15 @@ fn start_rpc_servers<H: FnMut() -> sc_rpc_server::RpcHandler<sc_rpc::Metadata>>(
address, address,
config.rpc_ws_max_connections, config.rpc_ws_max_connections,
config.rpc_cors.as_ref(), config.rpc_cors.as_ref(),
gen_handler(), gen_handler(deny_unsafe(&config.rpc_ws, config.unsafe_rpc_expose)),
), ),
)?.map(|s| waiting::WsServer(Some(s))).map(Mutex::new), )?.map(|s| waiting::WsServer(Some(s))),
))) )))
} }
/// Starts RPC servers that run in their own thread, and returns an opaque object that keeps them alive. /// Starts RPC servers that run in their own thread, and returns an opaque object that keeps them alive.
#[cfg(target_os = "unknown")] #[cfg(target_os = "unknown")]
fn start_rpc_servers<H: FnMut() -> sc_rpc_server::RpcHandler<sc_rpc::Metadata>>( fn start_rpc_servers<H: FnMut(sc_rpc::DenyUnsafe) -> sc_rpc_server::RpcHandler<sc_rpc::Metadata>>(
_: &Configuration, _: &Configuration,
_: H _: H
) -> Result<Box<dyn std::any::Any + Send + Sync>, error::Error> { ) -> Result<Box<dyn std::any::Any + Send + Sync>, error::Error> {
+1
View File
@@ -184,6 +184,7 @@ fn node_config<G: RuntimeGenesis + 'static, E: ChainSpecExtension + Clone + 'sta
chain_spec: Box::new((*spec).clone()), chain_spec: Box::new((*spec).clone()),
wasm_method: sc_service::config::WasmExecutionMethod::Interpreted, wasm_method: sc_service::config::WasmExecutionMethod::Interpreted,
execution_strategies: Default::default(), execution_strategies: Default::default(),
unsafe_rpc_expose: false,
rpc_http: None, rpc_http: None,
rpc_ws: None, rpc_ws: None,
rpc_ws_max_connections: None, rpc_ws_max_connections: None,
+1
View File
@@ -86,6 +86,7 @@ where
rpc_cors: Default::default(), rpc_cors: Default::default(),
rpc_http: Default::default(), rpc_http: Default::default(),
rpc_ws: Default::default(), rpc_ws: Default::default(),
unsafe_rpc_expose: false,
rpc_ws_max_connections: Default::default(), rpc_ws_max_connections: Default::default(),
state_cache_child_ratio: Default::default(), state_cache_child_ratio: Default::default(),
state_cache_size: Default::default(), state_cache_size: Default::default(),