using System.IO;
using System.Net.Http;
namespace DorkNet.Launcher.Backend;
/// Resolves a usable cloudflared.exe: prefers a
/// side-by-side binary, falls back to one on PATH, otherwise downloads
/// the latest official release from Cloudflare's GitHub mirror into
/// and caches it there forever.
///
/// The cached binary lives at %LOCALAPPDATA%\DorkNet\cloudflared.exe.
/// Re-runs of the launcher reuse it; "update" happens by deleting the
/// cached file and letting the next launch re-download. Worth doing
/// occasionally for security fixes.
///
/// Download source: https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe
/// — GitHub redirects this stable URL to the most-recent release's
/// asset, so we never have to query the Releases API.
public static class CloudflaredInstaller
{
private const string LatestDownloadUrl =
"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe";
private static readonly HttpClient Http = new()
{
Timeout = TimeSpan.FromMinutes(5),
};
/// Returns the path to a usable cloudflared.exe. Downloads
/// + caches on first call if needed. Throws if download fails AND
/// nothing is found locally.
public static async Task EnsureAsync(
IProgress? progress = null,
CancellationToken ct = default)
{
if (TryFindLocal(out var existing)) return existing;
var dest = Path.Combine(AppPaths.LocalRoot, "cloudflared.exe");
Directory.CreateDirectory(AppPaths.LocalRoot);
await DownloadAsync(LatestDownloadUrl, dest, progress, ct);
if (!File.Exists(dest) || new FileInfo(dest).Length < 1_000_000)
throw new InvalidOperationException(
$"cloudflared download produced an unexpectedly small file at {dest}. " +
"Check internet connection + retry.");
return dest;
}
/// Synchronous lookup of an already-installed cloudflared.exe
/// without touching the network. Returns null if not found anywhere.
public static string? FindCached()
{
return TryFindLocal(out var path) ? path : null;
}
private static bool TryFindLocal(out string path)
{
var sideBySide = Path.Combine(AppContext.BaseDirectory, "cloudflared.exe");
if (File.Exists(sideBySide)) { path = sideBySide; return true; }
var cached = Path.Combine(AppPaths.LocalRoot, "cloudflared.exe");
if (File.Exists(cached)) { path = cached; return true; }
var onPath = Environment.GetEnvironmentVariable("PATH")?
.Split(';')
.Select(p => Path.Combine(p.Trim(), "cloudflared.exe"))
.FirstOrDefault(File.Exists);
if (onPath is not null) { path = onPath; return true; }
path = string.Empty;
return false;
}
private static async Task DownloadAsync(
string url, string destPath,
IProgress? progress,
CancellationToken ct)
{
// Use a temp file + atomic move so a half-downloaded binary
// doesn't get picked up as cached on the next run.
var temp = destPath + ".download";
try
{
using var resp = await Http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
var total = resp.Content.Headers.ContentLength ?? -1L;
await using (var src = await resp.Content.ReadAsStreamAsync(ct))
await using (var dst = File.Create(temp))
{
var buf = new byte[81920];
long readTotal = 0;
int read;
while ((read = await src.ReadAsync(buf, ct)) > 0)
{
await dst.WriteAsync(buf.AsMemory(0, read), ct);
readTotal += read;
progress?.Report(new DownloadProgress(readTotal, total));
}
}
if (File.Exists(destPath)) File.Delete(destPath);
File.Move(temp, destPath);
}
catch
{
try { if (File.Exists(temp)) File.Delete(temp); } catch { }
throw;
}
}
}