use sha2::{Digest, Sha256};
use std::path::Path;
use std::process::{Child, Command, Stdio};
const TUNNEL_ID: &str = "b12525df-6971-4c47-9a0d-61ee57a5cbd5";
const CLOUDFLARED_VERSION: &str = "2026.2.0";
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
const CLOUDFLARED_FILENAME: &str = "cloudflared-windows-amd64.exe";
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
const CLOUDFLARED_SHA256: &str = "b3279f2186a1c3c438ad5865e802bbbec26090c5d3fdb4ac1113f1143a94837a";
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const CLOUDFLARED_FILENAME: &str = "cloudflared-linux-amd64";
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const CLOUDFLARED_SHA256: &str = "176746db3be7dc7bd48f3dd287c8930a4645ebb6e6700f883fddda5a4c307c16";
#[cfg(not(any(
all(target_os = "windows", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "x86_64")
)))]
const CLOUDFLARED_FILENAME: &str = "";
#[cfg(not(any(
all(target_os = "windows", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "x86_64")
)))]
const CLOUDFLARED_SHA256: &str = "";
fn cloudflared_path(base: &Path) -> std::path::PathBuf {
let bin_name = if cfg!(target_os = "windows") {
"cloudflared.exe"
} else {
"cloudflared"
};
let candidate = base.join("bin").join(bin_name);
if candidate.exists() {
return candidate;
}
std::path::PathBuf::from(if cfg!(target_os = "windows") {
"cloudflared.exe"
} else {
"cloudflared"
})
}
fn config_path(base: &Path) -> std::path::PathBuf {
base.join("data").join("cloudflared.yml")
}
pub async fn ensure_cloudflared(base: &Path) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let bin_name = if cfg!(target_os = "windows") {
"cloudflared.exe"
} else {
"cloudflared"
};
let dest = base.join("bin").join(bin_name);
if dest.exists() && dest.metadata()?.len() > 0 {
tracing::info!("cloudflared already present: {}", dest.display());
return Ok(true);
}
if CLOUDFLARED_FILENAME.is_empty() {
return Err("ensure-cloudflared: unsupported platform (need Windows x86_64 or Linux x86_64)".into());
}
let url = format!(
"https://github.com/cloudflare/cloudflared/releases/download/{}/{}",
CLOUDFLARED_VERSION,
CLOUDFLARED_FILENAME
);
tracing::info!("Downloading cloudflared {} for {}...", CLOUDFLARED_VERSION, std::env::consts::OS);
let client = reqwest::Client::builder()
.user_agent("cochranblock/1.0")
.build()?;
let bytes = client.get(&url).send().await?.bytes().await?;
let got_sha = format!("{:x}", Sha256::digest(&bytes));
if got_sha != CLOUDFLARED_SHA256 {
return Err(format!("cloudflared checksum mismatch: got {}, expected {}", got_sha, CLOUDFLARED_SHA256).into());
}
std::fs::create_dir_all(base.join("bin"))?;
std::fs::write(&dest, &bytes)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&dest)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&dest, perms)?;
}
tracing::info!("cloudflared installed: {}", dest.display());
Ok(true)
}
pub fn spawn(base: &Path) -> Result<Child, Box<dyn std::error::Error + Send + Sync>> {
let exe = cloudflared_path(base);
let config = config_path(base);
if !config.exists() {
return Err(format!(
"Tunnel config not found: {}. Run approuter once to generate data/cloudflared.yml",
config.display()
)
.into());
}
let mut cmd = Command::new(&exe);
cmd.arg("tunnel")
.arg("--config")
.arg(&config)
.arg("run")
.arg(TUNNEL_ID)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let child = cmd.spawn()?;
tracing::info!(
"Cloudflare Tunnel started (config={})",
config.display()
);
Ok(child)
}