Skip to content
This repository was archived by the owner on Sep 4, 2024. It is now read-only.

Commit 7c94adf

Browse files
committed
Merge #70: Add SOCKS5 Proxy support in http transport
ce8f318 Add SOCKS5 Support for SimpleHttpTransport (rajarshimaitra) dca6844 Ground Work: Refactor URL check from builder api (rajarshimaitra) Pull request description: This is an attempt at #57. Which was a request for adding socks5 Proxy support into the http transport. This is useful to connect to RPC via Tor. Summary: - Adds a new feature flag `proxy` which does some extra steps to connect the `url` address via a `proxy` address. - New fields are added inside `SimpleHttpTransport` which are only visible in `proxy` feature. - The first PR contains a refactoring which takes the URL sanity checking logic to its own function. So that it can be used for the `proxy-addr` checking also. - New constructor: Client::http_proxy() - Test covering basic behavior. ACKs for top commit: apoelstra: ACK ce8f318 Tree-SHA512: 1a01cecd3b5bdcd85035a55b02aab5b8b5b02f1664106eadde48151e2be1b6bd28143ad8c33ef87d5ea043c04ee12995986d36f3d0082bb8da3d015f68194a14
2 parents e3d92e3 + ce8f318 commit 7c94adf

File tree

3 files changed

+169
-63
lines changed

3 files changed

+169
-63
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ simple_http = [ "base64" ]
2323
simple_tcp = []
2424
# Basic transport over a raw UnixStream
2525
simple_uds = []
26+
# Enable Socks5 Proxy in transport
27+
proxy = ["socks"]
2628

2729

2830
[dependencies]
2931
serde = { version = "1", features = ["derive"] }
3032
serde_json = { version = "1", features = [ "raw_value" ] }
3133

3234
base64 = { version = "0.13.0", optional = true }
35+
socks = { version = "0.3.4", optional = true}

contrib/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/sh -ex
22

3-
FEATURES="simple_http simple_tcp simple_uds"
3+
FEATURES="simple_http simple_tcp simple_uds proxy"
44

55
cargo --version
66
rustc --version

src/simple_http.rs

Lines changed: 165 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
//! round-tripper that works with the bitcoind RPC server. This can be used
33
//! if minimal dependencies are a goal and synchronous communication is ok.
44
5+
#[cfg(feature = "proxy")]
6+
use socks::Socks5Stream;
57
use std::io::{BufRead, BufReader, Write};
6-
use std::net::{TcpStream, ToSocketAddrs};
8+
#[cfg(not(feature = "proxy"))]
9+
use std::net::TcpStream;
10+
use std::net::{SocketAddr, ToSocketAddrs};
711
use std::time::{Duration, Instant};
812
use std::{error, fmt, io, net, thread};
913

@@ -18,6 +22,9 @@ use crate::{Request, Response};
1822
/// Set to 8332, the default RPC port for bitcoind.
1923
pub const DEFAULT_PORT: u16 = 8332;
2024

25+
/// The Default SOCKS5 Port to use for proxy connection.
26+
pub const DEFAULT_PROXY_PORT: u16 = 9050;
27+
2128
/// Simple HTTP transport that implements the necessary subset of HTTP for
2229
/// running a bitcoind RPC client.
2330
#[derive(Clone, Debug)]
@@ -27,6 +34,10 @@ pub struct SimpleHttpTransport {
2734
timeout: Duration,
2835
/// The value of the `Authorization` HTTP header.
2936
basic_auth: Option<String>,
37+
#[cfg(feature = "proxy")]
38+
proxy_addr: net::SocketAddr,
39+
#[cfg(feature = "proxy")]
40+
proxy_auth: Option<(String, String)>,
3041
}
3142

3243
impl Default for SimpleHttpTransport {
@@ -39,6 +50,13 @@ impl Default for SimpleHttpTransport {
3950
path: "/".to_owned(),
4051
timeout: Duration::from_secs(15),
4152
basic_auth: None,
53+
#[cfg(feature = "proxy")]
54+
proxy_addr: net::SocketAddr::new(
55+
net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)),
56+
DEFAULT_PROXY_PORT,
57+
),
58+
#[cfg(feature = "proxy")]
59+
proxy_auth: None,
4260
}
4361
}
4462
}
@@ -60,6 +78,20 @@ impl SimpleHttpTransport {
6078
{
6179
// Open connection
6280
let request_deadline = Instant::now() + self.timeout;
81+
#[cfg(feature = "proxy")]
82+
let mut sock = if let Some((username, password)) = &self.proxy_auth {
83+
Socks5Stream::connect_with_password(
84+
self.proxy_addr,
85+
self.addr,
86+
username.as_str(),
87+
password.as_str(),
88+
)?
89+
.into_inner()
90+
} else {
91+
Socks5Stream::connect(self.proxy_addr, self.addr)?.into_inner()
92+
};
93+
94+
#[cfg(not(feature = "proxy"))]
6395
let mut sock = TcpStream::connect_timeout(&self.addr, self.timeout)?;
6496

6597
sock.set_read_timeout(Some(self.timeout))?;
@@ -246,6 +278,65 @@ fn get_lines<R: BufRead>(reader: &mut R) -> Result<String, Error> {
246278
Ok(body)
247279
}
248280

281+
/// Do some very basic manual URL parsing because the uri/url crates
282+
/// all have unicode-normalization as a dependency and that's broken.
283+
fn check_url(url: &str) -> Result<(SocketAddr, String), Error> {
284+
// The fallback port in case no port was provided.
285+
// This changes when the http or https scheme was provided.
286+
let mut fallback_port = DEFAULT_PORT;
287+
288+
// We need to get the hostname and the port.
289+
// (1) Split scheme
290+
let after_scheme = {
291+
let mut split = url.splitn(2, "://");
292+
let s = split.next().unwrap();
293+
match split.next() {
294+
None => s, // no scheme present
295+
Some(after) => {
296+
// Check if the scheme is http or https.
297+
if s == "http" {
298+
fallback_port = 80;
299+
} else if s == "https" {
300+
fallback_port = 443;
301+
} else {
302+
return Err(Error::url(url, "scheme should be http or https"));
303+
}
304+
after
305+
}
306+
}
307+
};
308+
// (2) split off path
309+
let (before_path, path) = {
310+
if let Some(slash) = after_scheme.find('/') {
311+
(&after_scheme[0..slash], &after_scheme[slash..])
312+
} else {
313+
(after_scheme, "/")
314+
}
315+
};
316+
// (3) split off auth part
317+
let after_auth = {
318+
let mut split = before_path.splitn(2, '@');
319+
let s = split.next().unwrap();
320+
split.next().unwrap_or(s)
321+
};
322+
323+
// (4) Parse into socket address.
324+
// At this point we either have <host_name> or <host_name_>:<port>
325+
// `std::net::ToSocketAddrs` requires `&str` to have <host_name_>:<port> format.
326+
let mut addr = match after_auth.to_socket_addrs() {
327+
Ok(addr) => addr,
328+
Err(_) => {
329+
// Invalid socket address. Try to add port.
330+
format!("{}:{}", after_auth, fallback_port).to_socket_addrs()?
331+
}
332+
};
333+
334+
match addr.next() {
335+
Some(a) => Ok((a, path.to_owned())),
336+
None => Err(Error::url(url, "invalid hostname: error extracting socket address")),
337+
}
338+
}
339+
249340
impl Transport for SimpleHttpTransport {
250341
fn send_request(&self, req: Request) -> Result<Response, crate::Error> {
251342
Ok(self.request(req)?)
@@ -282,66 +373,9 @@ impl Builder {
282373

283374
/// Set the URL of the server to the transport.
284375
pub fn url(mut self, url: &str) -> Result<Self, Error> {
285-
// Do some very basic manual URL parsing because the uri/url crates
286-
// all have unicode-normalization as a dependency and that's broken.
287-
288-
// The fallback port in case no port was provided.
289-
// This changes when the http or https scheme was provided.
290-
let mut fallback_port = DEFAULT_PORT;
291-
292-
// We need to get the hostname and the port.
293-
// (1) Split scheme
294-
let after_scheme = {
295-
let mut split = url.splitn(2, "://");
296-
let s = split.next().unwrap();
297-
match split.next() {
298-
None => s, // no scheme present
299-
Some(after) => {
300-
// Check if the scheme is http or https.
301-
if s == "http" {
302-
fallback_port = 80;
303-
} else if s == "https" {
304-
fallback_port = 443;
305-
} else {
306-
return Err(Error::url(url, "scheme schould be http or https"));
307-
}
308-
after
309-
}
310-
}
311-
};
312-
// (2) split off path
313-
let (before_path, path) = {
314-
if let Some(slash) = after_scheme.find('/') {
315-
(&after_scheme[0..slash], &after_scheme[slash..])
316-
} else {
317-
(after_scheme, "/")
318-
}
319-
};
320-
// (3) split off auth part
321-
let after_auth = {
322-
let mut split = before_path.splitn(2, '@');
323-
let s = split.next().unwrap();
324-
split.next().unwrap_or(s)
325-
};
326-
327-
// (4) Parse into socket address.
328-
// At this point we either have <host_name> or <host_name_>:<port>
329-
// `std::net::ToSocketAddrs` requires `&str` to have <host_name_>:<port> format.
330-
let mut addr = match after_auth.to_socket_addrs() {
331-
Ok(addr) => addr,
332-
Err(_) => {
333-
// Invalid socket address. Try to add port.
334-
format!("{}:{}", after_auth, fallback_port).to_socket_addrs()?
335-
}
336-
};
337-
338-
self.tp.addr = match addr.next() {
339-
Some(a) => a,
340-
None => {
341-
return Err(Error::url(url, "invalid hostname: error extracting socket address"))
342-
}
343-
};
344-
self.tp.path = path.to_owned();
376+
let url = check_url(url)?;
377+
self.tp.addr = url.0;
378+
self.tp.path = url.1;
345379
Ok(self)
346380
}
347381

@@ -362,6 +396,22 @@ impl Builder {
362396
self
363397
}
364398

399+
#[cfg(feature = "proxy")]
400+
/// Add proxy address to the transport for SOCKS5 proxy
401+
pub fn proxy_addr<S: AsRef<str>>(mut self, proxy_addr: S) -> Result<Self, Error> {
402+
// We don't expect path in proxy address.
403+
self.tp.proxy_addr = check_url(proxy_addr.as_ref())?.0;
404+
Ok(self)
405+
}
406+
407+
#[cfg(feature = "proxy")]
408+
/// Add optional proxy authentication as ('username', 'password')
409+
pub fn proxy_auth<S: AsRef<str>>(mut self, user: S, pass: S) -> Self {
410+
self.tp.proxy_auth =
411+
Some((user, pass)).map(|(u, p)| (u.as_ref().to_string(), p.as_ref().to_string()));
412+
self
413+
}
414+
365415
/// Builds the final `SimpleHttpTransport`
366416
pub fn build(self) -> SimpleHttpTransport {
367417
self.tp
@@ -387,11 +437,34 @@ impl crate::Client {
387437
}
388438
Ok(crate::Client::with_transport(builder.build()))
389439
}
440+
441+
#[cfg(feature = "proxy")]
442+
/// Create a new JSON_RPC client using a HTTP-Socks5 proxy transport.
443+
pub fn http_proxy(
444+
url: &str,
445+
user: Option<String>,
446+
pass: Option<String>,
447+
proxy_addr: &str,
448+
proxy_auth: Option<(&str, &str)>,
449+
) -> Result<crate::Client, Error> {
450+
let mut builder = Builder::new().url(url)?;
451+
if let Some(user) = user {
452+
builder = builder.auth(user, pass);
453+
}
454+
builder = builder.proxy_addr(proxy_addr)?;
455+
if let Some((user, pass)) = proxy_auth {
456+
builder = builder.proxy_auth(user, pass);
457+
}
458+
let tp = builder.build();
459+
Ok(crate::Client::with_transport(tp))
460+
}
390461
}
391462

392463
#[cfg(test)]
393464
mod tests {
394465
use std::net;
466+
#[cfg(feature = "proxy")]
467+
use std::str::FromStr;
395468

396469
use super::*;
397470
use crate::Client;
@@ -432,7 +505,14 @@ mod tests {
432505
"http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
433506
];
434507
for u in &valid_urls {
435-
Builder::new().url(*u).unwrap_or_else(|_| panic!("error for: {}", u));
508+
let (addr, path) = check_url(u).unwrap();
509+
let builder = Builder::new().url(*u).unwrap_or_else(|_| panic!("error for: {}", u));
510+
assert_eq!(builder.tp.addr, addr);
511+
assert_eq!(builder.tp.path, path);
512+
assert_eq!(builder.tp.timeout, Duration::from_secs(15));
513+
assert_eq!(builder.tp.basic_auth, None);
514+
#[cfg(feature = "proxy")]
515+
assert_eq!(builder.tp.proxy_addr, SocketAddr::from_str("127.0.0.1:9050").unwrap());
436516
}
437517

438518
let invalid_urls = [
@@ -462,4 +542,27 @@ mod tests {
462542

463543
let _ = Client::simple_http("localhost:22", None, None).unwrap();
464544
}
545+
546+
#[cfg(feature = "proxy")]
547+
#[test]
548+
fn construct_with_proxy() {
549+
let tp = Builder::new()
550+
.timeout(Duration::from_millis(100))
551+
.url("localhost:22")
552+
.unwrap()
553+
.auth("user", None)
554+
.proxy_addr("127.0.0.1:9050")
555+
.unwrap()
556+
.build();
557+
let _ = Client::with_transport(tp);
558+
559+
let _ = Client::http_proxy(
560+
"localhost:22",
561+
None,
562+
None,
563+
"127.0.0.1:9050",
564+
Some(("user", "password")),
565+
)
566+
.unwrap();
567+
}
465568
}

0 commit comments

Comments
 (0)