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)
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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<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?;
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>>;
|
||||
|
@ -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<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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue