From 0a6e10f8c635827d113d6baa6edc011ff95c7195 Mon Sep 17 00:00:00 2001 From: Dzejkop Date: Fri, 18 Jul 2025 10:00:11 +0200 Subject: [PATCH] allow configuring CORS --- Cargo.lock | 34 +++++++++++++++++++++++++ Cargo.toml | 10 ++++++-- src/any_or_value.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++ src/cli.rs | 40 +++++++++++++++++++++++++++++- src/lib.rs | 1 + 5 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/any_or_value.rs diff --git a/Cargo.lock b/Cargo.lock index 234ffcb..e2688ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4126,6 +4126,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "test-case-core", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4574,6 +4607,7 @@ dependencies = [ "rollup-boost", "rustls", "serde_json", + "test-case", "tokio", "tower 0.4.13", "tower-http 0.6.2", diff --git a/Cargo.toml b/Cargo.toml index ad0c3fd..945525d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,13 +16,18 @@ http-body-util = "0.1.3" hyper = { version = "1.6.0", features = ["full"] } hyper-rustls = "0.27.5" hyper-util = { version = "0.1.11", features = ["full"] } -jsonrpsee = { version = "0.24", features = ["server", "http-client", "macros", "client"] } +jsonrpsee = { version = "0.24", features = [ + "server", + "http-client", + "macros", + "client", +] } paste = "1.0.15" rustls = { version = "0.23.25", features = ["ring"] } serde_json = "1.0.140" tokio = { version = "1.44.1", features = ["full"] } tower = { version = "0.4.13", features = ["timeout"] } -tower-http = { version = "0.6.2", features = ["decompression-full"] } +tower-http = { version = "0.6.2", features = ["cors", "decompression-full"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } metrics-exporter-prometheus = "0.16.2" @@ -45,6 +50,7 @@ metrics-derive = "0.1.0" metrics = "0.24.2" [dev-dependencies] +test-case = "3" ctor = "0.3.5" alloy-primitives = "0.8.25" reqwest = "0.12.15" diff --git a/src/any_or_value.rs b/src/any_or_value.rs new file mode 100644 index 0000000..518e899 --- /dev/null +++ b/src/any_or_value.rs @@ -0,0 +1,60 @@ +use std::str::FromStr; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnyOr { + Any, + Specific(T), +} + +impl AnyOr +where + T: Clone, +{ + pub fn coalesce(items: &[AnyOr]) -> AnyOr> { + let mut ret = vec![]; + + for item in items { + match item { + AnyOr::Any => return AnyOr::Any, + AnyOr::Specific(v) => ret.push(v.clone()), + } + } + + AnyOr::Specific(ret) + } +} + +impl FromStr for AnyOr +where + T: FromStr, +{ + type Err = ::Err; + + fn from_str(s: &str) -> Result { + match s { + "*" => Ok(Self::Any), + s => s.parse().map(AnyOr::Specific), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + #[test_case("*" => AnyOr::Any)] + #[test_case("123" => AnyOr::Specific(123))] + fn parsing(s: &str) -> AnyOr { + s.parse().unwrap() + } + + #[test_case(&["*", "1", "2"] => AnyOr::Any)] + #[test_case(&["47", "2"] => AnyOr::Specific(vec![47, 2]))] + #[test_case(&["1", "*"] => AnyOr::Any)] + fn coalesce(items: &[&str]) -> AnyOr> { + let items: Vec> = items.iter().map(|s| s.parse().unwrap()).collect(); + + AnyOr::coalesce(items) + } +} diff --git a/src/cli.rs b/src/cli.rs index b6e5596..c0a08f9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,4 @@ +use crate::any_or_value::AnyOr; use crate::auth::{AuthLayer, JwtAuthValidator}; use crate::metrics::ProxyMetrics; use crate::proxy::ProxyLayer; @@ -6,7 +7,7 @@ use alloy_rpc_types_engine::JwtSecret; use clap::Parser; use eyre::Context as _; use eyre::{Result, eyre}; -use http::{Request, Response, StatusCode}; +use http::{HeaderName, HeaderValue, Method, Request, Response, StatusCode}; use http_body_util::Full; use hyper::Uri; use hyper::body::Bytes; @@ -29,6 +30,7 @@ use std::path::PathBuf; use std::sync::Arc; use tokio::net::TcpListener; use tokio::signal::unix::{SignalKind, signal}; +use tower_http::cors::{Any, CorsLayer}; use tracing::level_filters::LevelFilter; use tracing::{Level, Metadata}; use tracing::{error, info}; @@ -74,6 +76,24 @@ pub struct Cli { #[clap(long, env, default_value_t = DEFAULT_HTTP_PORT)] pub http_port: u16, + /// The allowed CORS origins. + /// + /// Can be specific values or a '*' wildcard + #[clap(long = "cors.allow-origin")] + pub cors_allow_origin: Vec>, + + /// The allowed CORS methods. + /// + /// Can be specific values or a '*' wildcard + #[clap(long = "cors.allow-methods")] + pub cors_allow_methods: Vec>, + + /// The allowed CORS headers + /// + /// Can be specific values or a '*' wildcard + #[clap(long = "cors.allow-headers")] + pub cors_allow_headers: Vec>, + /// Enable Prometheus metrics #[arg(long, env, default_value = "false")] pub metrics: bool, @@ -347,6 +367,22 @@ impl Cli { metrics: Arc, ) -> Result { let module = RpcModule::new(()); + + let mut cors = CorsLayer::new(); + + match AnyOr::coalesce(&self.cors_allow_origin) { + AnyOr::Any => cors = cors.allow_origin(Any), + AnyOr::Specific(origins) => cors = cors.allow_origin(origins), + } + match AnyOr::coalesce(&self.cors_allow_methods) { + AnyOr::Any => cors = cors.allow_methods(Any), + AnyOr::Specific(methods) => cors = cors.allow_methods(methods), + } + match AnyOr::coalesce(&self.cors_allow_headers) { + AnyOr::Any => cors = cors.allow_headers(Any), + AnyOr::Specific(headers) => cors = cors.allow_headers(headers), + } + if let Some(secret) = jwt_secret { let middleware = tower::ServiceBuilder::new() .layer(AuthLayer::new(JwtAuthValidator::new(secret))) @@ -355,6 +391,7 @@ impl Cli { self.builder_targets.build()?, metrics.clone(), )) + .layer(cors) .layer(ProxyLayer::new(self.l2_targets.build()?, metrics.clone())); let server = Server::builder() @@ -373,6 +410,7 @@ impl Cli { self.builder_targets.build()?, metrics.clone(), )) + .layer(cors) .layer(ProxyLayer::new(self.l2_targets.build()?, metrics.clone())); let server = Server::builder() diff --git a/src/lib.rs b/src/lib.rs index 9bb7522..81c3222 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] use dotenvy as _; +pub mod any_or_value; pub mod auth; pub mod cli; pub mod client;