-
Notifications
You must be signed in to change notification settings - Fork 224
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
v0.23.x: Add support for HTTP Basic authentication to HTTP and WebSoc…
…ket RPC clients (#1170) * Add support for HTTP Basic authentication to HTTP and WebSocket RPC clients * Add changelog entry * Add unit tests * Remove `username` and `password` fields from `Url` struct * Address review comments * Add more testcases
- Loading branch information
Showing
6 changed files
with
307 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
- `[tendermint-rpc]` Add support for HTTP Basic authentication to HTTP and WebSocket RPC clients | ||
([#1169](https://github.com/informalsystems/tendermint-rs/issues/1169)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
//! Tendermint RPC client implementations for different transports. | ||
|
||
mod auth; | ||
pub mod mock; | ||
mod router; | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
//! This module defines the `Authorization` type for | ||
//! authorizing a HTTP or WebSocket RPC client using | ||
//! HTTP Basic authentication. | ||
|
||
use alloc::string::{String, ToString}; | ||
use core::fmt; | ||
|
||
use http::Uri; | ||
use subtle_encoding::base64; | ||
|
||
/// An HTTP authorization. | ||
/// | ||
/// Currently only HTTP Basic authentication is supported. | ||
#[derive(Clone, Debug, PartialEq, Eq)] | ||
pub enum Authorization { | ||
Basic(String), | ||
} | ||
|
||
impl fmt::Display for Authorization { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match self { | ||
Self::Basic(cred) => write!(f, "Basic {}", cred), | ||
} | ||
} | ||
} | ||
|
||
/// Extract the authorization, if any, from the authority part of the given URI. | ||
/// | ||
/// This authorization can then be supplied to the RPC server via | ||
/// the `Authorization` HTTP header. | ||
pub fn authorize(uri: &Uri) -> Option<Authorization> { | ||
let authority = uri.authority()?; | ||
|
||
if let Some((userpass, _)) = authority.as_str().split_once('@') { | ||
let bytes = base64::encode(userpass); | ||
let credentials = String::from_utf8_lossy(bytes.as_slice()); | ||
Some(Authorization::Basic(credentials.to_string())) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use core::str::FromStr; | ||
|
||
use http::Uri; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn extract_auth_absent() { | ||
let uri = Uri::from_str("http://example.com").unwrap(); | ||
assert_eq!(authorize(&uri), None); | ||
} | ||
|
||
#[test] | ||
fn extract_auth_username_only() { | ||
let uri = Uri::from_str("http://[email protected]").unwrap(); | ||
let base64 = "dG90bw==".to_string(); | ||
assert_eq!(authorize(&uri), Some(Authorization::Basic(base64))); | ||
} | ||
|
||
#[test] | ||
fn extract_auth_username_password() { | ||
let uri = Uri::from_str("http://toto:[email protected]").unwrap(); | ||
let base64 = "dG90bzp0YXRh".to_string(); | ||
assert_eq!(authorize(&uri), Some(Authorization::Basic(base64))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -154,8 +154,10 @@ impl TryFrom<HttpClientUrl> for hyper::Uri { | |
} | ||
|
||
mod sealed { | ||
use crate::client::transport::auth::authorize; | ||
use crate::prelude::*; | ||
use crate::{Error, Response, SimpleRequest}; | ||
use http::header::AUTHORIZATION; | ||
use hyper::body::Buf; | ||
use hyper::client::connect::Connect; | ||
use hyper::client::HttpConnector; | ||
|
@@ -216,6 +218,10 @@ mod sealed { | |
.parse() | ||
.unwrap(), | ||
); | ||
|
||
if let Some(auth) = authorize(&self.uri) { | ||
headers.insert(AUTHORIZATION, auth.to_string().parse().unwrap()); | ||
} | ||
} | ||
|
||
Ok(request) | ||
|
@@ -294,3 +300,41 @@ mod sealed { | |
Ok(response_body) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use core::str::FromStr; | ||
|
||
use http::{header::AUTHORIZATION, Request, Uri}; | ||
use hyper::Body; | ||
|
||
use crate::endpoint::abci_info; | ||
|
||
use super::sealed::HyperClient; | ||
|
||
fn authorization(req: &Request<Body>) -> Option<&str> { | ||
req.headers() | ||
.get(AUTHORIZATION) | ||
.map(|h| h.to_str().unwrap()) | ||
} | ||
|
||
#[test] | ||
fn without_basic_auth() { | ||
let uri = Uri::from_str("http://example.com").unwrap(); | ||
let inner = hyper::Client::new(); | ||
let client = HyperClient::new(uri, inner); | ||
let req = client.build_request(abci_info::Request).unwrap(); | ||
|
||
assert_eq!(authorization(&req), None); | ||
} | ||
|
||
#[test] | ||
fn with_basic_auth() { | ||
let uri = Uri::from_str("http://toto:[email protected]").unwrap(); | ||
let inner = hyper::Client::new(); | ||
let client = HyperClient::new(uri, inner); | ||
let req = client.build_request(abci_info::Request).unwrap(); | ||
|
||
assert_eq!(authorization(&req), Some("Basic dG90bzp0YXRh")); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -258,6 +258,7 @@ mod sealed { | |
}; | ||
|
||
use crate::client::sync::{unbounded, ChannelTx}; | ||
use crate::client::transport::auth::authorize; | ||
use crate::prelude::*; | ||
use crate::query::Query; | ||
use crate::request::Wrapper; | ||
|
@@ -266,7 +267,7 @@ mod sealed { | |
|
||
use async_tungstenite::{ | ||
tokio::{connect_async_with_config, connect_async_with_tls_connector_and_config}, | ||
tungstenite::protocol::WebSocketConfig, | ||
tungstenite::{client::IntoClientRequest, protocol::WebSocketConfig}, | ||
}; | ||
|
||
use tracing::debug; | ||
|
@@ -306,7 +307,6 @@ mod sealed { | |
url: Url, | ||
config: Option<WebSocketConfig>, | ||
) -> Result<(Self, WebSocketClientDriver), Error> { | ||
let url = url.to_string(); | ||
debug!("Connecting to unsecure WebSocket endpoint: {}", url); | ||
|
||
let (stream, _response) = connect_async_with_config(url, config) | ||
|
@@ -338,7 +338,6 @@ mod sealed { | |
url: Url, | ||
config: Option<WebSocketConfig>, | ||
) -> Result<(Self, WebSocketClientDriver), Error> { | ||
let url = url.to_string(); | ||
debug!("Connecting to secure WebSocket endpoint: {}", url); | ||
|
||
// Not supplying a connector means async_tungstenite will create the | ||
|
@@ -476,6 +475,38 @@ mod sealed { | |
} | ||
} | ||
} | ||
|
||
use async_tungstenite::tungstenite; | ||
|
||
impl IntoClientRequest for Url { | ||
fn into_client_request( | ||
self, | ||
) -> tungstenite::Result<tungstenite::handshake::client::Request> { | ||
let uri = self.to_string().parse::<http::Uri>().unwrap(); | ||
|
||
let builder = tungstenite::handshake::client::Request::builder() | ||
.method("GET") | ||
.header("Host", self.host()) | ||
.header("Connection", "Upgrade") | ||
.header("Upgrade", "websocket") | ||
.header("Sec-WebSocket-Version", "13") | ||
.header( | ||
"Sec-WebSocket-Key", | ||
tungstenite::handshake::client::generate_key(), | ||
); | ||
|
||
let builder = if let Some(auth) = authorize(&uri) { | ||
builder.header("Authorization", auth.to_string()) | ||
} else { | ||
builder | ||
}; | ||
|
||
builder | ||
.uri(uri) | ||
.body(()) | ||
.map_err(tungstenite::error::Error::HttpFormat) | ||
} | ||
} | ||
} | ||
|
||
// The different types of commands that can be sent from the WebSocketClient to | ||
|
@@ -805,8 +836,11 @@ mod test { | |
use crate::{request, Id, Method}; | ||
use alloc::collections::BTreeMap as HashMap; | ||
use async_tungstenite::tokio::{accept_async, TokioAdapter}; | ||
use async_tungstenite::tungstenite::client::IntoClientRequest; | ||
use core::str::FromStr; | ||
use futures::StreamExt; | ||
use http::header::AUTHORIZATION; | ||
use http::Uri; | ||
use std::path::PathBuf; | ||
use std::println; | ||
use tendermint_config::net; | ||
|
@@ -1157,4 +1191,26 @@ mod test { | |
); | ||
} | ||
} | ||
|
||
fn authorization(req: &http::Request<()>) -> Option<&str> { | ||
req.headers() | ||
.get(AUTHORIZATION) | ||
.map(|h| h.to_str().unwrap()) | ||
} | ||
|
||
#[test] | ||
fn without_basic_auth() { | ||
let uri = Uri::from_str("http://example.com").unwrap(); | ||
let req = uri.into_client_request().unwrap(); | ||
|
||
assert_eq!(authorization(&req), None); | ||
} | ||
|
||
#[test] | ||
fn with_basic_auth() { | ||
let uri = Uri::from_str("http://toto:[email protected]").unwrap(); | ||
let req = uri.into_client_request().unwrap(); | ||
|
||
assert_eq!(authorization(&req), None); | ||
} | ||
} |
Oops, something went wrong.