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:
parent
5da2eb1eb9
commit
a97526d6fe
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue