From 8b2189a32f19107b7a588cfb7648d79bf3c6e698 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 25 Jul 2022 17:26:59 +0300 Subject: [PATCH 01/45] http: Add inner server data structure Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 40943e4808..e61575f3ba 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -383,6 +383,33 @@ impl Future for ServerHandle { } } +/// Data required by the server to expose the a tower service. +#[derive(Debug, Clone)] +struct RPSeeSvcData { + /// Remote server address. + remote_addr: Option, + /// Registered server methods. + methods: Methods, + /// Access control. + acl: AccessControl, + /// Tracker for currently used resources on the server. + resources: Resources, + /// User provided middleware. + middleware: M, + /// Health API. + health_api: Option, + /// Max request body size. + max_request_body_size: u32, + /// Max response body size. + max_response_body_size: u32, + /// Max length for logging for request and response + /// + /// Logs bigger than this limit will be truncated. + max_log_length: u32, + /// Whether batch requests are supported by this server or not. + batch_requests_supported: bool, +} + /// An HTTP JSON RPC server. #[derive(Debug)] pub struct Server { From 669a43bebaaf1021ce2e515dbbcf05d85e10e7a0 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 25 Jul 2022 17:35:27 +0300 Subject: [PATCH 02/45] http: Handle RPC messages Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 119 +++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index e61575f3ba..abcae51ec9 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -38,7 +38,7 @@ use hyper::header::{HeaderMap, HeaderValue}; use hyper::server::conn::AddrStream; use hyper::server::{conn::AddrIncoming, Builder as HyperBuilder}; use hyper::service::{make_service_fn, service_fn}; -use hyper::{Error as HyperError, Method}; +use hyper::{Body, Error as HyperError, Method}; use jsonrpsee_core::error::{Error, GenericTransportError}; use jsonrpsee_core::http_helpers::{self, read_body}; use jsonrpsee_core::middleware::{self, HttpMiddleware as Middleware}; @@ -410,6 +410,123 @@ struct RPSeeSvcData { batch_requests_supported: bool, } +impl RPSeeSvcData { + /// Default behavior for RPSee handling of requests. + pub async fn handle_request(self, request: hyper::Request) -> Result, HyperError> { + let RPSeeSvcData { + remote_addr, + methods, + acl, + resources, + middleware, + health_api, + max_request_body_size, + max_response_body_size, + max_log_length, + batch_requests_supported, + } = self; + + let remote_addr = match remote_addr { + Some(addr) => addr, + // Default RPC port + None => SocketAddr::from(([127, 0, 0, 1], 9944)), + }; + + let request_start = middleware.on_request(remote_addr, request.headers()); + + let keys = request.headers().keys().map(|k| k.as_str()); + let cors_request_headers = http_helpers::get_cors_request_headers(request.headers()); + + let host = match http_helpers::read_header_value(request.headers(), "host") { + Some(origin) => origin, + None => return Ok(malformed()), + }; + let maybe_origin = http_helpers::read_header_value(request.headers(), "origin"); + + if let Err(e) = acl.verify_host(host) { + tracing::warn!("Denied request: {:?}", e); + return Ok(response::host_not_allowed()); + } + + if let Err(e) = acl.verify_origin(maybe_origin, host) { + tracing::warn!("Denied request: {:?}", e); + return Ok(response::invalid_allow_origin()); + } + + if let Err(e) = acl.verify_headers(keys, cors_request_headers) { + tracing::warn!("Denied request: {:?}", e); + return Ok(response::invalid_allow_headers()); + } + + // Only `POST` and `OPTIONS` methods are allowed. + match *request.method() { + // An OPTIONS request is a CORS preflight request. We've done our access check + // above so we just need to tell the browser that the request is OK. + Method::OPTIONS => { + let origin = match maybe_origin { + Some(origin) => origin, + None => return Ok(malformed()), + }; + + let allowed_headers = acl.allowed_headers().to_cors_header_value(); + let allowed_header_bytes = allowed_headers.as_bytes(); + + let res = hyper::Response::builder() + .header("access-control-allow-origin", origin) + .header("access-control-allow-methods", "POST") + .header("access-control-allow-headers", allowed_header_bytes) + .body(hyper::Body::empty()) + .unwrap_or_else(|e| { + tracing::error!("Error forming preflight response: {}", e); + internal_error() + }); + + Ok(res) + } + // The actual request. If it's a CORS request we need to remember to add + // the access-control-allow-origin header (despite preflight) to allow it + // to be read in a browser. + Method::POST if content_type_is_json(&request) => { + let origin = return_origin_if_different_from_host(request.headers()).cloned(); + let mut res = process_validated_request(ProcessValidatedRequest { + request, + middleware, + methods, + resources, + max_request_body_size, + max_response_body_size, + max_log_length, + batch_requests_supported, + request_start, + }) + .await?; + + if let Some(origin) = origin { + res.headers_mut().insert("access-control-allow-origin", origin); + } + Ok(res) + } + Method::GET => match health_api.as_ref() { + Some(health) if health.path.as_str() == request.uri().path() => { + process_health_request( + health, + middleware, + methods, + max_response_body_size, + request_start, + max_log_length, + ) + .await + } + _ => Ok(response::method_not_allowed()), + }, + // Error scenarios: + Method::POST => Ok(response::unsupported_content_type()), + _ => Ok(response::method_not_allowed()), + } + } +} + /// An HTTP JSON RPC server. #[derive(Debug)] pub struct Server { From aa0936ab04fcf92b47758e9a09f5812723b7e9d4 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 25 Jul 2022 17:40:37 +0300 Subject: [PATCH 03/45] http: Implement equivalent of `service_fn` Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index abcae51ec9..02445f76c1 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -412,7 +412,10 @@ struct RPSeeSvcData { impl RPSeeSvcData { /// Default behavior for RPSee handling of requests. - pub async fn handle_request(self, request: hyper::Request) -> Result, HyperError> { + pub async fn handle_request( + self, + request: hyper::Request, + ) -> Result, HyperError> { let RPSeeSvcData { remote_addr, methods, @@ -527,6 +530,30 @@ impl RPSeeSvcData { } } +/// JsonRPSee service compatible with `tower`. +/// +/// # Note +/// This is similar to [`hyper::service::service_fn`]. +pub struct RPSeeServerSvc { + inner: RPSeeSvcData, +} + +impl hyper::service::Service> for RPSeeServerSvc { + type Response = hyper::Response; + type Error = hyper::Error; + type Future = Pin> + Send>>; + + /// Opens door for back pressure implementation. + fn poll_ready(&mut self, _: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, request: hyper::Request) -> Self::Future { + let data = self.inner.clone(); + Box::pin(data.handle_request(request)) + } +} + /// An HTTP JSON RPC server. #[derive(Debug)] pub struct Server { From 50725dca5c352b337f3fd9651f050d264bc0c987 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 25 Jul 2022 17:42:44 +0300 Subject: [PATCH 04/45] http: Implement equivalent of `make_service_fn` Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 02445f76c1..8c21f7ad54 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -554,6 +554,30 @@ impl hyper::service::Service> for RPS } } +/// JsonRPSee service compatible with `tower`. +/// +/// # Note +/// This is similar to [`hyper::service::make_service_fn`]. +pub struct RPSeeServerMakeSvc { + inner: RPSeeSvcData, +} + +impl hyper::service::Service for RPSeeServerMakeSvc { + type Response = RPSeeServerSvc; + type Error = hyper::Error; + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, _: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _connection: T) -> Self::Future { + let inner = self.inner.clone(); + let fut = async move { Ok(RPSeeServerSvc { inner }) }; + Box::pin(fut) + } +} + /// An HTTP JSON RPC server. #[derive(Debug)] pub struct Server { From aab7f31977b48b5cbe3c3635f20fdc30683ac0d4 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 25 Jul 2022 17:46:15 +0300 Subject: [PATCH 05/45] http: Expose `tower` compatible service Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 8c21f7ad54..f298daca51 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -611,6 +611,26 @@ impl Server { self.local_addr.ok_or_else(|| Error::Custom("Local address not found".into())) } + /// Returns a service that can be utilised with `tower` compatible crates. + pub fn to_service(self, methods: impl Into) -> Result, Error> { + let methods = methods.into().initialize_resources(&self.resources)?; + + Ok(RPSeeServerMakeSvc { + inner: RPSeeSvcData { + remote_addr: None, + methods, + acl: self.access_control, + resources: self.resources, + middleware: self.middleware, + health_api: self.health_api, + max_request_body_size: self.max_response_body_size, + max_response_body_size: self.max_response_body_size, + max_log_length: self.max_log_length, + batch_requests_supported: self.batch_requests_supported, + }, + }) + } + /// Start the server. pub fn start(mut self, methods: impl Into) -> Result { let max_request_body_size = self.max_request_body_size; From 569f2b6feb17f92e075812531a85e5b4f303a31e Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 25 Jul 2022 17:57:24 +0300 Subject: [PATCH 06/45] http: Prebuild http server with optional listener Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index f298daca51..f9fd7527fb 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -251,7 +251,7 @@ impl Builder { ) -> Result, Error> { Ok(Server { access_control: self.access_control, - listener, + listener: Some(listener), local_addr: Some(local_addr), max_request_body_size: self.max_request_body_size, max_response_body_size: self.max_response_body_size, @@ -295,7 +295,7 @@ impl Builder { let listener = hyper::Server::from_tcp(listener)?; Ok(Server { - listener, + listener: Some(listener), local_addr, access_control: self.access_control, max_request_body_size: self.max_request_body_size, @@ -331,7 +331,7 @@ impl Builder { let listener = hyper::Server::from_tcp(listener)?.tcp_nodelay(true); Ok(Server { - listener, + listener: Some(listener), local_addr, access_control: self.access_control, max_request_body_size: self.max_request_body_size, @@ -344,6 +344,23 @@ impl Builder { health_api: self.health_api, }) } + + /// Pre-build the server as is. + pub fn pre_build(self) -> Server { + Server { + listener: None, + local_addr: None, + access_control: self.access_control, + max_request_body_size: self.max_request_body_size, + max_response_body_size: self.max_response_body_size, + batch_requests_supported: self.batch_requests_supported, + resources: self.resources, + tokio_runtime: self.tokio_runtime, + middleware: self.middleware, + max_log_length: self.max_log_length, + health_api: self.health_api, + } + } } #[derive(Debug, Clone)] @@ -582,7 +599,7 @@ impl hyper::service::Service for RPSeeServerMak #[derive(Debug)] pub struct Server { /// Hyper server. - listener: HyperBuilder, + listener: Option>, /// Local address local_addr: Option, /// Max request body size. @@ -767,7 +784,8 @@ impl Server { }; let handle = rt.spawn(async move { - let server = listener.serve(make_service); + // TODO: Hande unwrap. + let server = listener.unwrap().serve(make_service); let _ = server.with_graceful_shutdown(async move { rx.next().await.map_or((), |_| ()) }).await; }); From 7c74b0fc6ea9df004aaf7208495473b753b87045 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 25 Jul 2022 17:58:56 +0300 Subject: [PATCH 07/45] examples: WIP tower service Signed-off-by: Alexandru Vasile --- examples/Cargo.toml | 3 + examples/examples/tower_http.rs | 103 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 examples/examples/tower_http.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 453b3b240e..69ec279d07 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -15,3 +15,6 @@ tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } tokio = { version = "1.16", features = ["full"] } tokio-stream = { version = "0.1", features = ["sync"] } serde_json = { version = "1" } +tower-http = { version = "0.3.4", features = ["full"] } +tower = { version = "0.4.13", features = ["full"] } +hyper = "0.14.20" diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs new file mode 100644 index 0000000000..9f668bcbe1 --- /dev/null +++ b/examples/examples/tower_http.rs @@ -0,0 +1,103 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +use hyper::Server; +use std::iter::once; +use std::net::SocketAddr; +use std::time::Instant; +use tokio::net::TcpListener; +use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer; +use tower_http::trace::TraceLayer; + +use jsonrpsee::core::client::ClientT; +use jsonrpsee::core::middleware::{self, Headers, Params}; +use jsonrpsee::http_client::HttpClientBuilder; +use jsonrpsee::http_server::{HttpServerBuilder, HttpServerHandle, RpcModule}; + +#[derive(Clone)] +struct Timings; + +impl middleware::HttpMiddleware for Timings { + type Instant = Instant; + + fn on_request(&self, remote_addr: SocketAddr, headers: &Headers) -> Self::Instant { + println!("[Middleware::on_request] remote_addr {}, headers: {:?}", remote_addr, headers); + Instant::now() + } + + fn on_call(&self, name: &str, params: Params, kind: middleware::MethodKind) { + println!("[Middleware::on_call] method: '{}', params: {:?}, kind: {}", name, params, kind); + } + + fn on_result(&self, name: &str, succeess: bool, started_at: Self::Instant) { + println!("[Middleware::on_result] '{}', worked? {}, time elapsed {:?}", name, succeess, started_at.elapsed()); + } + + fn on_response(&self, result: &str, started_at: Self::Instant) { + println!("[Middleware::on_response] result: {}, time elapsed {:?}", result, started_at.elapsed()); + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::FmtSubscriber::builder() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .try_init() + .expect("setting default subscriber failed"); + + let addr = run_server().await?; + let url = format!("http://{}", addr); + println!("[main]: URL {:?}", url); + + let client = HttpClientBuilder::default().build(&url)?; + let response: String = client.request("say_hello", None).await?; + println!("[main]: response: {:?}", response); + let _response: Result = client.request("unknown_method", None).await; + let _ = client.request::("say_hello", None).await?; + + Ok(()) +} + +async fn run_server() -> anyhow::Result { + let mut module = RpcModule::new(()); + module.register_method("say_hello", |_, _| Ok("lo"))?; + + println!("[run_server]: Creating RPC service"); + let rpc_service = HttpServerBuilder::new().set_middleware(Timings).pre_build().to_service(module); + + println!("[run_server]: Tower builder"); + let service_builder = tower::ServiceBuilder::new() + // Mark the `Authorization` request header as sensitive so it doesn't show in logs + .layer(SetSensitiveRequestHeadersLayer::new(once(hyper::header::AUTHORIZATION))) + .service(rpc_service); + + let addr = SocketAddr::from(([127, 0, 0, 1], 9935)); + + println!("[run_server]: Bind server"); + tokio::spawn(async move { Server::bind(&addr).serve(service_builder).expect("server error") }); + + Ok(addr) +} From e2da10caa888f19ab47c7a6c95cbfbdace190faf Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 13:04:30 +0300 Subject: [PATCH 08/45] http: Fix warnings Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index f9fd7527fb..1c3025ce19 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -38,7 +38,7 @@ use hyper::header::{HeaderMap, HeaderValue}; use hyper::server::conn::AddrStream; use hyper::server::{conn::AddrIncoming, Builder as HyperBuilder}; use hyper::service::{make_service_fn, service_fn}; -use hyper::{Body, Error as HyperError, Method}; +use hyper::{Error as HyperError, Method}; use jsonrpsee_core::error::{Error, GenericTransportError}; use jsonrpsee_core::http_helpers::{self, read_body}; use jsonrpsee_core::middleware::{self, HttpMiddleware as Middleware}; @@ -429,7 +429,7 @@ struct RPSeeSvcData { impl RPSeeSvcData { /// Default behavior for RPSee handling of requests. - pub async fn handle_request( + async fn handle_request( self, request: hyper::Request, ) -> Result, HyperError> { @@ -551,6 +551,7 @@ impl RPSeeSvcData { /// /// # Note /// This is similar to [`hyper::service::service_fn`]. +#[derive(Debug)] pub struct RPSeeServerSvc { inner: RPSeeSvcData, } @@ -575,6 +576,7 @@ impl hyper::service::Service> for RPS /// /// # Note /// This is similar to [`hyper::service::make_service_fn`]. +#[derive(Debug)] pub struct RPSeeServerMakeSvc { inner: RPSeeSvcData, } From 26871deabbc5f15e89b25feb3ddf78228d75585e Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 13:35:41 +0300 Subject: [PATCH 09/45] tower_http: Fix warnings Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index 9f668bcbe1..49aea38174 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -28,14 +28,12 @@ use hyper::Server; use std::iter::once; use std::net::SocketAddr; use std::time::Instant; -use tokio::net::TcpListener; use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer; -use tower_http::trace::TraceLayer; use jsonrpsee::core::client::ClientT; use jsonrpsee::core::middleware::{self, Headers, Params}; use jsonrpsee::http_client::HttpClientBuilder; -use jsonrpsee::http_server::{HttpServerBuilder, HttpServerHandle, RpcModule}; +use jsonrpsee::http_server::{HttpServerBuilder, RpcModule}; #[derive(Clone)] struct Timings; @@ -97,7 +95,8 @@ async fn run_server() -> anyhow::Result { let addr = SocketAddr::from(([127, 0, 0, 1], 9935)); println!("[run_server]: Bind server"); - tokio::spawn(async move { Server::bind(&addr).serve(service_builder).expect("server error") }); + + tokio::spawn(async move { Server::bind(&addr).serve(service_builder) }); Ok(addr) } From 1e7dcdfa41250a333e5b5b757c1af45cacd49875 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 15:27:53 +0300 Subject: [PATCH 10/45] http: Ensure service works with tower Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 65 ++++++++++++++++++++++++--------- http-server/src/lib.rs | 4 +- http-server/src/server.rs | 4 +- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index 49aea38174..a8d6ebf524 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -24,16 +24,25 @@ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +use hyper::body::Bytes; +use hyper::service::make_service_fn; use hyper::Server; +use std::convert::Infallible; +use std::future::Future; use std::iter::once; +use std::marker::PhantomData; use std::net::SocketAddr; -use std::time::Instant; -use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; +use tower_http::sensitive_headers::{SetSensitiveRequestHeaders, SetSensitiveRequestHeadersLayer}; +use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; +use tower_http::LatencyUnit; use jsonrpsee::core::client::ClientT; use jsonrpsee::core::middleware::{self, Headers, Params}; use jsonrpsee::http_client::HttpClientBuilder; -use jsonrpsee::http_server::{HttpServerBuilder, RpcModule}; +use jsonrpsee::http_server::{HttpServerBuilder, RPSeeServerSvc, RpcModule}; #[derive(Clone)] struct Timings; @@ -80,23 +89,43 @@ async fn main() -> anyhow::Result<()> { } async fn run_server() -> anyhow::Result { - let mut module = RpcModule::new(()); - module.register_method("say_hello", |_, _| Ok("lo"))?; - - println!("[run_server]: Creating RPC service"); - let rpc_service = HttpServerBuilder::new().set_middleware(Timings).pre_build().to_service(module); - - println!("[run_server]: Tower builder"); - let service_builder = tower::ServiceBuilder::new() - // Mark the `Authorization` request header as sensitive so it doesn't show in logs - .layer(SetSensitiveRequestHeadersLayer::new(once(hyper::header::AUTHORIZATION))) - .service(rpc_service); - let addr = SocketAddr::from(([127, 0, 0, 1], 9935)); - println!("[run_server]: Bind server"); - - tokio::spawn(async move { Server::bind(&addr).serve(service_builder) }); + let make_service = make_service_fn(move |_| { + async move { + let mut module = RpcModule::new(()); + module.register_method("say_hello", |_, _| Ok("lo")).unwrap(); + + println!("[run_server]: Creating RPC service"); + let rpc_svc = HttpServerBuilder::new().set_middleware(Timings).pre_build().to_service(module).unwrap(); + + println!("[run_server]: Tower builder"); + let tower_svc = tower::ServiceBuilder::new() + // Add high level tracing/logging to all requests + .layer( + TraceLayer::new_for_http() + .on_body_chunk(|chunk: &Bytes, latency: Duration, _: &tracing::Span| { + tracing::trace!(size_bytes = chunk.len(), latency = ?latency, "sending body chunk") + }) + .make_span_with(DefaultMakeSpan::new().include_headers(true)) + .on_response(DefaultOnResponse::new().include_headers(true).latency_unit(LatencyUnit::Micros)), + ) + // Mark the `Authorization` request header as sensitive so it doesn't show in logs + .layer(SetSensitiveRequestHeadersLayer::new(once(hyper::header::AUTHORIZATION))) + .timeout(Duration::from_secs(2)) + .service(rpc_svc); + + Ok::<_, Infallible>(tower_svc) + } + }); + + tokio::spawn(async move { + println!("[run_server]: Bind server"); + Server::bind(&addr).serve(make_service).await + }); + + // Race with server start / client connect present in all examples + tokio::time::sleep(Duration::from_secs(5)).await; Ok(addr) } diff --git a/http-server/src/lib.rs b/http-server/src/lib.rs index fab3147bb8..ee959043ce 100644 --- a/http-server/src/lib.rs +++ b/http-server/src/lib.rs @@ -38,7 +38,9 @@ pub mod response; pub use jsonrpsee_core::server::access_control::{AccessControl, AccessControlBuilder}; pub use jsonrpsee_core::server::rpc_module::RpcModule; pub use jsonrpsee_types as types; -pub use server::{Builder as HttpServerBuilder, Server as HttpServer, ServerHandle as HttpServerHandle}; +pub use server::{ + Builder as HttpServerBuilder, RPSeeServerSvc, Server as HttpServer, ServerHandle as HttpServerHandle, +}; pub use tracing; #[cfg(test)] diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 1c3025ce19..a67af6aff9 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -631,10 +631,10 @@ impl Server { } /// Returns a service that can be utilised with `tower` compatible crates. - pub fn to_service(self, methods: impl Into) -> Result, Error> { + pub fn to_service(self, methods: impl Into) -> Result, Error> { let methods = methods.into().initialize_resources(&self.resources)?; - Ok(RPSeeServerMakeSvc { + Ok(RPSeeServerSvc { inner: RPSeeSvcData { remote_addr: None, methods, From 50b9a0ce90870fcff6ddc5f02cfa3437e3a774ff Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 15:30:21 +0300 Subject: [PATCH 11/45] http: Remove `RPSeeServerMakeSvc` to allow further flexibility Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index a67af6aff9..1a612b8680 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -572,31 +572,6 @@ impl hyper::service::Service> for RPS } } -/// JsonRPSee service compatible with `tower`. -/// -/// # Note -/// This is similar to [`hyper::service::make_service_fn`]. -#[derive(Debug)] -pub struct RPSeeServerMakeSvc { - inner: RPSeeSvcData, -} - -impl hyper::service::Service for RPSeeServerMakeSvc { - type Response = RPSeeServerSvc; - type Error = hyper::Error; - type Future = Pin> + Send>>; - - fn poll_ready(&mut self, _: &mut Context) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _connection: T) -> Self::Future { - let inner = self.inner.clone(); - let fut = async move { Ok(RPSeeServerSvc { inner }) }; - Box::pin(fut) - } -} - /// An HTTP JSON RPC server. #[derive(Debug)] pub struct Server { From 25e331705c726c80b0072ad0c1ec5195a13f0f3b Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 15:31:53 +0300 Subject: [PATCH 12/45] tower_http: Fix warnings Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index a8d6ebf524..8ddc4716cf 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -28,21 +28,17 @@ use hyper::body::Bytes; use hyper::service::make_service_fn; use hyper::Server; use std::convert::Infallible; -use std::future::Future; use std::iter::once; -use std::marker::PhantomData; use std::net::SocketAddr; -use std::pin::Pin; -use std::task::{Context, Poll}; use std::time::{Duration, Instant}; -use tower_http::sensitive_headers::{SetSensitiveRequestHeaders, SetSensitiveRequestHeadersLayer}; +use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; use tower_http::LatencyUnit; use jsonrpsee::core::client::ClientT; use jsonrpsee::core::middleware::{self, Headers, Params}; use jsonrpsee::http_client::HttpClientBuilder; -use jsonrpsee::http_server::{HttpServerBuilder, RPSeeServerSvc, RpcModule}; +use jsonrpsee::http_server::{HttpServerBuilder, RpcModule}; #[derive(Clone)] struct Timings; From 5af1282471b20d34e4601f935c7fdacfc33fdada Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 15:45:48 +0300 Subject: [PATCH 13/45] tower_http: Resubmit the same request for testing Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index 8ddc4716cf..abffa04fe0 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -81,6 +81,9 @@ async fn main() -> anyhow::Result<()> { let _response: Result = client.request("unknown_method", None).await; let _ = client.request::("say_hello", None).await?; + // Make the same request again. + let _ = client.request::("say_hello", None).await?; + Ok(()) } From e8fa8b5d278a30c07b6615dc0989d8a2fec2d992 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 16:02:40 +0300 Subject: [PATCH 14/45] http: Transform builder into service directly Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 2 +- http-server/src/server.rs | 53 +++++++++++---------------------- 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index abffa04fe0..056eb78ddd 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -96,7 +96,7 @@ async fn run_server() -> anyhow::Result { module.register_method("say_hello", |_, _| Ok("lo")).unwrap(); println!("[run_server]: Creating RPC service"); - let rpc_svc = HttpServerBuilder::new().set_middleware(Timings).pre_build().to_service(module).unwrap(); + let rpc_svc = HttpServerBuilder::new().set_middleware(Timings).to_service(module).unwrap(); println!("[run_server]: Tower builder"); let tower_svc = tower::ServiceBuilder::new() diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 1a612b8680..1dee77ae7a 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -345,21 +345,24 @@ impl Builder { }) } - /// Pre-build the server as is. - pub fn pre_build(self) -> Server { - Server { - listener: None, - local_addr: None, - access_control: self.access_control, - max_request_body_size: self.max_request_body_size, - max_response_body_size: self.max_response_body_size, - batch_requests_supported: self.batch_requests_supported, - resources: self.resources, - tokio_runtime: self.tokio_runtime, - middleware: self.middleware, - max_log_length: self.max_log_length, - health_api: self.health_api, - } + /// Returns a service that can be utilised with `tower` compatible crates. + pub fn to_service(self, methods: impl Into) -> Result, Error> { + let methods = methods.into().initialize_resources(&self.resources)?; + + Ok(RPSeeServerSvc { + inner: RPSeeSvcData { + remote_addr: None, + methods, + acl: self.access_control, + resources: self.resources, + middleware: self.middleware, + health_api: self.health_api, + max_request_body_size: self.max_response_body_size, + max_response_body_size: self.max_response_body_size, + max_log_length: self.max_log_length, + batch_requests_supported: self.batch_requests_supported, + }, + }) } } @@ -605,26 +608,6 @@ impl Server { self.local_addr.ok_or_else(|| Error::Custom("Local address not found".into())) } - /// Returns a service that can be utilised with `tower` compatible crates. - pub fn to_service(self, methods: impl Into) -> Result, Error> { - let methods = methods.into().initialize_resources(&self.resources)?; - - Ok(RPSeeServerSvc { - inner: RPSeeSvcData { - remote_addr: None, - methods, - acl: self.access_control, - resources: self.resources, - middleware: self.middleware, - health_api: self.health_api, - max_request_body_size: self.max_response_body_size, - max_response_body_size: self.max_response_body_size, - max_log_length: self.max_log_length, - batch_requests_supported: self.batch_requests_supported, - }, - }) - } - /// Start the server. pub fn start(mut self, methods: impl Into) -> Result { let max_request_body_size = self.max_request_body_size; From 52ef967fd660c74184c6789d1cdf3fc99e5bbba0 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 16:04:29 +0300 Subject: [PATCH 15/45] http: Rename `RPSeeServerSvc` into user friendly `TowerService` Signed-off-by: Alexandru Vasile --- http-server/src/lib.rs | 4 +--- http-server/src/server.rs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/http-server/src/lib.rs b/http-server/src/lib.rs index ee959043ce..ab3fd0b61c 100644 --- a/http-server/src/lib.rs +++ b/http-server/src/lib.rs @@ -38,9 +38,7 @@ pub mod response; pub use jsonrpsee_core::server::access_control::{AccessControl, AccessControlBuilder}; pub use jsonrpsee_core::server::rpc_module::RpcModule; pub use jsonrpsee_types as types; -pub use server::{ - Builder as HttpServerBuilder, RPSeeServerSvc, Server as HttpServer, ServerHandle as HttpServerHandle, -}; +pub use server::{Builder as HttpServerBuilder, Server as HttpServer, ServerHandle as HttpServerHandle, TowerService}; pub use tracing; #[cfg(test)] diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 1dee77ae7a..fb021f9f95 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -346,11 +346,11 @@ impl Builder { } /// Returns a service that can be utilised with `tower` compatible crates. - pub fn to_service(self, methods: impl Into) -> Result, Error> { + pub fn to_service(self, methods: impl Into) -> Result, Error> { let methods = methods.into().initialize_resources(&self.resources)?; - Ok(RPSeeServerSvc { - inner: RPSeeSvcData { + Ok(TowerService { + inner: TowerServiceData { remote_addr: None, methods, acl: self.access_control, @@ -405,7 +405,7 @@ impl Future for ServerHandle { /// Data required by the server to expose the a tower service. #[derive(Debug, Clone)] -struct RPSeeSvcData { +struct TowerServiceData { /// Remote server address. remote_addr: Option, /// Registered server methods. @@ -430,13 +430,13 @@ struct RPSeeSvcData { batch_requests_supported: bool, } -impl RPSeeSvcData { +impl TowerServiceData { /// Default behavior for RPSee handling of requests. async fn handle_request( self, request: hyper::Request, ) -> Result, HyperError> { - let RPSeeSvcData { + let TowerServiceData { remote_addr, methods, acl, @@ -555,11 +555,11 @@ impl RPSeeSvcData { /// # Note /// This is similar to [`hyper::service::service_fn`]. #[derive(Debug)] -pub struct RPSeeServerSvc { - inner: RPSeeSvcData, +pub struct TowerService { + inner: TowerServiceData, } -impl hyper::service::Service> for RPSeeServerSvc { +impl hyper::service::Service> for TowerService { type Response = hyper::Response; type Error = hyper::Error; type Future = Pin> + Send>>; From 172b6f79b6f6e8c668076671ea742b9775bcb397 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 16:20:35 +0300 Subject: [PATCH 16/45] http: Rely on internal TowerService to handle requests Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 133 ++++++-------------------------------- 1 file changed, 19 insertions(+), 114 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index fb021f9f95..60e38e06d3 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -37,7 +37,7 @@ use futures_util::stream::{StreamExt, TryStreamExt}; use hyper::header::{HeaderMap, HeaderValue}; use hyper::server::conn::AddrStream; use hyper::server::{conn::AddrIncoming, Builder as HyperBuilder}; -use hyper::service::{make_service_fn, service_fn}; +use hyper::service::make_service_fn; use hyper::{Error as HyperError, Method}; use jsonrpsee_core::error::{Error, GenericTransportError}; use jsonrpsee_core::http_helpers::{self, read_body}; @@ -623,119 +623,24 @@ impl Server { let health_api = self.health_api; let make_service = make_service_fn(move |conn: &AddrStream| { - let remote_addr = conn.remote_addr(); - let methods = methods.clone(); - let acl = acl.clone(); - let resources = resources.clone(); - let middleware = middleware.clone(); - let health_api = health_api.clone(); - - async move { - Ok::<_, HyperError>(service_fn(move |request| { - let request_start = middleware.on_request(remote_addr, request.headers()); - - let methods = methods.clone(); - let acl = acl.clone(); - let resources = resources.clone(); - let middleware = middleware.clone(); - let health_api = health_api.clone(); - - // Run some validation on the http request, then read the body and try to deserialize it into one of - // two cases: a single RPC request or a batch of RPC requests. - async move { - let keys = request.headers().keys().map(|k| k.as_str()); - let cors_request_headers = http_helpers::get_cors_request_headers(request.headers()); - - let host = match http_helpers::read_header_value(request.headers(), "host") { - Some(origin) => origin, - None => return Ok(malformed()), - }; - let maybe_origin = http_helpers::read_header_value(request.headers(), "origin"); - - if let Err(e) = acl.verify_host(host) { - tracing::warn!("Denied request: {:?}", e); - return Ok(response::host_not_allowed()); - } - - if let Err(e) = acl.verify_origin(maybe_origin, host) { - tracing::warn!("Denied request: {:?}", e); - return Ok(response::invalid_allow_origin()); - } - - if let Err(e) = acl.verify_headers(keys, cors_request_headers) { - tracing::warn!("Denied request: {:?}", e); - return Ok(response::invalid_allow_headers()); - } - - // Only `POST` and `OPTIONS` methods are allowed. - match *request.method() { - // An OPTIONS request is a CORS preflight request. We've done our access check - // above so we just need to tell the browser that the request is OK. - Method::OPTIONS => { - let origin = match maybe_origin { - Some(origin) => origin, - None => return Ok(malformed()), - }; - - let allowed_headers = acl.allowed_headers().to_cors_header_value(); - let allowed_header_bytes = allowed_headers.as_bytes(); - - let res = hyper::Response::builder() - .header("access-control-allow-origin", origin) - .header("access-control-allow-methods", "POST") - .header("access-control-allow-headers", allowed_header_bytes) - .body(hyper::Body::empty()) - .unwrap_or_else(|e| { - tracing::error!("Error forming preflight response: {}", e); - internal_error() - }); - - Ok(res) - } - // The actual request. If it's a CORS request we need to remember to add - // the access-control-allow-origin header (despite preflight) to allow it - // to be read in a browser. - Method::POST if content_type_is_json(&request) => { - let origin = return_origin_if_different_from_host(request.headers()).cloned(); - let mut res = process_validated_request(ProcessValidatedRequest { - request, - middleware, - methods, - resources, - max_request_body_size, - max_response_body_size, - max_log_length, - batch_requests_supported, - request_start, - }) - .await?; - - if let Some(origin) = origin { - res.headers_mut().insert("access-control-allow-origin", origin); - } - Ok(res) - } - Method::GET => match health_api.as_ref() { - Some(health) if health.path.as_str() == request.uri().path() => { - process_health_request( - health, - middleware, - methods, - max_response_body_size, - request_start, - max_log_length, - ) - .await - } - _ => Ok(response::method_not_allowed()), - }, - // Error scenarios: - Method::POST => Ok(response::unsupported_content_type()), - _ => Ok(response::method_not_allowed()), - } - } - })) - } + let service = TowerService { + inner: TowerServiceData { + remote_addr: Some(conn.remote_addr()), + methods: methods.clone(), + acl: acl.clone(), + resources: resources.clone(), + middleware: middleware.clone(), + health_api: health_api.clone(), + max_request_body_size, + max_response_body_size, + max_log_length, + batch_requests_supported, + }, + }; + + // For every request the `TowerService` is calling into `TowerServiceData::handle_request` + // where the RPSee bare implementation resides. + async move { Ok::<_, HyperError>(service) } }); let rt = match self.tokio_runtime.take() { From 45a810592ba0476accf64bad9b043d6e0b3d1218 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 27 Jul 2022 16:27:29 +0300 Subject: [PATCH 17/45] Fix middleware typo Signed-off-by: Alexandru Vasile --- examples/examples/middleware_http.rs | 4 ++-- examples/examples/middleware_ws.rs | 4 ++-- examples/examples/multi_middleware.rs | 4 ++-- examples/examples/tower_http.rs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/examples/middleware_http.rs b/examples/examples/middleware_http.rs index 8a553b5e06..f9f0a209e2 100644 --- a/examples/examples/middleware_http.rs +++ b/examples/examples/middleware_http.rs @@ -47,8 +47,8 @@ impl middleware::HttpMiddleware for Timings { println!("[Middleware::on_call] method: '{}', params: {:?}, kind: {}", name, params, kind); } - fn on_result(&self, name: &str, succeess: bool, started_at: Self::Instant) { - println!("[Middleware::on_result] '{}', worked? {}, time elapsed {:?}", name, succeess, started_at.elapsed()); + fn on_result(&self, name: &str, success: bool, started_at: Self::Instant) { + println!("[Middleware::on_result] '{}', worked? {}, time elapsed {:?}", name, success, started_at.elapsed()); } fn on_response(&self, result: &str, started_at: Self::Instant) { diff --git a/examples/examples/middleware_ws.rs b/examples/examples/middleware_ws.rs index 08113fa23f..8fcbda6164 100644 --- a/examples/examples/middleware_ws.rs +++ b/examples/examples/middleware_ws.rs @@ -51,8 +51,8 @@ impl middleware::WsMiddleware for Timings { println!("[Middleware::on_call] method: '{}', params: {:?}, kind: {}", name, params, kind); } - fn on_result(&self, name: &str, succeess: bool, started_at: Self::Instant) { - println!("[Middleware::on_result] '{}', worked? {}, time elapsed {:?}", name, succeess, started_at.elapsed()); + fn on_result(&self, name: &str, success: bool, started_at: Self::Instant) { + println!("[Middleware::on_result] '{}', worked? {}, time elapsed {:?}", name, success, started_at.elapsed()); } fn on_response(&self, result: &str, started_at: Self::Instant) { diff --git a/examples/examples/multi_middleware.rs b/examples/examples/multi_middleware.rs index 6a8b1489b0..cbbf7c78c2 100644 --- a/examples/examples/multi_middleware.rs +++ b/examples/examples/multi_middleware.rs @@ -56,8 +56,8 @@ impl middleware::WsMiddleware for Timings { println!("[Timings:on_call] method: '{}', params: {:?}, kind: {}", name, params, kind); } - fn on_result(&self, name: &str, succeess: bool, started_at: Self::Instant) { - println!("[Timings] call={}, worked? {}, duration {:?}", name, succeess, started_at.elapsed()); + fn on_result(&self, name: &str, success: bool, started_at: Self::Instant) { + println!("[Timings] call={}, worked? {}, duration {:?}", name, success, started_at.elapsed()); } fn on_response(&self, _result: &str, started_at: Self::Instant) { diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index 056eb78ddd..756c27ad65 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -55,8 +55,8 @@ impl middleware::HttpMiddleware for Timings { println!("[Middleware::on_call] method: '{}', params: {:?}, kind: {}", name, params, kind); } - fn on_result(&self, name: &str, succeess: bool, started_at: Self::Instant) { - println!("[Middleware::on_result] '{}', worked? {}, time elapsed {:?}", name, succeess, started_at.elapsed()); + fn on_result(&self, name: &str, success: bool, started_at: Self::Instant) { + println!("[Middleware::on_result] '{}', worked? {}, time elapsed {:?}", name, success, started_at.elapsed()); } fn on_response(&self, result: &str, started_at: Self::Instant) { From bd0e68710018453dad59d63c51697e04173096d0 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 8 Aug 2022 14:35:48 +0300 Subject: [PATCH 18/45] http-server: Improve API builder for tower service Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 7 +++++-- http-server/src/server.rs | 35 +++++++++++++-------------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index 756c27ad65..229a71151a 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -31,6 +31,7 @@ use std::convert::Infallible; use std::iter::once; use std::net::SocketAddr; use std::time::{Duration, Instant}; +use hyper::server::conn::AddrStream; use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; use tower_http::LatencyUnit; @@ -90,13 +91,15 @@ async fn main() -> anyhow::Result<()> { async fn run_server() -> anyhow::Result { let addr = SocketAddr::from(([127, 0, 0, 1], 9935)); - let make_service = make_service_fn(move |_| { + let make_service = make_service_fn(move |conn: &AddrStream| { + let remote_addr = conn.remote_addr(); async move { let mut module = RpcModule::new(()); module.register_method("say_hello", |_, _| Ok("lo")).unwrap(); println!("[run_server]: Creating RPC service"); - let rpc_svc = HttpServerBuilder::new().set_middleware(Timings).to_service(module).unwrap(); + + let rpc_svc = HttpServerBuilder::new().set_middleware(Timings).to_service(module, remote_addr).unwrap(); println!("[run_server]: Tower builder"); let tower_svc = tower::ServiceBuilder::new() diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 60e38e06d3..7a71999a1d 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -96,7 +96,7 @@ impl Builder { } } -impl Builder { +impl Builder { /// Add a middleware to the builder [`Middleware`](../jsonrpsee_core/middleware/trait.Middleware.html). /// /// ``` @@ -251,7 +251,7 @@ impl Builder { ) -> Result, Error> { Ok(Server { access_control: self.access_control, - listener: Some(listener), + listener, local_addr: Some(local_addr), max_request_body_size: self.max_request_body_size, max_response_body_size: self.max_response_body_size, @@ -295,7 +295,7 @@ impl Builder { let listener = hyper::Server::from_tcp(listener)?; Ok(Server { - listener: Some(listener), + listener, local_addr, access_control: self.access_control, max_request_body_size: self.max_request_body_size, @@ -331,7 +331,7 @@ impl Builder { let listener = hyper::Server::from_tcp(listener)?.tcp_nodelay(true); Ok(Server { - listener: Some(listener), + listener, local_addr, access_control: self.access_control, max_request_body_size: self.max_request_body_size, @@ -346,17 +346,17 @@ impl Builder { } /// Returns a service that can be utilised with `tower` compatible crates. - pub fn to_service(self, methods: impl Into) -> Result, Error> { + pub fn to_service(self, methods: impl Into, addr: SocketAddr) -> Result, Error> { let methods = methods.into().initialize_resources(&self.resources)?; Ok(TowerService { inner: TowerServiceData { - remote_addr: None, + remote_addr: addr, methods, - acl: self.access_control, - resources: self.resources, - middleware: self.middleware, - health_api: self.health_api, + acl: self.access_control.clone(), + resources: self.resources.clone(), + middleware: self.middleware.clone(), + health_api: self.health_api.clone(), max_request_body_size: self.max_response_body_size, max_response_body_size: self.max_response_body_size, max_log_length: self.max_log_length, @@ -407,7 +407,7 @@ impl Future for ServerHandle { #[derive(Debug, Clone)] struct TowerServiceData { /// Remote server address. - remote_addr: Option, + remote_addr: SocketAddr, /// Registered server methods. methods: Methods, /// Access control. @@ -449,12 +449,6 @@ impl TowerServiceData { batch_requests_supported, } = self; - let remote_addr = match remote_addr { - Some(addr) => addr, - // Default RPC port - None => SocketAddr::from(([127, 0, 0, 1], 9944)), - }; - let request_start = middleware.on_request(remote_addr, request.headers()); let keys = request.headers().keys().map(|k| k.as_str()); @@ -579,7 +573,7 @@ impl hyper::service::Service> for Tow #[derive(Debug)] pub struct Server { /// Hyper server. - listener: Option>, + listener: HyperBuilder, /// Local address local_addr: Option, /// Max request body size. @@ -625,7 +619,7 @@ impl Server { let make_service = make_service_fn(move |conn: &AddrStream| { let service = TowerService { inner: TowerServiceData { - remote_addr: Some(conn.remote_addr()), + remote_addr: conn.remote_addr(), methods: methods.clone(), acl: acl.clone(), resources: resources.clone(), @@ -649,8 +643,7 @@ impl Server { }; let handle = rt.spawn(async move { - // TODO: Hande unwrap. - let server = listener.unwrap().serve(make_service); + let server = listener.serve(make_service); let _ = server.with_graceful_shutdown(async move { rx.next().await.map_or((), |_| ()) }).await; }); From 0f432368c37578448598704cf24f8f2d7b58d32b Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 8 Aug 2022 16:55:45 +0300 Subject: [PATCH 19/45] Rename the inner service data and check comments Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 0b5162800e..39e0337730 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -351,7 +351,7 @@ impl Builder { let methods = methods.into().initialize_resources(&self.resources)?; Ok(TowerService { - inner: TowerServiceData { + inner: ServiceData { remote_addr: addr, methods, acl: self.access_control.clone(), @@ -404,9 +404,9 @@ impl Future for ServerHandle { } } -/// Data required by the server to expose the a tower service. +/// Data required by the server to handle requests. #[derive(Debug, Clone)] -struct TowerServiceData { +struct ServiceData { /// Remote server address. remote_addr: SocketAddr, /// Registered server methods. @@ -431,13 +431,13 @@ struct TowerServiceData { batch_requests_supported: bool, } -impl TowerServiceData { - /// Default behavior for RPSee handling of requests. +impl ServiceData { + /// Default behavior for handling the RPC requests. async fn handle_request( self, request: hyper::Request, ) -> Result, HyperError> { - let TowerServiceData { + let ServiceData { remote_addr, methods, acl, @@ -551,7 +551,7 @@ impl TowerServiceData { /// This is similar to [`hyper::service::service_fn`]. #[derive(Debug)] pub struct TowerService { - inner: TowerServiceData, + inner: ServiceData, } impl hyper::service::Service> for TowerService { @@ -619,7 +619,7 @@ impl Server { let make_service = make_service_fn(move |conn: &AddrStream| { let service = TowerService { - inner: TowerServiceData { + inner: ServiceData { remote_addr: conn.remote_addr(), methods: methods.clone(), acl: acl.clone(), From c328f86301670d6e0480b15da1c0f33153b532dc Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 8 Aug 2022 17:03:05 +0300 Subject: [PATCH 20/45] examples: Add comments Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index 77d16d80d4..ba26a847b5 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -41,6 +41,10 @@ use jsonrpsee::core::logger::{self, Params, Request}; use jsonrpsee::http_client::HttpClientBuilder; use jsonrpsee::http_server::{HttpServerBuilder, RpcModule}; +/// Define a custom logging mechanism to detect the time passed +/// between receiving the request and proving the response. +/// +/// The implementation relies upon [logger::HttpLogger]. #[derive(Clone)] struct Timings; @@ -74,7 +78,6 @@ async fn main() -> anyhow::Result<()> { let addr = run_server().await?; let url = format!("http://{}", addr); - println!("[main]: URL {:?}", url); let client = HttpClientBuilder::default().build(&url)?; let response: String = client.request("say_hello", None).await?; @@ -82,26 +85,22 @@ async fn main() -> anyhow::Result<()> { let _response: Result = client.request("unknown_method", None).await; let _ = client.request::("say_hello", None).await?; - // Make the same request again. - let _ = client.request::("say_hello", None).await?; - Ok(()) } async fn run_server() -> anyhow::Result { - let addr = SocketAddr::from(([127, 0, 0, 1], 9935)); - + // Construct a custom service for handling the RPC requests. let make_service = make_service_fn(move |conn: &AddrStream| { let remote_addr = conn.remote_addr(); async move { let mut module = RpcModule::new(()); module.register_method("say_hello", |_, _| Ok("lo")).unwrap(); - println!("[run_server]: Creating RPC service"); - + // Obtain the tower service relying on the RPC implementation. + // NOTE: RPC's logger can be chained with tower. let rpc_svc = HttpServerBuilder::new().set_logger(Timings).to_service(module, remote_addr).unwrap(); - println!("[run_server]: Tower builder"); + // Chain multiple tower compatible layers on top of the RPC's service. let tower_svc = tower::ServiceBuilder::new() // Add high level tracing/logging to all requests .layer( @@ -121,10 +120,8 @@ async fn run_server() -> anyhow::Result { } }); - tokio::spawn(async move { - println!("[run_server]: Bind server"); - Server::bind(&addr).serve(make_service).await - }); + let addr = SocketAddr::from(([127, 0, 0, 1], 9935)); + tokio::spawn(async move { Server::bind(&addr).serve(make_service).await }); // Race with server start / client connect present in all examples tokio::time::sleep(Duration::from_secs(5)).await; From 602d4818c14561abe4be059e6ca7500002793a63 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 9 Aug 2022 13:01:39 +0300 Subject: [PATCH 21/45] http-server: Receive tower service builder as param Signed-off-by: Alexandru Vasile --- http-server/Cargo.toml | 1 + http-server/src/server.rs | 97 +++++++++++++++++++++++++++++---------- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/http-server/Cargo.toml b/http-server/Cargo.toml index 9558805bdf..a3225641d4 100644 --- a/http-server/Cargo.toml +++ b/http-server/Cargo.toml @@ -20,6 +20,7 @@ tracing-futures = "0.2.5" serde_json = { version = "1.0", features = ["raw_value"] } serde = "1" tokio = { version = "1.16", features = ["rt-multi-thread", "macros"] } +tower = "0.4.13" [dev-dependencies] tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 39e0337730..35993e99b4 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -34,11 +34,12 @@ use crate::response::{internal_error, malformed}; use futures_channel::mpsc; use futures_util::future::FutureExt; use futures_util::stream::{StreamExt, TryStreamExt}; +use hyper::body::HttpBody; use hyper::header::{HeaderMap, HeaderValue}; use hyper::server::conn::AddrStream; use hyper::server::{conn::AddrIncoming, Builder as HyperBuilder}; -use hyper::service::make_service_fn; -use hyper::{Error as HyperError, Method}; +use hyper::service::{make_service_fn, Service}; +use hyper::{Body, Error as HyperError, Method}; use jsonrpsee_core::error::{Error, GenericTransportError}; use jsonrpsee_core::http_helpers::{self, read_body}; use jsonrpsee_core::logger::{self, HttpLogger as Logger}; @@ -51,8 +52,10 @@ use jsonrpsee_core::tracing::{rx_log_from_json, rx_log_from_str, tx_log_from_str use jsonrpsee_core::TEN_MB_SIZE_BYTES; use jsonrpsee_types::error::{ErrorCode, ErrorObject, BATCHES_NOT_SUPPORTED_CODE, BATCHES_NOT_SUPPORTED_MSG}; use jsonrpsee_types::{Id, Notification, Params, Request}; +use serde::de::StdError; use serde_json::value::RawValue; use tokio::net::{TcpListener, ToSocketAddrs}; +use tower::Layer; use tracing_futures::Instrument; type Notif<'a> = Notification<'a, Option<&'a RawValue>>; @@ -96,7 +99,7 @@ impl Builder { } } -impl Builder { +impl Builder { /// Add a logger to the builder [`Logger`](../jsonrpsee_core/logger/trait.Logger.html). /// /// ``` @@ -345,26 +348,6 @@ impl Builder { health_api: self.health_api, }) } - - /// Returns a service that can be utilised with `tower` compatible crates. - pub fn to_service(self, methods: impl Into, addr: SocketAddr) -> Result, Error> { - let methods = methods.into().initialize_resources(&self.resources)?; - - Ok(TowerService { - inner: ServiceData { - remote_addr: addr, - methods, - acl: self.access_control.clone(), - resources: self.resources.clone(), - logger: self.logger.clone(), - health_api: self.health_api.clone(), - max_request_body_size: self.max_response_body_size, - max_response_body_size: self.max_response_body_size, - max_log_length: self.max_log_length, - batch_requests_supported: self.batch_requests_supported, - }, - }) - } } #[derive(Debug, Clone)] @@ -603,6 +586,72 @@ impl Server { self.local_addr.ok_or_else(|| Error::Custom("Local address not found".into())) } + /// Start the server with a custom tower builder. + pub fn start_with_builder( + mut self, + methods: impl Into, + builder: tower::ServiceBuilder, + ) -> Result + where + T: Layer> + Send + 'static, + >>::Service: Send + + Service< + hyper::Request, + Response = hyper::Response, + Error = Box<(dyn StdError + Send + Sync + 'static)>, + >, + <>>::Service as Service>>::Future: Send, + U: HttpBody + Send + 'static, + ::Error: Send + Sync + StdError, + ::Data: Send, + { + let max_request_body_size = self.max_request_body_size; + let max_response_body_size = self.max_response_body_size; + let max_log_length = self.max_log_length; + let acl = self.access_control; + let (tx, mut rx) = mpsc::channel(1); + let listener = self.listener; + let resources = self.resources; + let logger = self.logger; + let batch_requests_supported = self.batch_requests_supported; + let methods = methods.into().initialize_resources(&resources)?; + let health_api = self.health_api; + + let make_service = make_service_fn(move |conn: &AddrStream| { + let service = TowerService { + inner: ServiceData { + remote_addr: conn.remote_addr(), + methods: methods.clone(), + acl: acl.clone(), + resources: resources.clone(), + logger: logger.clone(), + health_api: health_api.clone(), + max_request_body_size, + max_response_body_size, + max_log_length, + batch_requests_supported, + }, + }; + + let server = builder.service(service); + // For every request the `TowerService` is calling into `ServiceData::handle_request` + // where the RPSee bare implementation resides. + async move { Ok::<_, HyperError>(server) } + }); + + let rt = match self.tokio_runtime.take() { + Some(rt) => rt, + None => tokio::runtime::Handle::current(), + }; + + let handle = rt.spawn(async move { + let server = listener.serve(make_service); + let _ = server.with_graceful_shutdown(async move { rx.next().await.map_or((), |_| ()) }).await; + }); + + Ok(ServerHandle { handle: Some(handle), stop_sender: tx }) + } + /// Start the server. pub fn start(mut self, methods: impl Into) -> Result { let max_request_body_size = self.max_request_body_size; @@ -633,7 +682,7 @@ impl Server { }, }; - // For every request the `TowerService` is calling into `TowerServiceData::handle_request` + // For every request the `TowerService` is calling into `ServiceData::handle_request` // where the RPSee bare implementation resides. async move { Ok::<_, HyperError>(service) } }); From d64d1af48fffd30fca81722500b927159b7b8d43 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 9 Aug 2022 13:02:15 +0300 Subject: [PATCH 22/45] examples: Adjust tower_http example Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 73 +++++++++++++-------------------- 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index ba26a847b5..30ee967ed2 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -25,10 +25,7 @@ // DEALINGS IN THE SOFTWARE. use hyper::body::Bytes; -use hyper::server::conn::AddrStream; -use hyper::service::make_service_fn; -use hyper::{Body, Server}; -use std::convert::Infallible; +use hyper::Body; use std::iter::once; use std::net::SocketAddr; use std::time::{Duration, Instant}; @@ -39,7 +36,7 @@ use tower_http::LatencyUnit; use jsonrpsee::core::client::ClientT; use jsonrpsee::core::logger::{self, Params, Request}; use jsonrpsee::http_client::HttpClientBuilder; -use jsonrpsee::http_server::{HttpServerBuilder, RpcModule}; +use jsonrpsee::http_server::{HttpServerBuilder, HttpServerHandle, RpcModule}; /// Define a custom logging mechanism to detect the time passed /// between receiving the request and proving the response. @@ -76,7 +73,7 @@ async fn main() -> anyhow::Result<()> { .try_init() .expect("setting default subscriber failed"); - let addr = run_server().await?; + let (addr, _handler) = run_server().await?; let url = format!("http://{}", addr); let client = HttpClientBuilder::default().build(&url)?; @@ -88,43 +85,29 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -async fn run_server() -> anyhow::Result { - // Construct a custom service for handling the RPC requests. - let make_service = make_service_fn(move |conn: &AddrStream| { - let remote_addr = conn.remote_addr(); - async move { - let mut module = RpcModule::new(()); - module.register_method("say_hello", |_, _| Ok("lo")).unwrap(); - - // Obtain the tower service relying on the RPC implementation. - // NOTE: RPC's logger can be chained with tower. - let rpc_svc = HttpServerBuilder::new().set_logger(Timings).to_service(module, remote_addr).unwrap(); - - // Chain multiple tower compatible layers on top of the RPC's service. - let tower_svc = tower::ServiceBuilder::new() - // Add high level tracing/logging to all requests - .layer( - TraceLayer::new_for_http() - .on_body_chunk(|chunk: &Bytes, latency: Duration, _: &tracing::Span| { - tracing::trace!(size_bytes = chunk.len(), latency = ?latency, "sending body chunk") - }) - .make_span_with(DefaultMakeSpan::new().include_headers(true)) - .on_response(DefaultOnResponse::new().include_headers(true).latency_unit(LatencyUnit::Micros)), - ) - // Mark the `Authorization` request header as sensitive so it doesn't show in logs - .layer(SetSensitiveRequestHeadersLayer::new(once(hyper::header::AUTHORIZATION))) - .timeout(Duration::from_secs(2)) - .service(rpc_svc); - - Ok::<_, Infallible>(tower_svc) - } - }); - - let addr = SocketAddr::from(([127, 0, 0, 1], 9935)); - tokio::spawn(async move { Server::bind(&addr).serve(make_service).await }); - - // Race with server start / client connect present in all examples - tokio::time::sleep(Duration::from_secs(5)).await; - - Ok(addr) +async fn run_server() -> anyhow::Result<(SocketAddr, HttpServerHandle)> { + // Custom tower service to handle the RPC requests + let builder = tower::ServiceBuilder::new() + // Add high level tracing/logging to all requests + .layer( + TraceLayer::new_for_http() + .on_body_chunk(|chunk: &Bytes, latency: Duration, _: &tracing::Span| { + tracing::trace!(size_bytes = chunk.len(), latency = ?latency, "sending body chunk") + }) + .make_span_with(DefaultMakeSpan::new().include_headers(true)) + .on_response(DefaultOnResponse::new().include_headers(true).latency_unit(LatencyUnit::Micros)), + ) + // Mark the `Authorization` request header as sensitive so it doesn't show in logs + .layer(SetSensitiveRequestHeadersLayer::new(once(hyper::header::AUTHORIZATION))) + .timeout(Duration::from_secs(2)); + + let server = HttpServerBuilder::new().set_logger(Timings).build("127.0.0.1:0".parse::()?).await?; + let addr = server.local_addr()?; + + let mut module = RpcModule::new(()); + module.register_method("say_hello", |_, _| Ok("lo")).unwrap(); + + let handler = server.start_with_builder(module, builder)?; + + Ok((addr, handler)) } From 69441f346b8cbce0987be32444ff9ea710cd4495 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 9 Aug 2022 16:49:21 +0300 Subject: [PATCH 23/45] http-server: Add tower middleware on the HttpBuilder Signed-off-by: Alexandru Vasile --- examples/examples/tower_http.rs | 42 ++------ http-server/src/server.rs | 172 +++++++++++++++++--------------- 2 files changed, 100 insertions(+), 114 deletions(-) diff --git a/examples/examples/tower_http.rs b/examples/examples/tower_http.rs index 30ee967ed2..dfa3f1d1e4 100644 --- a/examples/examples/tower_http.rs +++ b/examples/examples/tower_http.rs @@ -24,48 +24,20 @@ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +//! This example sets a custom tower service middleware to the RPC implementation. + use hyper::body::Bytes; -use hyper::Body; use std::iter::once; use std::net::SocketAddr; -use std::time::{Duration, Instant}; +use std::time::Duration; use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; use tower_http::LatencyUnit; use jsonrpsee::core::client::ClientT; -use jsonrpsee::core::logger::{self, Params, Request}; use jsonrpsee::http_client::HttpClientBuilder; use jsonrpsee::http_server::{HttpServerBuilder, HttpServerHandle, RpcModule}; -/// Define a custom logging mechanism to detect the time passed -/// between receiving the request and proving the response. -/// -/// The implementation relies upon [logger::HttpLogger]. -#[derive(Clone)] -struct Timings; - -impl logger::HttpLogger for Timings { - type Instant = Instant; - - fn on_request(&self, remote_addr: SocketAddr, request: &Request) -> Self::Instant { - println!("[Logger::on_request] remote_addr {}, request: {:?}", remote_addr, request); - Instant::now() - } - - fn on_call(&self, name: &str, params: Params, kind: logger::MethodKind) { - println!("[Logger::on_call] method: '{}', params: {:?}, kind: {}", name, params, kind); - } - - fn on_result(&self, name: &str, success: bool, started_at: Self::Instant) { - println!("[Logger::on_result] '{}', worked? {}, time elapsed {:?}", name, success, started_at.elapsed()); - } - - fn on_response(&self, result: &str, started_at: Self::Instant) { - println!("[Logger::on_response] result: {}, time elapsed {:?}", result, started_at.elapsed()); - } -} - #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::FmtSubscriber::builder() @@ -87,7 +59,7 @@ async fn main() -> anyhow::Result<()> { async fn run_server() -> anyhow::Result<(SocketAddr, HttpServerHandle)> { // Custom tower service to handle the RPC requests - let builder = tower::ServiceBuilder::new() + let service_builder = tower::ServiceBuilder::new() // Add high level tracing/logging to all requests .layer( TraceLayer::new_for_http() @@ -101,13 +73,15 @@ async fn run_server() -> anyhow::Result<(SocketAddr, HttpServerHandle)> { .layer(SetSensitiveRequestHeadersLayer::new(once(hyper::header::AUTHORIZATION))) .timeout(Duration::from_secs(2)); - let server = HttpServerBuilder::new().set_logger(Timings).build("127.0.0.1:0".parse::()?).await?; + let server = + HttpServerBuilder::new().set_middleware(service_builder).build("127.0.0.1:0".parse::()?).await?; + let addr = server.local_addr()?; let mut module = RpcModule::new(()); module.register_method("say_hello", |_, _| Ok("lo")).unwrap(); - let handler = server.start_with_builder(module, builder)?; + let handler = server.start(module)?; Ok((addr, handler)) } diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 35993e99b4..a8adefd8e4 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -24,6 +24,7 @@ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +use std::convert::Infallible; use std::future::Future; use std::net::{SocketAddr, TcpListener as StdTcpListener}; use std::pin::Pin; @@ -34,6 +35,7 @@ use crate::response::{internal_error, malformed}; use futures_channel::mpsc; use futures_util::future::FutureExt; use futures_util::stream::{StreamExt, TryStreamExt}; +use futures_util::TryFutureExt; use hyper::body::HttpBody; use hyper::header::{HeaderMap, HeaderValue}; use hyper::server::conn::AddrStream; @@ -55,6 +57,7 @@ use jsonrpsee_types::{Id, Notification, Params, Request}; use serde::de::StdError; use serde_json::value::RawValue; use tokio::net::{TcpListener, ToSocketAddrs}; +use tower::layer::util::Identity; use tower::Layer; use tracing_futures::Instrument; @@ -62,7 +65,7 @@ type Notif<'a> = Notification<'a, Option<&'a RawValue>>; /// Builder to create JSON-RPC HTTP server. #[derive(Debug)] -pub struct Builder { +pub struct Builder { /// Access control based on HTTP headers. access_control: AccessControl, resources: Resources, @@ -74,6 +77,7 @@ pub struct Builder { logger: L, max_log_length: u32, health_api: Option, + service_builder: tower::ServiceBuilder, } impl Default for Builder { @@ -88,6 +92,7 @@ impl Default for Builder { logger: (), max_log_length: 4096, health_api: None, + service_builder: tower::ServiceBuilder::new(), } } } @@ -99,9 +104,11 @@ impl Builder { } } -impl Builder { +impl Builder { /// Add a logger to the builder [`Logger`](../jsonrpsee_core/logger/trait.Logger.html). /// + /// # Examples + /// /// ``` /// use std::{time::Instant, net::SocketAddr}; /// use hyper::Request; @@ -141,7 +148,7 @@ impl Builder { /// /// let builder = HttpServerBuilder::new().set_logger(MyLogger); /// ``` - pub fn set_logger(self, logger: T) -> Builder { + pub fn set_logger(self, logger: T) -> Builder { Builder { access_control: self.access_control, max_request_body_size: self.max_request_body_size, @@ -152,6 +159,7 @@ impl Builder { logger, max_log_length: self.max_log_length, health_api: self.health_api, + service_builder: self.service_builder, } } @@ -216,8 +224,49 @@ impl Builder { Ok(self) } + /// Configure a custom [`tower::ServiceBuilder`] middleware for composing layers to be applied to the RPC service. + /// + /// Default: No tower layers are applied to the RPC service. + /// + /// # Examples + /// + /// ```rust + /// + /// use std::time::Duration; + /// use std::net::SocketAddr; + /// use jsonrpsee_http_server::HttpServerBuilder; + /// + /// #[tokio::main] + /// async fn main() { + /// let builder = tower::ServiceBuilder::new() + /// .timeout(Duration::from_secs(2)); + /// + /// let server = HttpServerBuilder::new() + /// .set_middleware(builder) + /// .build("127.0.0.1:0".parse::().unwrap()) + /// .await + /// .unwrap(); + /// } + /// ``` + pub fn set_middleware(self, service_builder: tower::ServiceBuilder) -> Builder { + Builder { + access_control: self.access_control, + max_request_body_size: self.max_request_body_size, + max_response_body_size: self.max_response_body_size, + batch_requests_supported: self.batch_requests_supported, + resources: self.resources, + tokio_runtime: self.tokio_runtime, + logger: self.logger, + max_log_length: self.max_log_length, + health_api: self.health_api, + service_builder, + } + } + /// Finalizes the configuration of the server with customized TCP settings on the socket and on hyper. /// + /// # Examples + /// /// ```rust /// use jsonrpsee_http_server::HttpServerBuilder; /// use socket2::{Domain, Socket, Type}; @@ -252,7 +301,7 @@ impl Builder { self, listener: hyper::server::Builder, local_addr: SocketAddr, - ) -> Result, Error> { + ) -> Result, Error> { Ok(Server { access_control: self.access_control, listener, @@ -265,6 +314,7 @@ impl Builder { logger: self.logger, max_log_length: self.max_log_length, health_api: self.health_api, + service_builder: self.service_builder, }) } @@ -292,7 +342,7 @@ impl Builder { /// let server = HttpServerBuilder::new().build_from_tcp(socket).unwrap(); /// } /// ``` - pub fn build_from_tcp(self, listener: impl Into) -> Result, Error> { + pub fn build_from_tcp(self, listener: impl Into) -> Result, Error> { let listener = listener.into(); let local_addr = listener.local_addr().ok(); @@ -310,6 +360,7 @@ impl Builder { logger: self.logger, max_log_length: self.max_log_length, health_api: self.health_api, + service_builder: self.service_builder, }) } @@ -328,7 +379,7 @@ impl Builder { /// assert!(jsonrpsee_http_server::HttpServerBuilder::default().build(addrs).await.is_ok()); /// } /// ``` - pub async fn build(self, addrs: impl ToSocketAddrs) -> Result, Error> { + pub async fn build(self, addrs: impl ToSocketAddrs) -> Result, Error> { let listener = TcpListener::bind(addrs).await?.into_std()?; let local_addr = listener.local_addr().ok(); @@ -346,6 +397,7 @@ impl Builder { logger: self.logger, max_log_length: self.max_log_length, health_api: self.health_api, + service_builder: self.service_builder, }) } } @@ -419,7 +471,7 @@ impl ServiceData { async fn handle_request( self, request: hyper::Request, - ) -> Result, HyperError> { + ) -> Result, Infallible> { let ServiceData { remote_addr, methods, @@ -539,7 +591,14 @@ pub struct TowerService { impl hyper::service::Service> for TowerService { type Response = hyper::Response; - type Error = hyper::Error; + + // NOTE(lexnv): The `handle_request` method returns `Result<_, Infallible>`. + // This is because the RPC service will always return a valid HTTP response (ie return `Ok(_)`). + // + // The following associated type is required by the `impl Server` bounds. + // It satisfies the server's bounds when the `tower::ServiceBuilder` is not set (ie `B: Identity`). + type Error = Box<(dyn StdError + Send + Sync + 'static)>; + type Future = Pin> + Send>>; /// Opens door for back pressure implementation. @@ -549,13 +608,16 @@ impl hyper::service::Service> for TowerSe fn call(&mut self, request: hyper::Request) -> Self::Future { let data = self.inner.clone(); - Box::pin(data.handle_request(request)) + // Note that `handle_request` will never return error. + // The dummy error is set in place to satisfy the server's trait bounds regarding the + // `tower::ServiceBuilder` and the error will never be mapped. + Box::pin(data.handle_request(request).map_err(|_| "".into())) } } /// An HTTP JSON RPC server. #[derive(Debug)] -pub struct Server { +pub struct Server { /// Hyper server. listener: HyperBuilder, /// Local address @@ -578,80 +640,28 @@ pub struct Server { tokio_runtime: Option, logger: L, health_api: Option, + service_builder: tower::ServiceBuilder, } -impl Server { +impl Server +where + B: Layer> + Send + 'static, + >>::Service: Send + + Service< + hyper::Request, + Response = hyper::Response, + Error = Box<(dyn StdError + Send + Sync + 'static)>, + >, + <>>::Service as Service>>::Future: Send, + U: HttpBody + Send + 'static, + ::Error: Send + Sync + StdError, + ::Data: Send, +{ /// Returns socket address to which the server is bound. pub fn local_addr(&self) -> Result { self.local_addr.ok_or_else(|| Error::Custom("Local address not found".into())) } - /// Start the server with a custom tower builder. - pub fn start_with_builder( - mut self, - methods: impl Into, - builder: tower::ServiceBuilder, - ) -> Result - where - T: Layer> + Send + 'static, - >>::Service: Send - + Service< - hyper::Request, - Response = hyper::Response, - Error = Box<(dyn StdError + Send + Sync + 'static)>, - >, - <>>::Service as Service>>::Future: Send, - U: HttpBody + Send + 'static, - ::Error: Send + Sync + StdError, - ::Data: Send, - { - let max_request_body_size = self.max_request_body_size; - let max_response_body_size = self.max_response_body_size; - let max_log_length = self.max_log_length; - let acl = self.access_control; - let (tx, mut rx) = mpsc::channel(1); - let listener = self.listener; - let resources = self.resources; - let logger = self.logger; - let batch_requests_supported = self.batch_requests_supported; - let methods = methods.into().initialize_resources(&resources)?; - let health_api = self.health_api; - - let make_service = make_service_fn(move |conn: &AddrStream| { - let service = TowerService { - inner: ServiceData { - remote_addr: conn.remote_addr(), - methods: methods.clone(), - acl: acl.clone(), - resources: resources.clone(), - logger: logger.clone(), - health_api: health_api.clone(), - max_request_body_size, - max_response_body_size, - max_log_length, - batch_requests_supported, - }, - }; - - let server = builder.service(service); - // For every request the `TowerService` is calling into `ServiceData::handle_request` - // where the RPSee bare implementation resides. - async move { Ok::<_, HyperError>(server) } - }); - - let rt = match self.tokio_runtime.take() { - Some(rt) => rt, - None => tokio::runtime::Handle::current(), - }; - - let handle = rt.spawn(async move { - let server = listener.serve(make_service); - let _ = server.with_graceful_shutdown(async move { rx.next().await.map_or((), |_| ()) }).await; - }); - - Ok(ServerHandle { handle: Some(handle), stop_sender: tx }) - } - /// Start the server. pub fn start(mut self, methods: impl Into) -> Result { let max_request_body_size = self.max_request_body_size; @@ -682,9 +692,11 @@ impl Server { }, }; + let server = self.service_builder.service(service); + // For every request the `TowerService` is calling into `ServiceData::handle_request` // where the RPSee bare implementation resides. - async move { Ok::<_, HyperError>(service) } + async move { Ok::<_, HyperError>(server) } }); let rt = match self.tokio_runtime.take() { @@ -749,7 +761,7 @@ struct ProcessValidatedRequest { /// Process a verified request, it implies a POST request with content type JSON. async fn process_validated_request( input: ProcessValidatedRequest, -) -> Result, HyperError> { +) -> Result, Infallible> { let ProcessValidatedRequest { request, logger, @@ -825,7 +837,7 @@ async fn process_health_request( max_response_body_size: u32, request_start: L::Instant, max_log_length: u32, -) -> Result, HyperError> { +) -> Result, Infallible> { let trace = RpcTracing::method_call(&health_api.method); async { tx_log_from_str("HTTP health API", max_log_length); From 47c1b4dca6bedb330f4e14b80aabfc07419abd61 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Tue, 9 Aug 2022 17:00:37 +0300 Subject: [PATCH 24/45] http-server: Do not expose the internal `TowerService` for now Signed-off-by: Alexandru Vasile --- http-server/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/lib.rs b/http-server/src/lib.rs index ab3fd0b61c..fab3147bb8 100644 --- a/http-server/src/lib.rs +++ b/http-server/src/lib.rs @@ -38,7 +38,7 @@ pub mod response; pub use jsonrpsee_core::server::access_control::{AccessControl, AccessControlBuilder}; pub use jsonrpsee_core::server::rpc_module::RpcModule; pub use jsonrpsee_types as types; -pub use server::{Builder as HttpServerBuilder, Server as HttpServer, ServerHandle as HttpServerHandle, TowerService}; +pub use server::{Builder as HttpServerBuilder, Server as HttpServer, ServerHandle as HttpServerHandle}; pub use tracing; #[cfg(test)] From 7369bcb7576842e13887addac4f60e858876a7e5 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Date: Tue, 9 Aug 2022 17:18:56 +0300 Subject: [PATCH 25/45] Update http-server/src/server.rs Co-authored-by: Niklas Adolfsson --- http-server/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index a8adefd8e4..dbc0eb0882 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -597,7 +597,7 @@ impl hyper::service::Service> for TowerSe // // The following associated type is required by the `impl Server` bounds. // It satisfies the server's bounds when the `tower::ServiceBuilder` is not set (ie `B: Identity`). - type Error = Box<(dyn StdError + Send + Sync + 'static)>; + type Error = Box; type Future = Pin> + Send>>; From c026b0a16972e06e6592bdc81d08fc7c00111cb3 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 10 Aug 2022 14:20:23 +0300 Subject: [PATCH 26/45] http-server: Use `std::error::Error` Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index dbc0eb0882..eb7ed9ab56 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -54,7 +54,7 @@ use jsonrpsee_core::tracing::{rx_log_from_json, rx_log_from_str, tx_log_from_str use jsonrpsee_core::TEN_MB_SIZE_BYTES; use jsonrpsee_types::error::{ErrorCode, ErrorObject, BATCHES_NOT_SUPPORTED_CODE, BATCHES_NOT_SUPPORTED_MSG}; use jsonrpsee_types::{Id, Notification, Params, Request}; -use serde::de::StdError; +use std::error::Error as StdError; use serde_json::value::RawValue; use tokio::net::{TcpListener, ToSocketAddrs}; use tower::layer::util::Identity; From de1276d3baab6996b49780ed4cff64e6071593a8 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 10 Aug 2022 14:23:49 +0300 Subject: [PATCH 27/45] Fix fmt Signed-off-by: Alexandru Vasile --- http-server/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index eb7ed9ab56..e391e44225 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -54,8 +54,8 @@ use jsonrpsee_core::tracing::{rx_log_from_json, rx_log_from_str, tx_log_from_str use jsonrpsee_core::TEN_MB_SIZE_BYTES; use jsonrpsee_types::error::{ErrorCode, ErrorObject, BATCHES_NOT_SUPPORTED_CODE, BATCHES_NOT_SUPPORTED_MSG}; use jsonrpsee_types::{Id, Notification, Params, Request}; -use std::error::Error as StdError; use serde_json::value::RawValue; +use std::error::Error as StdError; use tokio::net::{TcpListener, ToSocketAddrs}; use tower::layer::util::Identity; use tower::Layer; From 55192229c6d545490248b2c41661b7d5d9d56f0e Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 10 Aug 2022 16:00:44 +0300 Subject: [PATCH 28/45] http-server: Remove header and CORS validation Signed-off-by: Alexandru Vasile --- http-server/src/response.rs | 9 --------- http-server/src/server.rs | 13 +------------ 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/http-server/src/response.rs b/http-server/src/response.rs index 6a35556304..4a0b2a576f 100644 --- a/http-server/src/response.rs +++ b/http-server/src/response.rs @@ -65,15 +65,6 @@ pub fn invalid_allow_origin() -> hyper::Response { ) } -/// Create a text/plain response for invalid CORS "Allow-*" headers. -pub fn invalid_allow_headers() -> hyper::Response { - from_template( - hyper::StatusCode::FORBIDDEN, - "Requested headers are not allowed for CORS. CORS headers would not be sent and any side-effects were cancelled as well.\n".to_owned(), - TEXT, - ) -} - /// Create a json response for oversized requests (413) pub fn too_large(limit: u32) -> hyper::Response { let error = serde_json::to_string(&ErrorResponse::borrowed(reject_too_big_request(limit), Id::Null)) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index e391e44225..8dd8b32f95 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -487,9 +487,6 @@ impl ServiceData { let request_start = logger.on_request(remote_addr, &request); - let keys = request.headers().keys().map(|k| k.as_str()); - let cors_request_headers = http_helpers::get_cors_request_headers(request.headers()); - let host = match http_helpers::read_header_value(request.headers(), "host") { Some(origin) => origin, None => return Ok(malformed()), @@ -506,11 +503,6 @@ impl ServiceData { return Ok(response::invalid_allow_origin()); } - if let Err(e) = acl.verify_headers(keys, cors_request_headers) { - tracing::warn!("Denied request: {:?}", e); - return Ok(response::invalid_allow_headers()); - } - // Only `POST` and `OPTIONS` methods are allowed. match *request.method() { // An OPTIONS request is a CORS preflight request. We've done our access check @@ -521,13 +513,10 @@ impl ServiceData { None => return Ok(malformed()), }; - let allowed_headers = acl.allowed_headers().to_cors_header_value(); - let allowed_header_bytes = allowed_headers.as_bytes(); - let res = hyper::Response::builder() .header("access-control-allow-origin", origin) .header("access-control-allow-methods", "POST") - .header("access-control-allow-headers", allowed_header_bytes) + .header("access-control-allow-headers", "*".as_bytes()) .body(hyper::Body::empty()) .unwrap_or_else(|e| { tracing::error!("Error forming preflight response: {}", e); From fa373b309da48ec8b35b2200c9ba14a0690bab85 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Wed, 10 Aug 2022 16:03:37 +0300 Subject: [PATCH 29/45] core: Remove CORS logic Signed-off-by: Alexandru Vasile --- core/src/server/access_control/cors.rs | 596 ---------------------- core/src/server/access_control/host.rs | 12 +- core/src/server/access_control/matcher.rs | 4 +- core/src/server/access_control/mod.rs | 82 +-- core/src/server/access_control/origin.rs | 359 +++++++++++++ examples/examples/cors_server.rs | 2 +- 6 files changed, 379 insertions(+), 676 deletions(-) delete mode 100644 core/src/server/access_control/cors.rs create mode 100644 core/src/server/access_control/origin.rs diff --git a/core/src/server/access_control/cors.rs b/core/src/server/access_control/cors.rs deleted file mode 100644 index 4f428ba128..0000000000 --- a/core/src/server/access_control/cors.rs +++ /dev/null @@ -1,596 +0,0 @@ -// Copyright 2019-2021 Parity Technologies (UK) Ltd. -// -// Permission is hereby granted, free of charge, to any -// person obtaining a copy of this software and associated -// documentation files (the "Software"), to deal in the -// Software without restriction, including without -// limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software -// is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice -// shall be included in all copies or substantial portions -// of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -//! CORS handling utility functions - -use std::collections::HashSet; -use std::{fmt, ops}; - -use crate::server::access_control::host::{Host, Port}; -use crate::server::access_control::matcher::{Matcher, Pattern}; -use crate::Cow; -use lazy_static::lazy_static; -use unicase::Ascii; - -/// Origin Protocol -#[derive(Clone, Hash, Debug, PartialEq, Eq)] -pub enum OriginProtocol { - /// Http protocol - Http, - /// Https protocol - Https, - /// Custom protocol - Custom(String), -} - -/// Request Origin -#[derive(Clone, PartialEq, Eq, Debug, Hash)] -pub struct Origin { - protocol: OriginProtocol, - host: Host, - host_with_proto: String, - matcher: Matcher, -} - -impl> From for Origin { - fn from(origin: T) -> Self { - Origin::parse(origin.as_ref()) - } -} - -impl Origin { - fn with_host(protocol: OriginProtocol, host: Host) -> Self { - let host_with_proto = Self::host_with_proto(&protocol, &host); - let matcher = Matcher::new(&host_with_proto); - - Origin { protocol, host, host_with_proto, matcher } - } - - /// Creates new origin given protocol, hostname and port parts. - /// Pre-processes input data if necessary. - pub fn new>(protocol: OriginProtocol, host: &str, port: T) -> Self { - Self::with_host(protocol, Host::new(host, port)) - } - - /// Attempts to parse given string as a `Origin`. - /// NOTE: This method always succeeds and falls back to sensible defaults. - pub fn parse(origin: &str) -> Self { - let mut parts = origin.split("://"); - let proto = parts.next().expect("split always returns non-empty iterator."); - let hostname = parts.next(); - - let (proto, hostname) = match hostname { - None => (None, proto), - Some(hostname) => (Some(proto), hostname), - }; - - let proto = proto.map(str::to_lowercase); - let hostname = Host::parse(hostname); - - let protocol = match proto { - None => OriginProtocol::Http, - Some(ref p) if p == "http" => OriginProtocol::Http, - Some(ref p) if p == "https" => OriginProtocol::Https, - Some(other) => OriginProtocol::Custom(other), - }; - - Origin::with_host(protocol, hostname) - } - - fn host_with_proto(protocol: &OriginProtocol, host: &Host) -> String { - format!( - "{}://{}", - match *protocol { - OriginProtocol::Http => "http", - OriginProtocol::Https => "https", - OriginProtocol::Custom(ref protocol) => protocol, - }, - &**host, - ) - } -} - -impl Pattern for Origin { - fn matches>(&self, other: T) -> bool { - self.matcher.matches(other) - } -} - -impl ops::Deref for Origin { - type Target = str; - fn deref(&self) -> &Self::Target { - &self.host_with_proto - } -} - -/// Origins allowed to access -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AllowOrigin { - /// Specific origin. - Origin(Origin), - /// null-origin (file:///, sandboxed iframe) - Null, - /// Any non-null origin - Any, -} - -impl fmt::Display for AllowOrigin { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}", - match *self { - Self::Any => "*", - Self::Null => "null", - Self::Origin(ref val) => val, - } - ) - } -} - -impl> From for AllowOrigin { - fn from(s: T) -> Self { - match s.into().as_str() { - "all" | "*" | "any" => Self::Any, - "null" => Self::Null, - origin => Self::Origin(origin.into()), - } - } -} - -/// Headers allowed to access -#[derive(Debug, Clone, PartialEq)] -pub enum AllowHeaders { - /// Specific headers - Only(Vec), - /// Any header - Any, -} - -impl AllowHeaders { - /// Return an appropriate value for the CORS header "Access-Control-Allow-Headers". - pub fn to_cors_header_value(&self) -> Cow<'_, str> { - match self { - AllowHeaders::Any => "*".into(), - AllowHeaders::Only(headers) => headers.join(", ").into(), - } - } -} - -/// CORS response headers -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AllowCors { - /// CORS header was not required. Origin is not present in the request. - NotRequired, - /// CORS header is not returned, Origin is not allowed to access the resource. - Invalid, - /// CORS header to include in the response. Origin is allowed to access the resource. - Ok(T), -} - -impl AllowCors { - /// Maps `Ok` variant of `AllowCors`. - pub fn map(self, f: F) -> AllowCors - where - F: FnOnce(T) -> O, - { - use self::AllowCors::*; - - match self { - NotRequired => NotRequired, - Invalid => Invalid, - Ok(val) => Ok(f(val)), - } - } -} - -impl From> for Option { - fn from(cors: AllowCors) -> Option { - use self::AllowCors::*; - - match cors { - NotRequired | Invalid => None, - Ok(header) => Some(header), - } - } -} - -/// Returns correct CORS header (if any) given list of allowed origins and current origin. -pub(super) fn get_cors_allow_origin( - origin: Option<&str>, - allowed: &Option>, - host: Option<&str>, -) -> AllowCors { - match origin { - None => AllowCors::NotRequired, - Some(ref origin) => { - if let Some(host) = host { - // Request initiated from the same server. - if origin.ends_with(host) { - // Additional check - let origin = Origin::parse(origin); - if &*origin.host == host { - return AllowCors::NotRequired; - } - } - } - - match allowed.as_ref() { - None if *origin == "null" => AllowCors::Ok(AllowOrigin::Null), - None => AllowCors::Ok(AllowOrigin::Origin(Origin::parse(origin))), - Some(allowed) if *origin == "null" => allowed - .iter() - .find(|cors| **cors == AllowOrigin::Null) - .cloned() - .map(AllowCors::Ok) - .unwrap_or(AllowCors::Invalid), - Some(allowed) => allowed - .iter() - .find(|cors| match **cors { - AllowOrigin::Any => true, - AllowOrigin::Origin(ref val) if val.matches(origin) => true, - _ => false, - }) - .map(|_| AllowOrigin::Origin(Origin::parse(origin))) - .map(AllowCors::Ok) - .unwrap_or(AllowCors::Invalid), - } - } - } -} - -/// Validates if the headers in the request are allowed. -/// -/// headers: all the headers in the request. -/// cors_request_headers: `values` in the `access-control-request-headers` header. -/// cors_allow_headers: whitelisted headers by the user. -pub(crate) fn get_cors_allow_headers, O, F: Fn(T) -> O>( - mut headers: impl Iterator, - cors_request_headers: impl Iterator, - cors_allow_headers: &AllowHeaders, - to_result: F, -) -> AllowCors> { - // Check if the header fields which were sent in the request are allowed - if let AllowHeaders::Only(only) = cors_allow_headers { - let are_all_allowed = headers.all(|header| { - let name = &Ascii::new(header.as_ref()); - only.iter().any(|h| Ascii::new(&*h) == name) || ALWAYS_ALLOWED_HEADERS.contains(name) - }); - - if !are_all_allowed { - return AllowCors::Invalid; - } - } - - // Check if `AccessControlRequestHeaders` contains fields which were allowed - let (filtered, headers) = match cors_allow_headers { - AllowHeaders::Any => { - let headers = cors_request_headers.map(to_result).collect(); - (false, headers) - } - AllowHeaders::Only(only) => { - let mut filtered = false; - let headers: Vec<_> = cors_request_headers - .filter(|header| { - let name = &Ascii::new(header.as_ref()); - filtered = true; - only.iter().any(|h| Ascii::new(&*h) == name) || ALWAYS_ALLOWED_HEADERS.contains(name) - }) - .map(to_result) - .collect(); - - (filtered, headers) - } - }; - - if headers.is_empty() { - if filtered { - AllowCors::Invalid - } else { - AllowCors::NotRequired - } - } else { - AllowCors::Ok(headers) - } -} - -lazy_static! { - /// Returns headers which are always allowed. - static ref ALWAYS_ALLOWED_HEADERS: HashSet> = { - let mut hs = HashSet::new(); - hs.insert(Ascii::new("Accept")); - hs.insert(Ascii::new("Accept-Language")); - hs.insert(Ascii::new("Access-Control-Request-Headers")); - hs.insert(Ascii::new("Content-Language")); - hs.insert(Ascii::new("Content-Type")); - hs.insert(Ascii::new("Host")); - hs.insert(Ascii::new("Origin")); - hs.insert(Ascii::new("Content-Length")); - hs.insert(Ascii::new("Connection")); - hs.insert(Ascii::new("User-Agent")); - hs - }; -} - -#[cfg(test)] -mod tests { - use std::iter; - - use super::*; - use crate::server::access_control::host::Host; - - #[test] - fn should_parse_origin() { - use self::OriginProtocol::*; - - assert_eq!(Origin::parse("http://parity.io"), Origin::new(Http, "parity.io", None)); - assert_eq!(Origin::parse("https://parity.io:8443"), Origin::new(Https, "parity.io", Some(8443))); - assert_eq!( - Origin::parse("chrome-extension://124.0.0.1"), - Origin::new(Custom("chrome-extension".into()), "124.0.0.1", None) - ); - assert_eq!(Origin::parse("parity.io/somepath"), Origin::new(Http, "parity.io", None)); - assert_eq!(Origin::parse("127.0.0.1:8545/somepath"), Origin::new(Http, "127.0.0.1", Some(8545))); - } - - #[test] - fn should_not_allow_partially_matching_origin() { - // given - let origin1 = Origin::parse("http://subdomain.somedomain.io"); - let origin2 = Origin::parse("http://somedomain.io:8080"); - let host = Host::parse("http://somedomain.io"); - - let origin1 = Some(&*origin1); - let origin2 = Some(&*origin2); - let host = Some(&*host); - - // when - let res1 = get_cors_allow_origin(origin1, &Some(vec![]), host); - let res2 = get_cors_allow_origin(origin2, &Some(vec![]), host); - - // then - assert_eq!(res1, AllowCors::Invalid); - assert_eq!(res2, AllowCors::Invalid); - } - - #[test] - fn should_allow_origins_that_matches_hosts() { - // given - let origin = Origin::parse("http://127.0.0.1:8080"); - let host = Host::parse("http://127.0.0.1:8080"); - - let origin = Some(&*origin); - let host = Some(&*host); - - // when - let res = get_cors_allow_origin(origin, &None, host); - - // then - assert_eq!(res, AllowCors::NotRequired); - } - - #[test] - fn should_return_none_when_there_are_no_cors_domains_and_no_origin() { - // given - let origin = None; - let host = None; - - // when - let res = get_cors_allow_origin(origin, &None, host); - - // then - assert_eq!(res, AllowCors::NotRequired); - } - - #[test] - fn should_return_domain_when_all_are_allowed() { - // given - let origin = Some("parity.io"); - let host = None; - - // when - let res = get_cors_allow_origin(origin, &None, host); - - // then - assert_eq!(res, AllowCors::Ok("parity.io".into())); - } - - #[test] - fn should_return_none_for_empty_origin() { - // given - let origin = None; - let host = None; - - // when - let res = get_cors_allow_origin(origin, &Some(vec![AllowOrigin::Origin("http://ethereum.org".into())]), host); - - // then - assert_eq!(res, AllowCors::NotRequired); - } - - #[test] - fn should_return_none_for_empty_list() { - // given - let origin = None; - let host = None; - - // when - let res = get_cors_allow_origin(origin, &Some(Vec::new()), host); - - // then - assert_eq!(res, AllowCors::NotRequired); - } - - #[test] - fn should_return_none_for_not_matching_origin() { - // given - let origin = Some("http://parity.io"); - let host = None; - - // when - let res = get_cors_allow_origin(origin, &Some(vec![AllowOrigin::Origin("http://ethereum.org".into())]), host); - - // then - assert_eq!(res, AllowCors::Invalid); - } - - #[test] - fn should_return_specific_origin_if_we_allow_any() { - // given - let origin = Some("http://parity.io"); - let host = None; - - // when - let res = get_cors_allow_origin(origin, &Some(vec![AllowOrigin::Any]), host); - - // then - assert_eq!(res, AllowCors::Ok(AllowOrigin::Origin("http://parity.io".into()))); - } - - #[test] - fn should_return_none_if_origin_is_not_defined() { - // given - let origin = None; - let host = None; - - // when - let res = get_cors_allow_origin(origin, &Some(vec![AllowOrigin::Null]), host); - - // then - assert_eq!(res, AllowCors::NotRequired); - } - - #[test] - fn should_return_null_if_origin_is_null() { - // given - let origin = Some("null"); - let host = None; - - // when - let res = get_cors_allow_origin(origin, &Some(vec![AllowOrigin::Null]), host); - - // then - assert_eq!(res, AllowCors::Ok(AllowOrigin::Null)); - } - - #[test] - fn should_return_specific_origin_if_there_is_a_match() { - // given - let origin = Some("http://parity.io"); - let host = None; - - // when - let res = get_cors_allow_origin( - origin, - &Some(vec![ - AllowOrigin::Origin("http://ethereum.org".into()), - AllowOrigin::Origin("http://parity.io".into()), - ]), - host, - ); - - // then - assert_eq!(res, AllowCors::Ok(AllowOrigin::Origin("http://parity.io".into()))); - } - - #[test] - fn should_support_wildcards() { - // given - let origin1 = Some("http://parity.io"); - let origin2 = Some("http://parity.iot"); - let origin3 = Some("chrome-extension://test"); - let host = None; - let allowed = - Some(vec![AllowOrigin::Origin("http://*.io".into()), AllowOrigin::Origin("chrome-extension://*".into())]); - - // when - let res1 = get_cors_allow_origin(origin1, &allowed, host); - let res2 = get_cors_allow_origin(origin2, &allowed, host); - let res3 = get_cors_allow_origin(origin3, &allowed, host); - - // then - assert_eq!(res1, AllowCors::Ok(AllowOrigin::Origin("http://parity.io".into()))); - assert_eq!(res2, AllowCors::Invalid); - assert_eq!(res3, AllowCors::Ok(AllowOrigin::Origin("chrome-extension://test".into()))); - } - - #[test] - fn should_return_invalid_if_header_not_allowed() { - // given - let cors_allow_headers = AllowHeaders::Only(vec!["x-allowed".to_owned()]); - let headers = vec!["Access-Control-Request-Headers"]; - let requested = vec!["x-not-allowed"]; - - // when - let res = get_cors_allow_headers(headers.iter(), requested.iter(), &cors_allow_headers, |x| x); - - // then - assert_eq!(res, AllowCors::Invalid); - } - - #[test] - fn should_return_valid_if_header_allowed() { - // given - let allowed = vec!["x-allowed".to_owned()]; - let cors_allow_headers = AllowHeaders::Only(allowed); - let headers = vec!["Access-Control-Request-Headers"]; - let requested = vec!["x-allowed"]; - - // when - let res = get_cors_allow_headers(headers.iter(), requested.iter(), &cors_allow_headers, |x| (*x).to_owned()); - - // then - let allowed = vec!["x-allowed".to_owned()]; - assert_eq!(res, AllowCors::Ok(allowed)); - } - - #[test] - fn should_return_no_allowed_headers_if_none_in_request() { - // given - let allowed = vec!["x-allowed".to_owned()]; - let cors_allow_headers = AllowHeaders::Only(allowed); - let headers: Vec = vec![]; - - // when - let res = get_cors_allow_headers(headers.iter(), iter::empty(), &cors_allow_headers, |x| x); - - // then - assert_eq!(res, AllowCors::NotRequired); - } - - #[test] - fn should_return_not_required_if_any_header_allowed() { - // given - let cors_allow_headers = AllowHeaders::Any; - let headers: Vec = vec![]; - - // when - let res = get_cors_allow_headers(headers.iter(), iter::empty(), &cors_allow_headers, |x| x); - - // then - assert_eq!(res, AllowCors::NotRequired); - } -} diff --git a/core/src/server/access_control/host.rs b/core/src/server/access_control/host.rs index 30dbc5178f..96d3752982 100644 --- a/core/src/server/access_control/host.rs +++ b/core/src/server/access_control/host.rs @@ -127,7 +127,7 @@ impl Host { } impl Pattern for Host { - fn matches>(&self, other: T) -> bool { + fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { self.matcher.matches(other) } } @@ -186,22 +186,22 @@ mod tests { #[test] fn should_reject_if_header_not_on_the_list() { - assert!((AllowHosts::Only(vec![].into())).verify("parity.io").is_err()); + assert!((AllowHosts::Only(vec![])).verify("parity.io").is_err()); } #[test] fn should_accept_if_on_the_list() { - assert!((AllowHosts::Only(vec!["parity.io".into()].into())).verify("parity.io").is_ok()); + assert!((AllowHosts::Only(vec!["parity.io".into()])).verify("parity.io").is_ok()); } #[test] fn should_accept_if_on_the_list_with_port() { - assert!((AllowHosts::Only(vec!["parity.io:443".into()].into())).verify("parity.io:443").is_ok()); - assert!((AllowHosts::Only(vec!["parity.io".into()].into())).verify("parity.io:443").is_err()); + assert!((AllowHosts::Only(vec!["parity.io:443".into()])).verify("parity.io:443").is_ok()); + assert!((AllowHosts::Only(vec!["parity.io".into()])).verify("parity.io:443").is_err()); } #[test] fn should_support_wildcards() { - assert!((AllowHosts::Only(vec!["*.web3.site:*".into()].into())).verify("parity.web3.site:8180").is_ok()); + assert!((AllowHosts::Only(vec!["*.web3.site:*".into()])).verify("parity.web3.site:8180").is_ok()); } } diff --git a/core/src/server/access_control/matcher.rs b/core/src/server/access_control/matcher.rs index 9479070ef1..b62d1829bf 100644 --- a/core/src/server/access_control/matcher.rs +++ b/core/src/server/access_control/matcher.rs @@ -32,7 +32,7 @@ use tracing::warn; /// Pattern that can be matched to string. pub(crate) trait Pattern { /// Returns true if given string matches the pattern. - fn matches>(&self, other: T) -> bool; + fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool; } #[derive(Clone)] @@ -53,7 +53,7 @@ impl Matcher { } impl Pattern for Matcher { - fn matches>(&self, other: T) -> bool { + fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { let s = other.as_ref(); match self.0 { Some(ref matcher) => matcher.is_match(s), diff --git a/core/src/server/access_control/mod.rs b/core/src/server/access_control/mod.rs index 5796a83455..7a9883a915 100644 --- a/core/src/server/access_control/mod.rs +++ b/core/src/server/access_control/mod.rs @@ -1,22 +1,19 @@ //! Access control based on HTTP headers -pub mod cors; +pub mod origin; pub mod host; mod matcher; -pub use cors::{AllowHeaders, AllowOrigin, Origin}; -pub use host::{AllowHosts, Host}; +pub use origin::{AllowOrigins, OriginType}; +pub use host::{Host, AllowHosts}; use crate::Error; -use self::cors::get_cors_allow_origin; - /// Define access on control on HTTP layer. #[derive(Clone, Debug)] pub struct AccessControl { allowed_hosts: AllowHosts, - allowed_origins: Option>, - allowed_headers: AllowHeaders, + allowed_origins: AllowOrigins, } impl AccessControl { @@ -32,46 +29,13 @@ impl AccessControl { /// `host` is the return value from the `host header` /// `origin` is the value from the `origin header`. pub fn verify_origin(&self, origin: Option<&str>, host: &str) -> Result<(), Error> { - if let cors::AllowCors::Invalid = get_cors_allow_origin(origin, &self.allowed_origins, Some(host)) { - Err(Error::HttpHeaderRejected("origin", origin.unwrap_or("").into())) - } else { - Ok(()) - } - } - - /// Validate incoming request by CORS(`access-control-request-headers`). - /// - /// header_name: all keys of the header in the request - /// cors_request_headers: values of `access-control-request-headers` headers. - /// - pub fn verify_headers(&self, header_names: I, cors_request_headers: II) -> Result<(), Error> - where - T: AsRef, - I: Iterator, - II: Iterator, - { - let header = - cors::get_cors_allow_headers(header_names, cors_request_headers, &self.allowed_headers, |name| name); - - if let cors::AllowCors::Invalid = header { - Err(Error::HttpHeaderRejected( - "access-control-request-headers", - "".into(), - )) - } else { - Ok(()) - } - } - - /// Return the allowed headers we've set - pub fn allowed_headers(&self) -> &AllowHeaders { - &self.allowed_headers + self.allowed_origins.verify(origin, host) } } impl Default for AccessControl { fn default() -> Self { - Self { allowed_hosts: AllowHosts::Any, allowed_origins: None, allowed_headers: AllowHeaders::Any } + Self { allowed_hosts: AllowHosts::Any, allowed_origins: AllowOrigins::Any } } } @@ -79,13 +43,12 @@ impl Default for AccessControl { #[derive(Debug)] pub struct AccessControlBuilder { allowed_hosts: AllowHosts, - allowed_origins: Option>, - allowed_headers: AllowHeaders, + allowed_origins: AllowOrigins, } impl Default for AccessControlBuilder { fn default() -> Self { - Self { allowed_hosts: AllowHosts::Any, allowed_origins: None, allowed_headers: AllowHeaders::Any } + Self { allowed_hosts: AllowHosts::Any, allowed_origins: AllowOrigins::Any } } } @@ -103,13 +66,7 @@ impl AccessControlBuilder { /// Allow all origins. pub fn allow_all_origins(mut self) -> Self { - self.allowed_origins = None; - self - } - - /// Allow all headers. - pub fn allow_all_headers(mut self) -> Self { - self.allowed_headers = AllowHeaders::Any; + self.allowed_origins = AllowOrigins::Any; self } @@ -137,27 +94,11 @@ impl AccessControlBuilder { List: IntoIterator, Origin: Into, { - let allowed_origins: Vec = list.into_iter().map(Into::into).map(Into::into).collect(); + let allowed_origins: Vec = list.into_iter().map(Into::into).map(Into::into).collect(); if allowed_origins.is_empty() { return Err(Error::EmptyAllowList("Origin")); } - self.allowed_origins = Some(allowed_origins); - Ok(self) - } - - /// Configure allowed CORS headers. - /// - /// Default - allow all. - pub fn set_allowed_headers(mut self, list: List) -> Result - where - List: IntoIterator, - Header: Into, - { - let allowed_headers: Vec = list.into_iter().map(Into::into).collect(); - if allowed_headers.is_empty() { - return Err(Error::EmptyAllowList("Header")); - } - self.allowed_headers = AllowHeaders::Only(allowed_headers); + self.allowed_origins = AllowOrigins::Only(allowed_origins); Ok(self) } @@ -166,7 +107,6 @@ impl AccessControlBuilder { AccessControl { allowed_hosts: self.allowed_hosts, allowed_origins: self.allowed_origins, - allowed_headers: self.allowed_headers, } } } diff --git a/core/src/server/access_control/origin.rs b/core/src/server/access_control/origin.rs new file mode 100644 index 0000000000..c5dd08ab7b --- /dev/null +++ b/core/src/server/access_control/origin.rs @@ -0,0 +1,359 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Origin filtering functions + +use std::{fmt, ops}; + +use crate::server::access_control::host::{Host, Port}; +use crate::server::access_control::matcher::{Matcher, Pattern}; +use crate::Error; + +/// Origin Protocol +#[derive(Clone, Hash, Debug, PartialEq, Eq)] +pub enum OriginProtocol { + /// Http protocol + Http, + /// Https protocol + Https, + /// Custom protocol + Custom(String), +} + +/// Request Origin +#[derive(Clone, PartialEq, Eq, Debug, Hash)] +pub struct Origin { + protocol: OriginProtocol, + host: Host, + host_with_proto: String, + matcher: Matcher, +} + +impl> From for Origin { + fn from(origin: T) -> Self { + Origin::parse(origin.as_ref()) + } +} + +impl Origin { + fn with_host(protocol: OriginProtocol, host: Host) -> Self { + let host_with_proto = Self::host_with_proto(&protocol, &host); + let matcher = Matcher::new(&host_with_proto); + + Origin { protocol, host, host_with_proto, matcher } + } + + /// Creates new origin given protocol, hostname and port parts. + /// Pre-processes input data if necessary. + pub fn new>(protocol: OriginProtocol, host: &str, port: T) -> Self { + Self::with_host(protocol, Host::new(host, port)) + } + + /// Attempts to parse given string as a `Origin`. + /// NOTE: This method always succeeds and falls back to sensible defaults. + pub fn parse(origin: &str) -> Self { + let mut parts = origin.split("://"); + let proto = parts.next().expect("split always returns non-empty iterator."); + let hostname = parts.next(); + + let (proto, hostname) = match hostname { + None => (None, proto), + Some(hostname) => (Some(proto), hostname), + }; + + let proto = proto.map(str::to_lowercase); + let hostname = Host::parse(hostname); + + let protocol = match proto { + None => OriginProtocol::Http, + Some(ref p) if p == "http" => OriginProtocol::Http, + Some(ref p) if p == "https" => OriginProtocol::Https, + Some(other) => OriginProtocol::Custom(other), + }; + + Origin::with_host(protocol, hostname) + } + + fn host_with_proto(protocol: &OriginProtocol, host: &Host) -> String { + format!( + "{}://{}", + match *protocol { + OriginProtocol::Http => "http", + OriginProtocol::Https => "https", + OriginProtocol::Custom(ref protocol) => protocol, + }, + &**host, + ) + } +} + +impl Pattern for Origin { + fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { + self.matcher.matches(other) + } +} + +impl ops::Deref for Origin { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.host_with_proto + } +} + +/// Origin type allowed to access. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OriginType { + /// Specific origin. + Origin(Origin), + /// Null origin (file:///, sandboxed iframe) + Null, + /// Any non-null origin. + AnyNonNull, +} + +impl Pattern for OriginType { + fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { + if other.as_ref() == "null" { + return *self == OriginType::Null; + } + + match self { + OriginType::AnyNonNull => true, + OriginType::Null => false, + OriginType::Origin(ref origin) => origin.matches(other), + } + } +} + + +impl fmt::Display for OriginType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match *self { + Self::AnyNonNull => "*", + Self::Null => "null", + Self::Origin(ref val) => val, + } + ) + } +} + +impl> From for OriginType { + fn from(s: T) -> Self { + match s.into().as_str() { + "all" | "*" | "any" => Self::AnyNonNull, + "null" => Self::Null, + origin => Self::Origin(origin.into()), + } + } +} + +/// Policy for validating the `HTTP origin header`. +#[derive(Clone, Debug)] +pub enum AllowOrigins { + /// Allow all origins (no filter). + Any, + /// Allow only specified origins. + Only(Vec), +} + +impl AllowOrigins { + /// Verify a origin. + pub fn verify(&self, origin: Option<&str>, host: &str) -> Result<(), Error> { + // Nothing to be checked if origin is not part of the request's headers. + let origin = match origin { + Some(ref origin) => origin, + None => return Ok(()), + }; + + // Requests initiated from the same server are allowed. + if origin.ends_with(host) { + // Additional check + let origin = Origin::parse(origin); + if &*origin.host == host { + return Ok(()); + } + } + + match self { + AllowOrigins::Any => return Ok(()), + AllowOrigins::Only(list) => { + if list.iter().find(|allowed_origin| allowed_origin.matches(*origin)).is_none() { + return Err(Error::HttpHeaderRejected("origin", origin.to_string())); + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::server::access_control::host::Host; + + #[test] + fn should_parse_origin() { + use self::OriginProtocol::*; + + assert_eq!(Origin::parse("http://parity.io"), Origin::new(Http, "parity.io", None)); + assert_eq!(Origin::parse("https://parity.io:8443"), Origin::new(Https, "parity.io", Some(8443))); + assert_eq!( + Origin::parse("chrome-extension://124.0.0.1"), + Origin::new(Custom("chrome-extension".into()), "124.0.0.1", None) + ); + assert_eq!(Origin::parse("parity.io/somepath"), Origin::new(Http, "parity.io", None)); + assert_eq!(Origin::parse("127.0.0.1:8545/somepath"), Origin::new(Http, "127.0.0.1", Some(8545))); + } + + #[test] + fn should_not_allow_partially_matching_origin() { + let origin1 = Origin::parse("http://subdomain.somedomain.io"); + let origin2 = Origin::parse("http://somedomain.io:8080"); + let host = Host::parse("http://somedomain.io"); + + let origin1 = Some(&*origin1); + let origin2 = Some(&*origin2); + + let allow_origins = AllowOrigins::Only(vec![]); + + assert!(allow_origins.verify(origin1, &*host).is_err()); + assert!(allow_origins.verify(origin2, &*host).is_err()); + } + + #[test] + fn should_allow_origins_that_matches_hosts() { + let origin = Origin::parse("http://127.0.0.1:8080"); + let host = Host::parse("http://127.0.0.1:8080"); + + let origin = Some(&*origin); + let allow_origins = AllowOrigins::Any; + + assert!(allow_origins.verify(origin, &*host).is_ok()); + } + + #[test] + fn should_allow_when_there_are_no_domains_and_no_origin() { + let origin = None; + let host = ""; + let allow_origins = AllowOrigins::Any; + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_allow_domain_when_all_are_allowed() { + let origin = Some("parity.io"); + let host = ""; + let allow_origins = AllowOrigins::Any; + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_allow_for_empty_origin() { + let origin = None; + let host = ""; + let allow_origins = AllowOrigins::Only(vec![OriginType::Origin("http://ethereum.org".into())]); + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_allow_specific_empty_list() { + let origin = None; + let host = ""; + let allow_origins = AllowOrigins::Only(vec![]); + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_deny_for_different_origin() { + let origin = Some("http://parity.io"); + let host = ""; + let allow_origins = AllowOrigins::Only(vec![OriginType::Origin("http://ethereum.org".into())]); + + assert!(allow_origins.verify(origin, host).is_err()); + } + + #[test] + fn should_allow_for_any() { + let origin = Some("http://parity.io"); + let host = ""; + let allow_origins = AllowOrigins::Only(vec![OriginType::AnyNonNull]); + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_allow_if_origin_is_not_defined() { + let origin = None; + let host = ""; + let allow_origins = AllowOrigins::Only(vec![OriginType::Null]); + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_allow_if_origin_is_null() { + let origin = Some("null"); + let host = ""; + let allow_origins = AllowOrigins::Only(vec![OriginType::Null]); + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_allow_if_there_is_a_match() { + let origin = Some("http://parity.io"); + let host = ""; + + let allow_origins = AllowOrigins::Only(vec![ + OriginType::Origin("http://ethereum.org".into()), + OriginType::Origin("http://parity.io".into()), + ]); + + assert!(allow_origins.verify(origin, host).is_ok()); + } + + #[test] + fn should_support_wildcards() { + let origin1 = Some("http://parity.io"); + let origin2 = Some("http://parity.iot"); + let origin3 = Some("chrome-extension://test"); + let host = ""; + let allow_origins = + AllowOrigins::Only(vec![OriginType::Origin("http://*.io".into()), OriginType::Origin("chrome-extension://*".into())]); + + assert!(allow_origins.verify(origin1, host).is_ok()); + assert!(allow_origins.verify(origin2, host).is_err()); + assert!(allow_origins.verify(origin3, host).is_ok()); + } +} diff --git a/examples/examples/cors_server.rs b/examples/examples/cors_server.rs index 4374f8f175..1423ae068a 100644 --- a/examples/examples/cors_server.rs +++ b/examples/examples/cors_server.rs @@ -68,7 +68,7 @@ async fn main() -> anyhow::Result<()> { } async fn run_server() -> anyhow::Result<(SocketAddr, HttpServerHandle)> { - let acl = AccessControlBuilder::new().allow_all_headers().allow_all_origins().allow_all_hosts().build(); + let acl = AccessControlBuilder::new().allow_all_origins().allow_all_hosts().build(); let server = HttpServerBuilder::default().set_access_control(acl).build("127.0.0.1:0".parse::()?).await?; From 1578163b26ef71a715b69dcf2f22cbcd3618e0ed Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Thu, 11 Aug 2022 13:14:19 +0300 Subject: [PATCH 30/45] examples: Add custom CORS layer to the RPC Signed-off-by: Alexandru Vasile --- examples/examples/cors_server.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/examples/examples/cors_server.rs b/examples/examples/cors_server.rs index 1423ae068a..3b12f097a9 100644 --- a/examples/examples/cors_server.rs +++ b/examples/examples/cors_server.rs @@ -24,7 +24,12 @@ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +//! This example adds upstream CORS layers to the RPC service, +//! with access control allowing requests from all hosts. + +use hyper::Method; use std::net::SocketAddr; +use tower_http::cors::{Any, CorsLayer}; use jsonrpsee::{ core::server::access_control::AccessControlBuilder, @@ -68,10 +73,23 @@ async fn main() -> anyhow::Result<()> { } async fn run_server() -> anyhow::Result<(SocketAddr, HttpServerHandle)> { - let acl = AccessControlBuilder::new().allow_all_origins().allow_all_hosts().build(); + // RPC access control that allows all hosts. + let acl = AccessControlBuilder::new().allow_all_hosts().build(); + + // Add a CORS middleware for handling HTTP requests. + let cors = CorsLayer::new() + // Allow `POST` when accessing the resource + .allow_methods([Method::POST]) + // Allow requests from any origin + .allow_origin(Any) + .allow_headers([hyper::header::CONTENT_TYPE]); + let middleware = tower::ServiceBuilder::new().layer(cors); - let server = - HttpServerBuilder::default().set_access_control(acl).build("127.0.0.1:0".parse::()?).await?; + let server = HttpServerBuilder::default() + .set_access_control(acl) + .set_middleware(middleware) + .build("127.0.0.1:0".parse::()?) + .await?; let mut module = RpcModule::new(()); module.register_method("say_hello", |_, _| { From e2f1ed5fc71fe7e91a6d95e35cf7da9d5d7032a3 Mon Sep 17 00:00:00 2001 From: Niklas Adolfsson Date: Fri, 12 Aug 2022 10:00:26 +0200 Subject: [PATCH 31/45] address some grumbles --- http-server/src/server.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index e391e44225..1aabc5cfa6 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -643,8 +643,18 @@ pub struct Server { service_builder: tower::ServiceBuilder, } -impl Server +impl Server { + /// Returns socket address to which the server is bound. + pub fn local_addr(&self) -> Result { + self.local_addr.ok_or_else(|| Error::Custom("Local address not found".into())) + } +} + +// Required trait bounds for the middleware service. +// TODO: consider introducing a marker trait for this. +impl Server where + L: Logger, B: Layer> + Send + 'static, >>::Service: Send + Service< @@ -657,11 +667,6 @@ where ::Error: Send + Sync + StdError, ::Data: Send, { - /// Returns socket address to which the server is bound. - pub fn local_addr(&self) -> Result { - self.local_addr.ok_or_else(|| Error::Custom("Local address not found".into())) - } - /// Start the server. pub fn start(mut self, methods: impl Into) -> Result { let max_request_body_size = self.max_request_body_size; From 3801eb3edff5cca84c00d8516eabc91e93ed09b0 Mon Sep 17 00:00:00 2001 From: Niklas Adolfsson Date: Fri, 12 Aug 2022 12:25:39 +0200 Subject: [PATCH 32/45] fix more grumbles: no more Infallible --- http-server/src/server.rs | 57 +++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 1aabc5cfa6..a2d7152d25 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -24,7 +24,6 @@ // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -use std::convert::Infallible; use std::future::Future; use std::net::{SocketAddr, TcpListener as StdTcpListener}; use std::pin::Pin; @@ -35,7 +34,6 @@ use crate::response::{internal_error, malformed}; use futures_channel::mpsc; use futures_util::future::FutureExt; use futures_util::stream::{StreamExt, TryStreamExt}; -use futures_util::TryFutureExt; use hyper::body::HttpBody; use hyper::header::{HeaderMap, HeaderValue}; use hyper::server::conn::AddrStream; @@ -468,10 +466,7 @@ struct ServiceData { impl ServiceData { /// Default behavior for handling the RPC requests. - async fn handle_request( - self, - request: hyper::Request, - ) -> Result, Infallible> { + async fn handle_request(self, request: hyper::Request) -> hyper::Response { let ServiceData { remote_addr, methods, @@ -492,23 +487,23 @@ impl ServiceData { let host = match http_helpers::read_header_value(request.headers(), "host") { Some(origin) => origin, - None => return Ok(malformed()), + None => return malformed(), }; let maybe_origin = http_helpers::read_header_value(request.headers(), "origin"); if let Err(e) = acl.verify_host(host) { tracing::warn!("Denied request: {:?}", e); - return Ok(response::host_not_allowed()); + return response::host_not_allowed(); } if let Err(e) = acl.verify_origin(maybe_origin, host) { tracing::warn!("Denied request: {:?}", e); - return Ok(response::invalid_allow_origin()); + return response::invalid_allow_origin(); } if let Err(e) = acl.verify_headers(keys, cors_request_headers) { tracing::warn!("Denied request: {:?}", e); - return Ok(response::invalid_allow_headers()); + return response::invalid_allow_headers(); } // Only `POST` and `OPTIONS` methods are allowed. @@ -518,13 +513,13 @@ impl ServiceData { Method::OPTIONS => { let origin = match maybe_origin { Some(origin) => origin, - None => return Ok(malformed()), + None => return malformed(), }; let allowed_headers = acl.allowed_headers().to_cors_header_value(); let allowed_header_bytes = allowed_headers.as_bytes(); - let res = hyper::Response::builder() + hyper::Response::builder() .header("access-control-allow-origin", origin) .header("access-control-allow-methods", "POST") .header("access-control-allow-headers", allowed_header_bytes) @@ -532,9 +527,7 @@ impl ServiceData { .unwrap_or_else(|e| { tracing::error!("Error forming preflight response: {}", e); internal_error() - }); - - Ok(res) + }) } // The actual request. If it's a CORS request we need to remember to add // the access-control-allow-origin header (despite preflight) to allow it @@ -552,12 +545,12 @@ impl ServiceData { batch_requests_supported, request_start, }) - .await?; + .await; if let Some(origin) = origin { res.headers_mut().insert("access-control-allow-origin", origin); } - Ok(res) + res } Method::GET => match health_api.as_ref() { Some(health) if health.path.as_str() == request.uri().path() => { @@ -571,11 +564,11 @@ impl ServiceData { ) .await } - _ => Ok(response::method_not_allowed()), + _ => response::method_not_allowed(), }, // Error scenarios: - Method::POST => Ok(response::unsupported_content_type()), - _ => Ok(response::method_not_allowed()), + Method::POST => response::unsupported_content_type(), + _ => response::method_not_allowed(), } } } @@ -611,7 +604,7 @@ impl hyper::service::Service> for TowerSe // Note that `handle_request` will never return error. // The dummy error is set in place to satisfy the server's trait bounds regarding the // `tower::ServiceBuilder` and the error will never be mapped. - Box::pin(data.handle_request(request).map_err(|_| "".into())) + Box::pin(data.handle_request(request).map(|res| Ok(res))) } } @@ -764,9 +757,7 @@ struct ProcessValidatedRequest { } /// Process a verified request, it implies a POST request with content type JSON. -async fn process_validated_request( - input: ProcessValidatedRequest, -) -> Result, Infallible> { +async fn process_validated_request(input: ProcessValidatedRequest) -> hyper::Response { let ProcessValidatedRequest { request, logger, @@ -783,11 +774,11 @@ async fn process_validated_request( let (body, is_single) = match read_body(&parts.headers, body, max_request_body_size).await { Ok(r) => r, - Err(GenericTransportError::TooLarge) => return Ok(response::too_large(max_request_body_size)), - Err(GenericTransportError::Malformed) => return Ok(response::malformed()), + Err(GenericTransportError::TooLarge) => return response::too_large(max_request_body_size), + Err(GenericTransportError::Malformed) => return response::malformed(), Err(GenericTransportError::Inner(e)) => { tracing::error!("Internal error reading request body: {}", e); - return Ok(response::internal_error()); + return response::internal_error(); } }; @@ -804,7 +795,7 @@ async fn process_validated_request( }; let response = process_single_request(body, call).await; logger.on_response(&response.result, request_start); - Ok(response::ok_response(response.result)) + response::ok_response(response.result) } // Batch of requests or notifications else if !batch_requests_supported { @@ -813,7 +804,7 @@ async fn process_validated_request( ErrorObject::borrowed(BATCHES_NOT_SUPPORTED_CODE, &BATCHES_NOT_SUPPORTED_MSG, None), ); logger.on_response(&err.result, request_start); - Ok(response::ok_response(err.result)) + response::ok_response(err.result) } // Batch of requests or notifications else { @@ -831,7 +822,7 @@ async fn process_validated_request( }) .await; logger.on_response(&response.result, request_start); - Ok(response::ok_response(response.result)) + response::ok_response(response.result) } } @@ -842,7 +833,7 @@ async fn process_health_request( max_response_body_size: u32, request_start: L::Instant, max_log_length: u32, -) -> Result, Infallible> { +) -> hyper::Response { let trace = RpcTracing::method_call(&health_api.method); async { tx_log_from_str("HTTP health API", max_log_length); @@ -874,9 +865,9 @@ async fn process_health_request( let payload: RpcPayload = serde_json::from_str(&response.result) .expect("valid JSON-RPC response must have a result field and be valid JSON; qed"); - Ok(response::ok_response(payload.result.to_string())) + response::ok_response(payload.result.to_string()) } else { - Ok(response::internal_error()) + response::internal_error() } } .instrument(trace.into_span()) From 6aa8b4fac54957ab6669eb14868f568a8c559a30 Mon Sep 17 00:00:00 2001 From: Niklas Adolfsson Date: Fri, 12 Aug 2022 12:29:10 +0200 Subject: [PATCH 33/45] make clippy happy --- http-server/src/server.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index a2d7152d25..c8e9b24a1b 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -585,9 +585,6 @@ pub struct TowerService { impl hyper::service::Service> for TowerService { type Response = hyper::Response; - // NOTE(lexnv): The `handle_request` method returns `Result<_, Infallible>`. - // This is because the RPC service will always return a valid HTTP response (ie return `Ok(_)`). - // // The following associated type is required by the `impl Server` bounds. // It satisfies the server's bounds when the `tower::ServiceBuilder` is not set (ie `B: Identity`). type Error = Box; @@ -604,7 +601,7 @@ impl hyper::service::Service> for TowerSe // Note that `handle_request` will never return error. // The dummy error is set in place to satisfy the server's trait bounds regarding the // `tower::ServiceBuilder` and the error will never be mapped. - Box::pin(data.handle_request(request).map(|res| Ok(res))) + Box::pin(data.handle_request(request).map(Ok)) } } From 5685da9dc1ed8baa751c75407335a4f1124cb7c6 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 12 Aug 2022 13:48:28 +0300 Subject: [PATCH 34/45] Rename tower http example Signed-off-by: Alexandru Vasile --- examples/examples/{tower_http.rs => http_middleware.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/examples/{tower_http.rs => http_middleware.rs} (100%) diff --git a/examples/examples/tower_http.rs b/examples/examples/http_middleware.rs similarity index 100% rename from examples/examples/tower_http.rs rename to examples/examples/http_middleware.rs From 76a947548a3379604e3e76d9b06b5156119a115a Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 12 Aug 2022 17:07:54 +0300 Subject: [PATCH 35/45] http-server: Remove handling of OPTIONS request Signed-off-by: Alexandru Vasile --- http-server/src/response.rs | 8 ++---- http-server/src/server.rs | 49 +++---------------------------------- 2 files changed, 5 insertions(+), 52 deletions(-) diff --git a/http-server/src/response.rs b/http-server/src/response.rs index 4a0b2a576f..301a588d07 100644 --- a/http-server/src/response.rs +++ b/http-server/src/response.rs @@ -56,13 +56,9 @@ pub fn method_not_allowed() -> hyper::Response { ) } -/// Create a text/plain response for invalid CORS "Origin" headers. +/// Create a text/plain response for invalid "Origin" headers. pub fn invalid_allow_origin() -> hyper::Response { - from_template( - hyper::StatusCode::FORBIDDEN, - "Origin of the request is not whitelisted. CORS headers would not be sent and any side-effects were cancelled as well.\n".to_owned(), - TEXT, - ) + from_template(hyper::StatusCode::FORBIDDEN, "Origin of the request is not whitelisted.\n".to_owned(), TEXT) } /// Create a json response for oversized requests (413) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index 8dd8b32f95..ab2580a784 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -31,13 +31,11 @@ use std::pin::Pin; use std::task::{Context, Poll}; use crate::response; -use crate::response::{internal_error, malformed}; use futures_channel::mpsc; use futures_util::future::FutureExt; use futures_util::stream::{StreamExt, TryStreamExt}; use futures_util::TryFutureExt; use hyper::body::HttpBody; -use hyper::header::{HeaderMap, HeaderValue}; use hyper::server::conn::AddrStream; use hyper::server::{conn::AddrIncoming, Builder as HyperBuilder}; use hyper::service::{make_service_fn, Service}; @@ -489,7 +487,7 @@ impl ServiceData { let host = match http_helpers::read_header_value(request.headers(), "host") { Some(origin) => origin, - None => return Ok(malformed()), + None => return Ok(response::malformed()), }; let maybe_origin = http_helpers::read_header_value(request.headers(), "origin"); @@ -503,34 +501,10 @@ impl ServiceData { return Ok(response::invalid_allow_origin()); } - // Only `POST` and `OPTIONS` methods are allowed. + // Only the `POST` method is allowed. match *request.method() { - // An OPTIONS request is a CORS preflight request. We've done our access check - // above so we just need to tell the browser that the request is OK. - Method::OPTIONS => { - let origin = match maybe_origin { - Some(origin) => origin, - None => return Ok(malformed()), - }; - - let res = hyper::Response::builder() - .header("access-control-allow-origin", origin) - .header("access-control-allow-methods", "POST") - .header("access-control-allow-headers", "*".as_bytes()) - .body(hyper::Body::empty()) - .unwrap_or_else(|e| { - tracing::error!("Error forming preflight response: {}", e); - internal_error() - }); - - Ok(res) - } - // The actual request. If it's a CORS request we need to remember to add - // the access-control-allow-origin header (despite preflight) to allow it - // to be read in a browser. Method::POST if content_type_is_json(&request) => { - let origin = return_origin_if_different_from_host(request.headers()).cloned(); - let mut res = process_validated_request(ProcessValidatedRequest { + let res = process_validated_request(ProcessValidatedRequest { request, logger, methods, @@ -543,9 +517,6 @@ impl ServiceData { }) .await?; - if let Some(origin) = origin { - res.headers_mut().insert("access-control-allow-origin", origin); - } Ok(res) } Method::GET => match health_api.as_ref() { @@ -702,20 +673,6 @@ where } } -// Checks the origin and host headers. If they both exist, return the origin if it does not match the host. -// If one of them doesn't exist (origin most probably), or they are identical, return None. -fn return_origin_if_different_from_host(headers: &HeaderMap) -> Option<&HeaderValue> { - if let (Some(origin), Some(host)) = (headers.get("origin"), headers.get("host")) { - if origin != host { - Some(origin) - } else { - None - } - } else { - None - } -} - /// Checks that content type of received request is valid for JSON-RPC. fn content_type_is_json(request: &hyper::Request) -> bool { is_json(request.headers().get("content-type")) From 21a247ad19978ef737aeebd0fe6c52e344331f81 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 12 Aug 2022 17:28:47 +0300 Subject: [PATCH 36/45] tests: Test CORS with external layers Signed-off-by: Alexandru Vasile --- tests/Cargo.toml | 2 ++ tests/tests/helpers.rs | 8 ++++++-- tests/tests/integration_tests.rs | 8 +++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 8192f85006..1001e347ce 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -18,3 +18,5 @@ serde_json = "1" hyper = { version = "0.14", features = ["http1", "client"] } tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } tokio-stream = "0.1" +tower-http = { version = "0.3.4", features = ["full"] } +tower = { version = "0.4.13", features = ["full"] } diff --git a/tests/tests/helpers.rs b/tests/tests/helpers.rs index 778b1e7638..180014e6c9 100644 --- a/tests/tests/helpers.rs +++ b/tests/tests/helpers.rs @@ -36,6 +36,7 @@ use jsonrpsee::ws_server::{WsServerBuilder, WsServerHandle}; use jsonrpsee::RpcModule; use tokio::time::interval; use tokio_stream::wrappers::IntervalStream; +use tower_http::cors::CorsLayer; pub async fn websocket_server_with_subscription() -> (SocketAddr, WsServerHandle) { let server = WsServerBuilder::default().build("127.0.0.1:0").await.unwrap(); @@ -218,12 +219,15 @@ pub async fn websocket_server_with_sleeping_subscription(tx: futures::channel::m } pub async fn http_server() -> (SocketAddr, HttpServerHandle) { - http_server_with_access_control(AccessControlBuilder::default().build()).await + http_server_with_access_control(AccessControlBuilder::default().build(), CorsLayer::default()).await } -pub async fn http_server_with_access_control(acl: AccessControl) -> (SocketAddr, HttpServerHandle) { +pub async fn http_server_with_access_control(acl: AccessControl, cors: CorsLayer) -> (SocketAddr, HttpServerHandle) { + let middleware = tower::ServiceBuilder::new().layer(cors); + let server = HttpServerBuilder::default() .set_access_control(acl) + .set_middleware(middleware) .health_api("/health", "system_health") .unwrap() .build("127.0.0.1:0") diff --git a/tests/tests/integration_tests.rs b/tests/tests/integration_tests.rs index 81fe1153b3..07ecdde702 100644 --- a/tests/tests/integration_tests.rs +++ b/tests/tests/integration_tests.rs @@ -32,6 +32,7 @@ use std::time::Duration; use futures::{channel::mpsc, StreamExt, TryStreamExt}; use helpers::{http_server, http_server_with_access_control, websocket_server, websocket_server_with_subscription}; +use hyper::http::HeaderValue; use jsonrpsee::core::client::{ClientT, IdKind, Subscription, SubscriptionClientT}; use jsonrpsee::core::error::SubscriptionClosed; use jsonrpsee::core::{Error, JsonValue}; @@ -42,6 +43,7 @@ use jsonrpsee::types::error::ErrorObject; use jsonrpsee::ws_client::WsClientBuilder; use tokio::time::interval; use tokio_stream::wrappers::IntervalStream; +use tower_http::cors::CorsLayer; mod helpers; @@ -768,7 +770,11 @@ async fn http_cors_preflight_works() { init_logger(); let acl = AccessControlBuilder::new().set_allowed_origins(vec!["https://foo.com"]).unwrap().build(); - let (server_addr, _handle) = http_server_with_access_control(acl).await; + let cors = CorsLayer::new() + .allow_methods([Method::POST]) + .allow_origin("https://foo.com".parse::().unwrap()) + .allow_headers([hyper::header::CONTENT_TYPE]); + let (server_addr, _handle) = http_server_with_access_control(acl, cors).await; let http_client = Client::new(); let uri = format!("http://{}", server_addr); From fa2a0913a7ce657fcd55e0901ff582a4cd4a395d Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 12 Aug 2022 17:30:07 +0300 Subject: [PATCH 37/45] examples: Document access control and external CORS layer Signed-off-by: Alexandru Vasile --- examples/examples/cors_server.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/examples/examples/cors_server.rs b/examples/examples/cors_server.rs index 3b12f097a9..490a84d42d 100644 --- a/examples/examples/cors_server.rs +++ b/examples/examples/cors_server.rs @@ -73,10 +73,17 @@ async fn main() -> anyhow::Result<()> { } async fn run_server() -> anyhow::Result<(SocketAddr, HttpServerHandle)> { - // RPC access control that allows all hosts. - let acl = AccessControlBuilder::new().allow_all_hosts().build(); + // RPC access control that allows all hosts and all origins. + // Note: the access control does not modify the response headers, + // it only acts as a filter. + // If you need the ORIGIN header to be mirrored back in the response, + // please use the CORS layer. + let acl = AccessControlBuilder::new().allow_all_hosts().allow_all_origins().build(); // Add a CORS middleware for handling HTTP requests. + // This middleware does affect the response, including appropriate + // headers to satisfy CORS. Because any origins are allowed, the + // "Access-Control-Allow-Origin: *" header is appended to the response. let cors = CorsLayer::new() // Allow `POST` when accessing the resource .allow_methods([Method::POST]) @@ -85,6 +92,10 @@ async fn run_server() -> anyhow::Result<(SocketAddr, HttpServerHandle)> { .allow_headers([hyper::header::CONTENT_TYPE]); let middleware = tower::ServiceBuilder::new().layer(cors); + // The RPC exposes the access control for filtering and the middleware for + // modifying requests / responses. These features are independent of one another + // and can also be used separately. + // In this example, we use both features. let server = HttpServerBuilder::default() .set_access_control(acl) .set_middleware(middleware) From 750fb2f358315137b10797ad150e414e1c759a64 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Fri, 12 Aug 2022 18:16:08 +0300 Subject: [PATCH 38/45] Remove unused deps Signed-off-by: Alexandru Vasile --- core/Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 1262650906..01f504a43f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -32,8 +32,6 @@ tokio = { version = "1.16", optional = true } wasm-bindgen-futures = { version = "0.4.19", optional = true } futures-timer = { version = "3", optional = true } globset = { version = "0.4", optional = true } -lazy_static = { version = "1", optional = true } -unicase = { version = "2.6.0", optional = true } http = { version = "0.2.7", optional = true } [features] @@ -48,8 +46,6 @@ server = [ "rand", "tokio/rt", "tokio/sync", - "lazy_static", - "unicase", "http", "hyper", ] From 3b7ca5da8dcda88efe0e5c721bfcbd9c448595d2 Mon Sep 17 00:00:00 2001 From: Niklas Adolfsson Date: Mon, 15 Aug 2022 14:48:22 +0200 Subject: [PATCH 39/45] remove unused CORS code --- core/src/http_helpers.rs | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/core/src/http_helpers.rs b/core/src/http_helpers.rs index 67d10b3d29..c1e16e3ed2 100644 --- a/core/src/http_helpers.rs +++ b/core/src/http_helpers.rs @@ -105,21 +105,9 @@ pub fn read_header_values<'a>( headers.get_all(header_name) } -/// Get the header values from the `access-control-request-headers` header. -pub fn get_cors_request_headers<'a>(headers: &'a hyper::header::HeaderMap) -> impl Iterator { - const ACCESS_CONTROL_REQUEST_HEADERS: &str = "access-control-request-headers"; - - read_header_values(headers, ACCESS_CONTROL_REQUEST_HEADERS) - .iter() - .filter_map(|val| val.to_str().ok()) - .flat_map(|val| val.split(',')) - // The strings themselves might contain leading and trailing whitespaces - .map(|s| s.trim()) -} - #[cfg(test)] mod tests { - use super::{get_cors_request_headers, read_body, read_header_content_length}; + use super::{read_body, read_header_content_length}; #[tokio::test] async fn body_to_bytes_size_limit_works() { @@ -144,23 +132,4 @@ mod tests { headers.insert(hyper::header::CONTENT_LENGTH, "18446744073709551616".parse().unwrap()); assert_eq!(read_header_content_length(&headers), None); } - - #[test] - fn get_cors_headers_works() { - let mut headers = hyper::header::HeaderMap::new(); - - // access-control-request-headers - headers.insert(hyper::header::ACCESS_CONTROL_REQUEST_HEADERS, "Content-Type,x-requested-with".parse().unwrap()); - - let values: Vec<&str> = get_cors_request_headers(&headers).collect(); - assert_eq!(values, vec!["Content-Type", "x-requested-with"]); - - headers.insert( - hyper::header::ACCESS_CONTROL_REQUEST_HEADERS, - "Content-Type, x-requested-with ".parse().unwrap(), - ); - - let values: Vec<&str> = get_cors_request_headers(&headers).collect(); - assert_eq!(values, vec!["Content-Type", "x-requested-with"]); - } } From 0e8a74c2a3c12a3d1a8d0c8eaacf2b4613cfbd96 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 15 Aug 2022 15:56:40 +0300 Subject: [PATCH 40/45] Remove extra lifetime param Signed-off-by: Alexandru Vasile --- core/src/server/access_control/host.rs | 2 +- core/src/server/access_control/matcher.rs | 4 ++-- core/src/server/access_control/origin.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/server/access_control/host.rs b/core/src/server/access_control/host.rs index 96d3752982..c937ee1198 100644 --- a/core/src/server/access_control/host.rs +++ b/core/src/server/access_control/host.rs @@ -127,7 +127,7 @@ impl Host { } impl Pattern for Host { - fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { + fn matches>(&self, other: T) -> bool { self.matcher.matches(other) } } diff --git a/core/src/server/access_control/matcher.rs b/core/src/server/access_control/matcher.rs index b62d1829bf..9479070ef1 100644 --- a/core/src/server/access_control/matcher.rs +++ b/core/src/server/access_control/matcher.rs @@ -32,7 +32,7 @@ use tracing::warn; /// Pattern that can be matched to string. pub(crate) trait Pattern { /// Returns true if given string matches the pattern. - fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool; + fn matches>(&self, other: T) -> bool; } #[derive(Clone)] @@ -53,7 +53,7 @@ impl Matcher { } impl Pattern for Matcher { - fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { + fn matches>(&self, other: T) -> bool { let s = other.as_ref(); match self.0 { Some(ref matcher) => matcher.is_match(s), diff --git a/core/src/server/access_control/origin.rs b/core/src/server/access_control/origin.rs index c5dd08ab7b..a5edb1eea4 100644 --- a/core/src/server/access_control/origin.rs +++ b/core/src/server/access_control/origin.rs @@ -111,7 +111,7 @@ impl Origin { } impl Pattern for Origin { - fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { + fn matches>(&self, other: T) -> bool { self.matcher.matches(other) } } @@ -135,7 +135,7 @@ pub enum OriginType { } impl Pattern for OriginType { - fn matches<'a, T: AsRef + PartialEq<&'a str>>(&self, other: T) -> bool { + fn matches>(&self, other: T) -> bool { if other.as_ref() == "null" { return *self == OriginType::Null; } From f26b40106b4c903d8b66d8047fb223cd5d12ed16 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 15 Aug 2022 16:04:12 +0300 Subject: [PATCH 41/45] Rename `invalid_allow_origin` to `origin_rejected` Signed-off-by: Alexandru Vasile --- http-server/src/response.rs | 10 +++++++--- http-server/src/server.rs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/http-server/src/response.rs b/http-server/src/response.rs index 301a588d07..ffbe6fd82a 100644 --- a/http-server/src/response.rs +++ b/http-server/src/response.rs @@ -56,9 +56,13 @@ pub fn method_not_allowed() -> hyper::Response { ) } -/// Create a text/plain response for invalid "Origin" headers. -pub fn invalid_allow_origin() -> hyper::Response { - from_template(hyper::StatusCode::FORBIDDEN, "Origin of the request is not whitelisted.\n".to_owned(), TEXT) +/// Create a text/plain response for rejected "Origin" headers. +pub fn origin_rejected(origin: Option<&str>) -> hyper::Response { + from_template( + hyper::StatusCode::FORBIDDEN, + format!("Origin: `{}` is not whitelisted.\n", origin.unwrap_or("")), + TEXT, + ) } /// Create a json response for oversized requests (413) diff --git a/http-server/src/server.rs b/http-server/src/server.rs index cd7d2be17e..174b89f485 100644 --- a/http-server/src/server.rs +++ b/http-server/src/server.rs @@ -493,7 +493,7 @@ impl ServiceData { if let Err(e) = acl.verify_origin(maybe_origin, host) { tracing::warn!("Denied request: {:?}", e); - return response::invalid_allow_origin(); + return response::origin_rejected(maybe_origin); } // Only the `POST` method is allowed. From f8783ccf04d86f9fb548aa46399af222d4fffad0 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 15 Aug 2022 16:06:43 +0300 Subject: [PATCH 42/45] Fix clippy Signed-off-by: Alexandru Vasile --- core/src/server/access_control/origin.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/server/access_control/origin.rs b/core/src/server/access_control/origin.rs index a5edb1eea4..c6d8aadbdc 100644 --- a/core/src/server/access_control/origin.rs +++ b/core/src/server/access_control/origin.rs @@ -124,6 +124,7 @@ impl ops::Deref for Origin { } /// Origin type allowed to access. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq)] pub enum OriginType { /// Specific origin. @@ -203,7 +204,7 @@ impl AllowOrigins { match self { AllowOrigins::Any => return Ok(()), AllowOrigins::Only(list) => { - if list.iter().find(|allowed_origin| allowed_origin.matches(*origin)).is_none() { + if !list.iter().any(|allowed_origin| allowed_origin.matches(*origin)) { return Err(Error::HttpHeaderRejected("origin", origin.to_string())); } } From d8a32d880d2c6ec33fa8d5ac1d9c87de0f471c6c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile <60601340+lexnv@users.noreply.github.com> Date: Mon, 15 Aug 2022 16:09:01 +0300 Subject: [PATCH 43/45] Update core/src/server/access_control/origin.rs Co-authored-by: Niklas Adolfsson --- core/src/server/access_control/origin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/server/access_control/origin.rs b/core/src/server/access_control/origin.rs index c6d8aadbdc..1ada228b2f 100644 --- a/core/src/server/access_control/origin.rs +++ b/core/src/server/access_control/origin.rs @@ -131,7 +131,7 @@ pub enum OriginType { Origin(Origin), /// Null origin (file:///, sandboxed iframe) Null, - /// Any non-null origin. + /// Allow all origins i.e, the literal value "*" which is regarded as a wildcard AnyNonNull, } From a6cf3f804336b7e6888b9fbc5bba6d471d1db90c Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 15 Aug 2022 16:10:15 +0300 Subject: [PATCH 44/45] Rename `AnyNonNull` to `Wildcard` Signed-off-by: Alexandru Vasile --- core/src/server/access_control/origin.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/server/access_control/origin.rs b/core/src/server/access_control/origin.rs index 1ada228b2f..29b9160d6c 100644 --- a/core/src/server/access_control/origin.rs +++ b/core/src/server/access_control/origin.rs @@ -129,10 +129,10 @@ impl ops::Deref for Origin { pub enum OriginType { /// Specific origin. Origin(Origin), - /// Null origin (file:///, sandboxed iframe) + /// Null origin (file:///, sandboxed iframe). Null, - /// Allow all origins i.e, the literal value "*" which is regarded as a wildcard - AnyNonNull, + /// Allow all origins i.e, the literal value "*" which is regarded as a wildcard. + Wildcard, } impl Pattern for OriginType { @@ -142,7 +142,7 @@ impl Pattern for OriginType { } match self { - OriginType::AnyNonNull => true, + OriginType::Wildcard => true, OriginType::Null => false, OriginType::Origin(ref origin) => origin.matches(other), } @@ -156,7 +156,7 @@ impl fmt::Display for OriginType { f, "{}", match *self { - Self::AnyNonNull => "*", + Self::Wildcard => "*", Self::Null => "null", Self::Origin(ref val) => val, } @@ -167,7 +167,7 @@ impl fmt::Display for OriginType { impl> From for OriginType { fn from(s: T) -> Self { match s.into().as_str() { - "all" | "*" | "any" => Self::AnyNonNull, + "all" | "*" | "any" => Self::Wildcard, "null" => Self::Null, origin => Self::Origin(origin.into()), } @@ -308,7 +308,7 @@ mod tests { fn should_allow_for_any() { let origin = Some("http://parity.io"); let host = ""; - let allow_origins = AllowOrigins::Only(vec![OriginType::AnyNonNull]); + let allow_origins = AllowOrigins::Only(vec![OriginType::Wildcard]); assert!(allow_origins.verify(origin, host).is_ok()); } From 9857e7fe6a30489537be59e362f5b1be5242b102 Mon Sep 17 00:00:00 2001 From: Alexandru Vasile Date: Mon, 15 Aug 2022 16:15:28 +0300 Subject: [PATCH 45/45] Rename `OriginType` to `Origin` Signed-off-by: Alexandru Vasile --- core/src/server/access_control/mod.rs | 10 ++-- core/src/server/access_control/origin.rs | 72 ++++++++++++------------ 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/core/src/server/access_control/mod.rs b/core/src/server/access_control/mod.rs index 7a9883a915..84cc02c03b 100644 --- a/core/src/server/access_control/mod.rs +++ b/core/src/server/access_control/mod.rs @@ -4,7 +4,7 @@ pub mod origin; pub mod host; mod matcher; -pub use origin::{AllowOrigins, OriginType}; +pub use origin::{AllowOrigins, Origin}; pub use host::{Host, AllowHosts}; use crate::Error; @@ -89,12 +89,12 @@ impl AccessControlBuilder { /// Configure allowed origins. /// /// Default - allow all. - pub fn set_allowed_origins(mut self, list: List) -> Result + pub fn set_allowed_origins(mut self, list: List) -> Result where - List: IntoIterator, - Origin: Into, + List: IntoIterator, + O: Into, { - let allowed_origins: Vec = list.into_iter().map(Into::into).map(Into::into).collect(); + let allowed_origins: Vec = list.into_iter().map(Into::into).map(Into::into).collect(); if allowed_origins.is_empty() { return Err(Error::EmptyAllowList("Origin")); } diff --git a/core/src/server/access_control/origin.rs b/core/src/server/access_control/origin.rs index 29b9160d6c..3ed62d9d01 100644 --- a/core/src/server/access_control/origin.rs +++ b/core/src/server/access_control/origin.rs @@ -45,25 +45,25 @@ pub enum OriginProtocol { /// Request Origin #[derive(Clone, PartialEq, Eq, Debug, Hash)] -pub struct Origin { +pub struct InnerOrigin { protocol: OriginProtocol, host: Host, host_with_proto: String, matcher: Matcher, } -impl> From for Origin { +impl> From for InnerOrigin { fn from(origin: T) -> Self { - Origin::parse(origin.as_ref()) + InnerOrigin::parse(origin.as_ref()) } } -impl Origin { +impl InnerOrigin { fn with_host(protocol: OriginProtocol, host: Host) -> Self { let host_with_proto = Self::host_with_proto(&protocol, &host); let matcher = Matcher::new(&host_with_proto); - Origin { protocol, host, host_with_proto, matcher } + InnerOrigin { protocol, host, host_with_proto, matcher } } /// Creates new origin given protocol, hostname and port parts. @@ -94,7 +94,7 @@ impl Origin { Some(other) => OriginProtocol::Custom(other), }; - Origin::with_host(protocol, hostname) + InnerOrigin::with_host(protocol, hostname) } fn host_with_proto(protocol: &OriginProtocol, host: &Host) -> String { @@ -110,13 +110,13 @@ impl Origin { } } -impl Pattern for Origin { +impl Pattern for InnerOrigin { fn matches>(&self, other: T) -> bool { self.matcher.matches(other) } } -impl ops::Deref for Origin { +impl ops::Deref for InnerOrigin { type Target = str; fn deref(&self) -> &Self::Target { &self.host_with_proto @@ -126,31 +126,31 @@ impl ops::Deref for Origin { /// Origin type allowed to access. #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Eq)] -pub enum OriginType { +pub enum Origin { /// Specific origin. - Origin(Origin), + Origin(InnerOrigin), /// Null origin (file:///, sandboxed iframe). Null, /// Allow all origins i.e, the literal value "*" which is regarded as a wildcard. Wildcard, } -impl Pattern for OriginType { +impl Pattern for Origin { fn matches>(&self, other: T) -> bool { if other.as_ref() == "null" { - return *self == OriginType::Null; + return *self == Origin::Null; } match self { - OriginType::Wildcard => true, - OriginType::Null => false, - OriginType::Origin(ref origin) => origin.matches(other), + Origin::Wildcard => true, + Origin::Null => false, + Origin::Origin(ref origin) => origin.matches(other), } } } -impl fmt::Display for OriginType { +impl fmt::Display for Origin { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -164,7 +164,7 @@ impl fmt::Display for OriginType { } } -impl> From for OriginType { +impl> From for Origin { fn from(s: T) -> Self { match s.into().as_str() { "all" | "*" | "any" => Self::Wildcard, @@ -180,7 +180,7 @@ pub enum AllowOrigins { /// Allow all origins (no filter). Any, /// Allow only specified origins. - Only(Vec), + Only(Vec), } impl AllowOrigins { @@ -195,7 +195,7 @@ impl AllowOrigins { // Requests initiated from the same server are allowed. if origin.ends_with(host) { // Additional check - let origin = Origin::parse(origin); + let origin = InnerOrigin::parse(origin); if &*origin.host == host { return Ok(()); } @@ -223,20 +223,20 @@ mod tests { fn should_parse_origin() { use self::OriginProtocol::*; - assert_eq!(Origin::parse("http://parity.io"), Origin::new(Http, "parity.io", None)); - assert_eq!(Origin::parse("https://parity.io:8443"), Origin::new(Https, "parity.io", Some(8443))); + assert_eq!(InnerOrigin::parse("http://parity.io"), InnerOrigin::new(Http, "parity.io", None)); + assert_eq!(InnerOrigin::parse("https://parity.io:8443"), InnerOrigin::new(Https, "parity.io", Some(8443))); assert_eq!( - Origin::parse("chrome-extension://124.0.0.1"), - Origin::new(Custom("chrome-extension".into()), "124.0.0.1", None) + InnerOrigin::parse("chrome-extension://124.0.0.1"), + InnerOrigin::new(Custom("chrome-extension".into()), "124.0.0.1", None) ); - assert_eq!(Origin::parse("parity.io/somepath"), Origin::new(Http, "parity.io", None)); - assert_eq!(Origin::parse("127.0.0.1:8545/somepath"), Origin::new(Http, "127.0.0.1", Some(8545))); + assert_eq!(InnerOrigin::parse("parity.io/somepath"), InnerOrigin::new(Http, "parity.io", None)); + assert_eq!(InnerOrigin::parse("127.0.0.1:8545/somepath"), InnerOrigin::new(Http, "127.0.0.1", Some(8545))); } #[test] fn should_not_allow_partially_matching_origin() { - let origin1 = Origin::parse("http://subdomain.somedomain.io"); - let origin2 = Origin::parse("http://somedomain.io:8080"); + let origin1 = InnerOrigin::parse("http://subdomain.somedomain.io"); + let origin2 = InnerOrigin::parse("http://somedomain.io:8080"); let host = Host::parse("http://somedomain.io"); let origin1 = Some(&*origin1); @@ -250,7 +250,7 @@ mod tests { #[test] fn should_allow_origins_that_matches_hosts() { - let origin = Origin::parse("http://127.0.0.1:8080"); + let origin = InnerOrigin::parse("http://127.0.0.1:8080"); let host = Host::parse("http://127.0.0.1:8080"); let origin = Some(&*origin); @@ -281,7 +281,7 @@ mod tests { fn should_allow_for_empty_origin() { let origin = None; let host = ""; - let allow_origins = AllowOrigins::Only(vec![OriginType::Origin("http://ethereum.org".into())]); + let allow_origins = AllowOrigins::Only(vec![Origin::Origin("http://ethereum.org".into())]); assert!(allow_origins.verify(origin, host).is_ok()); } @@ -299,7 +299,7 @@ mod tests { fn should_deny_for_different_origin() { let origin = Some("http://parity.io"); let host = ""; - let allow_origins = AllowOrigins::Only(vec![OriginType::Origin("http://ethereum.org".into())]); + let allow_origins = AllowOrigins::Only(vec![Origin::Origin("http://ethereum.org".into())]); assert!(allow_origins.verify(origin, host).is_err()); } @@ -308,7 +308,7 @@ mod tests { fn should_allow_for_any() { let origin = Some("http://parity.io"); let host = ""; - let allow_origins = AllowOrigins::Only(vec![OriginType::Wildcard]); + let allow_origins = AllowOrigins::Only(vec![Origin::Wildcard]); assert!(allow_origins.verify(origin, host).is_ok()); } @@ -317,7 +317,7 @@ mod tests { fn should_allow_if_origin_is_not_defined() { let origin = None; let host = ""; - let allow_origins = AllowOrigins::Only(vec![OriginType::Null]); + let allow_origins = AllowOrigins::Only(vec![Origin::Null]); assert!(allow_origins.verify(origin, host).is_ok()); } @@ -326,7 +326,7 @@ mod tests { fn should_allow_if_origin_is_null() { let origin = Some("null"); let host = ""; - let allow_origins = AllowOrigins::Only(vec![OriginType::Null]); + let allow_origins = AllowOrigins::Only(vec![Origin::Null]); assert!(allow_origins.verify(origin, host).is_ok()); } @@ -337,8 +337,8 @@ mod tests { let host = ""; let allow_origins = AllowOrigins::Only(vec![ - OriginType::Origin("http://ethereum.org".into()), - OriginType::Origin("http://parity.io".into()), + Origin::Origin("http://ethereum.org".into()), + Origin::Origin("http://parity.io".into()), ]); assert!(allow_origins.verify(origin, host).is_ok()); @@ -351,7 +351,7 @@ mod tests { let origin3 = Some("chrome-extension://test"); let host = ""; let allow_origins = - AllowOrigins::Only(vec![OriginType::Origin("http://*.io".into()), OriginType::Origin("chrome-extension://*".into())]); + AllowOrigins::Only(vec![Origin::Origin("http://*.io".into()), Origin::Origin("chrome-extension://*".into())]); assert!(allow_origins.verify(origin1, host).is_ok()); assert!(allow_origins.verify(origin2, host).is_err());