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 = { version = "0.13", optional = true }

# 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", "base64"]
ipc = ["tokio", "tokio/io-util", "tokio-util", "bytes"]

openssl = ["tokio-tungstenite/native-tls", "reqwest/native-tls"]
Expand Down
9 changes: 9 additions & 0 deletions ethers-providers/src/transports/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ 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, String),
th4s marked this conversation as resolved.
Show resolved Hide resolved
Bearer(String),
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
42 changes: 37 additions & 5 deletions ethers-providers/src/transports/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 All @@ -33,6 +33,7 @@ pub struct Provider {
id: AtomicU64,
client: Client,
url: Url,
authorization: Option<Authorization>,
}

#[derive(Error, Debug)]
Expand Down Expand Up @@ -69,10 +70,19 @@ 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?;
let mut client = self.client.post(self.url.as_ref()).json(&payload);
if let Some(auth) = &self.authorization {
client = match auth {
Authorization::Basic(username, password) => {
client.basic_auth(username, Some(password))
}
Authorization::Bearer(token) => client.bearer_auth(token),
};
}
th4s marked this conversation as resolved.
Show resolved Hide resolved

let res = client.send().await?;
let text = res.text().await?;
let res: Response<R> =
serde_json::from_str(&text).map_err(|err| ClientError::SerdeJson { err, text })?;
Expand All @@ -94,7 +104,24 @@ 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 { id: AtomicU64::new(0), client: Client::new(), url: url.into(), authorization: None }
}

/// 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".into(), "good_password".into()));
/// ```
pub fn new_with_auth(url: impl Into<Url>, auth: Authorization) -> Self {
let mut provider = Self::new(url);
provider.authorization = Some(auth);
provider
th4s marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand All @@ -109,6 +136,11 @@ impl FromStr for Provider {

impl Clone for Provider {
fn clone(&self) -> Self {
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(),
authorization: None,
}
}
}
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
39 changes: 39 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,26 @@ 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 auth_header = match auth {
Authorization::Basic(username, password) => {
String::from("Basic ") + base64::encode(username + ":" + &password).as_str()
th4s marked this conversation as resolved.
Show resolved Hide resolved
}
Authorization::Bearer(token) => String::from("Bearer ") + &token,
th4s marked this conversation as resolved.
Show resolved Hide resolved
};
request
.headers_mut()
.insert(http::header::AUTHORIZATION, http::HeaderValue::from_str(&auth_header)?);
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 +466,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