Skip to content

Commit

Permalink
Replace wss streaming with http calls
Browse files Browse the repository at this point in the history
  • Loading branch information
cristipufu committed Aug 5, 2024
1 parent 92639f3 commit 5137109
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 18 deletions.
15 changes: 2 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
<br/>

- 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)
Binary file added docs/http_tunneling.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/http_tunneling.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@startuml
actor "External Client" as EC
participant "Public Tunneling Server\n(Multiple Pods)" as PTS
participant "Azure SignalR" as AS
participant "Tunneling Client" as TC
participant "Intranet Server" as IS

EC -> 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
74 changes: 71 additions & 3 deletions src/WebSocketTunnel.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static async Task Main(string[] args)
private static async Task<TunnelResponse> RegisterTunnelAsync(string localUrl, string publicUrl, Guid clientId, TunnelResponse? existingTunnel)
{
var response = await ServerHttpClient.PostAsJsonAsync(
$"{publicUrl}/register-tunnel",
$"{publicUrl}/tunnelite/tunnel",
new Tunnel
{
LocalUrl = localUrl,
Expand Down Expand Up @@ -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 =>
Expand Down Expand Up @@ -126,7 +127,74 @@ 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 finalResponse = await ServerHttpClient.SendAsync(publicRequest);
finalResponse.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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/WebSocketTunnel.Client/WebSocketTunnel.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<Description>Tool for tunneling URLs</Description>
<PackageProjectUrl>https://github.com/cristipufu/ws-tunnel-signalr</PackageProjectUrl>
<RepositoryUrl>https://github.com/cristipufu/ws-tunnel-signalr</RepositoryUrl>
<Version>1.0.3</Version>
<Version>1.0.4</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
112 changes: 111 additions & 1 deletion src/WebSocketTunnel.Server/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using WebSocketTunnel.Server.Request;
Expand Down Expand Up @@ -34,7 +35,7 @@

app.UseHttpsRedirection();

app.MapPost("/register-tunnel", async (HttpContext context, [FromBody] Tunnel payload, TunnelStore tunnelStore, ILogger<Program> logger) =>
app.MapPost("/tunnelite/tunnel", async (HttpContext context, [FromBody] Tunnel payload, TunnelStore tunnelStore, ILogger<Program> logger) =>
{
try
{
Expand Down Expand Up @@ -97,6 +98,115 @@ await context.Response.WriteAsJsonAsync(new
}
});

app.MapGet("/tunnelite/request/{requestId}", async (HttpContext context, [FromRoute] Guid requestId, RequestsQueue requestsQueue, ILogger<Program> 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<Program> 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";
Expand Down

0 comments on commit 5137109

Please sign in to comment.