Skip to content

Commit

Permalink
WIP: implement client side SOCKS5 support
Browse files Browse the repository at this point in the history
  • Loading branch information
elsirion committed Nov 3, 2022
1 parent e649f38 commit 8ec1b4a
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 21 deletions.
1 change: 1 addition & 0 deletions client/http-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ async-trait = "0.1"
rustc-hash = "1"
hyper = { version = "0.14.10", features = ["client", "http1", "http2", "tcp"] }
hyper-rustls = { version = "0.23", optional = true }
hyper-socks2 = "0.7.0"
jsonrpsee-types = { path = "../../types", version = "0.15.1" }
jsonrpsee-core = { path = "../../core", version = "0.15.1", features = ["client", "http-helpers"] }
serde = { version = "1.0", default-features = false, features = ["derive"] }
Expand Down
10 changes: 10 additions & 0 deletions client/http-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use crate::transport::HttpTransportClient;
use crate::types::{ErrorResponse, Id, NotificationSer, RequestSer, Response};
use async_trait::async_trait;
use hyper::http::HeaderMap;
use hyper::Uri;
use jsonrpsee_core::client::{CertificateStore, ClientT, IdKind, RequestIdManager, Subscription, SubscriptionClientT};
use jsonrpsee_core::params::BatchRequestBuilder;
use jsonrpsee_core::traits::ToRpcParams;
Expand Down Expand Up @@ -73,6 +74,7 @@ pub struct HttpClientBuilder {
id_kind: IdKind,
max_log_length: u32,
headers: HeaderMap,
socks_proxy: Option<Uri>,
}

impl HttpClientBuilder {
Expand Down Expand Up @@ -122,6 +124,12 @@ impl HttpClientBuilder {
self
}

/// Forward connections via SOCKS5 proxy
pub fn set_socks5_proxy(mut self, proxy: Uri) -> Self {
self.socks_proxy = Some(proxy);
self
}

/// Build the HTTP client with target to connect to.
pub fn build(self, target: impl AsRef<str>) -> Result<HttpClient, Error> {
let transport = HttpTransportClient::new(
Expand All @@ -130,6 +138,7 @@ impl HttpClientBuilder {
self.certificate_store,
self.max_log_length,
self.headers,
self.socks_proxy,
)
.map_err(|e| Error::Transport(e.into()))?;
Ok(HttpClient {
Expand All @@ -150,6 +159,7 @@ impl Default for HttpClientBuilder {
id_kind: IdKind::Number,
max_log_length: 4096,
headers: HeaderMap::new(),
socks_proxy: None,
}
}
}
Expand Down
64 changes: 43 additions & 21 deletions client/http-client/src/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use hyper::client::{Client, HttpConnector};
use hyper::http::{HeaderMap, HeaderValue};
use hyper::Uri;
use hyper_socks2::SocksConnector;
use jsonrpsee_core::client::CertificateStore;
use jsonrpsee_core::error::GenericTransportError;
use jsonrpsee_core::http_helpers;
Expand All @@ -24,6 +25,11 @@ enum HyperClient {
Https(Client<hyper_rustls::HttpsConnector<HttpConnector>>),
/// Hyper client with http connector.
Http(Client<HttpConnector>),
/// SOCKS5-proxied Hyper client with https connector.
#[cfg(feature = "tls")]
HttpsSocks(Client<hyper_rustls::HttpsConnector<SocksConnector<HttpConnector>>>),
/// SOCKS5-proxied Hyper client with http connector.
HttpSocks(Client<SocksConnector<HttpConnector>>),
}

impl HyperClient {
Expand All @@ -32,6 +38,9 @@ impl HyperClient {
Self::Http(client) => client.request(req),
#[cfg(feature = "tls")]
Self::Https(client) => client.request(req),
#[cfg(feature = "tls")]
HyperClient::HttpsSocks(client) => client.request(req),
HyperClient::HttpSocks(client) => client.request(req),
}
}
}
Expand Down Expand Up @@ -61,30 +70,38 @@ impl HttpTransportClient {
cert_store: CertificateStore,
max_log_length: u32,
headers: HeaderMap,
socks_proxy: Option<Uri>,
) -> Result<Self, Error> {
let target: Uri = target.as_ref().parse().map_err(|e| Error::Url(format!("Invalid URL: {}", e)))?;
if target.port_u16().is_none() {
return Err(Error::Url("Port number is missing in the URL".into()));
}

let client = match target.scheme_str() {
Some("http") => HyperClient::Http(Client::new()),
let socks_connector =
socks_proxy.map(|proxy_addr| {
let mut tcp_connector = HttpConnector::new();
// We kind of abuse the HTTP connector to open the SOCKS5/TCP connections for us
tcp_connector.enforce_http(false);
SocksConnector { proxy_addr, auth: None, connector: tcp_connector }
});

let client = match (target.scheme_str(), socks_connector) {
(Some("http"), None) => HyperClient::Http(Client::new()),
(Some("http"), Some(connector)) => HyperClient::HttpSocks(Client::builder().build(connector)),
#[cfg(feature = "tls")]
Some("https") => {
let connector = match cert_store {
CertificateStore::Native => hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_or_http()
.enable_http1()
.build(),
CertificateStore::WebPki => hyper_rustls::HttpsConnectorBuilder::new()
.with_webpki_roots()
.https_or_http()
.enable_http1()
.build(),
(Some("https"), socks_connector) => {
let connector_builder = match cert_store {
CertificateStore::Native => hyper_rustls::HttpsConnectorBuilder::new().with_native_roots(),
CertificateStore::WebPki => hyper_rustls::HttpsConnectorBuilder::new().with_webpki_roots(),
_ => return Err(Error::InvalidCertficateStore),
};
HyperClient::Https(Client::builder().build::<_, hyper::Body>(connector))
let connector_builder = connector_builder.https_or_http().enable_http1();
match socks_connector {
None => HyperClient::Https(Client::builder().build::<_, hyper::Body>(connector_builder.build())),
Some(socks_connector) => HyperClient::HttpsSocks(
Client::builder().build::<_, hyper::Body>(connector_builder.wrap_connector(socks_connector)),
),
}
}
_ => {
#[cfg(feature = "tls")]
Expand Down Expand Up @@ -196,6 +213,8 @@ where

#[cfg(test)]
mod tests {
use jsonrpsee_core::client::ClientT;
use crate::HttpClientBuilder;
use super::*;

fn assert_target(
Expand All @@ -215,7 +234,7 @@ mod tests {

#[test]
fn invalid_http_url_rejected() {
let err = HttpTransportClient::new("ws://localhost:9933", 80, CertificateStore::Native, 80, HeaderMap::new())
let err = HttpTransportClient::new("ws://localhost:9933", 80, CertificateStore::Native, 80, HeaderMap::new(), None)
.unwrap_err();
assert!(matches!(err, Error::Url(_)));
}
Expand All @@ -224,7 +243,7 @@ mod tests {
#[test]
fn https_works() {
let client =
HttpTransportClient::new("https://localhost:9933", 80, CertificateStore::Native, 80, HeaderMap::new())
HttpTransportClient::new("https://localhost:9933", 80, CertificateStore::Native, 80, HeaderMap::new(), None)
.unwrap();
assert_target(&client, "localhost", "https", "/", 9933, 80);
}
Expand All @@ -233,18 +252,18 @@ mod tests {
#[test]
fn https_fails_without_tls_feature() {
let err =
HttpTransportClient::new("https://localhost:9933", 80, CertificateStore::Native, 80, HeaderMap::new())
HttpTransportClient::new("https://localhost:9933", 80, CertificateStore::Native, 80, HeaderMap::new(), None)
.unwrap_err();
assert!(matches!(err, Error::Url(_)));
}

#[test]
fn faulty_port() {
let err = HttpTransportClient::new("http://localhost:-43", 80, CertificateStore::Native, 80, HeaderMap::new())
let err = HttpTransportClient::new("http://localhost:-43", 80, CertificateStore::Native, 80, HeaderMap::new(), None)
.unwrap_err();
assert!(matches!(err, Error::Url(_)));
let err =
HttpTransportClient::new("http://localhost:-99999", 80, CertificateStore::Native, 80, HeaderMap::new())
HttpTransportClient::new("http://localhost:-99999", 80, CertificateStore::Native, 80, HeaderMap::new(), None)
.unwrap_err();
assert!(matches!(err, Error::Url(_)));
}
Expand All @@ -257,6 +276,7 @@ mod tests {
CertificateStore::Native,
80,
HeaderMap::new(),
None
)
.unwrap();
assert_target(&client, "localhost", "http", "/my-special-path", 9944, 1337);
Expand All @@ -270,6 +290,7 @@ mod tests {
CertificateStore::WebPki,
80,
HeaderMap::new(),
None
)
.unwrap();
assert_target(&client, "127.0.0.1", "http", "/my?name1=value1&name2=value2", 9999, u32::MAX);
Expand All @@ -283,6 +304,7 @@ mod tests {
CertificateStore::Native,
80,
HeaderMap::new(),
None
)
.unwrap();
assert_target(&client, "127.0.0.1", "http", "/my.htm", 9944, 999);
Expand All @@ -292,7 +314,7 @@ mod tests {
async fn request_limit_works() {
let eighty_bytes_limit = 80;
let client =
HttpTransportClient::new("http://localhost:9933", 80, CertificateStore::WebPki, 99, HeaderMap::new())
HttpTransportClient::new("http://localhost:9933", 80, CertificateStore::WebPki, 99, HeaderMap::new(), None)
.unwrap();
assert_eq!(client.max_request_body_size, eighty_bytes_limit);

Expand Down

2 comments on commit 8ec1b4a

@flipchan
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elsirion did you get this working?

@elsirion
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never tested it, but should work afaik, just try it out. fedimint/fedimint#391 (comment)

Please sign in to comment.