cochranblock 0.7.0

Zero-cloud website in a single Rust binary. 15MB on x86, 8.2MB on ARM. $10/month infrastructure. cargo install and run.
Documentation
//! Cloudflare Tunnel — spawn cloudflared as child process.
//! Uses data/cloudflared.yml (generated by approuter).

// All Rights Reserved — The Cochran Block, LLC
// Contributors: Mattbusel (XFactor), GotEmCoach, KOVA, Claude Opus 4.6, SuperNinja, Composer 1.5, Google Gemini Pro 3

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 = "";

/// Resolve cloudflared binary path. Prefer bin/ relative to base, else "cloudflared" in PATH.
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"
    })
}

/// Resolve tunnel config path. data/cloudflared.yml (approuter-generated).
fn config_path(base: &Path) -> std::path::PathBuf {
    base.join("data").join("cloudflared.yml")
}

/// Download cloudflared to bin/ if supported platform. Returns true if binary exists after.
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)
}

/// Spawn cloudflared tunnel. Returns child handle; caller must kill on shutdown.
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)
}