From aa9c5c2f2caaa36cb8de678434ce71d565200d94 Mon Sep 17 00:00:00 2001 From: Cristian Pufu Date: Mon, 5 Aug 2024 14:25:10 +0300 Subject: [PATCH] Replace wss streaming with http calls --- README.md | 15 +-- docs/http_tunneling.png | Bin 0 -> 48462 bytes docs/http_tunneling.puml | 24 ++++ src/WebSocketTunnel.Client/Program.cs | 76 +++++++++++- .../WebSocketTunnel.Client.csproj | 2 +- src/WebSocketTunnel.Server/Program.cs | 111 +++++++++++++++++- 6 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 docs/http_tunneling.png create mode 100644 docs/http_tunneling.puml diff --git a/README.md b/README.md index d777a16..20ca8ba 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,5 @@ # Tunnelite -```plaintext - Public Intranet -+--------+ +---------------+ +--------------------------------------+ -| User | ---http---> | Tunnel Server | <---wss---> | Tunnel Client --http--> Application | -| | <---------- | | <---------> | <-------- | -+--------+ +---------------+ +--------------------------------------+ - -``` - Tunnelite is a .NET tool that allows you to create a secure tunnel from a public URL to your local application running on your machine. ## Installation @@ -32,8 +23,6 @@ This command returns a public URL with an auto-generated subdomain, such as `htt Tunnelite works by establishing a websocket connection to the public server and streaming all incoming data to your local application, effectively forwarding requests from the public URL to your local server. -## Features +
-- Easy to Use: Simple command-line interface. -- Secure: Uses WebSockets for secure data transmission. -- Auto-Generated URLs: Automatically generates a unique subdomain for each tunnel. +![image info](https://github.com/cristipufu/ws-tunnel-signalr/blob/master/docs/http_tunneling.png) diff --git a/docs/http_tunneling.png b/docs/http_tunneling.png new file mode 100644 index 0000000000000000000000000000000000000000..2371dbd5b04861f90e35a3700fa6c80e6e25807f GIT binary patch literal 48462 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�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 z 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

8uNP}AD?h`&9eZGgp}ziV4F3;$01O| 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 PTS: 1. HTTP Request +PTS -> AS: 2. Notify Client (based on subdomain) +AS --> TC: 3. WSS Notification (contains pod name) +TC -> PTS: 4. HTTP GET (with headers for pod routing) +PTS --> TC: 5. Stream HTTP Request Body +TC -> IS: 6. Forward Request +IS --> TC: 7. Forward Response +TC -> PTS: 8. HTTP POST (with headers for pod routing) +PTS --> EC: 9. Stream Response + +note right of PTS + Steps 4-5 and 8-9 are routed + to the same pod based on + HTTP headers +end note + +@enduml \ No newline at end of file diff --git a/src/WebSocketTunnel.Client/Program.cs b/src/WebSocketTunnel.Client/Program.cs index 4dc66f6..9d2c96b 100644 --- a/src/WebSocketTunnel.Client/Program.cs +++ b/src/WebSocketTunnel.Client/Program.cs @@ -53,7 +53,7 @@ public static async Task Main(string[] args) private static async Task RegisterTunnelAsync(string localUrl, string publicUrl, Guid clientId, TunnelResponse? existingTunnel) { var response = await ServerHttpClient.PostAsJsonAsync( - $"{publicUrl}/register-tunnel", + $"{publicUrl}/tunnelite/tunnel", new Tunnel { LocalUrl = localUrl, @@ -98,7 +98,8 @@ private static async Task ConnectToServerAsync(string localUrl, string publicUrl { Console.WriteLine($"Received tunneling request: [{requestMetadata.Method}]{requestMetadata.Path}"); - await TunnelRequestAsync(requestMetadata); + await TunnelRequestWithHttpAsync(publicUrl, requestMetadata); + //await TunnelRequestWithWssAsync(requestMetadata); }); Connection.Reconnected += async connectionId => @@ -126,7 +127,76 @@ private static async Task ConnectToServerAsync(string localUrl, string publicUrl } } - private static async Task TunnelRequestAsync(RequestMetadata requestMetadata) + private static async Task TunnelRequestWithHttpAsync(string publicUrl, RequestMetadata requestMetadata) + { + try + { + // Start the request to the public server + using var publicResponse = await ServerHttpClient.GetAsync( + $"{publicUrl}/tunnelite/request/{requestMetadata.RequestId}", + HttpCompletionOption.ResponseHeadersRead); + + publicResponse.EnsureSuccessStatusCode(); + + // Prepare the request to the local server + var localRequest = new HttpRequestMessage(new HttpMethod(requestMetadata.Method), requestMetadata.Path); + + // Copy headers from public response to local request + foreach (var header in publicResponse.Headers) + { + if (header.Key.StartsWith("X-TR-")) + { + localRequest.Headers.TryAddWithoutValidation(header.Key[5..], header.Value); + } + } + + // Set the content of the local request to stream the data from the public response + localRequest.Content = new StreamContent(await publicResponse.Content.ReadAsStreamAsync()); + + if (requestMetadata.ContentType != null) + { + localRequest.Content.Headers.ContentType = new MediaTypeHeaderValue(requestMetadata.ContentType); + } + + // Send the request to the local server and get the response + using var localResponse = await LocalHttpClient.SendAsync(localRequest); + + // Prepare the request back to the public server + var publicRequest = new HttpRequestMessage(HttpMethod.Post, $"{publicUrl}/tunnelite/request/{requestMetadata.RequestId}"); + + // Set the status code + publicRequest.Headers.Add("X-T-Status", ((int)localResponse.StatusCode).ToString()); + + // Copy headers from local response to public request + foreach (var header in localResponse.Headers) + { + publicRequest.Headers.TryAddWithoutValidation($"X-TR-{header.Key}", header.Value); + } + + // Copy content headers from local response to public request + foreach (var header in localResponse.Content.Headers) + { + publicRequest.Headers.TryAddWithoutValidation($"X-TC-{header.Key}", header.Value); + } + + // Set the content of the public request to stream from the local response + publicRequest.Content = new StreamContent(await localResponse.Content.ReadAsStreamAsync()); + + // Send the response back to the public server + using var response = await ServerHttpClient.SendAsync(publicRequest); + + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected error tunneling request: {ex.Message}"); + + // todo replace wss + await Connection!.SendAsync("CompleteWithErrorAsync", requestMetadata, ex.Message); + } + } + + private static async Task TunnelRequestWithWssAsync(RequestMetadata requestMetadata) { if (Connection == null) { diff --git a/src/WebSocketTunnel.Client/WebSocketTunnel.Client.csproj b/src/WebSocketTunnel.Client/WebSocketTunnel.Client.csproj index a403e4e..1888572 100644 --- a/src/WebSocketTunnel.Client/WebSocketTunnel.Client.csproj +++ b/src/WebSocketTunnel.Client/WebSocketTunnel.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.0.3 + 1.0.4 diff --git a/src/WebSocketTunnel.Server/Program.cs b/src/WebSocketTunnel.Server/Program.cs index 27173b6..f905a2a 100644 --- a/src/WebSocketTunnel.Server/Program.cs +++ b/src/WebSocketTunnel.Server/Program.cs @@ -34,7 +34,7 @@ app.UseHttpsRedirection(); -app.MapPost("/register-tunnel", async (HttpContext context, [FromBody] Tunnel payload, TunnelStore tunnelStore, ILogger logger) => +app.MapPost("/tunnelite/tunnel", async (HttpContext context, [FromBody] Tunnel payload, TunnelStore tunnelStore, ILogger logger) => { try { @@ -97,6 +97,115 @@ await context.Response.WriteAsJsonAsync(new } }); +app.MapGet("/tunnelite/request/{requestId}", async (HttpContext context, [FromRoute] Guid requestId, RequestsQueue requestsQueue, ILogger logger) => +{ + try + { + var deferredHttpContext = requestsQueue.GetHttpContext(requestId); + + if (deferredHttpContext == null) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + // Send method + context.Response.Headers.Append("X-T-Method", deferredHttpContext.Request.Method); + + // Send headers + foreach (var header in deferredHttpContext.Request.Headers) + { + context.Response.Headers.Append($"X-TR-{header.Key}", header.Value.ToString()); + } + + // Stream the body + await deferredHttpContext.Request.Body.CopyToAsync(context.Response.Body); + } + catch (Exception ex) + { + logger.LogError(ex, "Error fetching request body: {Message}", ex.Message); + + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsJsonAsync(new + { + Message = "An error occurred while fetching the request body", + Error = ex.Message, + }); + } +}); + +app.MapPost("/tunnelite/request/{requestId}", async (HttpContext context, [FromRoute] Guid requestId, RequestsQueue requestsQueue, ILogger logger) => +{ + try + { + var deferredHttpContext = requestsQueue.GetHttpContext(requestId); + + if (deferredHttpContext == null) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + // Set the status code + if (context.Request.Headers.TryGetValue("X-T-Status", out var statusCodeHeader) + && int.TryParse(statusCodeHeader, out var statusCode)) + { + deferredHttpContext.Response.StatusCode = statusCode; + } + else + { + deferredHttpContext.Response.StatusCode = 200; // Default to 200 OK if not specified + } + + // Copy headers from the tunneling client's request to the deferred response + var notAllowed = new string[] { "Connection", "Transfer-Encoding", "Keep-Alive", "Upgrade", "Proxy-Connection" }; + + foreach (var header in context.Request.Headers) + { + if (header.Key.StartsWith("X-TR-")) + { + var headerKey = header.Key[5..]; // Remove "X-TR-" prefix + + if (!notAllowed.Contains(headerKey)) + { + deferredHttpContext.Response.Headers.TryAdd(headerKey, header.Value); + } + } + + if (header.Key.StartsWith("X-TC-")) + { + var headerKey = header.Key[5..]; // Remove "X-TR-" prefix + + if (!notAllowed.Contains(headerKey)) + { + deferredHttpContext.Response.Headers.TryAdd(headerKey, header.Value); + } + } + } + + // Stream the body from the tunneling client's request to the deferred response + await context.Request.Body.CopyToAsync(deferredHttpContext.Response.Body); + + // Complete the deferred response + await requestsQueue.CompleteAsync(requestId); + + // Send a confirmation response to the tunneling client + context.Response.StatusCode = StatusCodes.Status200OK; + await context.Response.WriteAsJsonAsync(new { Message = "Ok" }); + } + catch (Exception ex) + { + logger.LogError(ex, "Error forwarding response body: {Message}", ex.Message); + + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + await context.Response.WriteAsJsonAsync(new + { + Message = "An error occurred while forwarding the response body", + Error = ex.Message, + }); + } +}); + app.MapGet("/favicon.ico", async context => { context.Response.ContentType = "image/x-icon";