using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace DorkNet.Launcher.Backend;
/// Wraps Cloudflare's cloudflared quick-tunnel so the
/// host doesn't need to open ports / configure DNS. Each launch gets
/// a fresh random *.trycloudflare.com URL.
///
/// cloudflared.exe is resolved via
/// , which checks
/// side-by-side first, then PATH, then downloads the latest official
/// release from Cloudflare's GitHub mirror on first run. Grandma never
/// has to "install Cloudflare Tunnel" manually.
public sealed class Tunnel : IAsyncDisposable
{
private Process? _process;
private TaskCompletionSource? _urlReady;
public string? PublicUrl { get; private set; }
public async Task StartAsync(
int localPort,
IProgress? downloadProgress = null,
CancellationToken ct = default)
{
var exe = await CloudflaredInstaller.EnsureAsync(downloadProgress, ct);
_urlReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var psi = new ProcessStartInfo
{
FileName = exe,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
psi.ArgumentList.Add("tunnel");
psi.ArgumentList.Add("--no-autoupdate");
psi.ArgumentList.Add("--url");
psi.ArgumentList.Add($"http://127.0.0.1:{localPort}");
_process = Process.Start(psi) ??
throw new InvalidOperationException("Failed to spawn cloudflared.exe");
_process.OutputDataReceived += (_, e) => ParseLine(e.Data);
_process.ErrorDataReceived += (_, e) => ParseLine(e.Data);
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));
PublicUrl = await _urlReady.Task.WaitAsync(timeoutCts.Token);
return PublicUrl;
}
private static readonly Regex UrlRegex = new(
@"https://[a-z0-9\-]+\.trycloudflare\.com",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private void ParseLine(string? line)
{
if (line is null || _urlReady is null || _urlReady.Task.IsCompleted) return;
var match = UrlRegex.Match(line);
if (match.Success) _urlReady.TrySetResult(match.Value);
}
public async ValueTask DisposeAsync()
{
if (_process is null) return;
try
{
if (!_process.HasExited) _process.Kill(entireProcessTree: true);
await _process.WaitForExitAsync();
}
catch { }
_process.Dispose();
}
}