re-add the "socks" feature (using tokio-socks) (#769)

The "socks" feature has been removed for a while now, the optional
dependency on the "socks" crate commented out.

The code for actually providing the socks feature was, however, still
mostly present, if a bit out of date.

This commit re-adds the socks feature using the tokio-socks (instead of
socks) crate.

Closes #620
This commit is contained in:
r-arias
2020-01-09 20:25:26 +00:00
committed by Sean McArthur
parent 6004623784
commit 20d50daa8b
6 changed files with 100 additions and 43 deletions

View File

@@ -62,7 +62,8 @@ jobs:
- "feat.: gzip" - "feat.: gzip"
- "feat.: json" - "feat.: json"
- "feat.: stream" - "feat.: stream"
# - "feat.: socks" - "feat.: socks/default-tls"
- "feat.: socks/rustls-tls"
# - "feat.: trust-dns" # - "feat.: trust-dns"
include: include:
@@ -115,8 +116,10 @@ jobs:
features: "--features json" features: "--features json"
- name: "feat.: stream" - name: "feat.: stream"
features: "--features stream" features: "--features stream"
# - name: "feat.: socks" - name: "feat.: socks/default-tls"
# features: "--features socks" features: "--features socks"
- name: "feat.: socks/rustls-tls"
features: "--features socks,rustls-tls"
# - name: "feat.: trust-dns" # - name: "feat.: trust-dns"
# features: "--features trust-dns" # features: "--features trust-dns"

View File

@@ -47,6 +47,8 @@ json = ["serde_json"]
stream = [] stream = []
socks = ["tokio-socks"]
# Internal (PRIVATE!) features used to aid testing. # Internal (PRIVATE!) features used to aid testing.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
@@ -107,7 +109,7 @@ async-compression = { version = "0.2.0", default-features = false, features = ["
serde_json = { version = "1.0", optional = true } serde_json = { version = "1.0", optional = true }
## socks ## socks
#socks = { version = "0.3.2", optional = true } tokio-socks = { version = "0.2", optional = true }
## trust-dns ## trust-dns
#trust-dns-resolver = { version = "0.11", optional = true } #trust-dns-resolver = { version = "0.11", optional = true }
@@ -156,6 +158,11 @@ name = "json_typed"
path = "examples/json_typed.rs" path = "examples/json_typed.rs"
required-features = ["json"] required-features = ["json"]
[[example]]
name = "tor_socks"
path = "examples/tor_socks.rs"
required-features = ["socks"]
[[test]] [[test]]
name = "blocking" name = "blocking"
path = "tests/blocking.rs" path = "tests/blocking.rs"

24
examples/tor_socks.rs Normal file
View File

@@ -0,0 +1,24 @@
#![deny(warnings)]
// This is using the `tokio` runtime. You'll need the following dependency:
//
// `tokio = { version = "0.2", features = ["macros"] }`
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
// Make sure you are running tor and this is your socks port
let proxy = reqwest::Proxy::all("socks5://127.0.0.1:9050").expect("tor proxy should be there");
let client = reqwest::Client::builder()
.proxy(proxy)
.build()
.expect("should be able to build reqwest client");
let res = client.get("https://check.torproject.org").send().await?;
println!("Status: {}", res.status());
let text = res.text().await?;
let is_tor = text.contains("Congratulations. This browser is configured to use Tor.");
println!("Is Tor: {}", is_tor);
assert!(is_tor);
Ok(())
}

View File

@@ -148,9 +148,9 @@ impl Connector {
#[cfg(feature = "socks")] #[cfg(feature = "socks")]
async fn connect_socks( async fn connect_socks(
&self, &self,
dst: Destination, dst: Uri,
proxy: ProxyScheme, proxy: ProxyScheme,
) -> Result<(Conn, Connected), io::Error> { ) -> Result<Conn, BoxError> {
let dns = match proxy { let dns = match proxy {
ProxyScheme::Socks5 { ProxyScheme::Socks5 {
remote_dns: false, .. remote_dns: false, ..
@@ -158,37 +158,43 @@ impl Connector {
ProxyScheme::Socks5 { ProxyScheme::Socks5 {
remote_dns: true, .. remote_dns: true, ..
} => socks::DnsResolve::Proxy, } => socks::DnsResolve::Proxy,
ProxyScheme::Http { .. } => { ProxyScheme::Http { .. } | ProxyScheme::Https { .. } => {
unreachable!("connect_socks is only called for socks proxies"); unreachable!("connect_socks is only called for socks proxies");
} },
}; };
match &self.inner { match &self.inner {
#[cfg(feature = "default-tls")] #[cfg(feature = "default-tls")]
Inner::DefaultTls(_http, tls) => { Inner::DefaultTls(_http, tls) => {
if dst.scheme() == "https" { if dst.scheme() == Some(&Scheme::HTTPS) {
use self::native_tls_async::TlsConnectorExt; let host = dst
.host()
let host = dst.host().to_owned(); .ok_or(io::Error::new(io::ErrorKind::Other, "no host in url"))?
let socks_connecting = socks::connect(proxy, dst, dns); .to_string();
let (conn, connected) = socks::connect(proxy, dst, dns).await?; let conn = socks::connect(proxy, dst, dns).await?;
let tls_connector = tokio_tls::TlsConnector::from(tls.clone()); let tls_connector = tokio_tls::TlsConnector::from(tls.clone());
let io = tls_connector let io = tls_connector
.connect(&host, conn) .connect(&host, conn)
.await .await
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok((Box::new(io) as Conn, connected)) return Ok(Conn {
inner: Box::new(NativeTlsConn { inner: io }),
is_proxy: false,
});
} }
} }
#[cfg(feature = "rustls-tls")] #[cfg(feature = "rustls-tls")]
Inner::RustlsTls { tls_proxy, .. } => { Inner::RustlsTls { tls_proxy, .. } => {
if dst.scheme() == "https" { if dst.scheme() == Some(&Scheme::HTTPS) {
use tokio_rustls::webpki::DNSNameRef; use tokio_rustls::webpki::DNSNameRef;
use tokio_rustls::TlsConnector as RustlsConnector; use tokio_rustls::TlsConnector as RustlsConnector;
let tls = tls_proxy.clone(); let tls = tls_proxy.clone();
let host = dst.host().to_owned(); let host = dst
let (conn, connected) = socks::connect(proxy, dst, dns); .host()
.ok_or(io::Error::new(io::ErrorKind::Other, "no host in url"))?
.to_string();
let conn = socks::connect(proxy, dst, dns).await?;
let dnsname = DNSNameRef::try_from_ascii_str(&host) let dnsname = DNSNameRef::try_from_ascii_str(&host)
.map(|dnsname| dnsname.to_owned()) .map(|dnsname| dnsname.to_owned())
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Invalid DNS Name"))?; .map_err(|_| io::Error::new(io::ErrorKind::Other, "Invalid DNS Name"))?;
@@ -196,12 +202,17 @@ impl Connector {
.connect(dnsname.as_ref(), conn) .connect(dnsname.as_ref(), conn)
.await .await
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
Ok((Box::new(io) as Conn, connected)) return Ok(Conn {
inner: Box::new(RustlsTlsConn { inner: io }),
is_proxy: false,
});
} }
} }
#[cfg(not(feature = "__tls"))] #[cfg(not(feature = "__tls"))]
Inner::Http(_) => socks::connect(proxy, dst, dns), Inner::Http(_) => ()
} }
socks::connect(proxy, dst, dns).await
} }
async fn connect_with_maybe_proxy( async fn connect_with_maybe_proxy(
@@ -277,7 +288,7 @@ impl Connector {
ProxyScheme::Http { host, auth } => (into_uri(Scheme::HTTP, host), auth), ProxyScheme::Http { host, auth } => (into_uri(Scheme::HTTP, host), auth),
ProxyScheme::Https { host, auth } => (into_uri(Scheme::HTTPS, host), auth), ProxyScheme::Https { host, auth } => (into_uri(Scheme::HTTPS, host), auth),
#[cfg(feature = "socks")] #[cfg(feature = "socks")]
ProxyScheme::Socks5 { .. } => return this.connect_socks(dst, proxy_scheme), ProxyScheme::Socks5 { .. } => return self.connect_socks(dst, proxy_scheme).await,
}; };
@@ -326,7 +337,8 @@ impl Connector {
use tokio_rustls::webpki::DNSNameRef; use tokio_rustls::webpki::DNSNameRef;
use tokio_rustls::TlsConnector as RustlsConnector; use tokio_rustls::TlsConnector as RustlsConnector;
let host = dst.host() let host = dst
.host()
.ok_or(io::Error::new(io::ErrorKind::Other, "no host in url"))? .ok_or(io::Error::new(io::ErrorKind::Other, "no host in url"))?
.to_string(); .to_string();
let port = dst.port().map(|r| r.as_u16()).unwrap_or(443); let port = dst.port().map(|r| r.as_u16()).unwrap_or(443);
@@ -430,6 +442,10 @@ pub(crate) trait AsyncConn: AsyncRead + AsyncWrite + Connection {}
impl<T: AsyncRead + AsyncWrite + Connection> AsyncConn for T {} impl<T: AsyncRead + AsyncWrite + Connection> AsyncConn for T {}
pin_project! { pin_project! {
/// Note: the `is_proxy` member means *is plain text HTTP proxy*.
/// This tells hyper whether the URI should be written in
/// * origin-form (`GET /just/a/path HTTP/1.1`), when `is_proxy == false`, or
/// * absolute-form (`GET http://foo.bar/and/a/path HTTP/1.1`), otherwise.
pub(crate) struct Conn { pub(crate) struct Conn {
#[pin] #[pin]
inner: Box<dyn AsyncConn + Send + Sync + Unpin + 'static>, inner: Box<dyn AsyncConn + Send + Sync + Unpin + 'static>,
@@ -779,15 +795,12 @@ mod rustls_tls_conn {
#[cfg(feature = "socks")] #[cfg(feature = "socks")]
mod socks { mod socks {
use http::Uri;
use tokio_socks::tcp::Socks5Stream;
use std::io; use std::io;
use futures::{future, Future};
use hyper::client::connect::{Connected, Destination};
use socks::Socks5Stream;
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
use tokio::{net::TcpStream, reactor};
use super::Connecting; use super::{BoxError, Scheme};
use crate::proxy::ProxyScheme; use crate::proxy::ProxyScheme;
pub(super) enum DnsResolve { pub(super) enum DnsResolve {
@@ -797,13 +810,19 @@ mod socks {
pub(super) async fn connect( pub(super) async fn connect(
proxy: ProxyScheme, proxy: ProxyScheme,
dst: Destination, dst: Uri,
dns: DnsResolve, dns: DnsResolve,
) -> Result<super::Conn, BoxError> { ) -> Result<super::Conn, BoxError> {
let https = dst.scheme() == Some(&Scheme::HTTPS); let https = dst.scheme() == Some(&Scheme::HTTPS);
let original_host = dst.host().to_owned(); let original_host = dst
let mut host = original_host.clone(); .host()
let port = dst.port().unwrap_or_else(|| if https { 443 } else { 80 }); .ok_or(io::Error::new(io::ErrorKind::Other, "no host in url"))?;
let mut host = original_host.to_owned();
let port = match dst.port() {
Some(p) => p.as_u16(),
None if https => 443u16,
_ => 80u16,
};
if let DnsResolve::Local = dns { if let DnsResolve::Local = dns {
let maybe_new_target = (host.as_str(), port).to_socket_addrs()?.next(); let maybe_new_target = (host.as_str(), port).to_socket_addrs()?.next();
@@ -826,13 +845,17 @@ mod socks {
&password, &password,
) )
.await .await
.map_err(|e| format!("socks connect error: {}", e))?
} else { } else {
let s = Socks5Stream::connect(socket_addr, (host.as_str(), port)).await; Socks5Stream::connect(socket_addr, (host.as_str(), port))
TcpStream::from_std(s.into_inner(), &reactor::Handle::default()) .await
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))? .map_err(|e| format!("socks connect error: {}", e))?
}; };
Ok((Box::new(s) as super::Conn, Connected::new())) Ok(super::Conn {
inner: Box::new( stream.into_inner() ),
is_proxy: false,
})
} }
} }

View File

@@ -169,6 +169,7 @@
//! - **gzip**: Provides response body gzip decompression. //! - **gzip**: Provides response body gzip decompression.
//! - **json**: Provides serialization and deserialization for JSON bodies. //! - **json**: Provides serialization and deserialization for JSON bodies.
//! - **stream**: Adds support for `futures::Stream`. //! - **stream**: Adds support for `futures::Stream`.
//! - **socks**: Provides SOCKS5 proxy support.
//! //!
//! //!
//! [hyper]: http://hyper.rs //! [hyper]: http://hyper.rs
@@ -180,8 +181,6 @@
//! [redirect]: crate::redirect //! [redirect]: crate::redirect
//! [Proxy]: ./struct.Proxy.html //! [Proxy]: ./struct.Proxy.html
//! [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section //! [cargo-features]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-features-section
////! - **socks**: Provides SOCKS5 proxy support.
////! - **trust-dns**: Enables a trust-dns async resolver instead of default ////! - **trust-dns**: Enables a trust-dns async resolver instead of default
////! threadpool using `getaddrinfo`. ////! threadpool using `getaddrinfo`.

View File

@@ -383,14 +383,15 @@ impl ProxyScheme {
// Resolve URL to a host and port // Resolve URL to a host and port
#[cfg(feature = "socks")] #[cfg(feature = "socks")]
let to_addr = || { let to_addr = || {
let addrs = try_!(url.socket_addrs(|| match url.scheme() { let addrs = url.socket_addrs(|| match url.scheme() {
"socks5" | "socks5h" => Some(1080), "socks5" | "socks5h" => Some(1080),
_ => None, _ => None,
})); })
.map_err(crate::error::builder)?;
addrs addrs
.into_iter() .into_iter()
.next() .next()
.ok_or_else(crate::error::unknown_proxy_scheme) .ok_or_else(|| crate::error::builder("unknown proxy scheme"))
}; };
let mut scheme = match url.scheme() { let mut scheme = match url.scheme() {
@@ -418,7 +419,7 @@ impl ProxyScheme {
ProxyScheme::Http { .. } => "http", ProxyScheme::Http { .. } => "http",
ProxyScheme::Https { .. } => "https", ProxyScheme::Https { .. } => "https",
#[cfg(feature = "socks")] #[cfg(feature = "socks")]
ProxyScheme::Socks5 => "socks5", ProxyScheme::Socks5 { .. } => "socks5",
} }
} }
@@ -428,7 +429,7 @@ impl ProxyScheme {
ProxyScheme::Http { host, .. } => host.as_str(), ProxyScheme::Http { host, .. } => host.as_str(),
ProxyScheme::Https { host, .. } => host.as_str(), ProxyScheme::Https { host, .. } => host.as_str(),
#[cfg(feature = "socks")] #[cfg(feature = "socks")]
ProxyScheme::Socks5 => panic!("socks5"), ProxyScheme::Socks5 { .. } => panic!("socks5"),
} }
} }
} }