.tcp.ngrok.io:")
.metadata("example tunnel metadata from rust")
.listen()
.await?;
handle_tunnel(tunnel, sess);
futures::future::pending().await
}
fn handle_tunnel(mut tunnel: impl EndpointInfo + Tunnel, sess: ngrok::Session) {
info!("bound new tunnel: {}", tunnel.url());
tokio::spawn(async move {
loop {
let stream = if let Some(stream) = tunnel.try_next().await? {
stream
} else {
info!("tunnel closed!");
break;
};
let sess = sess.clone();
let id: String = tunnel.id().into();
tokio::spawn(async move {
info!("accepted connection: {:?}", stream.remote_addr());
let (rx, mut tx) = io::split(stream);
let mut lines = BufReader::new(rx);
loop {
let mut buf = String::new();
let len = lines.read_line(&mut buf).await?;
if len == 0 {
break;
}
if buf.contains("bye!") {
info!("unbind requested");
tx.write_all("later!".as_bytes()).await?;
sess.close_tunnel(id).await?;
return Ok(());
} else if buf.contains("another!") {
info!("another requested");
let new_tunnel = sess.tcp_endpoint().listen().await?;
tx.write_all(new_tunnel.url().as_bytes()).await?;
handle_tunnel(new_tunnel, sess.clone());
} else {
info!("read line: {}", buf);
tx.write_all(buf.as_bytes()).await?;
info!("echoed line");
}
tx.flush().await?;
info!("flushed");
}
Result::<(), anyhow::Error>::Ok(())
});
}
anyhow::Result::<()>::Ok(())
});
}
================================================
FILE: ngrok/examples/domain.crt
================================================
-----BEGIN CERTIFICATE-----
MIIC+jCCAeICCQDobWtly6PonjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJV
UzERMA8GA1UECgwITm90IFJlYWwxHTAbBgNVBAMMFHJ1c3Qtc2RrLmV4YW1wbGUu
Y29tMB4XDTIyMTIwMjE4MzMxM1oXDTMyMTEyOTE4MzMxM1owPzELMAkGA1UEBhMC
VVMxETAPBgNVBAoMCE5vdCBSZWFsMR0wGwYDVQQDDBRydXN0LXNkay5leGFtcGxl
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKhsx8tZWzaqaz9i
gnyU9O/dCEX8qgCvU2yoeJBfGhwCnlFNQBUdBGlV+Cjf19ozagYlY6Cunu214AUR
CDHTZsgTmMhtHkJ3kWD0wgDu+uyUuW6akP1+o39lebDc6CbDV7j1ySBoPMROp5dB
pX+ltpH42CmJM6ciwfTD1uuW5LXJvb9d4HISZp2RWyHqb3a6pI7E+XLqXg/Yy9MY
eqQESZMrYCjC+Sn4blGhcQhjTVU2rM5ChoDtZuL8OJQ0UYmchlch8CNc5Lvj9hAT
BiafEAscGrdIAZkK50kjpcIOWPPSfjCRqz8elSQqoKFq/uQnHBF5NwmsEqE0sXhw
4UdngRMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKieeE6gzuxHGjVT2NKL5BFjL
XKxdQhI/Tt7ClKu39Ay62fXDRznTBpGRfyWsJ5r3wmsHFogw46a2HYZHyuTMfyPY
lKhE/9EPMf/faqhIa33nMBASNzuGB5yfcPaod4KJX6DBKZtIpgkm2+S6BivpuSEo
DJ0lNtlR80mcVPma9KR57A0oh/UIsHXxL0qIKdaxyZYOZ1Zhtm+hzZcZA4wHkqzN
olNk3SOfhC5vVFudg+5KtxPBZ/efS9sqDUstH8hmE1JnxCF9OBlHdKI4yUMnsEf7
aOy11K5g7Oc3m7EB1twEQkufBAJeYzMOCji17GyJHDojNuOLkrmoLgcgDym5LQ==
-----END CERTIFICATE-----
================================================
FILE: ngrok/examples/domain.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqGzHy1lbNqprP2KCfJT0790IRfyqAK9TbKh4kF8aHAKeUU1A
FR0EaVX4KN/X2jNqBiVjoK6e7bXgBREIMdNmyBOYyG0eQneRYPTCAO767JS5bpqQ
/X6jf2V5sNzoJsNXuPXJIGg8xE6nl0Glf6W2kfjYKYkzpyLB9MPW65bktcm9v13g
chJmnZFbIepvdrqkjsT5cupeD9jL0xh6pARJkytgKML5KfhuUaFxCGNNVTaszkKG
gO1m4vw4lDRRiZyGVyHwI1zku+P2EBMGJp8QCxwat0gBmQrnSSOlwg5Y89J+MJGr
Px6VJCqgoWr+5CccEXk3CawSoTSxeHDhR2eBEwIDAQABAoIBADKQLc8brWmU8gue
bGQwZ/RW3DP+rZ71A8ucLE3Tb0g3dQYddf6groFdINpMkUXdp5few7Eqm2Xr8ywy
N86Vk8a/M2AAelQkB04fTNrw4/4AjEbrOloQGc+WTFlPiJaSkJRjnZUQFiYtIt0j
BSd0PYJHPcYCfbJQmf/8h1pE+7ajNJlvEWrJ8UjDCjUuPPxq1aCOIA48aN8awDaO
2R0AeSBws6+6UgyBgy2juat0t8PvS+AiLv4rK3RGMD+x96KoPEoVVgOQLr5YTqRP
Q+HYrs5cSXx9Jb2cmuJzvPUJmE3HKhoshWrK7fz5Z8wVAqTGhX6dbuHoqMJnAdla
FFSBEokCgYEA0BAsrDrnSkls1uC54iqzrxPMvITj4UnBR+PK504NrtP2brlcVIDP
e0dTKPTqjIC0vpDIg2fhPPvKkeoyuL6huiUWL/DdYVphUlwTf2Mu6PUm3o4M1MWN
S7q09cqUp4HWCUbzN3MIJ8sOPY17Lq+fxi1Wf4mNh+8IIXcJQ4HgUiUCgYEAzzqx
L7ck6pBUTtpUFYFTCUQDOYdzPE72zOzHK/LpoWJEssQ479srKlmSnRPRZbPZGMGE
EXvhWROonux96rRrZjiBI4B5G4rzeY0Rs24kClEh+7s5Zw4xmfSJu5oSdLqiy+O+
IKMVhOm9qq+8+y9LwKyajwR27srLdHSijJoXNNcCgYEAgtc5EJH2MwwbisFFg8mw
t0+vN3omR91203uXdH/sMN4Qoa6lNmrOj0raK+5gtTyW7SPlRGWGCjCZQctSXEVd
NM7vtfQ1c2w/uWg3xqsbq9nGuLwBq6gT4+SkudDMTM5kR+87Mcp//W4/JUwcg85j
nl+Sfp+Exk/1//14cOByrZUCgYEAjrr7HUVEfPbJysHf1iwL2D7rBa3AdhJhNIYF
LMUTm59Gd+Zk3PeUxIeLTvs+Z5E2/zESWMR9UtASfNugYo6/xlk2wRAU2h6bUeYT
AgXjduOox2yNvehty389emRFP/boeAw1gN8yzCf+BdkjDdLmlx+LGORXUmOFPIG1
D6h2QWMCgYA0WysR3XMcRH/8GDAgNVry5JvKoxlVXTPqVScTjMRj3VAzPYPCV+ql
lNN6yh/TuJwdvNs+uhKd1Wu4cDIb9GqxkBbUTKoKBrVL1YB93IC7QIR5wVjhJF/i
lrFW1ogr3535UzHzyDD1oXvcnWV/JnTdadHf2oA3Em8n2oTQvXQAog==
-----END RSA PRIVATE KEY-----
================================================
FILE: ngrok/examples/labeled.rs
================================================
use std::{
convert::Infallible,
error::Error,
net::SocketAddr,
};
use axum::{
extract::ConnectInfo,
routing::get,
BoxError,
Router,
};
use futures::TryStreamExt;
use hyper::{
body::Incoming,
Request,
};
use hyper_util::{
rt::TokioExecutor,
server,
};
use ngrok::prelude::*;
use tower::{
util::ServiceExt,
Service,
};
#[tokio::main]
async fn main() -> Result<(), Box> {
// build our application with a single route
let app = Router::new().route(
"/",
get(
|ConnectInfo(remote_addr): ConnectInfo| async move {
format!("Hello, {remote_addr:?}!\r\n")
},
),
);
let sess = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?;
let mut listener = sess
.labeled_tunnel()
// .app_protocol("http2")
// .verify_upstream_tls(false)
.label("edge", "edghts_")
.metadata("example tunnel metadata from rust")
.listen()
.await?;
println!("Labeled listener started!");
let mut make_service = app.into_make_service_with_connect_info::();
let server = async move {
while let Some(conn) = listener.try_next().await? {
let remote_addr = conn.remote_addr();
let tower_service = unwrap_infallible(make_service.call(remote_addr).await);
tokio::spawn(async move {
let hyper_service =
hyper::service::service_fn(move |request: Request| {
tower_service.clone().oneshot(request)
});
if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(conn, hyper_service)
.await
{
eprintln!("failed to serve connection: {err:#}");
}
});
}
Ok::<(), BoxError>(())
};
server.await?;
Ok(())
}
fn unwrap_infallible(result: Result) -> T {
match result {
Ok(value) => value,
Err(err) => match err {},
}
}
================================================
FILE: ngrok/examples/mingrok.rs
================================================
use std::sync::{
Arc,
Mutex,
};
use anyhow::Error;
use futures::{
prelude::*,
select,
};
use ngrok::prelude::*;
use tokio::sync::oneshot;
use tracing::info;
use url::Url;
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.pretty()
.with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()))
.init();
let forwards_to = std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("missing forwarding address"))
.and_then(|s| Ok(Url::parse(&s)?))?;
loop {
let (stop_tx, stop_rx) = oneshot::channel();
let stop_tx = Arc::new(Mutex::new(Some(stop_tx)));
let (restart_tx, restart_rx) = oneshot::channel();
let restart_tx = Arc::new(Mutex::new(Some(restart_tx)));
let mut fwd = ngrok::Session::builder()
.authtoken_from_env()
.handle_stop_command(move |req| {
let stop_tx = stop_tx.clone();
async move {
info!(?req, "received stop command");
let _ = stop_tx.lock().unwrap().take().unwrap().send(());
Ok(())
}
})
.handle_restart_command(move |req| {
let restart_tx = restart_tx.clone();
async move {
info!(?req, "received restart command");
let _ = restart_tx.lock().unwrap().take().unwrap().send(());
Ok(())
}
})
.handle_update_command(|req| async move {
info!(?req, "received update command");
Err("unable to update".into())
})
.connect()
.await?
.http_endpoint()
.listen_and_forward(forwards_to.clone())
.await?;
info!(url = fwd.url(), %forwards_to, "started forwarder");
let mut fut = fwd.join().fuse();
let mut stop_rx = stop_rx.fuse();
let mut restart_rx = restart_rx.fuse();
select! {
res = fut => info!("{:?}", res?),
_ = stop_rx => return Ok(()),
_ = restart_rx => {
drop(fut);
let _ = fwd.close().await;
continue
},
}
}
}
================================================
FILE: ngrok/examples/tls.rs
================================================
use std::{
convert::Infallible,
error::Error,
net::SocketAddr,
};
use axum::{
extract::ConnectInfo,
routing::get,
BoxError,
Router,
};
use futures::TryStreamExt;
use hyper::{
body::Incoming,
Request,
};
use hyper_util::{
rt::TokioExecutor,
server,
};
use ngrok::prelude::*;
use tower::{
util::ServiceExt,
Service,
};
const CERT: &[u8] = include_bytes!("domain.crt");
const KEY: &[u8] = include_bytes!("domain.key");
// const CA_CERT: &[u8] = include_bytes!("ca.crt");
#[tokio::main]
async fn main() -> Result<(), Box> {
// build our application with a single route
let app = Router::new().route(
"/",
get(
|ConnectInfo(remote_addr): ConnectInfo| async move {
format!("Hello, {remote_addr:?}!\r\n")
},
),
);
let sess = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?;
let mut listener = sess
.tls_endpoint()
// .allow_cidr("0.0.0.0/0")
// .deny_cidr("10.1.1.1/32")
// .verify_upstream_tls(false)
// .domain(".ngrok.io")
// .forwards_to("example rust"),
// .mutual_tlsca(CA_CERT.into())
// .proxy_proto(ProxyProto::None)
.termination(CERT.into(), KEY.into())
.metadata("example tunnel metadata from rust")
.listen()
.await?;
let mut make_service = app.into_make_service_with_connect_info::();
let server = async move {
while let Some(conn) = listener.try_next().await? {
let remote_addr = conn.remote_addr();
let tower_service = unwrap_infallible(make_service.call(remote_addr).await);
tokio::spawn(async move {
let hyper_service =
hyper::service::service_fn(move |request: Request| {
tower_service.clone().oneshot(request)
});
if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(conn, hyper_service)
.await
{
eprintln!("failed to serve connection: {err:#}");
}
});
}
Ok::<(), BoxError>(())
};
server.await?;
Ok(())
}
fn unwrap_infallible(result: Result) -> T {
match result {
Ok(value) => value,
Err(err) => match err {},
}
}
================================================
FILE: ngrok/src/config/common.rs
================================================
use std::{
collections::HashMap,
env,
process,
};
use async_trait::async_trait;
use once_cell::sync::OnceCell;
use url::Url;
pub use crate::internals::proto::ProxyProto;
use crate::{
config::policies::Policy,
forwarder::Forwarder,
internals::proto::{
BindExtra,
BindOpts,
IpRestriction,
MutualTls,
},
session::RpcError,
Session,
Tunnel,
};
/// Represents the ingress configuration for an ngrok endpoint.
///
/// Bindings determine where and how your endpoint is exposed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Binding {
/// Publicly accessible endpoint (default for most configurations).
Public,
/// Internal-only endpoint, not accessible from the public internet.
Internal,
/// Kubernetes cluster binding for service mesh integration.
Kubernetes,
}
impl Binding {
/// Returns the string representation of this binding.
pub fn as_str(&self) -> &'static str {
match self {
Binding::Public => "public",
Binding::Internal => "internal",
Binding::Kubernetes => "kubernetes",
}
}
/// Validates if a string is a recognized binding value.
pub(crate) fn validate(s: &str) -> Result<(), String> {
match s.to_lowercase().as_str() {
"public" | "internal" | "kubernetes" => Ok(()),
_ => Err(format!(
"Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
s
)),
}
}
}
impl From for String {
fn from(binding: Binding) -> String {
binding.as_str().to_string()
}
}
impl std::str::FromStr for Binding {
type Err = String;
fn from_str(s: &str) -> Result {
match s.to_lowercase().as_str() {
"public" => Ok(Binding::Public),
"internal" => Ok(Binding::Internal),
"kubernetes" => Ok(Binding::Kubernetes),
_ => Err(format!(
"Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
s
)),
}
}
}
impl std::fmt::Display for Binding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub(crate) fn default_forwards_to() -> &'static str {
static FORWARDS_TO: OnceCell = OnceCell::new();
FORWARDS_TO
.get_or_init(|| {
let hostname = hostname::get()
.unwrap_or("".into())
.to_string_lossy()
.into_owned();
let exe = env::current_exe()
.unwrap_or("".into())
.to_string_lossy()
.into_owned();
let pid = process::id();
format!("app://{hostname}/{exe}?pid={pid}")
})
.as_str()
}
/// Trait representing things that can be built into an ngrok tunnel.
#[async_trait]
pub trait TunnelBuilder: From {
/// The ngrok tunnel type that this builder produces.
type Tunnel: Tunnel;
/// Begin listening for new connections on this tunnel.
async fn listen(&self) -> Result;
}
/// Trait representing things that can be built into an ngrok tunnel and then
/// forwarded to a provided URL.
#[async_trait]
pub trait ForwarderBuilder: TunnelBuilder {
/// Start listening for new connections on this tunnel and forward all
/// connections to the provided URL.
///
/// This will also set the `forwards_to` metadata for the tunnel.
async fn listen_and_forward(&self, to_url: Url) -> Result, RpcError>;
}
macro_rules! impl_builder {
($(#[$m:meta])* $name:ident, $opts:ty, $tun:ident, $edgepoint:tt) => {
$(#[$m])*
#[derive(Clone)]
pub struct $name {
options: $opts,
// Note: This is only optional for testing purposes.
session: Option,
}
mod __builder_impl {
use $crate::forwarder::Forwarder;
use $crate::config::common::ForwarderBuilder;
use $crate::config::common::TunnelBuilder;
use $crate::session::RpcError;
use async_trait::async_trait;
use url::Url;
use super::*;
impl From for $name {
fn from(session: Session) -> Self {
$name {
options: Default::default(),
session: session.into(),
}
}
}
#[async_trait]
impl TunnelBuilder for $name {
type Tunnel = $tun;
async fn listen(&self) -> Result<$tun, RpcError> {
Ok($tun {
inner: self
.session
.as_ref()
.unwrap()
.start_tunnel(&self.options)
.await?,
})
}
}
#[async_trait]
impl ForwarderBuilder for $name {
async fn listen_and_forward(&self, to_url: Url) -> Result, RpcError> {
let mut cfg = self.clone();
cfg.for_forwarding_to(&to_url).await;
let tunnel = cfg.listen().await?;
let info = tunnel.make_info();
$crate::forwarder::forward(tunnel, info, to_url)
}
}
}
};
}
/// Tunnel configuration trait, implemented by our top-level config objects.
pub(crate) trait TunnelConfig {
/// The "forwards to" metadata.
///
/// Only for display/informational purposes.
fn forwards_to(&self) -> String;
/// The L7 protocol the upstream service expects
fn forwards_proto(&self) -> String;
/// Whether to disable certificate verification for this tunnel.
fn verify_upstream_tls(&self) -> bool;
/// Internal-only, extra data sent when binding a tunnel.
fn extra(&self) -> BindExtra;
/// The protocol for this tunnel.
fn proto(&self) -> String;
/// The middleware and other configuration options for this tunnel.
fn opts(&self) -> Option;
/// The labels for this tunnel.
fn labels(&self) -> HashMap;
}
// delegate references
impl TunnelConfig for &T
where
T: TunnelConfig,
{
fn forwards_to(&self) -> String {
(**self).forwards_to()
}
fn forwards_proto(&self) -> String {
(**self).forwards_proto()
}
fn verify_upstream_tls(&self) -> bool {
(**self).verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
(**self).extra()
}
fn proto(&self) -> String {
(**self).proto()
}
fn opts(&self) -> Option {
(**self).opts()
}
fn labels(&self) -> HashMap {
(**self).labels()
}
}
/// Restrictions placed on the origin of incoming connections to the edge.
#[derive(Clone, Default)]
pub(crate) struct CidrRestrictions {
/// Rejects connections that do not match the given CIDRs
pub(crate) allowed: Vec,
/// Rejects connections that match the given CIDRs and allows all other CIDRs.
pub(crate) denied: Vec,
}
impl CidrRestrictions {
pub(crate) fn allow(&mut self, cidr: impl Into) {
self.allowed.push(cidr.into());
}
pub(crate) fn deny(&mut self, cidr: impl Into) {
self.denied.push(cidr.into());
}
}
// Common
#[derive(Default, Clone)]
pub(crate) struct CommonOpts {
// Restrictions placed on the origin of incoming connections to the edge.
pub(crate) cidr_restrictions: CidrRestrictions,
// The version of PROXY protocol to use with this tunnel, zero if not
// using.
pub(crate) proxy_proto: ProxyProto,
// Tunnel-specific opaque metadata. Viewable via the API.
pub(crate) metadata: Option,
// Tunnel backend metadata. Viewable via the dashboard and API, but has no
// bearing on tunnel behavior.
pub(crate) forwards_to: Option,
// Tunnel L7 app protocol
pub(crate) forwards_proto: Option,
// Whether to disable certificate verification for this tunnel.
verify_upstream_tls: Option,
// DEPRECATED: use traffic_policy instead.
pub(crate) policy: Option,
// Policy that defines rules that should be applied to incoming or outgoing
// connections to the edge.
pub(crate) traffic_policy: Option,
// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub(crate) pooling_enabled: Option,
}
impl CommonOpts {
// Get the proto version of cidr restrictions
pub(crate) fn ip_restriction(&self) -> Option {
(!self.cidr_restrictions.allowed.is_empty() || !self.cidr_restrictions.denied.is_empty())
.then_some(self.cidr_restrictions.clone().into())
}
pub(crate) fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.forwards_to = Some(to_url.as_str().into());
self
}
pub(crate) fn set_verify_upstream_tls(&mut self, verify_upstream_tls: bool) {
self.verify_upstream_tls = Some(verify_upstream_tls)
}
pub(crate) fn verify_upstream_tls(&self) -> bool {
self.verify_upstream_tls.unwrap_or(true)
}
}
// transform into the wire protocol format
impl From for IpRestriction {
fn from(cr: CidrRestrictions) -> Self {
IpRestriction {
allow_cidrs: cr.allowed,
deny_cidrs: cr.denied,
}
}
}
impl From<&[bytes::Bytes]> for MutualTls {
fn from(b: &[bytes::Bytes]) -> Self {
let mut aggregated = Vec::new();
b.iter().for_each(|c| aggregated.extend(c));
MutualTls {
mutual_tls_ca: aggregated,
}
}
}
================================================
FILE: ngrok/src/config/headers.rs
================================================
use std::collections::HashMap;
use crate::internals::proto::Headers as HeaderProto;
/// HTTP Headers to modify at the ngrok edge.
#[derive(Clone, Default)]
pub(crate) struct Headers {
/// Headers to add to requests or responses at the ngrok edge.
added: HashMap,
/// Header names to remove from requests or responses at the ngrok edge.
removed: Vec,
}
impl Headers {
pub(crate) fn add(&mut self, name: impl Into, value: impl Into) {
self.added.insert(name.into().to_lowercase(), value.into());
}
pub(crate) fn remove(&mut self, name: impl Into) {
self.removed.push(name.into().to_lowercase());
}
pub(crate) fn has_entries(&self) -> bool {
!self.added.is_empty() || !self.removed.is_empty()
}
}
// transform into the wire protocol format
impl From for HeaderProto {
fn from(headers: Headers) -> Self {
HeaderProto {
add: headers
.added
.iter()
.map(|a| format!("{}:{}", a.0, a.1))
.collect(),
remove: headers.removed,
add_parsed: HashMap::new(), // unused in this context
}
}
}
================================================
FILE: ngrok/src/config/http.rs
================================================
use std::{
borrow::Borrow,
collections::HashMap,
convert::From,
str::FromStr,
};
use bytes::Bytes;
use thiserror::Error;
use url::Url;
use super::{
common::ProxyProto,
Policy,
};
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::{
common::{
default_forwards_to,
Binding,
CommonOpts,
TunnelConfig,
},
headers::Headers,
oauth::OauthOptions,
oidc::OidcOptions,
webhook_verification::WebhookVerification,
},
internals::proto::{
BasicAuth,
BasicAuthCredential,
BindExtra,
BindOpts,
CircuitBreaker,
Compression,
HttpEndpoint,
UserAgentFilter,
WebsocketTcpConverter,
},
tunnel::HttpTunnel,
Session,
};
/// Error representing invalid string for Scheme
#[derive(Debug, Clone, Error)]
#[error("invalid scheme string: {}", .0)]
pub struct InvalidSchemeString(String);
/// The URL scheme for this HTTP endpoint.
///
/// [Scheme::HTTPS] will enable TLS termination at the ngrok edge.
#[derive(Clone, Default, Eq, PartialEq)]
pub enum Scheme {
/// The `http` URL scheme.
HTTP,
/// The `https` URL scheme.
#[default]
HTTPS,
}
impl FromStr for Scheme {
type Err = InvalidSchemeString;
fn from_str(s: &str) -> Result {
use Scheme::*;
Ok(match s.to_uppercase().as_str() {
"HTTP" => HTTP,
"HTTPS" => HTTPS,
_ => return Err(InvalidSchemeString(s.into())),
})
}
}
/// Restrictions placed on the origin of incoming connections to the edge.
#[derive(Clone, Default)]
pub(crate) struct UaFilter {
/// Rejects connections that do not match the given regular expression
pub(crate) allow: Vec,
/// Rejects connections that match the given regular expression and allows
/// all other regular expressions.
pub(crate) deny: Vec,
}
impl UaFilter {
pub(crate) fn allow(&mut self, allow: impl Into) {
self.allow.push(allow.into());
}
pub(crate) fn deny(&mut self, deny: impl Into) {
self.deny.push(deny.into());
}
}
impl From for UserAgentFilter {
fn from(ua: UaFilter) -> Self {
UserAgentFilter {
allow: ua.allow,
deny: ua.deny,
}
}
}
/// The options for a HTTP edge.
#[derive(Default, Clone)]
struct HttpOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) scheme: Scheme,
pub(crate) domain: Option,
pub(crate) mutual_tlsca: Vec,
pub(crate) compression: bool,
pub(crate) websocket_tcp_conversion: bool,
pub(crate) circuit_breaker: f64,
pub(crate) request_headers: Headers,
pub(crate) response_headers: Headers,
pub(crate) rewrite_host: bool,
pub(crate) basic_auth: Vec<(String, String)>,
pub(crate) oauth: Option,
pub(crate) oidc: Option,
pub(crate) webhook_verification: Option,
// Flitering placed on the origin of incoming connections to the edge.
pub(crate) user_agent_filter: UaFilter,
pub(crate) bindings: Vec,
}
impl HttpOptions {
fn user_agent_filter(&self) -> Option {
(!self.user_agent_filter.allow.is_empty() || !self.user_agent_filter.deny.is_empty())
.then_some(self.user_agent_filter.clone().into())
}
}
impl TunnelConfig for HttpOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn forwards_proto(&self) -> String {
self.common_opts.forwards_proto.clone().unwrap_or_default()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: self.bindings.clone(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
if self.scheme == Scheme::HTTP {
return "http".into();
}
"https".into()
}
fn opts(&self) -> Option {
let http_endpoint = HttpEndpoint {
proxy_proto: self.common_opts.proxy_proto,
domain: self.domain.clone().unwrap_or_default(),
hostname: String::new(),
compression: self.compression.then_some(Compression {}),
circuit_breaker: (self.circuit_breaker != 0f64).then_some(CircuitBreaker {
error_threshold: self.circuit_breaker,
}),
ip_restriction: self.common_opts.ip_restriction(),
basic_auth: (!self.basic_auth.is_empty()).then_some(self.basic_auth.as_slice().into()),
oauth: self.oauth.clone().map(From::from),
oidc: self.oidc.clone().map(From::from),
webhook_verification: self.webhook_verification.clone().map(From::from),
mutual_tls_ca: (!self.mutual_tlsca.is_empty())
.then_some(self.mutual_tlsca.as_slice().into()),
request_headers: self
.request_headers
.has_entries()
.then_some(self.request_headers.clone().into()),
response_headers: self
.response_headers
.has_entries()
.then_some(self.response_headers.clone().into()),
websocket_tcp_converter: self
.websocket_tcp_conversion
.then_some(WebsocketTcpConverter {}),
user_agent_filter: self.user_agent_filter(),
traffic_policy: if self.common_opts.traffic_policy.is_some() {
self.common_opts.traffic_policy.clone().map(From::from)
} else if self.common_opts.policy.is_some() {
self.common_opts.policy.clone().map(From::from)
} else {
None
},
..Default::default()
};
Some(BindOpts::Http(http_endpoint))
}
fn labels(&self) -> HashMap {
HashMap::new()
}
}
// transform into the wire protocol format
impl From<&[(String, String)]> for BasicAuth {
fn from(v: &[(String, String)]) -> Self {
BasicAuth {
credentials: v.iter().cloned().map(From::from).collect(),
}
}
}
// transform into the wire protocol format
impl From<(String, String)> for BasicAuthCredential {
fn from(b: (String, String)) -> Self {
BasicAuthCredential {
username: b.0,
cleartext_password: b.1,
hashed_password: vec![], // unused in this context
}
}
}
impl_builder! {
/// A builder for a tunnel backing an HTTP endpoint.
///
/// https://ngrok.com/docs/http/
HttpTunnelBuilder, HttpOptions, HttpTunnel, endpoint
}
impl HttpTunnelBuilder {
/// Add the provided CIDR to the allowlist.
///
/// https://ngrok.com/docs/http/ip-restrictions/
pub fn allow_cidr(&mut self, cidr: impl Into) -> &mut Self {
self.options.common_opts.cidr_restrictions.allow(cidr);
self
}
/// Add the provided CIDR to the denylist.
///
/// https://ngrok.com/docs/http/ip-restrictions/
pub fn deny_cidr(&mut self, cidr: impl Into) -> &mut Self {
self.options.common_opts.cidr_restrictions.deny(cidr);
self
}
/// Sets the PROXY protocol version for connections over this tunnel.
pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
self.options.common_opts.proxy_proto = proxy_proto;
self
}
/// Sets the opaque metadata string for this tunnel.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Sets the ingress configuration for this endpoint.
///
/// Valid binding values are:
/// - `"public"` - Publicly accessible endpoint
/// - `"internal"` - Internal-only endpoint
/// - `"kubernetes"` - Kubernetes cluster binding
///
/// If not specified, the ngrok service will use its default binding configuration.
///
/// # Panics
///
/// Panics if called more than once or if an invalid binding value is provided.
///
/// # Examples
///
/// ```no_run
/// # use ngrok::Session;
/// # use ngrok::config::TunnelBuilder;
/// # async fn example() -> Result<(), Box> {
/// let session = Session::builder().authtoken_from_env().connect().await?;
///
/// // Using string
/// let tunnel = session.http_endpoint().binding("internal").listen().await?;
///
/// // Using typed enum
/// use ngrok::config::Binding;
/// let tunnel = session.http_endpoint().binding(Binding::Public).listen().await?;
/// # Ok(())
/// # }
/// ```
pub fn binding(&mut self, binding: impl Into) -> &mut Self {
if !self.options.bindings.is_empty() {
panic!("binding() can only be called once");
}
let binding_str = binding.into();
if let Err(e) = Binding::validate(&binding_str) {
panic!("{}", e);
}
self.options.bindings.push(binding_str);
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into) -> &mut Self {
self.options.common_opts.forwards_to = Some(forwards_to.into());
self
}
/// Sets the L7 protocol for this tunnel.
pub fn app_protocol(&mut self, app_protocol: impl Into) -> &mut Self {
self.options.common_opts.forwards_proto = Some(app_protocol.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
/// Sets the scheme for this edge.
pub fn scheme(&mut self, scheme: Scheme) -> &mut Self {
self.options.scheme = scheme;
self
}
/// Sets the domain to request for this edge.
///
/// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#domains
pub fn domain(&mut self, domain: impl Into) -> &mut Self {
self.options.domain = Some(domain.into());
self
}
/// Adds a certificate in PEM format to use for mutual TLS authentication.
///
/// These will be used to authenticate client certificates for requests at
/// the ngrok edge.
///
/// https://ngrok.com/docs/http/mutual-tls/
pub fn mutual_tlsca(&mut self, mutual_tlsca: Bytes) -> &mut Self {
self.options.mutual_tlsca.push(mutual_tlsca);
self
}
/// Enables gzip compression.
///
/// https://ngrok.com/docs/http/compression/
pub fn compression(&mut self) -> &mut Self {
self.options.compression = true;
self
}
/// Enables the websocket-to-tcp converter.
///
/// https://ngrok.com/docs/http/websocket-tcp-converter/
pub fn websocket_tcp_conversion(&mut self) -> &mut Self {
self.options.websocket_tcp_conversion = true;
self
}
/// Sets the 5XX response ratio at which the ngrok edge will stop sending
/// requests to this tunnel.
///
/// https://ngrok.com/docs/http/circuit-breaker/
pub fn circuit_breaker(&mut self, circuit_breaker: f64) -> &mut Self {
self.options.circuit_breaker = circuit_breaker;
self
}
/// Automatically rewrite the host header to the one in the provided URL
/// when calling [ForwarderBuilder::listen_and_forward]. Does nothing if
/// using [TunnelBuilder::listen]. Defaults to `false`.
///
/// If you need to set the host header to a specific value, use
/// `cfg.request_header("host", "some.host.com")` instead.
pub fn host_header_rewrite(&mut self, rewrite: bool) -> &mut Self {
self.options.rewrite_host = rewrite;
self
}
/// Adds a header to all requests to this edge.
///
/// https://ngrok.com/docs/http/request-headers/
pub fn request_header(
&mut self,
name: impl Into,
value: impl Into,
) -> &mut Self {
self.options.request_headers.add(name, value);
self
}
/// Adds a header to all responses coming from this edge.
///
/// https://ngrok.com/docs/http/response-headers/
pub fn response_header(
&mut self,
name: impl Into,
value: impl Into,
) -> &mut Self {
self.options.response_headers.add(name, value);
self
}
/// Removes a header from requests to this edge.
///
/// https://ngrok.com/docs/http/request-headers/
pub fn remove_request_header(&mut self, name: impl Into) -> &mut Self {
self.options.request_headers.remove(name);
self
}
/// Removes a header from responses from this edge.
///
/// https://ngrok.com/docs/http/response-headers/
pub fn remove_response_header(&mut self, name: impl Into) -> &mut Self {
self.options.response_headers.remove(name);
self
}
/// Adds the provided credentials to the list of basic authentication
/// credentials.
///
/// https://ngrok.com/docs/http/basic-auth/
pub fn basic_auth(
&mut self,
username: impl Into,
password: impl Into,
) -> &mut Self {
self.options
.basic_auth
.push((username.into(), password.into()));
self
}
/// Set the OAuth configuraton for this edge.
///
/// https://ngrok.com/docs/http/oauth/
pub fn oauth(&mut self, oauth: impl Borrow) -> &mut Self {
self.options.oauth = Some(oauth.borrow().to_owned());
self
}
/// Set the OIDC configuration for this edge.
///
/// https://ngrok.com/docs/http/openid-connect/
pub fn oidc(&mut self, oidc: impl Borrow) -> &mut Self {
self.options.oidc = Some(oidc.borrow().to_owned());
self
}
/// Configures webhook verification for this edge.
///
/// https://ngrok.com/docs/http/webhook-verification/
pub fn webhook_verification(
&mut self,
provider: impl Into,
secret: impl Into,
) -> &mut Self {
self.options.webhook_verification = Some(WebhookVerification {
provider: provider.into(),
secret: secret.into().into(),
});
self
}
/// Add the provided regex to the allowlist.
///
/// https://ngrok.com/docs/http/user-agent-filter/
pub fn allow_user_agent(&mut self, regex: impl Into) -> &mut Self {
self.options.user_agent_filter.allow(regex);
self
}
/// Add the provided regex to the denylist.
///
/// https://ngrok.com/docs/http/user-agent-filter/
pub fn deny_user_agent(&mut self, regex: impl Into) -> &mut Self {
self.options.user_agent_filter.deny(regex);
self
}
/// DEPRECATED: use traffic_policy instead.
pub fn policy(&mut self, s: S) -> Result<&mut Self, S::Error>
where
S: TryInto,
{
self.options.common_opts.policy = Some(s.try_into()?);
Ok(self)
}
/// Set policy for this edge.
pub fn traffic_policy(&mut self, policy_str: impl Into) -> &mut Self {
self.options.common_opts.traffic_policy = Some(policy_str.into());
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
if let Some(host) = to_url.host_str().filter(|_| self.options.rewrite_host) {
self.request_header("host", host);
}
self
}
/// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub fn pooling_enabled(&mut self, pooling_enabled: impl Into) -> &mut Self {
self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
self
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::policies::test::POLICY_JSON;
const METADATA: &str = "testmeta";
const TEST_FORWARD: &str = "testforward";
const TEST_FORWARD_PROTO: &str = "http2";
const ALLOW_CIDR: &str = "0.0.0.0/0";
const DENY_CIDR: &str = "10.1.1.1/32";
const CA_CERT: &[u8] = "test ca cert".as_bytes();
const CA_CERT2: &[u8] = "test ca cert2".as_bytes();
const DOMAIN: &str = "test domain";
const ALLOW_AGENT: &str = r"bar/(\d)+";
const DENY_AGENT: &str = r"foo/(\d)+";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&HttpTunnelBuilder {
session: None,
options: Default::default(),
}
.allow_user_agent(ALLOW_AGENT)
.deny_user_agent(DENY_AGENT)
.allow_cidr(ALLOW_CIDR)
.deny_cidr(DENY_CIDR)
.proxy_proto(ProxyProto::V2)
.metadata(METADATA)
.scheme(Scheme::from_str("hTtPs").unwrap())
.domain(DOMAIN)
.mutual_tlsca(CA_CERT.into())
.mutual_tlsca(CA_CERT2.into())
.compression()
.websocket_tcp_conversion()
.circuit_breaker(0.5)
.request_header("X-Req-Yup", "true")
.response_header("X-Res-Yup", "true")
.remove_request_header("X-Req-Nope")
.remove_response_header("X-Res-Nope")
.oauth(OauthOptions::new("google"))
.oauth(
OauthOptions::new("google")
.allow_email("@")
.allow_domain("")
.scope(""),
)
.oidc(OidcOptions::new("", "", ""))
.oidc(
OidcOptions::new("", "", "")
.allow_email("@")
.allow_domain("")
.scope(""),
)
.webhook_verification("twilio", "asdf")
.basic_auth("ngrok", "online1line")
.forwards_to(TEST_FORWARD)
.app_protocol("http2")
.policy(POLICY_JSON)
.unwrap()
.options,
);
}
fn tunnel_test(tunnel_cfg: C)
where
C: TunnelConfig,
{
assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
assert_eq!(TEST_FORWARD_PROTO, tunnel_cfg.forwards_proto());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(Vec::::new(), extra.bindings);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("https", tunnel_cfg.proto());
let opts = tunnel_cfg.opts().unwrap();
assert!(matches!(opts, BindOpts::Http { .. }));
if let BindOpts::Http(endpoint) = opts {
assert_eq!(DOMAIN, endpoint.domain);
assert_eq!(String::default(), endpoint.subdomain);
assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
let ip_restriction = endpoint.ip_restriction.unwrap();
assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
let mutual_tls = endpoint.mutual_tls_ca.unwrap();
let mut agg = CA_CERT.to_vec();
agg.extend(CA_CERT2.to_vec());
assert_eq!(agg, mutual_tls.mutual_tls_ca);
assert!(endpoint.compression.is_some());
assert!(endpoint.websocket_tcp_converter.is_some());
assert_eq!(0.5f64, endpoint.circuit_breaker.unwrap().error_threshold);
let request_headers = endpoint.request_headers.unwrap();
assert_eq!(["x-req-yup:true"].to_vec(), request_headers.add);
assert_eq!(["x-req-nope"].to_vec(), request_headers.remove);
let response_headers = endpoint.response_headers.unwrap();
assert_eq!(["x-res-yup:true"].to_vec(), response_headers.add);
assert_eq!(["x-res-nope"].to_vec(), response_headers.remove);
let webhook = endpoint.webhook_verification.unwrap();
assert_eq!("twilio", webhook.provider);
assert_eq!("asdf", *webhook.secret);
assert!(webhook.sealed_secret.is_empty());
let creds = endpoint.basic_auth.unwrap().credentials;
assert_eq!(1, creds.len());
assert_eq!("ngrok", creds[0].username);
assert_eq!("online1line", creds[0].cleartext_password);
assert!(creds[0].hashed_password.is_empty());
let oauth = endpoint.oauth.unwrap();
assert_eq!("google", oauth.provider);
assert_eq!(["@"].to_vec(), oauth.allow_emails);
assert_eq!([""].to_vec(), oauth.allow_domains);
assert_eq!([""].to_vec(), oauth.scopes);
assert_eq!(String::default(), oauth.client_id);
assert_eq!(String::default(), *oauth.client_secret);
assert!(oauth.sealed_client_secret.is_empty());
let oidc = endpoint.oidc.unwrap();
assert_eq!("", oidc.issuer_url);
assert_eq!(["@"].to_vec(), oidc.allow_emails);
assert_eq!([""].to_vec(), oidc.allow_domains);
assert_eq!([""].to_vec(), oidc.scopes);
assert_eq!("", oidc.client_id);
assert_eq!("", *oidc.client_secret);
assert!(oidc.sealed_client_secret.is_empty());
let user_agent_filter = endpoint.user_agent_filter.unwrap();
assert_eq!(Vec::from([ALLOW_AGENT]), user_agent_filter.allow);
assert_eq!(Vec::from([DENY_AGENT]), user_agent_filter.deny);
}
assert_eq!(HashMap::new(), tunnel_cfg.labels());
}
#[test]
fn test_binding_valid_values() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
// Test "public"
builder.binding("public");
assert_eq!(vec!["public"], builder.options.bindings);
// Test "internal"
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal");
assert_eq!(vec!["internal"], builder.options.bindings);
// Test "kubernetes"
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("kubernetes");
assert_eq!(vec!["kubernetes"], builder.options.bindings);
// Test with Binding enum
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding(Binding::Internal);
assert_eq!(vec!["internal"], builder.options.bindings);
}
#[test]
#[should_panic(expected = "Invalid binding value")]
fn test_binding_invalid_value() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("invalid");
}
#[test]
#[should_panic(expected = "binding() can only be called once")]
fn test_binding_called_twice() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("public");
builder.binding("internal");
}
#[test]
fn test_binding_with_domain() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal").domain("foo.internal");
// Check that both binding and domain are set
assert_eq!(vec!["internal"], builder.options.bindings);
assert_eq!(Some("foo.internal".to_string()), builder.options.domain);
// Check that they're properly included in extra() and opts()
let extra = builder.options.extra();
assert_eq!(vec!["internal"], extra.bindings);
let opts = builder.options.opts().unwrap();
if let BindOpts::Http(endpoint) = opts {
assert_eq!("foo.internal", endpoint.domain);
} else {
panic!("Expected Http endpoint");
}
}
}
================================================
FILE: ngrok/src/config/labeled.rs
================================================
use std::collections::HashMap;
use url::Url;
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::common::{
default_forwards_to,
CommonOpts,
TunnelConfig,
},
internals::proto::{
BindExtra,
BindOpts,
},
tunnel::LabeledTunnel,
Session,
};
/// Options for labeled tunnels.
#[derive(Default, Clone)]
struct LabeledOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) labels: HashMap,
}
impl TunnelConfig for LabeledOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn forwards_proto(&self) -> String {
self.common_opts.forwards_proto.clone().unwrap_or_default()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: Vec::new(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
"".into()
}
fn opts(&self) -> Option {
None
}
fn labels(&self) -> HashMap {
self.labels.clone()
}
}
impl_builder! {
/// A builder for a labeled tunnel.
LabeledTunnelBuilder, LabeledOptions, LabeledTunnel, edge
}
impl LabeledTunnelBuilder {
/// Sets the opaque metadata string for this tunnel.
/// Viewable via the API.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Add a label, value pair for this tunnel.
///
/// https://ngrok.com/docs/network-edge/edges/#tunnel-group
pub fn label(&mut self, label: impl Into, value: impl Into) -> &mut Self {
self.options.labels.insert(label.into(), value.into());
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into) -> &mut Self {
self.options.common_opts.forwards_to = forwards_to.into().into();
self
}
/// Sets the L7 protocol string for this tunnel.
pub fn app_protocol(&mut self, app_protocol: impl Into) -> &mut Self {
self.options.common_opts.forwards_proto = Some(app_protocol.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
self
}
}
#[cfg(test)]
mod test {
use super::*;
const METADATA: &str = "testmeta";
const LABEL_KEY: &str = "edge";
const LABEL_VAL: &str = "edghts_2IC6RJ6CQnuh7waciWyaGKc50Nt";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&LabeledTunnelBuilder {
session: None,
options: Default::default(),
}
.metadata(METADATA)
.label(LABEL_KEY, LABEL_VAL)
.options,
);
}
fn tunnel_test(tunnel_cfg: &C)
where
C: TunnelConfig,
{
assert_eq!(default_forwards_to(), tunnel_cfg.forwards_to());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("", tunnel_cfg.proto());
assert!(tunnel_cfg.opts().is_none());
let mut labels: HashMap = HashMap::new();
labels.insert(LABEL_KEY.into(), LABEL_VAL.into());
assert_eq!(labels, tunnel_cfg.labels());
}
}
================================================
FILE: ngrok/src/config/oauth.rs
================================================
use crate::internals::proto::{
Oauth,
SecretString,
};
/// Oauth Options configuration
///
/// https://ngrok.com/docs/http/oauth/
#[derive(Clone, Default)]
pub struct OauthOptions {
/// The OAuth provider to use
provider: String,
/// The client ID, if a custom one is being used
client_id: String,
/// The client secret, if a custom one is being used
client_secret: SecretString,
/// Email addresses of users to authorize.
allow_emails: Vec,
/// Email domains of users to authorize.
allow_domains: Vec,
/// OAuth scopes to request from the provider.
scopes: Vec,
}
impl OauthOptions {
/// Create a new [OauthOptions] for the given provider.
pub fn new(provider: impl Into) -> Self {
OauthOptions {
provider: provider.into(),
..Default::default()
}
}
/// Provide an OAuth client ID for custom apps.
pub fn client_id(&mut self, id: impl Into) -> &mut Self {
self.client_id = id.into();
self
}
/// Provide an OAuth client secret for custom apps.
pub fn client_secret(&mut self, secret: impl Into) -> &mut Self {
self.client_secret = SecretString::from(secret.into());
self
}
/// Append an email address to the list of allowed emails.
pub fn allow_email(&mut self, email: impl Into) -> &mut Self {
self.allow_emails.push(email.into());
self
}
/// Append an email domain to the list of allowed domains.
pub fn allow_domain(&mut self, domain: impl Into) -> &mut Self {
self.allow_domains.push(domain.into());
self
}
/// Append a scope to the list of scopes to request.
pub fn scope(&mut self, scope: impl Into) -> &mut Self {
self.scopes.push(scope.into());
self
}
}
// transform into the wire protocol format
impl From for Oauth {
fn from(o: OauthOptions) -> Self {
Oauth {
provider: o.provider,
client_id: o.client_id,
client_secret: o.client_secret,
sealed_client_secret: Default::default(), // unused in this context
allow_emails: o.allow_emails,
allow_domains: o.allow_domains,
scopes: o.scopes,
}
}
}
================================================
FILE: ngrok/src/config/oidc.rs
================================================
use crate::internals::proto::{
Oidc,
SecretString,
};
/// Oidc Options configuration
///
/// https://ngrok.com/docs/http/openid-connect/
#[derive(Clone, Default)]
pub struct OidcOptions {
issuer_url: String,
client_id: String,
client_secret: SecretString,
allow_emails: Vec,
allow_domains: Vec,
scopes: Vec,
}
impl OidcOptions {
/// Create a new [OidcOptions] with the given issuer and client information.
pub fn new(
issuer_url: impl Into,
client_id: impl Into,
client_secret: impl Into,
) -> Self {
OidcOptions {
issuer_url: issuer_url.into(),
client_id: client_id.into(),
client_secret: client_secret.into().into(),
..Default::default()
}
}
/// Allow the oidc user with the given email to access the tunnel.
pub fn allow_email(&mut self, email: impl Into) -> &mut Self {
self.allow_emails.push(email.into());
self
}
/// Allow the oidc user with the given email domain to access the tunnel.
pub fn allow_domain(&mut self, domain: impl Into) -> &mut Self {
self.allow_domains.push(domain.into());
self
}
/// Request the given scope from the oidc provider.
pub fn scope(&mut self, scope: impl Into) -> &mut Self {
self.scopes.push(scope.into());
self
}
}
// transform into the wire protocol format
impl From for Oidc {
fn from(o: OidcOptions) -> Self {
Oidc {
issuer_url: o.issuer_url,
client_id: o.client_id,
client_secret: o.client_secret,
sealed_client_secret: Default::default(), // unused in this context
allow_emails: o.allow_emails,
allow_domains: o.allow_domains,
scopes: o.scopes,
}
}
}
================================================
FILE: ngrok/src/config/policies.rs
================================================
use std::{
fs::read_to_string,
io,
};
use serde::{
Deserialize,
Serialize,
};
use thiserror::Error;
use crate::internals::proto;
/// A policy that defines rules that should be applied to incoming or outgoing
/// connections to the edge.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct Policy {
inbound: Vec,
outbound: Vec,
}
/// A policy rule that should be applied
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct Rule {
name: String,
expressions: Vec,
actions: Vec,
}
/// An action that should be taken if the rule matches
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct Action {
#[serde(rename = "type")]
type_: String,
config: Option,
}
/// Errors in creating or serializing Policies
#[derive(Debug, Error)]
pub enum InvalidPolicy {
/// Error representing an invalid string for a Policy
#[error("failure to parse or generate policy")]
SerializationError(#[from] serde_json::Error),
/// An error loading a Policy from a file
#[error("failure to read policy file '{}'", .1)]
FileReadError(#[source] io::Error, String),
}
impl Policy {
/// Create a new empty [Policy] struct
pub fn new() -> Self {
Policy {
..Default::default()
}
}
/// Create a new [Policy] from a json string
fn from_json(json: impl AsRef) -> Result {
serde_json::from_str(json.as_ref()).map_err(InvalidPolicy::SerializationError)
}
/// Create a new [Policy] from a json file
pub fn from_file(json_file_path: impl AsRef) -> Result {
Policy::from_json(
read_to_string(json_file_path.as_ref()).map_err(|e| {
InvalidPolicy::FileReadError(e, json_file_path.as_ref().to_string())
})?,
)
}
/// Convert [Policy] to json string
pub fn to_json(&self) -> Result {
serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError)
}
/// Add an inbound policy
pub fn add_inbound(&mut self, rule: impl Into) -> &mut Self {
self.inbound.push(rule.into());
self
}
/// Add an outbound policy
pub fn add_outbound(&mut self, rule: impl Into) -> &mut Self {
self.outbound.push(rule.into());
self
}
}
impl TryFrom<&Policy> for Policy {
type Error = InvalidPolicy;
fn try_from(other: &Policy) -> Result {
Ok(other.clone())
}
}
impl TryFrom> for Policy {
type Error = InvalidPolicy;
fn try_from(other: Result) -> Result {
other
}
}
impl TryFrom<&str> for Policy {
type Error = InvalidPolicy;
fn try_from(other: &str) -> Result {
Policy::from_json(other)
}
}
impl Rule {
/// Create a new [Rule]
pub fn new(name: impl Into) -> Self {
Rule {
name: name.into(),
..Default::default()
}
}
/// Convert [Rule] to json string
pub fn to_json(&self) -> Result {
serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError)
}
/// Add an expression
pub fn add_expression(&mut self, expression: impl Into) -> &mut Self {
self.expressions.push(expression.into());
self
}
/// Add an action
pub fn add_action(&mut self, action: Action) -> &mut Self {
self.actions.push(action);
self
}
}
impl From<&mut Rule> for Rule {
fn from(other: &mut Rule) -> Self {
other.to_owned()
}
}
impl Action {
/// Create a new [Action]
pub fn new(type_: impl Into, config: Option<&str>) -> Result {
Ok(Action {
type_: type_.into(),
config: config
.map(|c| serde_json::from_str(c).map_err(InvalidPolicy::SerializationError))
.transpose()?,
})
}
/// Convert [Action] to json string
pub fn to_json(&self) -> Result {
serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError)
}
}
impl From for proto::PolicyWrapper {
fn from(value: Policy) -> Self {
proto::PolicyWrapper::Policy(value.into())
}
}
// transform into the wire protocol format
impl From for proto::Policy {
fn from(o: Policy) -> Self {
proto::Policy {
inbound: o.inbound.into_iter().map(|p| p.into()).collect(),
outbound: o.outbound.into_iter().map(|p| p.into()).collect(),
}
}
}
impl From for proto::Rule {
fn from(p: Rule) -> Self {
proto::Rule {
name: p.name,
expressions: p.expressions,
actions: p.actions.into_iter().map(|a| a.into()).collect(),
}
}
}
impl From for proto::Action {
fn from(a: Action) -> Self {
proto::Action {
type_: a.type_,
config: a
.config
.map(|c| c.to_string().into_bytes())
.unwrap_or_default(),
}
}
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
pub(crate) const POLICY_JSON: &str = r###"
{"inbound": [
{
"name": "test_in",
"expressions": ["req.Method == 'PUT'"],
"actions": [{"type": "deny"}]
}
],
"outbound": [
{
"name": "test_out",
"expressions": ["res.StatusCode == '200'"],
"actions": [{"type": "custom-response", "config": {"status_code":201}}]
}
]}
"###;
#[test]
fn test_json_to_policy() {
let policy: Policy = Policy::from_json(POLICY_JSON).unwrap();
assert_eq!(1, policy.inbound.len());
assert_eq!(1, policy.outbound.len());
let inbound = &policy.inbound[0];
let outbound = &policy.outbound[0];
assert_eq!("test_in", inbound.name);
assert_eq!(1, inbound.expressions.len());
assert_eq!(1, inbound.actions.len());
assert_eq!("req.Method == 'PUT'", inbound.expressions[0]);
assert_eq!("deny", inbound.actions[0].type_);
assert_eq!(None, inbound.actions[0].config);
assert_eq!("test_out", outbound.name);
assert_eq!(1, outbound.expressions.len());
assert_eq!(1, outbound.actions.len());
assert_eq!("res.StatusCode == '200'", outbound.expressions[0]);
assert_eq!("custom-response", outbound.actions[0].type_);
assert_eq!(
"{\"status_code\":201}",
outbound.actions[0].config.as_ref().unwrap().to_string()
);
}
#[test]
fn test_empty_json_to_policy() {
let policy: Policy = Policy::from_json("{}").unwrap();
assert_eq!(0, policy.inbound.len());
assert_eq!(0, policy.outbound.len());
}
#[test]
fn test_policy_to_json() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let json = policy.to_json().unwrap();
let policy2 = Policy::from_json(json).unwrap();
assert_eq!(policy, policy2);
}
#[test]
fn test_policy_to_json_error() {
let error = Policy::from_json("asdf").err().unwrap();
assert!(matches!(error, InvalidPolicy::SerializationError { .. }));
}
#[test]
fn test_rule_to_json() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let rule = &policy.outbound[0];
let json = rule.to_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let rule_map = parsed.as_object().unwrap();
assert_eq!("test_out", rule_map["name"]);
// expressions
let expressions = rule_map["expressions"].as_array().unwrap();
assert_eq!(1, expressions.len());
assert_eq!("res.StatusCode == '200'", expressions[0]);
// actions
let actions = rule_map["actions"].as_array().unwrap();
assert_eq!(1, actions.len());
assert_eq!("custom-response", actions[0]["type"]);
assert_eq!(201, actions[0]["config"]["status_code"]);
}
#[test]
fn test_action_to_json() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let action = &policy.outbound[0].actions[0];
let json = action.to_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let action_map = parsed.as_object().unwrap();
assert_eq!("custom-response", action_map["type"]);
assert_eq!(201, action_map["config"]["status_code"]);
}
#[test]
fn test_builders() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let policy2 = Policy::new()
.add_inbound(
Rule::new("test_in")
.add_expression("req.Method == 'PUT'")
.add_action(Action::new("deny", None).unwrap()),
)
.add_outbound(
Rule::new("test_out")
.add_expression("res.StatusCode == '200'")
// .add_action(Action::new("deny", ""))
.add_action(
Action::new("custom-response", Some("{\"status_code\":201}")).unwrap(),
),
)
.to_owned();
assert_eq!(policy, policy2);
}
#[test]
fn test_load_file() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let policy2 = Policy::from_file("assets/policy.json").unwrap();
assert_eq!("test_in", policy2.inbound[0].name);
assert_eq!("test_out", policy2.outbound[0].name);
assert_eq!(policy, policy2);
}
#[test]
fn test_load_inbound_file() {
let policy = Policy::from_file("assets/policy-inbound.json").unwrap();
assert_eq!("test_in", policy.inbound[0].name);
assert_eq!(0, policy.outbound.len());
}
#[test]
fn test_load_file_error() {
let error = Policy::from_file("assets/absent.json").err().unwrap();
assert!(matches!(error, InvalidPolicy::FileReadError { .. }));
}
}
================================================
FILE: ngrok/src/config/tcp.rs
================================================
use std::{
collections::HashMap,
convert::From,
};
use url::Url;
use super::{
common::ProxyProto,
Policy,
};
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::common::{
default_forwards_to,
Binding,
CommonOpts,
TunnelConfig,
},
internals::proto::{
self,
BindExtra,
BindOpts,
},
tunnel::TcpTunnel,
Session,
};
/// The options for a TCP edge.
#[derive(Default, Clone)]
struct TcpOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) remote_addr: Option,
pub(crate) bindings: Vec,
}
impl TunnelConfig for TcpOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: self.bindings.clone(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
"tcp".into()
}
fn forwards_proto(&self) -> String {
// not supported
String::new()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn opts(&self) -> Option {
// fill out all the options, translating to proto here
let mut tcp_endpoint = proto::TcpEndpoint::default();
if let Some(remote_addr) = self.remote_addr.as_ref() {
tcp_endpoint.addr = remote_addr.clone();
}
tcp_endpoint.proxy_proto = self.common_opts.proxy_proto;
tcp_endpoint.ip_restriction = self.common_opts.ip_restriction();
tcp_endpoint.traffic_policy = if self.common_opts.traffic_policy.is_some() {
self.common_opts.traffic_policy.clone().map(From::from)
} else if self.common_opts.policy.is_some() {
self.common_opts.policy.clone().map(From::from)
} else {
None
};
Some(BindOpts::Tcp(tcp_endpoint))
}
fn labels(&self) -> HashMap {
HashMap::new()
}
}
impl_builder! {
/// A builder for a tunnel backing a TCP endpoint.
///
/// https://ngrok.com/docs/tcp/
TcpTunnelBuilder, TcpOptions, TcpTunnel, endpoint
}
/// The options for a TCP edge.
impl TcpTunnelBuilder {
/// Add the provided CIDR to the allowlist.
///
/// https://ngrok.com/docs/tcp/ip-restrictions/
pub fn allow_cidr(&mut self, cidr: impl Into) -> &mut Self {
self.options.common_opts.cidr_restrictions.allow(cidr);
self
}
/// Add the provided CIDR to the denylist.
///
/// https://ngrok.com/docs/tcp/ip-restrictions/
pub fn deny_cidr(&mut self, cidr: impl Into) -> &mut Self {
self.options.common_opts.cidr_restrictions.deny(cidr);
self
}
/// Sets the PROXY protocol version for connections over this tunnel.
pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
self.options.common_opts.proxy_proto = proxy_proto;
self
}
/// Sets the opaque metadata string for this tunnel.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Sets the ingress configuration for this endpoint.
///
/// Valid binding values are:
/// - `"public"` - Publicly accessible endpoint
/// - `"internal"` - Internal-only endpoint
/// - `"kubernetes"` - Kubernetes cluster binding
///
/// If not specified, the ngrok service will use its default binding configuration.
///
/// # Panics
///
/// Panics if called more than once or if an invalid binding value is provided.
///
/// # Examples
///
/// ```no_run
/// # use ngrok::Session;
/// # use ngrok::config::TunnelBuilder;
/// # async fn example() -> Result<(), Box> {
/// let session = Session::builder().authtoken_from_env().connect().await?;
///
/// // Using string
/// let tunnel = session.tcp_endpoint().binding("internal").listen().await?;
///
/// // Using typed enum
/// use ngrok::config::Binding;
/// let tunnel = session.tcp_endpoint().binding(Binding::Public).listen().await?;
/// # Ok(())
/// # }
/// ```
pub fn binding(&mut self, binding: impl Into) -> &mut Self {
if !self.options.bindings.is_empty() {
panic!("binding() can only be called once");
}
let binding_str = binding.into();
if let Err(e) = Binding::validate(&binding_str) {
panic!("{}", e);
}
self.options.bindings.push(binding_str);
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into) -> &mut Self {
self.options.common_opts.forwards_to = Some(forwards_to.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
/// Sets the TCP address to request for this edge.
///
/// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#tcp-addresses
pub fn remote_addr(&mut self, remote_addr: impl Into) -> &mut Self {
self.options.remote_addr = Some(remote_addr.into());
self
}
/// DEPRECATED: use traffic_policy instead.
pub fn policy(&mut self, s: S) -> Result<&mut Self, S::Error>
where
S: TryInto,
{
self.options.common_opts.policy = Some(s.try_into()?);
Ok(self)
}
/// Set policy for this edge.
pub fn traffic_policy(&mut self, policy_str: impl Into) -> &mut Self {
self.options.common_opts.traffic_policy = Some(policy_str.into());
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
self
}
/// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub fn pooling_enabled(&mut self, pooling_enabled: impl Into) -> &mut Self {
self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
self
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::policies::test::POLICY_JSON;
const METADATA: &str = "testmeta";
const TEST_FORWARD: &str = "testforward";
const REMOTE_ADDR: &str = "4.tcp.ngrok.io:1337";
const ALLOW_CIDR: &str = "0.0.0.0/0";
const DENY_CIDR: &str = "10.1.1.1/32";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&TcpTunnelBuilder {
session: None,
options: Default::default(),
}
.allow_cidr(ALLOW_CIDR)
.deny_cidr(DENY_CIDR)
.proxy_proto(ProxyProto::V2)
.metadata(METADATA)
.remote_addr(REMOTE_ADDR)
.forwards_to(TEST_FORWARD)
.policy(POLICY_JSON)
.unwrap()
.options,
);
}
fn tunnel_test(tunnel_cfg: &C)
where
C: TunnelConfig,
{
assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(Vec::::new(), extra.bindings);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("tcp", tunnel_cfg.proto());
let opts = tunnel_cfg.opts().unwrap();
assert!(matches!(opts, BindOpts::Tcp { .. }));
if let BindOpts::Tcp(endpoint) = opts {
assert_eq!(REMOTE_ADDR, endpoint.addr);
assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
let ip_restriction = endpoint.ip_restriction.unwrap();
assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
}
assert_eq!(HashMap::new(), tunnel_cfg.labels());
}
#[test]
fn test_binding_valid_values() {
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
// Test "public"
builder.binding("public");
assert_eq!(vec!["public"], builder.options.bindings);
// Test "internal"
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal");
assert_eq!(vec!["internal"], builder.options.bindings);
// Test "kubernetes"
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("kubernetes");
assert_eq!(vec!["kubernetes"], builder.options.bindings);
// Test with Binding enum
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding(Binding::Public);
assert_eq!(vec!["public"], builder.options.bindings);
}
#[test]
#[should_panic(expected = "Invalid binding value")]
fn test_binding_invalid_value() {
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("invalid");
}
#[test]
#[should_panic(expected = "binding() can only be called once")]
fn test_binding_called_twice() {
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("public");
builder.binding("internal");
}
}
================================================
FILE: ngrok/src/config/tls.rs
================================================
use std::collections::HashMap;
use bytes::Bytes;
use url::Url;
use super::{
common::ProxyProto,
Policy,
};
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::common::{
default_forwards_to,
Binding,
CommonOpts,
TunnelConfig,
},
internals::proto::{
self,
BindExtra,
BindOpts,
TlsTermination,
},
tunnel::TlsTunnel,
Session,
};
/// The options for TLS edges.
#[derive(Default, Clone)]
struct TlsOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) domain: Option,
pub(crate) mutual_tlsca: Vec,
pub(crate) key_pem: Option,
pub(crate) cert_pem: Option,
pub(crate) bindings: Vec,
}
impl TunnelConfig for TlsOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn forwards_proto(&self) -> String {
// not supported
String::new()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: self.bindings.clone(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
"tls".into()
}
fn opts(&self) -> Option {
// fill out all the options, translating to proto here
let mut tls_endpoint = proto::TlsEndpoint::default();
if let Some(domain) = self.domain.as_ref() {
tls_endpoint.domain = domain.clone();
}
tls_endpoint.proxy_proto = self.common_opts.proxy_proto;
// doing some backflips to check both cert_pem and key_pem are set, and avoid unwrapping
let tls_termination = self
.cert_pem
.as_ref()
.zip(self.key_pem.as_ref())
.map(|(c, k)| TlsTermination {
cert: c.to_vec(),
key: k.to_vec().into(),
sealed_key: Vec::new(),
});
tls_endpoint.ip_restriction = self.common_opts.ip_restriction();
tls_endpoint.mutual_tls_at_edge =
(!self.mutual_tlsca.is_empty()).then_some(self.mutual_tlsca.as_slice().into());
tls_endpoint.tls_termination = tls_termination;
tls_endpoint.traffic_policy = if self.common_opts.traffic_policy.is_some() {
self.common_opts.traffic_policy.clone().map(From::from)
} else if self.common_opts.policy.is_some() {
self.common_opts.policy.clone().map(From::from)
} else {
None
};
Some(BindOpts::Tls(tls_endpoint))
}
fn labels(&self) -> HashMap {
HashMap::new()
}
}
impl_builder! {
/// A builder for a tunnel backing a TCP endpoint.
///
/// https://ngrok.com/docs/tls/
TlsTunnelBuilder, TlsOptions, TlsTunnel, endpoint
}
impl TlsTunnelBuilder {
/// Add the provided CIDR to the allowlist.
///
/// https://ngrok.com/docs/tls/ip-restrictions/
pub fn allow_cidr(&mut self, cidr: impl Into) -> &mut Self {
self.options.common_opts.cidr_restrictions.allow(cidr);
self
}
/// Add the provided CIDR to the denylist.
///
/// https://ngrok.com/docs/tls/ip-restrictions/
pub fn deny_cidr(&mut self, cidr: impl Into) -> &mut Self {
self.options.common_opts.cidr_restrictions.deny(cidr);
self
}
/// Sets the PROXY protocol version for connections over this tunnel.
pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
self.options.common_opts.proxy_proto = proxy_proto;
self
}
/// Sets the opaque metadata string for this tunnel.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Sets the ingress configuration for this endpoint.
///
/// Valid binding values are:
/// - `"public"` - Publicly accessible endpoint
/// - `"internal"` - Internal-only endpoint
/// - `"kubernetes"` - Kubernetes cluster binding
///
/// If not specified, the ngrok service will use its default binding configuration.
///
/// # Panics
///
/// Panics if called more than once or if an invalid binding value is provided.
///
/// # Examples
///
/// ```no_run
/// # use ngrok::Session;
/// # use ngrok::config::TunnelBuilder;
/// # async fn example() -> Result<(), Box> {
/// let session = Session::builder().authtoken_from_env().connect().await?;
///
/// // Using string
/// let tunnel = session.tls_endpoint().binding("internal").listen().await?;
///
/// // Using typed enum
/// use ngrok::config::Binding;
/// let tunnel = session.tls_endpoint().binding(Binding::Public).listen().await?;
/// # Ok(())
/// # }
/// ```
pub fn binding(&mut self, binding: impl Into) -> &mut Self {
if !self.options.bindings.is_empty() {
panic!("binding() can only be called once");
}
let binding_str = binding.into();
if let Err(e) = Binding::validate(&binding_str) {
panic!("{}", e);
}
self.options.bindings.push(binding_str);
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into) -> &mut Self {
self.options.common_opts.forwards_to = Some(forwards_to.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
/// Sets the domain to request for this edge.
///
/// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#domains
pub fn domain(&mut self, domain: impl Into) -> &mut Self {
self.options.domain = Some(domain.into());
self
}
/// Adds a certificate in PEM format to use for mutual TLS authentication.
///
/// These will be used to authenticate client certificates for requests at
/// the ngrok edge.
///
/// https://ngrok.com/docs/tls/mutual-tls/
pub fn mutual_tlsca(&mut self, mutual_tlsca: Bytes) -> &mut Self {
self.options.mutual_tlsca.push(mutual_tlsca);
self
}
/// Sets the key and certificate in PEM format for TLS termination at the
/// ngrok edge.
///
/// https://ngrok.com/docs/tls/tls-termination/
pub fn termination(&mut self, cert_pem: Bytes, key_pem: Bytes) -> &mut Self {
self.options.key_pem = Some(key_pem);
self.options.cert_pem = Some(cert_pem);
self
}
/// DEPRECATED: use traffic_policy instead.
pub fn policy(&mut self, s: S) -> Result<&mut Self, S::Error>
where
S: TryInto,
{
self.options.common_opts.policy = Some(s.try_into()?);
Ok(self)
}
/// Set policy for this edge.
pub fn traffic_policy(&mut self, policy_str: impl Into) -> &mut Self {
self.options.common_opts.traffic_policy = Some(policy_str.into());
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
self
}
/// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub fn pooling_enabled(&mut self, pooling_enabled: impl Into) -> &mut Self {
self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
self
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::policies::test::POLICY_JSON;
const METADATA: &str = "testmeta";
const TEST_FORWARD: &str = "testforward";
const ALLOW_CIDR: &str = "0.0.0.0/0";
const DENY_CIDR: &str = "10.1.1.1/32";
const CA_CERT: &[u8] = "test ca cert".as_bytes();
const CA_CERT2: &[u8] = "test ca cert2".as_bytes();
const KEY: &[u8] = "test cert".as_bytes();
const CERT: &[u8] = "test cert".as_bytes();
const DOMAIN: &str = "test domain";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&TlsTunnelBuilder {
session: None,
options: Default::default(),
}
.allow_cidr(ALLOW_CIDR)
.deny_cidr(DENY_CIDR)
.proxy_proto(ProxyProto::V2)
.metadata(METADATA)
.domain(DOMAIN)
.mutual_tlsca(CA_CERT.into())
.mutual_tlsca(CA_CERT2.into())
.termination(CERT.into(), KEY.into())
.forwards_to(TEST_FORWARD)
.policy(POLICY_JSON)
.unwrap()
.options,
);
}
fn tunnel_test(tunnel_cfg: C)
where
C: TunnelConfig,
{
assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(Vec::::new(), extra.bindings);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("tls", tunnel_cfg.proto());
let opts = tunnel_cfg.opts().unwrap();
assert!(matches!(opts, BindOpts::Tls { .. }));
if let BindOpts::Tls(endpoint) = opts {
assert_eq!(DOMAIN, endpoint.domain);
assert_eq!(String::default(), endpoint.subdomain);
assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
assert!(!endpoint.mutual_tls_at_agent);
let ip_restriction = endpoint.ip_restriction.unwrap();
assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
let tls_termination = endpoint.tls_termination.unwrap();
assert_eq!(CERT, tls_termination.cert);
assert_eq!(KEY, *tls_termination.key);
assert!(tls_termination.sealed_key.is_empty());
let mutual_tls = endpoint.mutual_tls_at_edge.unwrap();
let mut agg = CA_CERT.to_vec();
agg.extend(CA_CERT2.to_vec());
assert_eq!(agg, mutual_tls.mutual_tls_ca);
}
assert_eq!(HashMap::new(), tunnel_cfg.labels());
}
#[test]
fn test_binding_valid_values() {
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
// Test "public"
builder.binding("public");
assert_eq!(vec!["public"], builder.options.bindings);
// Test "internal"
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal");
assert_eq!(vec!["internal"], builder.options.bindings);
// Test "kubernetes"
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("kubernetes");
assert_eq!(vec!["kubernetes"], builder.options.bindings);
// Test with Binding enum
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding(Binding::Kubernetes);
assert_eq!(vec!["kubernetes"], builder.options.bindings);
}
#[test]
#[should_panic(expected = "Invalid binding value")]
fn test_binding_invalid_value() {
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("invalid");
}
#[test]
#[should_panic(expected = "binding() can only be called once")]
fn test_binding_called_twice() {
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("public");
builder.binding("internal");
}
}
================================================
FILE: ngrok/src/config/webhook_verification.rs
================================================
use crate::internals::proto::{
SecretString,
WebhookVerification as WebhookProto,
};
/// Configuration for webhook verification.
#[derive(Clone)]
pub(crate) struct WebhookVerification {
/// The webhook provider
pub(crate) provider: String,
/// The secret for verifying webhooks from this provider.
pub(crate) secret: SecretString,
}
impl WebhookVerification {}
// transform into the wire protocol format
impl From for WebhookProto {
fn from(wv: WebhookVerification) -> Self {
WebhookProto {
provider: wv.provider,
secret: wv.secret,
sealed_secret: vec![], // unused in this context
}
}
}
================================================
FILE: ngrok/src/conn.rs
================================================
use std::{
net::SocketAddr,
pin::Pin,
task::{
Context,
Poll,
},
};
// Support for axum's connection info trait.
#[cfg(feature = "axum")]
use axum::extract::connect_info::Connected;
#[cfg(feature = "hyper")]
use hyper::rt::{
Read as HyperRead,
Write as HyperWrite,
};
use muxado::typed::TypedStream;
use tokio::io::{
AsyncRead,
AsyncWrite,
};
use crate::{
config::ProxyProto,
internals::proto::{
EdgeType,
ProxyHeader,
},
};
/// A connection from an ngrok tunnel.
///
/// This implements [AsyncRead]/[AsyncWrite], as well as providing access to the
/// address from which the connection to the ngrok edge originated.
pub(crate) struct ConnInner {
pub(crate) info: Info,
pub(crate) stream: TypedStream,
}
#[derive(Clone)]
pub(crate) struct Info {
pub(crate) header: ProxyHeader,
pub(crate) remote_addr: SocketAddr,
pub(crate) proxy_proto: ProxyProto,
pub(crate) app_protocol: Option,
pub(crate) verify_upstream_tls: bool,
}
impl ConnInfo for Info {
fn remote_addr(&self) -> SocketAddr {
self.remote_addr
}
}
impl EdgeConnInfo for Info {
fn edge_type(&self) -> EdgeType {
self.header.edge_type
}
fn passthrough_tls(&self) -> bool {
self.header.passthrough_tls
}
}
impl EndpointConnInfo for Info {
fn proto(&self) -> &str {
self.header.proto.as_str()
}
}
// This codgen indirect is required to make the hyper io trait bounds
// dependent on the hyper feature. You can't put a #[cfg] on a single bound, so
// we're putting the whole trait def in a macro. Gross, but gets the job done.
macro_rules! conn_trait {
($($hyper_bound:tt)*) => {
/// An incoming connection over an ngrok tunnel.
/// Effectively a trait alias for async read+write, plus connection info.
pub trait Conn: ConnInfo + AsyncRead + AsyncWrite $($hyper_bound)* + Unpin + Send + 'static {}
}
}
#[cfg(not(feature = "hyper"))]
conn_trait!();
#[cfg(feature = "hyper")]
conn_trait! {
+ hyper::rt::Read + hyper::rt::Write
}
/// Information common to all ngrok connections.
pub trait ConnInfo {
/// Returns the client address that initiated the connection to the ngrok
/// edge.
fn remote_addr(&self) -> SocketAddr;
}
/// Information about connections via ngrok edges.
pub trait EdgeConnInfo {
/// Returns the edge type for this connection.
fn edge_type(&self) -> EdgeType;
/// Returns whether the connection includes the tls handshake and encrypted
/// stream.
fn passthrough_tls(&self) -> bool;
}
/// Information about connections via ngrok endpoints.
pub trait EndpointConnInfo {
/// Returns the endpoint protocol.
fn proto(&self) -> &str;
}
macro_rules! make_conn_type {
(info EdgeConnInfo, $wrapper:tt) => {
impl EdgeConnInfo for $wrapper {
fn edge_type(&self) -> EdgeType {
self.inner.info.edge_type()
}
fn passthrough_tls(&self) -> bool {
self.inner.info.passthrough_tls()
}
}
};
(info EndpointConnInfo, $wrapper:tt) => {
impl EndpointConnInfo for $wrapper {
fn proto(&self) -> &str {
self.inner.info.proto()
}
}
};
($(#[$outer:meta])* $wrapper:ident, $($m:tt),*) => {
$(#[$outer])*
pub struct $wrapper {
pub(crate) inner: ConnInner,
}
impl Conn for $wrapper {}
impl ConnInfo for $wrapper {
fn remote_addr(&self) -> SocketAddr {
self.inner.info.remote_addr()
}
}
impl AsyncRead for $wrapper {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll> {
Pin::new(&mut *self.inner.stream).poll_read(cx, buf)
}
}
#[cfg(feature = "hyper")]
impl HyperRead for $wrapper {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: hyper::rt::ReadBufCursor<'_>,
) -> Poll> {
let mut tokio_buf = tokio::io::ReadBuf::uninit(unsafe{ buf.as_mut() });
let res = std::task::ready!(Pin::new(&mut *self.inner.stream).poll_read(cx, &mut tokio_buf));
let filled = tokio_buf.filled().len();
unsafe { buf.advance(filled) };
Poll::Ready(res)
}
}
#[cfg(feature = "hyper")]
impl HyperWrite for $wrapper {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll> {
Pin::new(&mut *self.inner.stream).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll> {
Pin::new(&mut *self.inner.stream).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll> {
Pin::new(&mut *self.inner.stream).poll_shutdown(cx)
}
}
impl AsyncWrite for $wrapper {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll> {
Pin::new(&mut *self.inner.stream).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll> {
Pin::new(&mut *self.inner.stream).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll> {
Pin::new(&mut *self.inner.stream).poll_shutdown(cx)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
#[cfg(feature = "axum")]
impl Connected<&$wrapper> for SocketAddr {
fn connect_info(target: &$wrapper) -> Self {
target.inner.info.remote_addr()
}
}
$(
make_conn_type!(info $m, $wrapper);
)*
};
}
make_conn_type! {
/// A connection via an ngrok Edge.
EdgeConn, EdgeConnInfo
}
make_conn_type! {
/// A connection via an ngrok Endpoint.
EndpointConn, EndpointConnInfo
}
================================================
FILE: ngrok/src/forwarder.rs
================================================
use std::{
collections::HashMap,
error::Error as StdError,
};
use async_trait::async_trait;
use tokio::task::JoinHandle;
use url::Url;
use crate::{
prelude::{
EdgeInfo,
EndpointInfo,
TunnelCloser,
TunnelInfo,
},
session::RpcError,
Tunnel,
};
/// An ngrok forwarder.
///
/// Represents a tunnel that is being forwarded to a URL.
pub struct Forwarder