use std::net::SocketAddr;
use std::process::Command as OsCommand;
use crate::process::shell_command;
pub trait TunnelProvider: std::fmt::Debug + Send + Sync {
fn name(&self) -> &str;
fn build_command(&self, local: SocketAddr) -> OsCommand;
fn extract_url(&self, line: &str) -> Option<String>;
fn install_hint(&self) -> &str;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Cloudflared;
const TRYCLOUDFLARE_SUFFIX: &str = ".trycloudflare.com";
impl TunnelProvider for Cloudflared {
fn name(&self) -> &str {
"cloudflared"
}
fn build_command(&self, local: SocketAddr) -> OsCommand {
let mut cmd = OsCommand::new("cloudflared");
cmd.arg("tunnel")
.arg("--url")
.arg(format!("http://{}", local))
.arg("--no-autoupdate");
cmd
}
fn extract_url(&self, line: &str) -> Option<String> {
find_url(line, |url| {
url.strip_prefix("https://")
.and_then(|host| host.split('/').next())
.is_some_and(|host| host.ends_with(TRYCLOUDFLARE_SUFFIX))
})
}
fn install_hint(&self) -> &str {
"install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/ \
(or use --tunnel-command to drive a different tunnel client)"
}
}
#[derive(Debug, Clone)]
pub struct CustomCommand {
command_line: String,
}
impl CustomCommand {
pub fn new(command_line: impl Into<String>) -> Self {
Self {
command_line: command_line.into(),
}
}
}
impl TunnelProvider for CustomCommand {
fn name(&self) -> &str {
"tunnel-command"
}
fn build_command(&self, local: SocketAddr) -> OsCommand {
let mut cmd = shell_command(&self.command_line);
cmd.env("SHELL_TUNNEL_LOCAL_ADDR", local.to_string());
cmd.env("SHELL_TUNNEL_LOCAL_PORT", local.port().to_string());
cmd
}
fn extract_url(&self, line: &str) -> Option<String> {
find_url(line, |_| true)
}
fn install_hint(&self) -> &str {
"check that the command exists and is on PATH"
}
}
fn find_url(line: &str, accept: impl Fn(&str) -> bool) -> Option<String> {
let mut rest = line;
loop {
let start = rest
.find("http://")
.into_iter()
.chain(rest.find("https://"))
.min()?;
let candidate = &rest[start..];
let end = candidate
.find(|c: char| {
c.is_whitespace() || matches!(c, '|' | '"' | '\'' | '<' | '>' | ')' | ',')
})
.unwrap_or(candidate.len());
let url = candidate[..end].trim_end_matches(['.', ';', ':']);
if !url.is_empty() && accept(url) {
return Some(url.to_string());
}
rest = &rest[start + 4..];
}
}
#[cfg(test)]
mod tests {
use super::*;
fn addr() -> SocketAddr {
"127.0.0.1:3000".parse().unwrap()
}
#[test]
fn cloudflared_command_points_at_the_local_server() {
let cmd = Cloudflared.build_command(addr());
let args: Vec<_> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(cmd.get_program().to_string_lossy(), "cloudflared");
assert!(args.contains(&"--url".to_string()));
assert!(args.contains(&"http://127.0.0.1:3000".to_string()));
assert!(args.contains(&"--no-autoupdate".to_string()));
}
#[test]
fn cloudflared_extracts_the_boxed_quick_tunnel_url() {
let line = "| https://caring-brave-fox-mint.trycloudflare.com |";
assert_eq!(
Cloudflared.extract_url(line).as_deref(),
Some("https://caring-brave-fox-mint.trycloudflare.com")
);
}
#[test]
fn cloudflared_extracts_url_from_a_log_line() {
let line = "2026-07-21T03:20:56Z INF +--------------------+ url=https://foo-bar-baz.trycloudflare.com";
assert_eq!(
Cloudflared.extract_url(line).as_deref(),
Some("https://foo-bar-baz.trycloudflare.com")
);
}
#[test]
fn cloudflared_ignores_unrelated_urls() {
let line = "INF Thank you for trying Cloudflare Tunnel. See https://developers.cloudflare.com/cloudflare-one/";
assert!(Cloudflared.extract_url(line).is_none());
assert!(Cloudflared
.extract_url("INF Requesting new quick Tunnel on trycloudflare.com...")
.is_none());
}
#[test]
fn custom_command_accepts_any_url() {
let p = CustomCommand::new("ngrok http 3000");
assert_eq!(
p.extract_url("Forwarding https://1a2b.ngrok-free.app -> http://localhost:3000")
.as_deref(),
Some("https://1a2b.ngrok-free.app")
);
assert_eq!(
p.extract_url("listening at bore.pub:41234").as_deref(),
None
);
}
#[test]
fn custom_command_exports_the_local_address() {
let cmd = CustomCommand::new("echo hi").build_command(addr());
let envs: Vec<_> = cmd
.get_envs()
.map(|(k, v)| {
(
k.to_string_lossy().into_owned(),
v.map(|v| v.to_string_lossy().into_owned())
.unwrap_or_default(),
)
})
.collect();
assert!(envs.contains(&("SHELL_TUNNEL_LOCAL_PORT".to_string(), "3000".to_string())));
assert!(envs.contains(&(
"SHELL_TUNNEL_LOCAL_ADDR".to_string(),
"127.0.0.1:3000".to_string()
)));
}
#[test]
fn urls_are_trimmed_of_framing_and_punctuation() {
let p = CustomCommand::new("x");
assert_eq!(
p.extract_url("see https://example.com/a.").as_deref(),
Some("https://example.com/a")
);
assert_eq!(
p.extract_url("<https://example.com/b>").as_deref(),
Some("https://example.com/b")
);
}
#[test]
fn lines_without_a_url_yield_none() {
let p = CustomCommand::new("x");
assert!(p.extract_url("").is_none());
assert!(p.extract_url("starting up, no address yet").is_none());
assert!(p.extract_url("http").is_none());
}
}