Skip to content

Commit

Permalink
v0.23.x: Add support for HTTP Basic authentication to HTTP and WebSoc…
Browse files Browse the repository at this point in the history
…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
romac authored Aug 2, 2022
1 parent 2426a65 commit 590a038
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .changelog/unreleased/features/1169-basic-auth.md
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))
1 change: 1 addition & 0 deletions rpc/src/client/transport.rs
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;

Expand Down
70 changes: 70 additions & 0 deletions rpc/src/client/transport/auth.rs
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)));
}
}
44 changes: 44 additions & 0 deletions rpc/src/client/transport/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"));
}
}
62 changes: 59 additions & 3 deletions rpc/src/client/transport/websocket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading

0 comments on commit 590a038

Please sign in to comment.