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`
This commit is contained in:
th4s 2022-01-27 11:04:53 +01:00 committed by GitHub
parent 5da2eb1eb9
commit a97526d6fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 130 additions and 6 deletions

View File

@ -98,6 +98,8 @@
[640](https://github.com/gakonst/ethers-rs/pull/640) [640](https://github.com/gakonst/ethers-rs/pull/640)
### Unreleased ### 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 ### 0.5.3

2
Cargo.lock generated
View File

@ -1274,6 +1274,7 @@ version = "0.6.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"auto_impl", "auto_impl",
"base64 0.13.0",
"bytes", "bytes",
"ethers-core", "ethers-core",
"futures-channel", "futures-channel",
@ -1281,6 +1282,7 @@ dependencies = [
"futures-timer", "futures-timer",
"futures-util", "futures-util",
"hex", "hex",
"http",
"parking_lot", "parking_lot",
"pin-project", "pin-project",
"reqwest", "reqwest",

View File

@ -24,6 +24,8 @@ serde_json = { version = "1.0.64", default-features = false }
thiserror = { version = "1.0.30", default-features = false } thiserror = { version = "1.0.30", default-features = false }
url = { version = "2.2.2", default-features = false } url = { version = "2.2.2", default-features = false }
auto_impl = { version = "0.5.0", 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 # required for implementing stream on the filters
futures-core = { version = "0.3.16", default-features = false } futures-core = { version = "0.3.16", default-features = false }
@ -60,7 +62,7 @@ tempfile = "3.3.0"
[features] [features]
default = ["ws", "rustls"] default = ["ws", "rustls"]
celo = ["ethers-core/celo"] celo = ["ethers-core/celo"]
ws = ["tokio", "tokio-tungstenite"] ws = ["tokio", "tokio-tungstenite", "http"]
ipc = ["tokio", "tokio/io-util", "tokio-util", "bytes"] ipc = ["tokio", "tokio/io-util", "tokio-util", "bytes"]
openssl = ["tokio-tungstenite/native-tls", "reqwest/native-tls"] openssl = ["tokio-tungstenite/native-tls", "reqwest/native-tls"]

View File

@ -94,6 +94,35 @@ impl ResponseData<serde_json::Value> {
} }
} }
/// 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<String>, password: impl Into<String>) -> Self {
let auth_secret = base64::encode(username.into() + ":" + &password.into());
Self::Basic(auth_secret)
}
pub fn bearer(token: impl Into<String>) -> 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -2,7 +2,7 @@
use crate::{provider::ProviderError, JsonRpcClient}; use crate::{provider::ProviderError, JsonRpcClient};
use async_trait::async_trait; 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 serde::{de::DeserializeOwned, Serialize};
use std::{ use std::{
str::FromStr, str::FromStr,
@ -11,7 +11,7 @@ use std::{
use thiserror::Error; use thiserror::Error;
use url::Url; use url::Url;
use super::common::{JsonRpcError, Request, Response}; use super::common::{Authorization, JsonRpcError, Request, Response};
/// A low-level JSON-RPC Client over HTTP. /// A low-level JSON-RPC Client over HTTP.
/// ///
@ -69,7 +69,6 @@ impl JsonRpcClient for Provider {
params: T, params: T,
) -> Result<R, ClientError> { ) -> Result<R, ClientError> {
let next_id = self.id.fetch_add(1, Ordering::SeqCst); let next_id = self.id.fetch_add(1, Ordering::SeqCst);
let payload = Request::new(next_id, method, params); let payload = Request::new(next_id, method, params);
let res = self.client.post(self.url.as_ref()).json(&payload).send().await?; let res = self.client.post(self.url.as_ref()).json(&payload).send().await?;
@ -94,7 +93,49 @@ impl Provider {
/// let provider = Http::new(url); /// let provider = Http::new(url);
/// ``` /// ```
pub fn new(url: impl Into<Url>) -> Self { pub fn new(url: impl Into<Url>) -> 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<Url>,
auth: Authorization,
) -> Result<Self, HttpClientError> {
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<Url>, 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() } 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),
}

View File

@ -1,4 +1,5 @@
mod common; mod common;
pub use common::Authorization;
// only used with WS // only used with WS
#[cfg(feature = "ws")] #[cfg(feature = "ws")]
@ -24,7 +25,7 @@ mod ipc;
pub use ipc::Ipc; pub use ipc::Ipc;
mod http; mod http;
pub use http::{ClientError as HttpClientError, Provider as Http}; pub use self::http::{ClientError as HttpClientError, Provider as Http};
#[cfg(feature = "ws")] #[cfg(feature = "ws")]
mod ws; mod ws;

View File

@ -59,7 +59,11 @@ if_not_wasm! {
type Message = tungstenite::protocol::Message; type Message = tungstenite::protocol::Message;
type WsError = tungstenite::Error; type WsError = tungstenite::Error;
type WsStreamItem = Result<Message, WsError>; type WsStreamItem = Result<Message, WsError>;
use super::Authorization;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use http::Request as HttpRequest;
use http::Uri;
use std::str::FromStr;
} }
type Pending = oneshot::Sender<Result<serde_json::Value, JsonRpcError>>; type Pending = oneshot::Sender<Result<serde_json::Value, JsonRpcError>>;
@ -140,6 +144,22 @@ impl Ws {
Ok(Self::new(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<str> + Unpin,
auth: Authorization,
) -> Result<Self, ClientError> {
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> { fn send(&self, msg: Instruction) -> Result<(), ClientError> {
self.instructions.unbounded_send(msg).map_err(to_client_error) self.instructions.unbounded_send(msg).map_err(to_client_error)
} }
@ -442,6 +462,21 @@ pub enum ClientError {
/// Something caused the websocket to close /// Something caused the websocket to close
#[error("WebSocket connection closed unexpectedly")] #[error("WebSocket connection closed unexpectedly")]
UnexpectedClose, 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<ClientError> for ProviderError { impl From<ClientError> for ProviderError {