diff --git a/Cargo.lock b/Cargo.lock index 7d370bb1d90..316d978a531 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4659,6 +4659,7 @@ dependencies = [ "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "tokio", "tower 0.4.13", + "tower-http", ] [[package]] @@ -7090,16 +7091,6 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "byteorder", - "num-traits", -] - [[package]] name = "headers" version = "0.3.9" @@ -18106,14 +18097,9 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "hdrhistogram", - "indexmap 1.9.3", "pin-project", "pin-project-lite", - "rand", - "slab", "tokio", - "tokio-util", "tower-layer", "tower-service", "tracing", diff --git a/Cargo.toml b/Cargo.toml index c3f9bfbce23..71a23839950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -525,11 +525,14 @@ page_size = { version = "0.6", default-features = false } # pallets/gear pathdiff = { version = "0.2.1", default-features = false } # utils/wasm-builder rand_pcg = "0.3.1" # pallets/gear rustc_version = "0.4.0" # utils/wasm-builder -schnorrkel = "0.11.4" # gcli +schnorrkel = "0.11.4" # gcli scopeguard = { version = "1.2.0", default-features = false } # pallets/gear +hyper = "1.4.1" # ethexe/rpc tabled = "0.10.0" # utils/regression-analysis thousands = "0.2.0" # utils/regression-analysis toml = "0.8.14" # utils/wasm-builder +tower = "0.4.13" # ethexe/rpc +tower-http = "0.5.2" # ethexe/rpc tracing = "0.1.40" # utils/node-loader tracing-appender = "0.2" # utils/node-loader tracing-subscriber = "0.3.18" # utils/node-loader diff --git a/ethexe/cli/src/args.rs b/ethexe/cli/src/args.rs index 8ec865259e9..0dc0e8be807 100644 --- a/ethexe/cli/src/args.rs +++ b/ethexe/cli/src/args.rs @@ -117,7 +117,7 @@ pub struct Args { #[allow(missing_docs)] #[clap(flatten)] - pub rpc_params: RpcParams, + pub rpc_params: Option, #[command(subcommand)] pub extra_command: Option, diff --git a/ethexe/cli/src/config.rs b/ethexe/cli/src/config.rs index daf0a4ad456..031723fc0fd 100644 --- a/ethexe/cli/src/config.rs +++ b/ethexe/cli/src/config.rs @@ -215,7 +215,7 @@ impl TryFrom for Config { prometheus_config: args.prometheus_params.and_then(|params| { params.prometheus_config(DEFAULT_PROMETHEUS_PORT, "ethexe-dev".to_string()) }), - rpc_config: args.rpc_params.as_config(), + rpc_config: args.rpc_params.and_then(|v| v.as_config()), }) } } diff --git a/ethexe/cli/src/params/rpc.rs b/ethexe/cli/src/params/rpc.rs index 72cf961c1a6..675ecd0e17b 100644 --- a/ethexe/cli/src/params/rpc.rs +++ b/ethexe/cli/src/params/rpc.rs @@ -19,7 +19,10 @@ use clap::Args; use ethexe_rpc::RpcConfig; use serde::Deserialize; -use std::net::{Ipv4Addr, SocketAddr}; +use std::{ + net::{Ipv4Addr, SocketAddr}, + str::FromStr, +}; /// Parameters used to config prometheus. #[derive(Debug, Clone, Args, Deserialize)] @@ -35,6 +38,14 @@ pub struct RpcParams { /// Do not start rpc endpoint. #[arg(long, default_value = "false")] pub no_rpc: bool, + + /// 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 origin. + #[arg(long)] + pub rpc_cors: Option, } impl RpcParams { @@ -53,6 +64,64 @@ impl RpcParams { let listen_addr = SocketAddr::new(ip, self.rpc_port); - Some(RpcConfig { listen_addr }) + let cors = self + .rpc_cors + .clone() + .unwrap_or_else(|| { + Cors::List(vec![ + "http://localhost:*".into(), + "http://127.0.0.1:*".into(), + "https://localhost:*".into(), + "https://127.0.0.1:*".into(), + ]) + }) + .into(); + + Some(RpcConfig { listen_addr, cors }) + } +} + +/// CORS setting +/// +/// The type is introduced to overcome `Option>` handling of `clap`. +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +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), + } + } +} + +impl FromStr for Cors { + type Err = anyhow::Error; + + fn from_str(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)) + } } } diff --git a/ethexe/cli/src/service.rs b/ethexe/cli/src/service.rs index 0d5846bd410..7c4231e0253 100644 --- a/ethexe/cli/src/service.rs +++ b/ethexe/cli/src/service.rs @@ -951,6 +951,7 @@ mod tests { )), rpc_config: Some(RpcConfig { listen_addr: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 9944), + cors: None, }), }) .await diff --git a/ethexe/rpc/Cargo.toml b/ethexe/rpc/Cargo.toml index 8cf8f0bf0c2..1f7c7995782 100644 --- a/ethexe/rpc/Cargo.toml +++ b/ethexe/rpc/Cargo.toml @@ -16,9 +16,10 @@ futures.workspace = true gprimitives = { workspace = true, features = ["serde"] } ethexe-db.workspace = true ethexe-processor.workspace = true -jsonrpsee = { version = "0.24", features = ["server", "macros"] } -tower = { version = "0.4.13", features = ["full"] } -hyper = { version = "1.4.1", features = ["server"] } +tower = { workspace = true, features = ["util"] } +tower-http = { workspace = true, features = ["cors"] } +jsonrpsee = { workspace = true, features = ["server", "macros"] } +hyper = { workspace = true, features = ["server"] } log.workspace = true parity-scale-codec.workspace = true hex.workspace = true diff --git a/ethexe/rpc/src/lib.rs b/ethexe/rpc/src/lib.rs index c26aeb12b29..5026d95b0b3 100644 --- a/ethexe/rpc/src/lib.rs +++ b/ethexe/rpc/src/lib.rs @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use anyhow::anyhow; +use anyhow::{anyhow, Result}; use apis::{BlockApi, BlockServer, ProgramApi, ProgramServer}; use ethexe_db::Database; use futures::FutureExt; @@ -35,6 +35,8 @@ mod apis; mod common; mod errors; +pub(crate) mod util; + #[derive(Clone)] struct PerConnection { methods: Methods, @@ -47,6 +49,8 @@ struct PerConnection { pub struct RpcConfig { /// Listen address. pub listen_addr: SocketAddr, + /// CORS. + pub cors: Option>, } pub struct RpcService { @@ -63,10 +67,17 @@ impl RpcService { self.config.listen_addr.port() } - pub async fn run_server(self) -> anyhow::Result { + pub async fn run_server(self) -> Result { let listener = TcpListener::bind(self.config.listen_addr).await?; - let service_builder = Server::builder().to_service_builder(); + let cors = util::try_into_cors(self.config.cors)?; + + let http_middleware = tower::ServiceBuilder::new().layer(cors); + + let service_builder = Server::builder() + .set_http_middleware(http_middleware) + .to_service_builder(); + let mut module = JsonrpcModule::new(()); module.merge(ProgramServer::into_rpc(ProgramApi::new(self.db.clone())))?; module.merge(BlockServer::into_rpc(BlockApi::new(self.db.clone())))?; diff --git a/ethexe/rpc/src/util.rs b/ethexe/rpc/src/util.rs new file mode 100644 index 00000000000..8236795c701 --- /dev/null +++ b/ethexe/rpc/src/util.rs @@ -0,0 +1,36 @@ +// This file is part of Gear. +// +// Copyright (C) 2024 Gear Technologies Inc. +// 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 anyhow::Result; +use hyper::header::HeaderValue; +use tower_http::cors::{AllowOrigin, CorsLayer}; + +pub(crate) fn try_into_cors(maybe_cors: Option>) -> Result { + 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()) + } +}