From a97526d6fea02a95d33a5174988a6c966f4d379b Mon Sep 17 00:00:00 2001 From: th4s Date: Thu, 27 Jan 2022 11:04:53 +0100 Subject: [PATCH] Add authorization for http and websocket (#829) * Added basic and bearer authentication for http and websocket transport * Improved api for websocket `connect_with_auth` * Bugfix in doc * Moved use statement into non-wasm imports * Adapted changelog * Fixed doc test * Added constructors for Authorization * Improved code quality and implemented feedback * Made bas64 crate for basic auth encoding non-optional * Added `Display` for `Authorization` instead of `into_auth_string` --- CHANGELOG.md | 2 + Cargo.lock | 2 + ethers-providers/Cargo.toml | 4 +- ethers-providers/src/transports/common.rs | 29 +++++++++++ ethers-providers/src/transports/http.rs | 61 +++++++++++++++++++++-- ethers-providers/src/transports/mod.rs | 3 +- ethers-providers/src/transports/ws.rs | 35 +++++++++++++ 7 files changed, 130 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f1d6fb..52b185ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,8 @@ [640](https://github.com/gakonst/ethers-rs/pull/640) ### Unreleased +- Add support for basic and bearer authentication in http and non-wasm websockets. + [829](https://github.com/gakonst/ethers-rs/pull/829) ### 0.5.3 diff --git a/Cargo.lock b/Cargo.lock index 96c34e75..80323912 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,7 @@ version = "0.6.0" dependencies = [ "async-trait", "auto_impl", + "base64 0.13.0", "bytes", "ethers-core", "futures-channel", @@ -1281,6 +1282,7 @@ dependencies = [ "futures-timer", "futures-util", "hex", + "http", "parking_lot", "pin-project", "reqwest", diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml index b68d0652..ca494cb2 100644 --- a/ethers-providers/Cargo.toml +++ b/ethers-providers/Cargo.toml @@ -24,6 +24,8 @@ serde_json = { version = "1.0.64", default-features = false } thiserror = { version = "1.0.30", default-features = false } url = { version = "2.2.2", default-features = false } auto_impl = { version = "0.5.0", default-features = false } +http = { version = "0.2", optional = true } +base64 = "0.13" # required for implementing stream on the filters futures-core = { version = "0.3.16", default-features = false } @@ -60,7 +62,7 @@ tempfile = "3.3.0" [features] default = ["ws", "rustls"] celo = ["ethers-core/celo"] -ws = ["tokio", "tokio-tungstenite"] +ws = ["tokio", "tokio-tungstenite", "http"] ipc = ["tokio", "tokio/io-util", "tokio-util", "bytes"] openssl = ["tokio-tungstenite/native-tls", "reqwest/native-tls"] diff --git a/ethers-providers/src/transports/common.rs b/ethers-providers/src/transports/common.rs index 29917b76..6162bde6 100644 --- a/ethers-providers/src/transports/common.rs +++ b/ethers-providers/src/transports/common.rs @@ -94,6 +94,35 @@ impl ResponseData { } } +/// Basic or bearer authentication in http or websocket transport +/// +/// Use to inject username and password or an auth token into requests +#[derive(Clone, Debug)] +pub enum Authorization { + Basic(String), + Bearer(String), +} + +impl Authorization { + pub fn basic(username: impl Into, password: impl Into) -> Self { + let auth_secret = base64::encode(username.into() + ":" + &password.into()); + Self::Basic(auth_secret) + } + + pub fn bearer(token: impl Into) -> Self { + Self::Bearer(token.into()) + } +} + +impl fmt::Display for Authorization { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Authorization::Basic(auth_secret) => write!(f, "Basic {}", auth_secret), + Authorization::Bearer(token) => write!(f, "Bearer {}", token), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ethers-providers/src/transports/http.rs b/ethers-providers/src/transports/http.rs index 3db8d2f2..87fc57f9 100644 --- a/ethers-providers/src/transports/http.rs +++ b/ethers-providers/src/transports/http.rs @@ -2,7 +2,7 @@ use crate::{provider::ProviderError, JsonRpcClient}; use async_trait::async_trait; -use reqwest::{Client, Error as ReqwestError}; +use reqwest::{header::HeaderValue, Client, Error as ReqwestError}; use serde::{de::DeserializeOwned, Serialize}; use std::{ str::FromStr, @@ -11,7 +11,7 @@ use std::{ use thiserror::Error; use url::Url; -use super::common::{JsonRpcError, Request, Response}; +use super::common::{Authorization, JsonRpcError, Request, Response}; /// A low-level JSON-RPC Client over HTTP. /// @@ -69,7 +69,6 @@ impl JsonRpcClient for Provider { params: T, ) -> Result { let next_id = self.id.fetch_add(1, Ordering::SeqCst); - let payload = Request::new(next_id, method, params); let res = self.client.post(self.url.as_ref()).json(&payload).send().await?; @@ -94,7 +93,49 @@ impl Provider { /// let provider = Http::new(url); /// ``` pub fn new(url: impl Into) -> Self { - Self { id: AtomicU64::new(0), client: Client::new(), url: url.into() } + Self::new_with_client(url, Client::new()) + } + + /// Initializes a new HTTP Client with authentication + /// + /// # Example + /// + /// ``` + /// use ethers_providers::{Authorization, Http}; + /// use url::Url; + /// + /// let url = Url::parse("http://localhost:8545").unwrap(); + /// let provider = Http::new_with_auth(url, Authorization::basic("admin", "good_password")); + /// ``` + pub fn new_with_auth( + url: impl Into, + auth: Authorization, + ) -> Result { + let mut auth_value = HeaderValue::from_str(&auth.to_string())?; + auth_value.set_sensitive(true); + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert(reqwest::header::AUTHORIZATION, auth_value); + + let client = Client::builder().default_headers(headers).build()?; + + Ok(Self::new_with_client(url, client)) + } + + /// Allows to customize the provider by providing your own http client + /// + /// # Example + /// + /// ``` + /// use ethers_providers::Http; + /// use url::Url; + /// + /// let url = Url::parse("http://localhost:8545").unwrap(); + /// let client = reqwest::Client::builder().build().unwrap(); + /// let provider = Http::new_with_client(url, client); + /// ``` + pub fn new_with_client(url: impl Into, client: reqwest::Client) -> Self { + Self { id: AtomicU64::new(0), client, url: url.into() } } } @@ -112,3 +153,15 @@ impl Clone for Provider { Self { id: AtomicU64::new(0), client: self.client.clone(), url: self.url.clone() } } } + +#[derive(Error, Debug)] +/// Error thrown when dealing with Http clients +pub enum HttpClientError { + /// Thrown if unable to build headers for client + #[error(transparent)] + InvalidHeader(#[from] http::header::InvalidHeaderValue), + + /// Thrown if unable to build client + #[error(transparent)] + ClientBuild(#[from] reqwest::Error), +} diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/transports/mod.rs index 1c714eac..b7a6c64e 100644 --- a/ethers-providers/src/transports/mod.rs +++ b/ethers-providers/src/transports/mod.rs @@ -1,4 +1,5 @@ mod common; +pub use common::Authorization; // only used with WS #[cfg(feature = "ws")] @@ -24,7 +25,7 @@ mod ipc; pub use ipc::Ipc; mod http; -pub use http::{ClientError as HttpClientError, Provider as Http}; +pub use self::http::{ClientError as HttpClientError, Provider as Http}; #[cfg(feature = "ws")] mod ws; diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs index 44df77d1..fc1ae882 100644 --- a/ethers-providers/src/transports/ws.rs +++ b/ethers-providers/src/transports/ws.rs @@ -59,7 +59,11 @@ if_not_wasm! { type Message = tungstenite::protocol::Message; type WsError = tungstenite::Error; type WsStreamItem = Result; + use super::Authorization; use tracing::{debug, error, warn}; + use http::Request as HttpRequest; + use http::Uri; + use std::str::FromStr; } type Pending = oneshot::Sender>; @@ -140,6 +144,22 @@ impl Ws { Ok(Self::new(ws)) } + /// Initializes a new WebSocket Client with authentication + #[cfg(not(target_arch = "wasm32"))] + pub async fn connect_with_auth( + uri: impl AsRef + Unpin, + auth: Authorization, + ) -> Result { + let mut request: HttpRequest<()> = + HttpRequest::builder().method("GET").uri(Uri::from_str(uri.as_ref())?).body(())?; + + let mut auth_value = http::HeaderValue::from_str(&auth.to_string())?; + auth_value.set_sensitive(true); + + request.headers_mut().insert(http::header::AUTHORIZATION, auth_value); + Self::connect(request).await + } + fn send(&self, msg: Instruction) -> Result<(), ClientError> { self.instructions.unbounded_send(msg).map_err(to_client_error) } @@ -442,6 +462,21 @@ pub enum ClientError { /// Something caused the websocket to close #[error("WebSocket connection closed unexpectedly")] UnexpectedClose, + + /// Could not create an auth header for websocket handshake + #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] + WsAuth(#[from] http::header::InvalidHeaderValue), + + /// Unable to create a valid Uri + #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] + UriError(#[from] http::uri::InvalidUri), + + /// Unable to create a valid Request + #[error(transparent)] + #[cfg(not(target_arch = "wasm32"))] + RequestError(#[from] http::Error), } impl From for ProviderError {