Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Add authorization for http and websocket #829

Merged
merged 10 commits into from
Jan 27, 2022
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion ethers-providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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"]
Expand Down
29 changes: 29 additions & 0 deletions ethers-providers/src/transports/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
mod tests {
use super::*;
Expand Down
61 changes: 57 additions & 4 deletions ethers-providers/src/transports/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
///
Expand Down Expand Up @@ -69,7 +69,6 @@ impl JsonRpcClient for Provider {
params: T,
) -> Result<R, ClientError> {
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?;
Expand All @@ -94,7 +93,49 @@ impl Provider {
/// let provider = Http::new(url);
/// ```
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() }
}
}

Expand All @@ -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),
}
3 changes: 2 additions & 1 deletion ethers-providers/src/transports/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod common;
pub use common::Authorization;

// only used with WS
#[cfg(feature = "ws")]
Expand All @@ -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;
Expand Down
35 changes: 35 additions & 0 deletions ethers-providers/src/transports/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ if_not_wasm! {
type Message = tungstenite::protocol::Message;
type WsError = tungstenite::Error;
type WsStreamItem = Result<Message, WsError>;
use super::Authorization;
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>>;
Expand Down Expand Up @@ -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(
th4s marked this conversation as resolved.
Show resolved Hide resolved
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> {
self.instructions.unbounded_send(msg).map_err(to_client_error)
}
Expand Down Expand Up @@ -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<ClientError> for ProviderError {
Expand Down