diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b8e363..68aaf78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,8 @@ jobs: - "feat.: gzip" - "feat.: json" - "feat.: stream" - # - "feat.: socks" + - "feat.: socks/default-tls" + - "feat.: socks/rustls-tls" # - "feat.: trust-dns" include: @@ -115,8 +116,10 @@ jobs: features: "--features json" - name: "feat.: stream" features: "--features stream" - # - name: "feat.: socks" - # features: "--features socks" + - name: "feat.: socks/default-tls" + features: "--features socks" + - name: "feat.: socks/rustls-tls" + features: "--features socks,rustls-tls" # - name: "feat.: trust-dns" # features: "--features trust-dns" diff --git a/Cargo.toml b/Cargo.toml index ae61dae..dc88577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ json = ["serde_json"] stream = [] +socks = ["tokio-socks"] + # Internal (PRIVATE!) features used to aid testing. # 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 } ## socks -#socks = { version = "0.3.2", optional = true } +tokio-socks = { version = "0.2", optional = true } ## trust-dns #trust-dns-resolver = { version = "0.11", optional = true } @@ -156,6 +158,11 @@ name = "json_typed" path = "examples/json_typed.rs" required-features = ["json"] +[[example]] +name = "tor_socks" +path = "examples/tor_socks.rs" +required-features = ["socks"] + [[test]] name = "blocking" path = "tests/blocking.rs" diff --git a/examples/tor_socks.rs b/examples/tor_socks.rs new file mode 100644 index 0000000..d80835b --- /dev/null +++ b/examples/tor_socks.rs @@ -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(()) +} diff --git a/src/connect.rs b/src/connect.rs index 7dc87d3..100c58b 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -148,9 +148,9 @@ impl Connector { #[cfg(feature = "socks")] async fn connect_socks( &self, - dst: Destination, + dst: Uri, proxy: ProxyScheme, - ) -> Result<(Conn, Connected), io::Error> { + ) -> Result { let dns = match proxy { ProxyScheme::Socks5 { remote_dns: false, .. @@ -158,37 +158,43 @@ impl Connector { ProxyScheme::Socks5 { remote_dns: true, .. } => socks::DnsResolve::Proxy, - ProxyScheme::Http { .. } => { + ProxyScheme::Http { .. } | ProxyScheme::Https { .. } => { unreachable!("connect_socks is only called for socks proxies"); - } + }, }; match &self.inner { #[cfg(feature = "default-tls")] Inner::DefaultTls(_http, tls) => { - if dst.scheme() == "https" { - use self::native_tls_async::TlsConnectorExt; - - let host = dst.host().to_owned(); - let socks_connecting = socks::connect(proxy, dst, dns); - let (conn, connected) = socks::connect(proxy, dst, dns).await?; + if dst.scheme() == Some(&Scheme::HTTPS) { + let host = dst + .host() + .ok_or(io::Error::new(io::ErrorKind::Other, "no host in url"))? + .to_string(); + let conn = socks::connect(proxy, dst, dns).await?; let tls_connector = tokio_tls::TlsConnector::from(tls.clone()); let io = tls_connector .connect(&host, conn) .await .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")] Inner::RustlsTls { tls_proxy, .. } => { - if dst.scheme() == "https" { + if dst.scheme() == Some(&Scheme::HTTPS) { use tokio_rustls::webpki::DNSNameRef; use tokio_rustls::TlsConnector as RustlsConnector; let tls = tls_proxy.clone(); - let host = dst.host().to_owned(); - let (conn, connected) = socks::connect(proxy, dst, dns); + let host = dst + .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) .map(|dnsname| dnsname.to_owned()) .map_err(|_| io::Error::new(io::ErrorKind::Other, "Invalid DNS Name"))?; @@ -196,12 +202,17 @@ impl Connector { .connect(dnsname.as_ref(), conn) .await .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"))] - Inner::Http(_) => socks::connect(proxy, dst, dns), + Inner::Http(_) => () } + + socks::connect(proxy, dst, dns).await } async fn connect_with_maybe_proxy( @@ -277,7 +288,7 @@ impl Connector { ProxyScheme::Http { host, auth } => (into_uri(Scheme::HTTP, host), auth), ProxyScheme::Https { host, auth } => (into_uri(Scheme::HTTPS, host), auth), #[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::TlsConnector as RustlsConnector; - let host = dst.host() + let host = dst + .host() .ok_or(io::Error::new(io::ErrorKind::Other, "no host in url"))? .to_string(); let port = dst.port().map(|r| r.as_u16()).unwrap_or(443); @@ -430,6 +442,10 @@ pub(crate) trait AsyncConn: AsyncRead + AsyncWrite + Connection {} impl AsyncConn for T {} 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 { #[pin] inner: Box, @@ -779,15 +795,12 @@ mod rustls_tls_conn { #[cfg(feature = "socks")] mod socks { + use http::Uri; + use tokio_socks::tcp::Socks5Stream; use std::io; - - use futures::{future, Future}; - use hyper::client::connect::{Connected, Destination}; - use socks::Socks5Stream; use std::net::ToSocketAddrs; - use tokio::{net::TcpStream, reactor}; - use super::Connecting; + use super::{BoxError, Scheme}; use crate::proxy::ProxyScheme; pub(super) enum DnsResolve { @@ -797,13 +810,19 @@ mod socks { pub(super) async fn connect( proxy: ProxyScheme, - dst: Destination, + dst: Uri, dns: DnsResolve, ) -> Result { let https = dst.scheme() == Some(&Scheme::HTTPS); - let original_host = dst.host().to_owned(); - let mut host = original_host.clone(); - let port = dst.port().unwrap_or_else(|| if https { 443 } else { 80 }); + let original_host = dst + .host() + .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 { let maybe_new_target = (host.as_str(), port).to_socket_addrs()?.next(); @@ -826,13 +845,17 @@ mod socks { &password, ) .await + .map_err(|e| format!("socks connect error: {}", e))? } else { - let s = Socks5Stream::connect(socket_addr, (host.as_str(), port)).await; - TcpStream::from_std(s.into_inner(), &reactor::Handle::default()) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))? + Socks5Stream::connect(socket_addr, (host.as_str(), port)) + .await + .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, + }) } } diff --git a/src/lib.rs b/src/lib.rs index c038ff5..eb10cb3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,6 +169,7 @@ //! - **gzip**: Provides response body gzip decompression. //! - **json**: Provides serialization and deserialization for JSON bodies. //! - **stream**: Adds support for `futures::Stream`. +//! - **socks**: Provides SOCKS5 proxy support. //! //! //! [hyper]: http://hyper.rs @@ -180,8 +181,6 @@ //! [redirect]: crate::redirect //! [Proxy]: ./struct.Proxy.html //! [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 ////! threadpool using `getaddrinfo`. diff --git a/src/proxy.rs b/src/proxy.rs index dcb512a..25eefe2 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -383,14 +383,15 @@ impl ProxyScheme { // Resolve URL to a host and port #[cfg(feature = "socks")] let to_addr = || { - let addrs = try_!(url.socket_addrs(|| match url.scheme() { + let addrs = url.socket_addrs(|| match url.scheme() { "socks5" | "socks5h" => Some(1080), _ => None, - })); + }) + .map_err(crate::error::builder)?; addrs .into_iter() .next() - .ok_or_else(crate::error::unknown_proxy_scheme) + .ok_or_else(|| crate::error::builder("unknown proxy scheme")) }; let mut scheme = match url.scheme() { @@ -418,7 +419,7 @@ impl ProxyScheme { ProxyScheme::Http { .. } => "http", ProxyScheme::Https { .. } => "https", #[cfg(feature = "socks")] - ProxyScheme::Socks5 => "socks5", + ProxyScheme::Socks5 { .. } => "socks5", } } @@ -428,7 +429,7 @@ impl ProxyScheme { ProxyScheme::Http { host, .. } => host.as_str(), ProxyScheme::Https { host, .. } => host.as_str(), #[cfg(feature = "socks")] - ProxyScheme::Socks5 => panic!("socks5"), + ProxyScheme::Socks5 { .. } => panic!("socks5"), } } }