mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-28 00:18:47 -07:00
Localtunnel hosting + admin panel + AppData db
- Default hosting mode is now Localtunnel — public *.loca.lt URL, no account, auto-fetched on first run. Replaces the prototyped Tailscale mode. - Tailscale code purged from launcher + CLI; docs swapped to Localtunnel throughout (easy-setup, joining-a-server, server-cli, troubleshooting, faq, READMEs). - Launcher sidebar gains an Open admin panel button — opens https://admin.<apex> in the browser once the server is live. - SQLite db moves to %APPDATA%\DorkNet\dorknet.db (server reads Database__SqlitePath env var, launcher passes it). Existing dbs under <serverDir>\data\ are migrated on first run. - CLI Program.cs: --mode default flips from tailscale to localtunnel; csproj links in LocaltunnelTunnel + LocaltunnelInstaller. - New Backend files: LauncherNameServer, LocaltunnelInstaller, LocaltunnelTunnel, SingleOriginServiceMap. - .gitignore: exclude installer/out/.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace DorkNet.Launcher.Backend;
|
||||
|
||||
/// <summary>Runs a localtunnel client. Prefer angelobreuer/localtunnel.net
|
||||
/// because it is a native .NET CLI and does not require Node.js; fall
|
||||
/// back to npx localtunnel when users already have Node on PATH.</summary>
|
||||
public sealed class LocaltunnelTunnel : IAsyncDisposable
|
||||
{
|
||||
private Process? _process;
|
||||
private TaskCompletionSource<string>? _urlReady;
|
||||
private readonly object _outputLock = new();
|
||||
private readonly Queue<string> _recentOutput = new();
|
||||
|
||||
public string? PublicUrl { get; private set; }
|
||||
|
||||
public async Task<string> StartAsync(int localPort, CancellationToken ct = default)
|
||||
{
|
||||
var executable = await LocaltunnelInstaller.TryEnsureAsync(ct: ct);
|
||||
LocaltunnelKind kind;
|
||||
if (executable is not null)
|
||||
{
|
||||
kind = LocaltunnelKind.DotNet;
|
||||
}
|
||||
else
|
||||
{
|
||||
executable = ResolveExecutable(out kind);
|
||||
}
|
||||
_urlReady = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = executable,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
if (kind == LocaltunnelKind.DotNet)
|
||||
{
|
||||
psi.ArgumentList.Add("--no-dashboard");
|
||||
psi.ArgumentList.Add("--host");
|
||||
psi.ArgumentList.Add("127.0.0.1");
|
||||
psi.ArgumentList.Add("--port");
|
||||
psi.ArgumentList.Add(localPort.ToString());
|
||||
psi.ArgumentList.Add("http");
|
||||
}
|
||||
else
|
||||
{
|
||||
psi.ArgumentList.Add("--yes");
|
||||
psi.ArgumentList.Add("localtunnel");
|
||||
psi.ArgumentList.Add("--port");
|
||||
psi.ArgumentList.Add(localPort.ToString());
|
||||
psi.ArgumentList.Add("--local-host");
|
||||
psi.ArgumentList.Add("127.0.0.1");
|
||||
}
|
||||
|
||||
_process = Process.Start(psi)
|
||||
?? throw new InvalidOperationException("Failed to start localtunnel through npx.");
|
||||
_process.EnableRaisingEvents = true;
|
||||
_process.Exited += (_, _) =>
|
||||
{
|
||||
if (_urlReady is null || _urlReady.Task.IsCompleted) return;
|
||||
_urlReady.TrySetException(new InvalidOperationException(
|
||||
"localtunnel exited before reporting a public URL." + FormatRecentOutput()));
|
||||
};
|
||||
_process.OutputDataReceived += (_, e) => ParseLine(e.Data);
|
||||
_process.ErrorDataReceived += (_, e) => ParseLine(e.Data);
|
||||
_process.BeginOutputReadLine();
|
||||
_process.BeginErrorReadLine();
|
||||
|
||||
var completed = await Task.WhenAny(_urlReady.Task, Task.Delay(TimeSpan.FromSeconds(60), ct));
|
||||
if (completed != _urlReady.Task)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
throw new TimeoutException("localtunnel did not report a public URL within 60 seconds." + FormatRecentOutput());
|
||||
}
|
||||
|
||||
PublicUrl = await _urlReady.Task;
|
||||
return PublicUrl;
|
||||
}
|
||||
|
||||
private static string ResolveExecutable(out LocaltunnelKind kind)
|
||||
{
|
||||
var ltName = OperatingSystem.IsWindows() ? "localtunnel.exe" : "localtunnel";
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, ltName),
|
||||
Path.Combine(AppPaths.LocalRoot, ltName),
|
||||
};
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (!File.Exists(candidate)) continue;
|
||||
kind = LocaltunnelKind.DotNet;
|
||||
return candidate;
|
||||
}
|
||||
var ltOnPath = Environment.GetEnvironmentVariable("PATH")?
|
||||
.Split(Path.PathSeparator)
|
||||
.Select(p => Path.Combine(p.Trim(), ltName))
|
||||
.FirstOrDefault(File.Exists);
|
||||
if (ltOnPath is not null)
|
||||
{
|
||||
kind = LocaltunnelKind.DotNet;
|
||||
return ltOnPath;
|
||||
}
|
||||
|
||||
var name = OperatingSystem.IsWindows() ? "npx.cmd" : "npx";
|
||||
var onPath = Environment.GetEnvironmentVariable("PATH")?
|
||||
.Split(Path.PathSeparator)
|
||||
.Select(p => Path.Combine(p.Trim(), name))
|
||||
.FirstOrDefault(File.Exists);
|
||||
if (onPath is not null)
|
||||
{
|
||||
kind = LocaltunnelKind.Npx;
|
||||
return onPath;
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(
|
||||
"localtunnel was not found. Put angelobreuer/localtunnel.net's localtunnel.exe next to dorknet.exe, or install Node.js LTS for the npx fallback.");
|
||||
}
|
||||
|
||||
private static readonly Regex UrlRegex = new(
|
||||
@"https://[a-z0-9][a-z0-9\-]*\.(?:loca\.lt|localtunnel\.me)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private void ParseLine(string? line)
|
||||
{
|
||||
if (line is null) return;
|
||||
lock (_outputLock)
|
||||
{
|
||||
_recentOutput.Enqueue(line);
|
||||
while (_recentOutput.Count > 8) _recentOutput.Dequeue();
|
||||
}
|
||||
if (_urlReady is null || _urlReady.Task.IsCompleted) return;
|
||||
var match = UrlRegex.Match(line);
|
||||
if (match.Success) _urlReady.TrySetResult(match.Value);
|
||||
}
|
||||
|
||||
private string FormatRecentOutput()
|
||||
{
|
||||
lock (_outputLock)
|
||||
{
|
||||
return _recentOutput.Count == 0
|
||||
? ""
|
||||
: " Last localtunnel output: " + string.Join(" | ", _recentOutput);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_process is null) return;
|
||||
try
|
||||
{
|
||||
if (!_process.HasExited) _process.Kill(entireProcessTree: true);
|
||||
await _process.WaitForExitAsync();
|
||||
}
|
||||
catch { }
|
||||
_process.Dispose();
|
||||
}
|
||||
|
||||
private enum LocaltunnelKind { DotNet, Npx }
|
||||
}
|
||||
Reference in New Issue
Block a user