// This file is part of Substrate. // Copyright (C) 2018-2022 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 . use crate::{ arg_enums::RpcMethods, error::{Error, Result}, params::{ ImportParams, KeystoreParams, NetworkParams, OffchainWorkerParams, SharedParams, TransactionPoolParams, }, CliConfiguration, }; use clap::Parser; use regex::Regex; use sc_service::{ config::{BasePath, PrometheusConfig, TransactionPoolOptions}, ChainSpec, Role, }; use sc_telemetry::TelemetryEndpoints; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; /// The `run` command used to run a node. #[derive(Debug, Clone, Parser)] pub struct RunCmd { /// Enable validator mode. /// /// The node will be started with the authority role and actively /// participate in any consensus task that it can (e.g. depending on /// availability of local keys). #[arg(long)] pub validator: bool, /// Disable GRANDPA voter when running in validator mode, otherwise disable the GRANDPA /// observer. #[arg(long)] pub no_grandpa: bool, /// Listen to all RPC interfaces. /// /// Default is local. Note: not all RPC methods are safe to be exposed publicly. Use an RPC /// proxy server to filter out dangerous methods. More details: /// . /// Use `--unsafe-rpc-external` to suppress the warning if you understand the risks. #[arg(long)] pub rpc_external: bool, /// Listen to all RPC interfaces. /// /// Same as `--rpc-external`. #[arg(long)] pub unsafe_rpc_external: bool, /// RPC methods to expose. /// /// - `unsafe`: Exposes every RPC method. /// - `safe`: Exposes only a safe subset of RPC methods, denying unsafe RPC methods. /// - `auto`: Acts as `safe` if RPC is served externally, e.g. when `--{rpc,ws}-external` is /// passed, otherwise acts as `unsafe`. #[arg( long, value_name = "METHOD SET", value_enum, ignore_case = true, default_value_t = RpcMethods::Auto, verbatim_doc_comment )] pub rpc_methods: RpcMethods, /// Listen to all Websocket interfaces. /// /// Default is local. Note: not all RPC methods are safe to be exposed publicly. Use an RPC /// proxy server to filter out dangerous methods. More details: /// . /// Use `--unsafe-ws-external` to suppress the warning if you understand the risks. #[arg(long)] pub ws_external: bool, /// Listen to all Websocket interfaces. /// /// Same as `--ws-external` but doesn't warn you about it. #[arg(long)] pub unsafe_ws_external: bool, /// DEPRECATED, this has no affect anymore. Use `rpc_max_request_size` or /// `rpc_max_response_size` instead. #[arg(long)] pub rpc_max_payload: Option, /// Set the the maximum RPC request payload size for both HTTP and WS in megabytes. /// Default is 15MiB. #[arg(long)] pub rpc_max_request_size: Option, /// Set the the maximum RPC response payload size for both HTTP and WS in megabytes. /// Default is 15MiB. #[arg(long)] pub rpc_max_response_size: Option, /// Set the the maximum concurrent subscriptions per connection. /// Default is 1024. #[arg(long)] pub rpc_max_subscriptions_per_connection: Option, /// Expose Prometheus exporter on all interfaces. /// /// Default is local. #[arg(long)] pub prometheus_external: bool, /// DEPRECATED, IPC support has been removed. #[arg(long, value_name = "PATH")] pub ipc_path: Option, /// Specify HTTP RPC server TCP port. #[arg(long, value_name = "PORT")] pub rpc_port: Option, /// Specify WebSockets RPC server TCP port. #[arg(long, value_name = "PORT")] pub ws_port: Option, /// Maximum number of WS RPC server connections. #[arg(long, value_name = "COUNT")] pub ws_max_connections: Option, /// DEPRECATED, this has no affect anymore. Use `rpc_max_response_size` instead. #[arg(long)] pub ws_max_out_buffer_capacity: Option, /// Specify browser Origins allowed to access the HTTP & WS RPC servers. /// /// A comma-separated list of origins (protocol://domain or special `null` /// value). Value of `all` will disable origin validation. Default is to /// allow localhost and origins. When running in /// --dev mode the default is to allow all origins. #[arg(long, value_name = "ORIGINS", value_parser = parse_cors)] pub rpc_cors: Option, /// Specify Prometheus exporter TCP Port. #[arg(long, value_name = "PORT")] pub prometheus_port: Option, /// Do not expose a Prometheus exporter endpoint. /// /// Prometheus metric endpoint is enabled by default. #[arg(long)] pub no_prometheus: bool, /// The human-readable name for this node. /// /// The node name will be reported to the telemetry server, if enabled. #[arg(long, value_name = "NAME")] pub name: Option, /// Disable connecting to the Substrate telemetry server. /// /// Telemetry is on by default on global chains. #[arg(long)] pub no_telemetry: bool, /// The URL of the telemetry server to connect to. /// /// This flag can be passed multiple times as a means to specify multiple /// telemetry endpoints. Verbosity levels range from 0-9, with 0 denoting /// the least verbosity. /// Expected format is 'URL VERBOSITY', e.g. `--telemetry-url 'wss://foo/bar 0'`. #[arg(long = "telemetry-url", value_name = "URL VERBOSITY", value_parser = parse_telemetry_endpoints)] pub telemetry_endpoints: Vec<(String, u8)>, #[allow(missing_docs)] #[clap(flatten)] pub offchain_worker_params: OffchainWorkerParams, #[allow(missing_docs)] #[clap(flatten)] pub shared_params: SharedParams, #[allow(missing_docs)] #[clap(flatten)] pub import_params: ImportParams, #[allow(missing_docs)] #[clap(flatten)] pub network_params: NetworkParams, #[allow(missing_docs)] #[clap(flatten)] pub pool_config: TransactionPoolParams, /// Shortcut for `--name Alice --validator` with session keys for `Alice` added to keystore. #[arg(long, conflicts_with_all = &["bob", "charlie", "dave", "eve", "ferdie", "one", "two"])] pub alice: bool, /// Shortcut for `--name Bob --validator` with session keys for `Bob` added to keystore. #[arg(long, conflicts_with_all = &["alice", "charlie", "dave", "eve", "ferdie", "one", "two"])] pub bob: bool, /// Shortcut for `--name Charlie --validator` with session keys for `Charlie` added to /// keystore. #[arg(long, conflicts_with_all = &["alice", "bob", "dave", "eve", "ferdie", "one", "two"])] pub charlie: bool, /// Shortcut for `--name Dave --validator` with session keys for `Dave` added to keystore. #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "eve", "ferdie", "one", "two"])] pub dave: bool, /// Shortcut for `--name Eve --validator` with session keys for `Eve` added to keystore. #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "ferdie", "one", "two"])] pub eve: bool, /// Shortcut for `--name Ferdie --validator` with session keys for `Ferdie` added to keystore. #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "one", "two"])] pub ferdie: bool, /// Shortcut for `--name One --validator` with session keys for `One` added to keystore. #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "ferdie", "two"])] pub one: bool, /// Shortcut for `--name Two --validator` with session keys for `Two` added to keystore. #[arg(long, conflicts_with_all = &["alice", "bob", "charlie", "dave", "eve", "ferdie", "one"])] pub two: bool, /// Enable authoring even when offline. #[arg(long)] pub force_authoring: bool, #[allow(missing_docs)] #[clap(flatten)] pub keystore_params: KeystoreParams, /// The size of the instances cache for each runtime. /// /// The default value is 8 and the values higher than 256 are ignored. #[arg(long)] pub max_runtime_instances: Option, /// Maximum number of different runtimes that can be cached. #[arg(long, default_value_t = 2)] pub runtime_cache_size: u8, /// Run a temporary node. /// /// A temporary directory will be created to store the configuration and will be deleted /// at the end of the process. /// /// Note: the directory is random per process execution. This directory is used as base path /// which includes: database, node key and keystore. /// /// When `--dev` is given and no explicit `--base-path`, this option is implied. #[arg(long, conflicts_with = "base_path")] pub tmp: bool, } impl RunCmd { /// Get the `Sr25519Keyring` matching one of the flag. pub fn get_keyring(&self) -> Option { use sp_keyring::Sr25519Keyring::*; if self.alice { Some(Alice) } else if self.bob { Some(Bob) } else if self.charlie { Some(Charlie) } else if self.dave { Some(Dave) } else if self.eve { Some(Eve) } else if self.ferdie { Some(Ferdie) } else if self.one { Some(One) } else if self.two { Some(Two) } else { None } } } impl CliConfiguration for RunCmd { fn shared_params(&self) -> &SharedParams { &self.shared_params } fn import_params(&self) -> Option<&ImportParams> { Some(&self.import_params) } fn network_params(&self) -> Option<&NetworkParams> { Some(&self.network_params) } fn keystore_params(&self) -> Option<&KeystoreParams> { Some(&self.keystore_params) } fn offchain_worker_params(&self) -> Option<&OffchainWorkerParams> { Some(&self.offchain_worker_params) } fn node_name(&self) -> Result { let name: String = match (self.name.as_ref(), self.get_keyring()) { (Some(name), _) => name.to_string(), (_, Some(keyring)) => keyring.to_string(), (None, None) => crate::generate_node_name(), }; is_node_name_valid(&name).map_err(|msg| { Error::Input(format!( "Invalid node name '{}'. Reason: {}. If unsure, use none.", name, msg )) })?; Ok(name) } fn dev_key_seed(&self, is_dev: bool) -> Result> { Ok(self.get_keyring().map(|a| format!("//{}", a)).or_else(|| { if is_dev { Some("//Alice".into()) } else { None } })) } fn telemetry_endpoints( &self, chain_spec: &Box, ) -> Result> { Ok(if self.no_telemetry { None } else if !self.telemetry_endpoints.is_empty() { Some( TelemetryEndpoints::new(self.telemetry_endpoints.clone()) .map_err(|e| e.to_string())?, ) } else { chain_spec.telemetry_endpoints().clone() }) } fn role(&self, is_dev: bool) -> Result { let keyring = self.get_keyring(); let is_authority = self.validator || is_dev || keyring.is_some(); Ok(if is_authority { sc_service::Role::Authority } else { sc_service::Role::Full }) } fn force_authoring(&self) -> Result { // Imply forced authoring on --dev Ok(self.shared_params.dev || self.force_authoring) } fn prometheus_config( &self, default_listen_port: u16, chain_spec: &Box, ) -> Result> { Ok(if self.no_prometheus { None } else { let interface = if self.prometheus_external { Ipv4Addr::UNSPECIFIED } else { Ipv4Addr::LOCALHOST }; Some(PrometheusConfig::new_with_default_registry( SocketAddr::new( interface.into(), self.prometheus_port.unwrap_or(default_listen_port), ), chain_spec.id().into(), )) }) } fn disable_grandpa(&self) -> Result { Ok(self.no_grandpa) } fn rpc_ws_max_connections(&self) -> Result> { Ok(self.ws_max_connections) } fn rpc_cors(&self, is_dev: bool) -> Result>> { Ok(self .rpc_cors .clone() .unwrap_or_else(|| { if is_dev { log::warn!("Running in --dev mode, RPC CORS has been disabled."); Cors::All } else { Cors::List(vec![ "http://localhost:*".into(), "http://127.0.0.1:*".into(), "https://localhost:*".into(), "https://127.0.0.1:*".into(), "https://polkadot.js.org".into(), ]) } }) .into()) } fn rpc_http(&self, default_listen_port: u16) -> Result> { let interface = rpc_interface( self.rpc_external, self.unsafe_rpc_external, self.rpc_methods, self.validator, )?; Ok(Some(SocketAddr::new(interface, self.rpc_port.unwrap_or(default_listen_port)))) } fn rpc_ipc(&self) -> Result> { Ok(self.ipc_path.clone()) } fn rpc_ws(&self, default_listen_port: u16) -> Result> { let interface = rpc_interface( self.ws_external, self.unsafe_ws_external, self.rpc_methods, self.validator, )?; Ok(Some(SocketAddr::new(interface, self.ws_port.unwrap_or(default_listen_port)))) } fn rpc_methods(&self) -> Result { Ok(self.rpc_methods.into()) } fn rpc_max_payload(&self) -> Result> { Ok(self.rpc_max_payload) } fn rpc_max_request_size(&self) -> Result> { Ok(self.rpc_max_request_size) } fn rpc_max_response_size(&self) -> Result> { Ok(self.rpc_max_response_size) } fn rpc_max_subscriptions_per_connection(&self) -> Result> { Ok(self.rpc_max_subscriptions_per_connection) } fn ws_max_out_buffer_capacity(&self) -> Result> { Ok(self.ws_max_out_buffer_capacity) } fn transaction_pool(&self, is_dev: bool) -> Result { Ok(self.pool_config.transaction_pool(is_dev)) } fn max_runtime_instances(&self) -> Result> { Ok(self.max_runtime_instances.map(|x| x.min(256))) } fn runtime_cache_size(&self) -> Result { Ok(self.runtime_cache_size) } fn base_path(&self) -> Result> { Ok(if self.tmp { Some(BasePath::new_temp_dir()?) } else { match self.shared_params().base_path()? { Some(r) => Some(r), // If `dev` is enabled, we use the temp base path. None if self.shared_params().is_dev() => Some(BasePath::new_temp_dir()?), None => None, } }) } } /// Check whether a node name is considered as valid. pub fn is_node_name_valid(_name: &str) -> std::result::Result<(), &str> { let name = _name.to_string(); if name.chars().count() >= crate::NODE_NAME_MAX_LENGTH { return Err("Node name too long") } let invalid_chars = r"[\\.@]"; let re = Regex::new(invalid_chars).unwrap(); if re.is_match(&name) { return Err("Node name should not contain invalid chars such as '.' and '@'") } let invalid_patterns = r"(https?:\\/+)?(www)+"; let re = Regex::new(invalid_patterns).unwrap(); if re.is_match(&name) { return Err("Node name should not contain urls") } Ok(()) } fn rpc_interface( is_external: bool, is_unsafe_external: bool, rpc_methods: RpcMethods, is_validator: bool, ) -> Result { if is_external && is_validator && rpc_methods != RpcMethods::Unsafe { return Err(Error::Input( "--rpc-external and --ws-external options shouldn't be used if the node is running as \ a validator. Use `--unsafe-rpc-external` or `--rpc-methods=unsafe` if you understand \ the risks. See the options description for more information." .to_owned(), )) } if is_external || is_unsafe_external { if rpc_methods == RpcMethods::Unsafe { log::warn!( "It isn't safe to expose RPC publicly without a proxy server that filters \ available set of RPC methods." ); } Ok(Ipv4Addr::UNSPECIFIED.into()) } else { Ok(Ipv4Addr::LOCALHOST.into()) } } #[derive(Debug)] enum TelemetryParsingError { MissingVerbosity, VerbosityParsingError(std::num::ParseIntError), } impl std::error::Error for TelemetryParsingError {} impl std::fmt::Display for TelemetryParsingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TelemetryParsingError::MissingVerbosity => write!(f, "Verbosity level missing"), TelemetryParsingError::VerbosityParsingError(e) => write!(f, "{}", e), } } } fn parse_telemetry_endpoints(s: &str) -> std::result::Result<(String, u8), TelemetryParsingError> { let pos = s.find(' '); match pos { None => Err(TelemetryParsingError::MissingVerbosity), Some(pos_) => { let url = s[..pos_].to_string(); let verbosity = s[pos_ + 1..].parse().map_err(TelemetryParsingError::VerbosityParsingError)?; Ok((url, verbosity)) }, } } /// CORS setting /// /// The type is introduced to overcome `Option>` handling of `clap`. #[derive(Clone, Debug)] pub enum Cors { /// All hosts allowed. All, /// Only hosts on the list are allowed. List(Vec), } impl From for Option> { fn from(cors: Cors) -> Self { match cors { Cors::All => None, Cors::List(list) => Some(list), } } } /// Parse cors origins. fn parse_cors(s: &str) -> Result { let mut is_all = false; let mut origins = Vec::new(); for part in s.split(',') { match part { "all" | "*" => { is_all = true; break }, other => origins.push(other.to_owned()), } } if is_all { Ok(Cors::All) } else { Ok(Cors::List(origins)) } } #[cfg(test)] mod tests { use super::*; #[test] fn tests_node_name_good() { assert!(is_node_name_valid("short name").is_ok()); } #[test] fn tests_node_name_bad() { assert!(is_node_name_valid( "very very long names are really not very cool for the ui at all, really they're not" ) .is_err()); assert!(is_node_name_valid("Dots.not.Ok").is_err()); assert!(is_node_name_valid("http://visit.me").is_err()); assert!(is_node_name_valid("https://visit.me").is_err()); assert!(is_node_name_valid("www.visit.me").is_err()); assert!(is_node_name_valid("email@domain").is_err()); } }