Skip to content

Commit

Permalink
ngrok: support connecting through either http or socks5 proxies
Browse files Browse the repository at this point in the history
  • Loading branch information
jrobsonchase committed Aug 22, 2023
1 parent 91fd734 commit 196dbdd
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 3 deletions.
3 changes: 3 additions & 0 deletions ngrok/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ parking_lot = "0.12.1"
once_cell = "1.17.1"
hostname = "0.3.1"
regex = "1.7.3"
tokio-socks = "0.5.1"
hyper-proxy = "0.9.1"
http = "0.2.9"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.45.0", features = ["Win32_Foundation"] }
Expand Down
2 changes: 1 addition & 1 deletion ngrok/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub mod config {

mod headers;
mod http;
pub use http::*;
pub use self::http::*;
mod labeled;
pub use labeled::*;
mod oauth;
Expand Down
107 changes: 105 additions & 2 deletions ngrok/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ use futures::{
prelude::*,
FutureExt,
};
use http::Uri;
use hyper::{
client::HttpConnector,
service::Service,
};
use hyper_proxy::{
Intercept,
Proxy,
ProxyConnector,
};
use muxado::heartbeat::HeartbeatConfig;
pub use muxado::heartbeat::HeartbeatHandler;
use once_cell::sync::OnceCell;
Expand Down Expand Up @@ -221,6 +231,78 @@ pub async fn default_connect(
Ok(Box::new(tls_conn.compat()) as Box<dyn IoStream>)
}

fn connect_proxy(uri: Uri) -> Arc<dyn Connector> {
match uri.scheme().map(|s| s.as_str()) {
Some("http" | "https") => Arc::new(connect_http_proxy(uri)),
Some("socks5") => {
let host = uri.host().unwrap_or_default();
let port = uri.port();
let port = port.as_ref();
let port = port.map(|p| p.as_str()).unwrap_or("1080");
Arc::new(connect_socks_proxy(format!("{host}:{port}")))
}
_ => Arc::new(move |_, _, _| {
let uri_string = uri.to_string();
async move { Err(ConnectError::ProxyUnsupportedError(uri_string)) }
}),
}
}

fn connect_http_proxy(url: Uri) -> impl Connector {
move |addr: String, tls_config, _| {
let mut proxy = Proxy::new(Intercept::All, url.clone());
proxy.force_connect();
let connector = HttpConnector::new();
async move {
let mut connector = ProxyConnector::from_proxy(connector, proxy)
.map_err(|e| ConnectError::ProxyUnsupportedError(format!("{e}")))?;

let mut split = addr.split(':');
let host = split.next().unwrap();

let server_uri = format!("http://{addr}")
.parse()
.map_err(|_| ConnectError::InvalidServerAddr(addr.clone()))?;

let conn = connector
.call(server_uri)
.await
.map_err(|e| ConnectError::ProxyConnect(Box::new(e)))?
.compat();

let tls_conn = async_rustls::TlsConnector::from(tls_config)
.connect(rustls::ServerName::try_from(host)?, conn)
.await
.map_err(ConnectError::Tls)?;

Ok(Box::new(tls_conn.compat()) as Box<dyn IoStream>)
}
}
}

fn connect_socks_proxy(proxy_addr: String) -> impl Connector {
move |server_addr: String, tls_config, _| {
let proxy_addr = proxy_addr.clone();
async move {
let mut split = server_addr.split(':');
let host = split.next().unwrap();

let conn =
tokio_socks::tcp::Socks5Stream::connect(proxy_addr.as_str(), server_addr.clone())
.await
.map_err(|e| ConnectError::ProxyConnect(Box::new(e)))?
.compat();

let tls_conn = async_rustls::TlsConnector::from(tls_config)
.connect(rustls::ServerName::try_from(host)?, conn)
.await
.map_err(ConnectError::Tls)?;

Ok(Box::new(tls_conn.compat()) as Box<dyn IoStream>)
}
}
}

/// The builder for an ngrok [Session].
#[derive(Clone)]
pub struct SessionBuilder {
Expand Down Expand Up @@ -263,16 +345,19 @@ pub enum ConnectError {
/// The builder specified an invalid server name.
#[error("invalid server name")]
InvalidServerName(#[from] InvalidDnsNameError),
/// The builder provided an invalid server address
#[error("invalid server address: {0}")]
InvalidServerAddr(String),
/// An error occurred when establishing a TCP connection to the ngrok
/// server.
#[error("failed to establish tcp connection")]
Tcp(io::Error),
Tcp(#[source] io::Error),
/// A TLS handshake error occurred.
///
/// This is usually a certificate validation issue, or an attempt to connect
/// to something that doesn't actually speak TLS.
#[error("tls handshake error")]
Tls(io::Error),
Tls(#[source] io::Error),
/// An error occurred when starting the ngrok session.
///
/// This might occur when there's a protocol mismatch interfering with the
Expand All @@ -285,6 +370,12 @@ pub enum ConnectError {
/// An error occurred when rebinding tunnels during a reconnect
#[error("error rebinding tunnel after reconnect")]
Rebind(#[source] RpcError),
/// An error arising from a misconfigured proxy
#[error("unsupported proxy address: {0}")]
ProxyUnsupportedError(String),
/// An error arising from a misconfigured proxy
#[error("failed to connect through proxy")]
ProxyConnect(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
/// The (re)connect function gave up.
///
/// This will never be returned by the default connect function, and is
Expand Down Expand Up @@ -442,6 +533,18 @@ impl SessionBuilder {
self
}

/// Configures the session to connect to ngrok through an outbound
/// HTTP or SOCKS5 proxy. This parameter is ignored if you override the connector
/// with [SessionBuilder::connector].
///
/// See the [proxy url paramter in the ngrok docs] for additional details.
///
/// [proxy url paramter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#proxy_url
pub fn proxy_url(&mut self, url: Uri) -> &mut Self {
self.connector = connect_proxy(url);
self
}

/// Configures a function which is called when the ngrok service requests that
/// this [Session] stops. Your application may choose to interpret this callback
/// as a request to terminate the [Session] or the entire process.
Expand Down

0 comments on commit 196dbdd

Please sign in to comment.