From d7f614784a2d92327119af474f7c2fd713a6f3e1 Mon Sep 17 00:00:00 2001 From: Daniyar Itegulov Date: Wed, 18 Dec 2024 15:25:15 +1100 Subject: [PATCH] full revamp of CORS --- crates/cli/src/cli.rs | 4 +- crates/cli/src/main.rs | 32 +++--- crates/config/src/config.rs | 6 +- e2e-tests-rust/Cargo.lock | 21 ++++ e2e-tests-rust/Cargo.toml | 7 +- e2e-tests-rust/src/http_middleware.rs | 71 ++++++++++++ e2e-tests-rust/src/lib.rs | 6 +- e2e-tests-rust/src/provider/mod.rs | 4 +- e2e-tests-rust/src/provider/testing.rs | 146 +++++++++++++------------ e2e-tests-rust/tests/lib.rs | 115 +++++++++++-------- 10 files changed, 267 insertions(+), 145 deletions(-) create mode 100644 e2e-tests-rust/src/http_middleware.rs diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 84b6fc31..d5b71489 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -249,8 +249,8 @@ pub struct Cli { pub allow_origin: String, /// Disable CORS. - #[arg(long, default_missing_value = "true", num_args(0..=1), help_heading = "Server options")] - pub no_cors: Option, + #[arg(long, conflicts_with = "allow_origin", help_heading = "Server options")] + pub no_cors: bool, } #[derive(Debug, Subcommand, Clone)] diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 71005115..c97b9fd1 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -32,11 +32,11 @@ use futures::{ FutureExt, }; use http::Method; -use jsonrpsee::server::middleware::http::{HostFilterLayer, ProxyGetRequestLayer}; +use jsonrpsee::server::middleware::http::ProxyGetRequestLayer; use jsonrpsee::server::{RpcServiceBuilder, ServerBuilder}; use std::fs::File; use std::{env, net::SocketAddr, str::FromStr}; -use tower_http::cors::{AllowOrigin, Any, CorsLayer}; +use tower_http::cors::{AllowOrigin, CorsLayer}; use tracing_subscriber::filter::LevelFilter; use zksync_types::fee_model::{FeeModelConfigV2, FeeParams}; use zksync_types::H160; @@ -53,7 +53,7 @@ async fn build_json_http( node: InMemoryNode, enable_health_api: bool, cors_allow_origin: String, - disable_cors: bool, + enable_cors: bool, ) -> tokio::task::JoinHandle<()> { let (sender, recv) = oneshot::channel::<()>(); @@ -76,25 +76,19 @@ async fn build_json_http( .unwrap(); rpc.merge(Web3Namespace.into_rpc()).unwrap(); - let cors_layers = tower::util::option_layer(if !disable_cors { - // `CorsLayer` adds CORS-specific headers to responses but does not do filtering by itself + let cors_layers = tower::util::option_layer(enable_cors.then(|| { + // `CorsLayer` adds CORS-specific headers to responses but does not do filtering by itself. + // CORS relies on browsers respecting server's access list response headers. + // See [`tower_http::cors`](https://docs.rs/tower-http/latest/tower_http/cors/index.html) + // for more details. let cors_layer = CorsLayer::new() .allow_origin(AllowOrigin::exact( cors_allow_origin.parse().expect("malformed allow origin"), )) .allow_headers([http::header::CONTENT_TYPE]) - .allow_methods([Method::GET, Method::POST]) - .expose_headers(Any); - // `HostFilterLayer` filters requests based on "Host" header - let host_filter_layer = if cors_allow_origin == "*" { - HostFilterLayer::disable() - } else { - HostFilterLayer::new([cors_allow_origin]).expect("malformed allow origin") - }; - Some((cors_layer, host_filter_layer)) - } else { - None - }); + .allow_methods([Method::GET, Method::POST]); + cors_layer + })); let health_api_layer = tower::util::option_layer(if enable_health_api { Some(ProxyGetRequestLayer::new("/health", "web3_clientVersion").unwrap()) } else { @@ -107,7 +101,7 @@ async fn build_json_http( .layer(cors_layers) .layer(health_api_layer), ) - .set_rpc_middleware(RpcServiceBuilder::new().rpc_logger(100)); + .set_rpc_middleware(RpcServiceBuilder::new().rpc_logger(1024)); let server = server_builder.build(addr).await.unwrap(); @@ -352,7 +346,7 @@ async fn main() -> anyhow::Result<()> { node.clone(), config.health_check_endpoint, config.allow_origin.clone(), - config.no_cors, + !config.no_cors, ) })) .await; diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs index 4f582e7e..0df4bc37 100644 --- a/crates/config/src/config.rs +++ b/crates/config/src/config.rs @@ -888,10 +888,8 @@ impl TestNodeConfig { // Enable or disable CORS #[must_use] - pub fn with_no_cors(mut self, no_cors: Option) -> Self { - if let Some(no_cors) = no_cors { - self.no_cors = no_cors; - } + pub fn with_no_cors(mut self, no_cors: bool) -> Self { + self.no_cors = no_cors; self } } diff --git a/e2e-tests-rust/Cargo.lock b/e2e-tests-rust/Cargo.lock index eb2e6284..2c83ef84 100644 --- a/e2e-tests-rust/Cargo.lock +++ b/e2e-tests-rust/Cargo.lock @@ -47,6 +47,7 @@ dependencies = [ "alloy-core", "alloy-eips", "alloy-genesis", + "alloy-json-rpc", "alloy-network", "alloy-provider", "alloy-pubsub", @@ -682,8 +683,13 @@ dependencies = [ "async-trait", "fs2", "futures", + "http", "itertools 0.13.0", + "reqwest", + "reqwest-middleware", + "serde_json", "tokio", + "tower", ] [[package]] @@ -2563,6 +2569,21 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "reqwest-middleware" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ccd3b55e711f91a9885a2fa6fbbb2e39db1776420b062efc058c6410f7e5e3" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "thiserror", + "tower-service", +] + [[package]] name = "rfc6979" version = "0.4.0" diff --git a/e2e-tests-rust/Cargo.toml b/e2e-tests-rust/Cargo.toml index 03503dda..32620109 100644 --- a/e2e-tests-rust/Cargo.toml +++ b/e2e-tests-rust/Cargo.toml @@ -11,13 +11,18 @@ publish = false [dependencies] alloy-zksync = { git = "https://github.com/itegulov/alloy-zksync.git", rev = "692c5c2ca5defc88ac542f420d97c6756dadf9df" } -alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types", "getrandom", "provider-anvil-api"] } +alloy = { version = "0.6", features = ["full", "rlp", "serde", "sol-types", "getrandom", "provider-anvil-api", "json-rpc"] } anyhow = "1.0" fs2 = "0.4.3" tokio = { version = "1", features = ["time", "rt", "process"] } futures = "0.3.31" itertools = "0.13.0" async-trait = "0.1.83" +reqwest = "0.12.9" +reqwest-middleware = { version = "0.4", features = ["json"] } +serde_json = "1" +tower = "0.5" +http = "1.1.0" [dev-dependencies] diff --git a/e2e-tests-rust/src/http_middleware.rs b/e2e-tests-rust/src/http_middleware.rs new file mode 100644 index 00000000..fdc96aef --- /dev/null +++ b/e2e-tests-rust/src/http_middleware.rs @@ -0,0 +1,71 @@ +/// This file is a an adapted copy of [`alloy::transports::http::Http`] that can work with +/// [`reqwest_middleware`]. +// TODO: Consider upstreaming support to alloy +use alloy::rpc::json_rpc::{RequestPacket, ResponsePacket}; +use alloy::transports::{TransportError, TransportErrorKind, TransportFut}; +use reqwest_middleware::ClientWithMiddleware; +use std::task::{Context, Poll}; +use tower::Service; + +#[derive(Clone)] +pub struct HttpWithMiddleware { + client: ClientWithMiddleware, + url: reqwest::Url, +} + +impl HttpWithMiddleware { + /// Create a new [`HttpWithMiddleware`] transport with a custom client. + pub const fn with_client(client: ClientWithMiddleware, url: reqwest::Url) -> Self { + Self { client, url } + } + + /// Make a request. + fn request_reqwest(&self, req: RequestPacket) -> TransportFut<'static> { + let this = self.clone(); + Box::pin(async move { + let resp = this + .client + .post(this.url) + .json(&req) + .send() + .await + .map_err(TransportErrorKind::custom)?; + let status = resp.status(); + + // Unpack data from the response body. We do this regardless of + // the status code, as we want to return the error in the body + // if there is one. + let body = resp.bytes().await.map_err(TransportErrorKind::custom)?; + + if status != reqwest::StatusCode::OK { + return Err(TransportErrorKind::http_error( + status.as_u16(), + String::from_utf8_lossy(&body).into_owned(), + )); + } + + // Deserialize a Box from the body. If deserialization fails, return + // the body as a string in the error. The conversion to String + // is lossy and may not cover all the bytes in the body. + serde_json::from_slice(&body) + .map_err(|err| TransportError::deser_err(err, String::from_utf8_lossy(&body))) + }) + } +} + +impl Service for HttpWithMiddleware { + type Response = ResponsePacket; + type Error = TransportError; + type Future = TransportFut<'static>; + + #[inline] + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + // reqwest always returns ok + Poll::Ready(Ok(())) + } + + #[inline] + fn call(&mut self, req: RequestPacket) -> Self::Future { + self.request_reqwest(req) + } +} diff --git a/e2e-tests-rust/src/lib.rs b/e2e-tests-rust/src/lib.rs index 599f42f9..3f0aea03 100644 --- a/e2e-tests-rust/src/lib.rs +++ b/e2e-tests-rust/src/lib.rs @@ -1,8 +1,12 @@ #![allow(async_fn_in_trait)] mod ext; +mod http_middleware; mod provider; mod utils; pub use ext::{ReceiptExt, ZksyncWalletProviderExt}; -pub use provider::{init_testing_provider, init_testing_provider_with_http_headers, AnvilZKsyncApi, TestingProvider, DEFAULT_TX_VALUE}; +pub use provider::{ + init_testing_provider, init_testing_provider_with_client, AnvilZKsyncApi, TestingProvider, + DEFAULT_TX_VALUE, +}; diff --git a/e2e-tests-rust/src/provider/mod.rs b/e2e-tests-rust/src/provider/mod.rs index 1dbed218..33bf813a 100644 --- a/e2e-tests-rust/src/provider/mod.rs +++ b/e2e-tests-rust/src/provider/mod.rs @@ -2,4 +2,6 @@ mod anvil_zksync; mod testing; pub use anvil_zksync::AnvilZKsyncApi; -pub use testing::{init_testing_provider, init_testing_provider_with_http_headers, TestingProvider, DEFAULT_TX_VALUE}; +pub use testing::{ + init_testing_provider, init_testing_provider_with_client, TestingProvider, DEFAULT_TX_VALUE, +}; diff --git a/e2e-tests-rust/src/provider/testing.rs b/e2e-tests-rust/src/provider/testing.rs index 897d60e4..94944ddf 100644 --- a/e2e-tests-rust/src/provider/testing.rs +++ b/e2e-tests-rust/src/provider/testing.rs @@ -1,41 +1,41 @@ -use crate::utils::{LockedPort,get_node_binary_path}; +use crate::http_middleware::HttpWithMiddleware; +use crate::utils::{get_node_binary_path, LockedPort}; use crate::ReceiptExt; use alloy::network::primitives::{BlockTransactionsKind, HeaderResponse as _}; use alloy::network::{Network, ReceiptResponse as _, TransactionBuilder}; use alloy::primitives::{Address, U256}; -use alloy::signers::local::LocalSigner; use alloy::providers::{ PendingTransaction, PendingTransactionBuilder, PendingTransactionError, Provider, RootProvider, SendableTx, WalletProvider, }; use alloy::rpc::{ - types::{Block, TransactionRequest}, client::RpcClient, + types::{Block, TransactionRequest}, }; -use alloy::transports::http::{ - reqwest, - reqwest::{ - header::HeaderMap, - Client, - }, - Http -}; +use alloy::signers::local::LocalSigner; +use alloy::signers::Signer; use alloy::transports::{RpcError, Transport, TransportErrorKind, TransportResult}; use alloy_zksync::network::header_response::HeaderResponse; use alloy_zksync::network::receipt_response::ReceiptResponse; use alloy_zksync::network::transaction_response::TransactionResponse; use alloy_zksync::network::Zksync; -use alloy_zksync::node_bindings::{EraTestNode,EraTestNodeError::NoKeysAvailable}; -use alloy_zksync::provider::{zksync_provider, ProviderBuilderExt, layers::era_test_node::EraTestNodeLayer}; +use alloy_zksync::node_bindings::{EraTestNode, EraTestNodeError::NoKeysAvailable}; +use alloy_zksync::provider::{layers::era_test_node::EraTestNodeLayer, zksync_provider}; use alloy_zksync::wallet::ZksyncWallet; use anyhow::Context as _; +use async_trait::async_trait; +use http::HeaderMap; use itertools::Itertools; +use reqwest_middleware::{Middleware, Next}; +use std::convert::identity; use std::future::Future; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::pin::Pin; +use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; +use tokio::sync::RwLock; use tokio::task::JoinHandle; pub const DEFAULT_TX_VALUE: u64 = 100; @@ -68,67 +68,46 @@ where { inner: P, rich_accounts: Vec
, + /// Last seen response headers + last_response_headers: Arc>>, _pd: PhantomData, + + /// Underlying anvil-zksync instance's URL + pub url: reqwest::Url, } -// Outside of `TestingProvider` to avoid specifying `P` +// TODO: Consider creating a builder pattern pub async fn init_testing_provider( - f: impl FnOnce(EraTestNode) -> EraTestNode, -) -> anyhow::Result< - TestingProvider>, Http>, -> { - let locked_port = LockedPort::acquire_unused().await?; - let provider = zksync_provider() - .with_recommended_fillers() - .on_era_test_node_with_wallet_and_config(|node| { - f(node - .path(get_node_binary_path()) - .port(locked_port.port)) - }); - - // Grab default rich accounts right after init. Note that subsequent calls to this method - // might return different value as wallet's signers are dynamic and can be changed by the user. - let rich_accounts = provider.signer_addresses().collect::>(); - // Wait for anvil-zksync to get up and be able to respond - provider.get_chain_id().await?; - // Explicitly unlock the port to showcase why we waited above - drop(locked_port); - - Ok(TestingProvider { - inner: provider, - rich_accounts, - _pd: Default::default(), - }) + node_fn: impl FnOnce(EraTestNode) -> EraTestNode, +) -> anyhow::Result, HttpWithMiddleware>> +{ + init_testing_provider_with_client(node_fn, identity).await } -// Init testing provider which sends specified HTTP headers e.g. for authentication -// Outside of `TestingProvider` to avoid specifying `P` -pub async fn init_testing_provider_with_http_headers( - headers: HeaderMap, - f: impl FnOnce(EraTestNode) -> EraTestNode, -) -> anyhow::Result< - TestingProvider>, Http>, -> { - use alloy::signers::Signer; - +pub async fn init_testing_provider_with_client( + node_fn: impl FnOnce(EraTestNode) -> EraTestNode, + client_fn: impl FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder, +) -> anyhow::Result, HttpWithMiddleware>> +{ let locked_port = LockedPort::acquire_unused().await?; - let node_layer = EraTestNodeLayer::from( - f( - EraTestNode::new() - .path(get_node_binary_path()) - .port(locked_port.port) - ) - ); - - let client_with_headers = Client::builder().default_headers(headers).build()?; - let rpc_url = node_layer.endpoint_url(); - let http = Http::with_client(client_with_headers, rpc_url); + let node_layer = EraTestNodeLayer::from(node_fn( + EraTestNode::new() + .path(get_node_binary_path()) + .port(locked_port.port), + )); + + let last_response_headers = Arc::new(RwLock::new(None)); + let client = + reqwest_middleware::ClientBuilder::new(client_fn(reqwest::Client::builder()).build()?) + .with(ResponseHeadersInspector(last_response_headers.clone())) + .build(); + let url = node_layer.endpoint_url(); + let http = HttpWithMiddleware::with_client(client, url.clone()); let rpc_client = RpcClient::new(http, true); + let rich_accounts = node_layer.instance().addresses().iter().cloned().collect(); let default_keys = node_layer.instance().keys().to_vec(); - let (default_key, remaining_keys) = default_keys - .split_first() - .ok_or(NoKeysAvailable)?; + let (default_key, remaining_keys) = default_keys.split_first().ok_or(NoKeysAvailable)?; let default_signer = LocalSigner::from(default_key.clone()) .with_chain_id(Some(node_layer.instance().chain_id())); @@ -138,18 +117,16 @@ pub async fn init_testing_provider_with_http_headers( let signer = LocalSigner::from(key.clone()); wallet.register_signer(signer) } - + let provider = zksync_provider() .with_recommended_fillers() .wallet(wallet) .layer(node_layer) .on_client(rpc_client); - // Grab default rich accounts right after init. Note that subsequent calls to this method - // might return different value as wallet's signers are dynamic and can be changed by the user. - let rich_accounts = provider.signer_addresses().collect::>(); - // Wait for anvil-zksync to get up and be able to respond - // Ignore error response (should not fail here if provider is used with intentionally wrong origin for testing purposes) + // Wait for anvil-zksync to get up and be able to respond. + // Ignore error response (should not fail here if provider is used with intentionally wrong + // configuration for testing purposes). let _ = provider.get_chain_id().await; // Explicitly unlock the port to showcase why we waited above drop(locked_port); @@ -157,7 +134,10 @@ pub async fn init_testing_provider_with_http_headers( Ok(TestingProvider { inner: provider, rich_accounts, + last_response_headers, _pd: Default::default(), + + url, }) } @@ -174,6 +154,15 @@ where .get(index) .unwrap_or_else(|| panic!("not enough rich accounts (#{} was requested)", index,)) } + + /// Returns last seen response headers. Panics if there is none. + pub async fn last_response_headers_unwrap(&self) -> HeaderMap { + self.last_response_headers + .read() + .await + .clone() + .expect("no headers found") + } } impl TestingProvider @@ -600,3 +589,20 @@ impl RacedReceipts { Ok(self) } } + +/// A [`reqwest_middleware`]-compliant middleware that allows to inspect last seen response headers. +struct ResponseHeadersInspector(Arc>>); + +#[async_trait] +impl Middleware for ResponseHeadersInspector { + async fn handle( + &self, + req: reqwest::Request, + extensions: &mut http::Extensions, + next: Next<'_>, + ) -> reqwest_middleware::Result { + let resp = next.run(req, extensions).await?; + *self.0.write().await = Some(resp.headers().clone()); + Ok(resp) + } +} diff --git a/e2e-tests-rust/tests/lib.rs b/e2e-tests-rust/tests/lib.rs index 21bfcece..e96d4d02 100644 --- a/e2e-tests-rust/tests/lib.rs +++ b/e2e-tests-rust/tests/lib.rs @@ -1,15 +1,22 @@ use alloy::network::ReceiptResponse; use alloy::providers::ext::AnvilApi; use alloy::providers::Provider; -use alloy::transports::http::reqwest::header::{HeaderMap, HeaderValue, HOST, ORIGIN}; use alloy::{primitives::U256, signers::local::PrivateKeySigner}; use anvil_zksync_e2e_tests::{ - init_testing_provider, init_testing_provider_with_http_headers, AnvilZKsyncApi, ReceiptExt, + init_testing_provider, init_testing_provider_with_client, AnvilZKsyncApi, ReceiptExt, ZksyncWalletProviderExt, DEFAULT_TX_VALUE, }; +use http::header::{ + HeaderMap, HeaderValue, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, ORIGIN, +}; use std::convert::identity; use std::time::Duration; +const SOME_ORIGIN: HeaderValue = HeaderValue::from_static("http://some.origin"); +const OTHER_ORIGIN: HeaderValue = HeaderValue::from_static("http://other.origin"); +const ANY_ORIGIN: HeaderValue = HeaderValue::from_static("*"); + #[tokio::test] async fn interval_sealing_finalization() -> anyhow::Result<()> { // Test that we can submit a transaction and wait for it to finalize when anvil-zksync is @@ -375,63 +382,77 @@ async fn set_chain_id() -> anyhow::Result<()> { #[tokio::test] async fn cli_no_cors() -> anyhow::Result<()> { - let mut headers = HeaderMap::new(); - headers.insert(ORIGIN, HeaderValue::from_static("http://some.origin")); - headers.insert(HOST, HeaderValue::from_static("http://some.origin")); - - // Verify cors is disabled by default - let provider = init_testing_provider_with_http_headers(headers.clone(), identity).await?; - provider.get_chain_id().await?; - // Verify all origins are allowed by default - let provider = init_testing_provider_with_http_headers(headers.clone(), |node| { - node.arg("--no-cors=false") - }) - .await?; + let provider = init_testing_provider(identity).await?; provider.get_chain_id().await?; + let resp_headers = provider.last_response_headers_unwrap().await; + assert_eq!( + resp_headers.get(ACCESS_CONTROL_ALLOW_ORIGIN), + Some(&ANY_ORIGIN) + ); - // Verify requests fail with enabled cors and `null` allowed origins - let provider_with_no_cors = init_testing_provider_with_http_headers(headers.clone(), |node| { - node.arg("--no-cors=false").arg("--allow-origin=null") - }) - .await?; - let error_resp = provider_with_no_cors.get_chain_id().await.unwrap_err(); - assert!(error_resp - .to_string() - .contains("Provided Host header is not whitelisted"),); + // Making OPTIONS request reveals all access control settings + let client = reqwest::Client::new(); + let url = provider.url.clone(); + let resp = client.request(reqwest::Method::OPTIONS, url).send().await?; + assert_eq!( + resp.headers().get(ACCESS_CONTROL_ALLOW_METHODS), + Some(&HeaderValue::from_static("GET,POST")) + ); + assert_eq!( + resp.headers().get(ACCESS_CONTROL_ALLOW_HEADERS), + Some(&HeaderValue::from_static("content-type")) + ); + assert_eq!( + resp.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN), + Some(&ANY_ORIGIN) + ); + drop(provider); + + // Verify access control is disabled with --no-cors + let provider_no_cors = init_testing_provider(|node| node.arg("--no-cors")).await?; + provider_no_cors.get_chain_id().await?; + let resp_headers = provider_no_cors.last_response_headers_unwrap().await; + assert_eq!(resp_headers.get(ACCESS_CONTROL_ALLOW_ORIGIN), None); Ok(()) } #[tokio::test] async fn cli_allow_origin() -> anyhow::Result<()> { - let mut headers = HeaderMap::new(); - headers.insert(ORIGIN, HeaderValue::from_static("http://some.origin")); - headers.insert(HOST, HeaderValue::from_static("http://some.origin")); + let req_headers = HeaderMap::from_iter([(ORIGIN, SOME_ORIGIN)]); // Verify allowed origin can make requests - let provider_with_allowed_origin = - init_testing_provider_with_http_headers(headers.clone(), |node| { - node.arg("--no-cors=false") - .arg("--allow-origin=http://some.origin") - }) - .await?; + let provider_with_allowed_origin = init_testing_provider_with_client( + |node| node.arg(format!("--allow-origin={}", SOME_ORIGIN.to_str().unwrap())), + |client| client.default_headers(req_headers.clone()), + ) + .await?; provider_with_allowed_origin.get_chain_id().await?; - - // Verify different origin is not allowed - let provider_with_not_allowed_origin = - init_testing_provider_with_http_headers(headers.clone(), |node| { - node.arg("--no-cors=false") - .arg("--allow-origin=http://other.origin") - }) - .await?; - let error_resp = provider_with_not_allowed_origin - .get_chain_id() - .await - .unwrap_err(); - assert!(error_resp - .to_string() - .contains("Provided Host header is not whitelisted"),); + let resp_headers = provider_with_allowed_origin + .last_response_headers_unwrap() + .await; + assert_eq!( + resp_headers.get(ACCESS_CONTROL_ALLOW_ORIGIN), + Some(&SOME_ORIGIN) + ); + drop(provider_with_allowed_origin); + + // Verify different origin are also allowed to make requests. CORS is reliant on the browser + // to respect access control headers reported by the server. + let provider_with_not_allowed_origin = init_testing_provider_with_client( + |node| node.arg(format!("--allow-origin={}", OTHER_ORIGIN.to_str().unwrap())), + |client| client.default_headers(req_headers), + ) + .await?; + provider_with_not_allowed_origin.get_chain_id().await?; + let resp_headers = provider_with_not_allowed_origin + .last_response_headers_unwrap() + .await; + assert_eq!( + resp_headers.get(ACCESS_CONTROL_ALLOW_ORIGIN), + Some(&OTHER_ORIGIN) + ); Ok(()) }