From 615973bb0a936c13ad9533d26ac50156e2a12d99 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Tue, 27 Aug 2024 17:05:17 +0300 Subject: [PATCH] Improve CLI ui --- .../HttpTunnel/HttpTunnelClient.cs | 54 ++--- src/Tunnelite.Client/ITunnelClient.cs | 14 ++ src/Tunnelite.Client/Program.cs | 196 ++++++++++++++---- .../TcpTunnel/TcpTunnelClient.cs | 52 ++--- src/Tunnelite.Client/Tunnelite.Client.csproj | 3 +- .../HttpTunnel/HttpAppExtensions.cs | 54 ++--- src/Tunnelite.Server/wwwroot/404.html | 79 +++++++ src/Tunnelite.Server/wwwroot/500.html | 81 ++++++++ 8 files changed, 408 insertions(+), 125 deletions(-) create mode 100644 src/Tunnelite.Client/ITunnelClient.cs create mode 100644 src/Tunnelite.Server/wwwroot/404.html create mode 100644 src/Tunnelite.Server/wwwroot/500.html diff --git a/src/Tunnelite.Client/HttpTunnel/HttpTunnelClient.cs b/src/Tunnelite.Client/HttpTunnel/HttpTunnelClient.cs index e60ba40..6daa969 100644 --- a/src/Tunnelite.Client/HttpTunnel/HttpTunnelClient.cs +++ b/src/Tunnelite.Client/HttpTunnel/HttpTunnelClient.cs @@ -9,8 +9,18 @@ namespace Tunnelite.Client.HttpTunnel; -public class HttpTunnelClient +public class HttpTunnelClient : ITunnelClient { + public event Func? Connected; + public HubConnection Connection { get; } + public string? TunnelUrl + { + get + { + return _currentTunnel?.TunnelUrl; + } + } + private static readonly HttpClientHandler LocalHttpClientHandler = new() { ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true, @@ -19,10 +29,9 @@ public class HttpTunnelClient private static readonly HttpClient LocalHttpClient = new(LocalHttpClientHandler); private HttpTunnelResponse? _currentTunnel = null; - private readonly HubConnection Connection; private readonly HttpTunnelRequest Tunnel; - public HttpTunnelClient(HttpTunnelRequest tunnel, LogLevel logLevel) + public HttpTunnelClient(HttpTunnelRequest tunnel, LogLevel? logLevel) { Tunnel = tunnel; @@ -31,15 +40,18 @@ public HttpTunnelClient(HttpTunnelRequest tunnel, LogLevel logLevel) .AddMessagePackProtocol() .ConfigureLogging(logging => { - logging.SetMinimumLevel(logLevel); - logging.AddConsole(); + if (logLevel.HasValue) + { + logging.SetMinimumLevel(logLevel.Value); + logging.AddConsole(); + } }) .WithAutomaticReconnect() .Build(); Connection.On("NewHttpConnection", (httpConnection) => { - Console.WriteLine($"Received http tunneling request: [{httpConnection.Method}]{httpConnection.Path}"); + Program.LogRequest(httpConnection.Method, httpConnection.Path); _ = TunnelHttpConnectionAsync(httpConnection); @@ -48,7 +60,7 @@ public HttpTunnelClient(HttpTunnelRequest tunnel, LogLevel logLevel) Connection.On("NewWsConnection", (wsConnection) => { - Console.WriteLine($"Received ws tunneling request: {wsConnection.Path}"); + Program.LogRequest("WS", wsConnection.Path); _ = TunnelWsConnectionAsync(wsConnection); @@ -57,15 +69,11 @@ public HttpTunnelClient(HttpTunnelRequest tunnel, LogLevel logLevel) Connection.Reconnected += async connectionId => { - Console.WriteLine($"Reconnected. New ConnectionId {connectionId}"); - _currentTunnel = await RegisterTunnelAsync(tunnel); }; Connection.Closed += async (error) => { - Console.WriteLine("Connection closed... reconnecting"); - await Task.Delay(new Random().Next(0, 5) * 1000); if (await ConnectWithRetryAsync(Connection, CancellationToken.None)) @@ -149,7 +157,7 @@ private async Task TunnelHttpConnectionAsync(HttpConnection httpConnection) } catch (Exception ex) { - Console.WriteLine($"Unexpected error tunneling request: {ex.Message}"); + Program.LogError($"[HTTP] Unexpected error tunneling request: {ex.Message}."); using var errorRequest = new HttpRequestMessage(HttpMethod.Delete, requestUrl); using var response = await ServerHttpClient.SendAsync(errorRequest); } @@ -172,13 +180,13 @@ private async Task TunnelWsConnectionAsync(WsConnection wsConnection) } catch (Exception ex) { - Console.WriteLine($"Failed to connect to WebSocket for connection {wsConnection.RequestId}: {ex.Message}"); + Program.LogError($"[WS] Failed to connect for connection {wsConnection.RequestId}: {ex.Message}."); } finally { cts.Cancel(); - Console.WriteLine($"WS Connection {wsConnection.RequestId} done."); + Program.Log($"[WS] Connection {wsConnection.RequestId} closed."); } } @@ -207,7 +215,7 @@ private async Task StreamIncomingWsAsync(WebSocket webSocket, WsConnection wsCon } finally { - Console.WriteLine($"Writing data to WebSocket connection {wsConnection.RequestId} finished."); + Program.Log($"[WS] Writing data to connection {wsConnection.RequestId} finished."); } } @@ -238,7 +246,7 @@ private async Task StreamOutgoingWsAsync(WebSocket localWebSocket, WsConnection } finally { - Console.WriteLine($"Reading data from WebSocket connection {wsConnection.RequestId} finished."); + Program.Log($"[WS] Reading data from connection {wsConnection.RequestId} finished."); ArrayPool.Shared.Return(buffer); } @@ -258,18 +266,14 @@ private async Task StreamOutgoingWsAsync(WebSocket localWebSocket, WsConnection tunnelResponse = await response.Content.ReadFromJsonAsync(); - if (response.IsSuccessStatusCode) - { - Console.WriteLine($"Tunnel created successfully: {tunnelResponse!.TunnelUrl}"); - } - else + if (!response.IsSuccessStatusCode) { - Console.WriteLine($"{tunnelResponse!.Message}:{tunnelResponse.Error}"); + Program.LogError($"{tunnelResponse!.Message}:{tunnelResponse.Error}"); } } catch (Exception ex) { - Console.WriteLine($"An error occurred while registering the tunnel {ex.Message}"); + Program.LogError($"[HTTP] An error occurred while registering the tunnel {ex.Message}."); await Task.Delay(5000); } @@ -286,7 +290,7 @@ private async Task ConnectWithRetryAsync(HubConnection connection, Cancell { await connection.StartAsync(token); - Console.WriteLine($"Client connected to SignalR hub. ConnectionId: {connection.ConnectionId}"); + Connected?.Invoke(); return true; } @@ -296,7 +300,7 @@ private async Task ConnectWithRetryAsync(HubConnection connection, Cancell } catch { - Console.WriteLine($"Cannot connect to WebSocket server on {Tunnel.PublicUrl}"); + Program.LogError($"[HTTP] Cannot connect to the public server on {Tunnel.PublicUrl}."); await Task.Delay(5000, token); } diff --git a/src/Tunnelite.Client/ITunnelClient.cs b/src/Tunnelite.Client/ITunnelClient.cs new file mode 100644 index 0000000..b8e59dd --- /dev/null +++ b/src/Tunnelite.Client/ITunnelClient.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.SignalR.Client; + +namespace Tunnelite.Client; + +public interface ITunnelClient +{ + event Func? Connected; + + HubConnection Connection { get; } + + string? TunnelUrl { get; } + + Task ConnectAsync(); +} diff --git a/src/Tunnelite.Client/Program.cs b/src/Tunnelite.Client/Program.cs index a147133..588a1d5 100644 --- a/src/Tunnelite.Client/Program.cs +++ b/src/Tunnelite.Client/Program.cs @@ -1,24 +1,24 @@ using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; +using Spectre.Console; using System.CommandLine; -using Tunnelite.Client.TcpTunnel; using Tunnelite.Client.HttpTunnel; +using Tunnelite.Client.TcpTunnel; namespace Tunnelite.Client; public class Program { + private static ITunnelClient? Client; private static readonly Guid ClientId = Guid.NewGuid(); public static async Task Main(string[] args) { var localUrlArgument = new Argument("localUrl", "The local URL to tunnel to."); - - var logLevelOption = new Option( + var logLevelOption = new Option( "--log", - () => LogLevel.Warning, + () => null, "The logging level (e.g., Trace, Debug, Information, Warning, Error, Critical)"); - var publicUrlOption = new Option( "--publicUrl", () => "https://tunnelite.com", @@ -30,14 +30,13 @@ public static async Task Main(string[] args) publicUrlOption, logLevelOption }; - rootCommand.Description = "CLI tool to create a tunnel to a local server."; - rootCommand.SetHandler(async (string localUrl, string publicUrl, LogLevel logLevel) => + rootCommand.SetHandler(async (string localUrl, string publicUrl, LogLevel? logLevel) => { if (string.IsNullOrWhiteSpace(localUrl)) { - Console.WriteLine("Error: Local URL is required."); + AnsiConsole.MarkupLine("[red]Error: Local URL is required.[/]"); return; } @@ -48,60 +47,173 @@ public static async Task Main(string[] args) } catch (UriFormatException) { - Console.WriteLine("Error: Invalid URL format."); + AnsiConsole.MarkupLine("[red]Error: Invalid URL format.[/]"); return; } var scheme = uri.Scheme.ToLowerInvariant(); + publicUrl = publicUrl.TrimEnd('/'); - publicUrl = publicUrl.TrimEnd(['/']); - - switch (scheme) - { - case "tcp": + await AnsiConsole.Status() + .StartAsync("Initializing tunnel...", async ctx => + { + ctx.Spinner(Spinner.Known.Dots); + ctx.SpinnerStyle(Style.Parse("green")); - var tcpTunnel = new TcpTunnelRequest + switch (scheme) { - ClientId = ClientId, - LocalUrl = localUrl, - PublicUrl = publicUrl, - Host = uri.Host, - LocalPort = uri.Port, - }; + case "tcp": - var tcpTunnelClient = new TcpTunnelClient(tcpTunnel, logLevel); + await InitializeTcpTunnel(localUrl, publicUrl, uri, logLevel, ctx); + break; - await tcpTunnelClient.ConnectAsync(); + case "http": + case "https": - break; + await InitializeHttpTunnel(localUrl, publicUrl, logLevel, ctx); + break; - case "http": - case "https": + default: + AnsiConsole.MarkupLine("[red]Error: Unsupported protocol. Use tcp:// or http(s)://[/]"); + return; + } + }); - var httpTunnel = new HttpTunnelRequest - { - ClientId = ClientId, - LocalUrl = localUrl, - PublicUrl = publicUrl, - }; + await RunMainLoop(localUrl); - var httpTunnelClient = new HttpTunnelClient(httpTunnel, logLevel); + }, localUrlArgument, publicUrlOption, logLevelOption); + + await rootCommand.InvokeAsync(args); + } - await httpTunnelClient.ConnectAsync(); + private static async Task InitializeTcpTunnel(string localUrl, string publicUrl, Uri uri, LogLevel? logLevel, StatusContext ctx) + { + var tcpTunnel = new TcpTunnelRequest + { + ClientId = ClientId, + LocalUrl = localUrl, + PublicUrl = publicUrl, + Host = uri.Host, + LocalPort = uri.Port, + }; - break; + ctx.Status("Connecting to TCP tunnel..."); - default: + Client = new TcpTunnelClient(tcpTunnel, logLevel); + await Client.ConnectAsync(); - Console.WriteLine("Error: Unsupported protocol. Use tcp:// or http(s)://"); + ctx.Status("TCP tunnel established."); + } + + private static async Task InitializeHttpTunnel(string localUrl, string publicUrl, LogLevel? logLevel, StatusContext ctx) + { + var httpTunnel = new HttpTunnelRequest + { + ClientId = ClientId, + LocalUrl = localUrl, + PublicUrl = publicUrl, + }; + + ctx.Status("Connecting to HTTP tunnel..."); + + Client = new HttpTunnelClient(httpTunnel, logLevel); + await Client.ConnectAsync(); + + ctx.Status("HTTP tunnel established."); + } + + private static async Task RunMainLoop(string localUrl) + { + if (Client == null) + { + return; + } + + Table statusTable = WriteStatusTable(localUrl, Client.TunnelUrl, "green", "Connected"); + + Client.Connected += () => + { + statusTable = WriteStatusTable(localUrl, Client.TunnelUrl, "green", "Connected"); + return Task.CompletedTask; + }; - return; + Client.Connection.Closed += (error) => + { + statusTable = WriteStatusTable(localUrl, Client.TunnelUrl, "red", "Disconnected"); + return Task.CompletedTask; + }; + + Client.Connection.Reconnecting += (error) => + { + statusTable = WriteStatusTable(localUrl, Client.TunnelUrl, "yellow", "Reconnecting"); + return Task.CompletedTask; + }; + + Client.Connection.Reconnected += (connectionId) => + { + statusTable = WriteStatusTable(localUrl, Client.TunnelUrl, "green", "Connected"); + return Task.CompletedTask; + }; + + while (true) + { + if (Console.KeyAvailable) + { + var key = Console.ReadKey(true); + switch (key.KeyChar) + { + case 'c': + AnsiConsole.Clear(); + AnsiConsole.Write(statusTable); + WriteHelp(); + break; + case 'q': + return; + } } - }, localUrlArgument, publicUrlOption, logLevelOption); + await Task.Delay(100); + } + } - await rootCommand.InvokeAsync(args); + private static Table WriteStatusTable(string localUrl, string? tunnelUrl, string color, string currentStatus) + { + AnsiConsole.Clear(); - Console.ReadLine(); + var table = new Table() + .Border(TableBorder.Rounded) + .AddColumn(new TableColumn(new Markup($"[{color}]{currentStatus}[/]")).Centered()) + .AddRow($"Local URL: {localUrl}") + .AddRow($"Public URL: {tunnelUrl}"); + + AnsiConsole.Write(table); + + WriteHelp(); + + return table; + } + + private static void WriteHelp() + { + AnsiConsole.MarkupLine("\n[grey]Press 'c' to clear, 'q' to quit[/]"); + AnsiConsole.WriteLine(); + } + + public static void Log(string log) + { + string entry = $"[yellow] {DateTimeOffset.Now:HH:mm:ss}[/]: {Markup.Escape(log)}"; + AnsiConsole.MarkupLine(entry); + } + + public static void LogRequest(string method, string path) + { + string entry = $"[green] {DateTimeOffset.Now:HH:mm:ss} [[{method}]][/]: {Markup.Escape(path)}"; + AnsiConsole.MarkupLine(entry); + } + + public static void LogError(string message) + { + string entry = $"[red] {DateTimeOffset.Now:HH:mm:ss}[/]: {Markup.Escape(message)}"; + AnsiConsole.MarkupLine(entry); } -} +} \ No newline at end of file diff --git a/src/Tunnelite.Client/TcpTunnel/TcpTunnelClient.cs b/src/Tunnelite.Client/TcpTunnel/TcpTunnelClient.cs index 4efedd8..028fa93 100644 --- a/src/Tunnelite.Client/TcpTunnel/TcpTunnelClient.cs +++ b/src/Tunnelite.Client/TcpTunnel/TcpTunnelClient.cs @@ -7,13 +7,22 @@ namespace Tunnelite.Client.TcpTunnel; -public class TcpTunnelClient +public class TcpTunnelClient : ITunnelClient { - private readonly HubConnection Connection; + public event Func? Connected; + public HubConnection Connection { get; } + public string? TunnelUrl + { + get + { + return _currentTunnel?.TunnelUrl; + } + } + private readonly TcpTunnelRequest Tunnel; private TcpTunnelResponse? _currentTunnel = null; - public TcpTunnelClient(TcpTunnelRequest tunnel, LogLevel logLevel) + public TcpTunnelClient(TcpTunnelRequest tunnel, LogLevel? logLevel) { Tunnel = tunnel; @@ -22,15 +31,18 @@ public TcpTunnelClient(TcpTunnelRequest tunnel, LogLevel logLevel) .AddMessagePackProtocol() .ConfigureLogging(logging => { - logging.SetMinimumLevel(logLevel); - logging.AddConsole(); + if (logLevel.HasValue) + { + logging.SetMinimumLevel(logLevel.Value); + logging.AddConsole(); + } }) .WithAutomaticReconnect() .Build(); Connection.On("NewTcpConnection", (tcpConnection) => { - Console.WriteLine($"New TCP Connection {tcpConnection.RequestId}"); + Program.LogRequest("TCP", $"New Connection {tcpConnection.RequestId}"); _ = HandleNewTcpConnectionAsync(tcpConnection); @@ -39,22 +51,18 @@ public TcpTunnelClient(TcpTunnelRequest tunnel, LogLevel logLevel) Connection.On("TcpTunnelClosed", async (errorMessage) => { - Console.WriteLine($"TCP Tunnel closed by server: {errorMessage}"); + Program.LogError($"[TCP] Tunnel closed by server: {errorMessage}."); _currentTunnel = await RegisterTunnelAsync(tunnel); }); Connection.Reconnected += async connectionId => { - Console.WriteLine($"Reconnected. New ConnectionId {connectionId}"); - _currentTunnel = await RegisterTunnelAsync(tunnel); }; Connection.Closed += async (error) => { - Console.WriteLine("Connection closed... reconnecting"); - await Task.Delay(new Random().Next(0, 5) * 1000); if (await ConnectWithRetryAsync(Connection, CancellationToken.None)) @@ -84,18 +92,14 @@ public async Task ConnectAsync() { tunnelResponse = await Connection.InvokeAsync("RegisterTunnelAsync", tunnel); - if (string.IsNullOrEmpty(tunnelResponse.Error)) + if (!string.IsNullOrEmpty(tunnelResponse.Error)) { - Console.WriteLine($"Tunnel created successfully: {tunnelResponse!.TunnelUrl}"); - } - else - { - Console.WriteLine($"{tunnelResponse!.Message}:{tunnelResponse.Error}"); + Program.LogError($"[TCP] {tunnelResponse!.Message}:{tunnelResponse.Error}"); } } catch (Exception ex) { - Console.WriteLine($"An error occurred while registering the tunnel {ex.Message}"); + Program.LogError($"[TCP] An error occurred while registering the tunnel {ex.Message}"); await Task.Delay(5000); } @@ -120,13 +124,13 @@ private async Task HandleNewTcpConnectionAsync(TcpConnection tcpConnection) } catch (Exception ex) { - Console.WriteLine($"Error handling TCP connection {ex.Message}"); + Program.LogError($"[TCP] Error handling connection {ex.Message}"); } finally { cts.Cancel(); - Console.WriteLine($"TCP Connection {tcpConnection.RequestId} done."); + Program.Log($"[TCP] Connection {tcpConnection.RequestId} closed."); } } @@ -157,7 +161,7 @@ private async Task StreamIncomingAsync(TcpClient localClient, TcpConnection tcpC } finally { - Console.WriteLine($"Writing data to TCP connection {tcpConnection.RequestId} finished."); + Program.Log($"[TCP] Writing data to connection {tcpConnection.RequestId} finished."); } } @@ -185,7 +189,7 @@ private static async IAsyncEnumerable> StreamLocalTcpAsync( } finally { - Console.WriteLine($"Reading data from TCP connection {tcpConnection.RequestId} finished."); + Program.Log($"[TCP] Reading data from connection {tcpConnection.RequestId} finished."); ArrayPool.Shared.Return(buffer); } @@ -199,8 +203,6 @@ private async Task ConnectWithRetryAsync(HubConnection connection, Cancell { await connection.StartAsync(token); - Console.WriteLine($"Client connected to SignalR hub. ConnectionId: {connection.ConnectionId}"); - return true; } catch when (token.IsCancellationRequested) @@ -209,7 +211,7 @@ private async Task ConnectWithRetryAsync(HubConnection connection, Cancell } catch { - Console.WriteLine($"Cannot connect to WebSocket server on {Tunnel.PublicUrl}"); + Program.LogError($"[TCP] Cannot connect to the public server on {Tunnel.PublicUrl}"); await Task.Delay(5000, token); } diff --git a/src/Tunnelite.Client/Tunnelite.Client.csproj b/src/Tunnelite.Client/Tunnelite.Client.csproj index a7a73dc..b7c5f96 100644 --- a/src/Tunnelite.Client/Tunnelite.Client.csproj +++ b/src/Tunnelite.Client/Tunnelite.Client.csproj @@ -16,7 +16,7 @@ Tool for tunneling URLs https://github.com/cristipufu/ws-tunnel-signalr https://github.com/cristipufu/ws-tunnel-signalr - 1.1.3 + 1.1.4 @@ -30,6 +30,7 @@ + diff --git a/src/Tunnelite.Server/HttpTunnel/HttpAppExtensions.cs b/src/Tunnelite.Server/HttpTunnel/HttpAppExtensions.cs index ead3913..c44abef 100644 --- a/src/Tunnelite.Server/HttpTunnel/HttpAppExtensions.cs +++ b/src/Tunnelite.Server/HttpTunnel/HttpAppExtensions.cs @@ -209,11 +209,7 @@ await context.Response.WriteAsJsonAsync(new return; } - deferredHttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - await deferredHttpContext.Response.WriteAsJsonAsync(new - { - Message = "An error occurred while tunneling the request. The tunnel is broken, please check if the local application is online!", - }); + await ServerErrorAsync(app, deferredHttpContext); // Complete the deferred response await requestsQueue.CompleteAsync(requestId); @@ -267,17 +263,9 @@ static async Task TunnelRequestAsync(WebApplication app, HttpContext context, IH else if (subdomain.Equals("tunnelite")) { var filePath = Path.Combine(app.Environment.WebRootPath, "index.html"); - - if (File.Exists(filePath)) - { - context.Response.ContentType = "text/html"; - await context.Response.SendFileAsync(filePath); - } - else - { - context.Response.StatusCode = StatusCodes.Status404NotFound; - await context.Response.WriteAsync("Index page not found"); - } + context.Response.ContentType = "text/html"; + await context.Response.SendFileAsync(filePath); + return; } else @@ -287,15 +275,13 @@ static async Task TunnelRequestAsync(WebApplication app, HttpContext context, IH if (tunnel == null) { - context.Response.StatusCode = StatusCodes.Status404NotFound; - await context.Response.WriteAsync(NotFound); + await NotFoundAsync(app, context); return; } if (!tunnelStore.Connections.TryGetValue(tunnel!.ClientId, out var connectionId)) { - context.Response.StatusCode = StatusCodes.Status404NotFound; - await context.Response.WriteAsync(NotFound); + await NotFoundAsync(app, context); return; } @@ -329,6 +315,22 @@ static async Task TunnelRequestAsync(WebApplication app, HttpContext context, IH } } + static async Task NotFoundAsync(WebApplication app, HttpContext context) + { + var filePath = Path.Combine(app.Environment.WebRootPath, "404.html"); + context.Response.StatusCode = StatusCodes.Status404NotFound; + context.Response.ContentType = "text/html"; + await context.Response.SendFileAsync(filePath); + } + + static async Task ServerErrorAsync(WebApplication app, HttpContext context) + { + var filePath = Path.Combine(app.Environment.WebRootPath, "500.html"); + context.Response.StatusCode = StatusCodes.Status404NotFound; + context.Response.ContentType = "text/html"; + await context.Response.SendFileAsync(filePath); + } + static string RandomSubdomain(int length = 8) { Random random = new(); @@ -338,16 +340,4 @@ static string RandomSubdomain(int length = 8) } static readonly string[] NotAllowedHeaders = ["Connection", "Transfer-Encoding", "Keep-Alive", "Upgrade", "Proxy-Connection"]; - - const string NotFound = @" Oops! Lost in the Tunnel? -< ------------------------ > -< It seems you've wandered > -< into a mysterious realm > -< ------------------------ > - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || || -"; } \ No newline at end of file diff --git a/src/Tunnelite.Server/wwwroot/404.html b/src/Tunnelite.Server/wwwroot/404.html new file mode 100644 index 0000000..71cb4c2 --- /dev/null +++ b/src/Tunnelite.Server/wwwroot/404.html @@ -0,0 +1,79 @@ + + + + + + 404 - Page Not Found + + + +
+

404

+

Oops! Lost in the Tunnel?

+

It seems you've wandered into a mysterious realm

+
+ \ ^__^ + \ (oo)\_______ + (__)\ )\/\ + ||----w | + || || +
+ Find Your Way Back Home +
+ + \ No newline at end of file diff --git a/src/Tunnelite.Server/wwwroot/500.html b/src/Tunnelite.Server/wwwroot/500.html new file mode 100644 index 0000000..31637f8 --- /dev/null +++ b/src/Tunnelite.Server/wwwroot/500.html @@ -0,0 +1,81 @@ + + + + + + 500 - Internal Server Error + + + +
+

500

+

Oops! The Tunnel Collapsed!

+

We've encountered an internal server error, please check if the local application is online!

+
+ __ + -=(o '. + '.-.\ + /| \\ + '| || + _\_):,_ +
+ Return to Safety +
+ + \ No newline at end of file