refactor(wrapper): fixs for socket.io, SSE, and better performance
This commit is contained in:
@@ -1,26 +1,32 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Services.AddHttpClient("NodeApp", client =>
|
|
||||||
{
|
// Register HttpClient so we can proxy HTTP traffic
|
||||||
client.BaseAddress = new Uri("http://localhost:4000");
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Enable WebSocket support
|
// Enable WebSocket support
|
||||||
app.UseWebSockets();
|
app.UseWebSockets();
|
||||||
|
|
||||||
// Logging method
|
// Forwarded headers (important if behind IIS or another proxy)
|
||||||
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
|
||||||
|
ForwardLimit = 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Simple file logger
|
||||||
void LogToFile(string message)
|
void LogToFile(string message)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -28,44 +34,91 @@ void LogToFile(string message)
|
|||||||
string logDir = Path.Combine(AppContext.BaseDirectory, "logs");
|
string logDir = Path.Combine(AppContext.BaseDirectory, "logs");
|
||||||
Directory.CreateDirectory(logDir);
|
Directory.CreateDirectory(logDir);
|
||||||
string logFilePath = Path.Combine(logDir, "proxy_log.txt");
|
string logFilePath = Path.Combine(logDir, "proxy_log.txt");
|
||||||
File.AppendAllText(logFilePath, $"{DateTime.UtcNow}: {message}{Environment.NewLine}");
|
File.AppendAllText(logFilePath, $"{DateTime.UtcNow:u}: {message}{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Handle potential errors writing to log file
|
|
||||||
Console.WriteLine($"Logging error: {ex.Message}");
|
Console.WriteLine($"Logging error: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
|
||||||
{
|
|
||||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
|
|
||||||
// Increase the limit if you have multiple proxies
|
|
||||||
ForwardLimit = 2
|
|
||||||
});
|
|
||||||
// Middleware to handle WebSocket requests
|
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
if (context.WebSockets.IsWebSocketRequest && context.Request.Path.StartsWithSegments("/ws"))
|
if (context.Request.Headers.ContainsKey("Origin"))
|
||||||
{
|
{
|
||||||
// LogToFile($"WebSocket request received for path: {context.Request.Path}");
|
var origin = context.Request.Headers["Origin"].ToString();
|
||||||
|
context.Response.Headers["Access-Control-Allow-Origin"] = origin;
|
||||||
|
context.Response.Headers["Vary"] = "Origin";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Credentials"] = "true";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,PATCH,DELETE,OPTIONS";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-Requested-With";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(context.Request.Method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status204NoContent;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Single terminal middleware (Run instead of Use → no ambiguity)
|
||||||
|
app.Run(async (HttpContext context) =>
|
||||||
|
{
|
||||||
|
var rawPath = context.Request.Path.Value ?? "";
|
||||||
|
|
||||||
|
string backendHttpBase;
|
||||||
|
string backendWsBase;
|
||||||
|
|
||||||
|
if (rawPath.StartsWith("/socket.io", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
rawPath.StartsWith("/lst/socket.io", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
backendHttpBase = "http://localhost:8080";
|
||||||
|
backendWsBase = "ws://localhost:8080";
|
||||||
|
}
|
||||||
|
else if (rawPath.StartsWith("/lst/api/controller", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
rawPath.StartsWith("/api/controller", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
backendHttpBase = "http://localhost:8080";
|
||||||
|
backendWsBase = "ws://localhost:8080";
|
||||||
|
|
||||||
|
// Now strip only once
|
||||||
|
// var newPath = rawPath.Substring("/lst".Length);
|
||||||
|
// context.Request.Path = new PathString(newPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backendHttpBase = "http://localhost:4000";
|
||||||
|
backendWsBase = "ws://localhost:4000";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle WebSocket requests
|
||||||
|
if (context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var backendUri = new UriBuilder("ws", "localhost", 4000)
|
var backendUri = new UriBuilder(backendWsBase)
|
||||||
{
|
{
|
||||||
Path = context.Request.Path,
|
Path = context.Request.Path,
|
||||||
Query = context.Request.QueryString.ToString()
|
Query = context.Request.QueryString.ToString()
|
||||||
}.Uri;
|
}.Uri;
|
||||||
|
|
||||||
using var backendSocket = new ClientWebSocket();
|
using var backendSocket = new ClientWebSocket();
|
||||||
|
|
||||||
|
// Forward incoming headers
|
||||||
|
foreach (var header in context.Request.Headers)
|
||||||
|
{
|
||||||
|
try { backendSocket.Options.SetRequestHeader(header.Key, header.Value); }
|
||||||
|
catch { /* ignore headers WS client doesn't like */ }
|
||||||
|
}
|
||||||
|
|
||||||
await backendSocket.ConnectAsync(backendUri, context.RequestAborted);
|
await backendSocket.ConnectAsync(backendUri, context.RequestAborted);
|
||||||
|
|
||||||
using var frontendSocket = await context.WebSockets.AcceptWebSocketAsync();
|
using var frontendSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
// WebSocket forwarding tasks
|
var forwardToBackend = ForwardWebSocketAsync(frontendSocket, backendSocket, cts.Token);
|
||||||
var forwardToBackend = ForwardWebSocketAsync(frontendSocket, backendSocket, cts.Token);
|
|
||||||
var forwardToFrontend = ForwardWebSocketAsync(backendSocket, frontendSocket, cts.Token);
|
var forwardToFrontend = ForwardWebSocketAsync(backendSocket, frontendSocket, cts.Token);
|
||||||
|
|
||||||
await Task.WhenAny(forwardToBackend, forwardToFrontend);
|
await Task.WhenAny(forwardToBackend, forwardToFrontend);
|
||||||
@@ -73,33 +126,22 @@ app.Use(async (context, next) =>
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
//LogToFile($"WebSocket proxy error: {ex.Message}");
|
LogToFile($"WebSocket proxy error: {ex.Message}");
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
|
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
|
||||||
await context.Response.WriteAsync($"WebSocket proxy error: {ex.Message}");
|
await context.Response.WriteAsync($"WebSocket proxy error: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await next();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Middleware to handle HTTP requests
|
|
||||||
app.Use(async (context, next) =>
|
|
||||||
{
|
|
||||||
if (context.WebSockets.IsWebSocketRequest)
|
|
||||||
{
|
|
||||||
await next();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient("NodeApp");
|
// Otherwise: normal HTTP request
|
||||||
|
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient();
|
||||||
|
var targetUri = backendHttpBase + context.Request.Path + context.Request.QueryString;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(new HttpMethod(context.Request.Method),
|
var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), targetUri);
|
||||||
context.Request.Path + context.Request.QueryString);
|
|
||||||
|
|
||||||
|
// Copy headers
|
||||||
foreach (var header in context.Request.Headers)
|
foreach (var header in context.Request.Headers)
|
||||||
{
|
{
|
||||||
if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
|
if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
|
||||||
@@ -114,21 +156,45 @@ app.Use(async (context, next) =>
|
|||||||
request.Content = new StreamContent(context.Request.Body);
|
request.Content = new StreamContent(context.Request.Body);
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
var response = await client.SendAsync(request,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead,
|
||||||
|
context.RequestAborted);
|
||||||
|
|
||||||
context.Response.StatusCode = (int)response.StatusCode;
|
context.Response.StatusCode = (int)response.StatusCode;
|
||||||
|
// Copy backend headers
|
||||||
foreach (var header in response.Headers)
|
foreach (var header in response.Headers)
|
||||||
{
|
|
||||||
context.Response.Headers[header.Key] = header.Value.ToArray();
|
context.Response.Headers[header.Key] = header.Value.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var header in response.Content.Headers)
|
foreach (var header in response.Content.Headers)
|
||||||
{
|
|
||||||
context.Response.Headers[header.Key] = header.Value.ToArray();
|
context.Response.Headers[header.Key] = header.Value.ToArray();
|
||||||
|
context.Response.Headers.Remove("transfer-encoding");
|
||||||
|
|
||||||
|
// ✅ NOW inject/override CORS
|
||||||
|
if (context.Request.Headers.ContainsKey("Origin"))
|
||||||
|
{
|
||||||
|
var origin = context.Request.Headers["Origin"].ToString();
|
||||||
|
context.Response.Headers["Access-Control-Allow-Origin"] = origin;
|
||||||
|
context.Response.Headers["Vary"] = "Origin";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Credentials"] = "true";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Methods"] =
|
||||||
|
"GET,POST,PUT,PATCH,DELETE,OPTIONS";
|
||||||
|
context.Response.Headers["Access-Control-Allow-Headers"] =
|
||||||
|
"Content-Type, Authorization, X-Requested-With";
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Response.Headers.Remove("transfer-encoding");
|
//await response.Content.CopyToAsync(context.Response.Body);
|
||||||
await response.Content.CopyToAsync(context.Response.Body);
|
|
||||||
|
// changes to manage the SSE so we get it all at once
|
||||||
|
var stream = await response.Content.ReadAsStreamAsync(context.RequestAborted);
|
||||||
|
var buffer = new byte[8192];
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), context.RequestAborted)) > 0)
|
||||||
|
{
|
||||||
|
await context.Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), context.RequestAborted);
|
||||||
|
|
||||||
|
// 🔑 Force flush so chunks go straight to client
|
||||||
|
await context.Response.Body.FlushAsync(context.RequestAborted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
@@ -138,9 +204,10 @@ app.Use(async (context, next) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, CancellationToken cancellationToken)
|
// Helper to forward WS frames in both directions
|
||||||
|
static async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var buffer = new byte[4 * 1024];
|
var buffer = new byte[8192];
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (source.State == WebSocketState.Open &&
|
while (source.State == WebSocketState.Open &&
|
||||||
@@ -153,13 +220,22 @@ async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, Cancel
|
|||||||
await destination.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
|
await destination.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await destination.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
|
|
||||||
|
await destination.SendAsync(
|
||||||
|
new ArraySegment<byte>(buffer, 0, result.Count),
|
||||||
|
result.MessageType,
|
||||||
|
result.EndOfMessage,
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (WebSocketException ex)
|
catch (WebSocketException ex)
|
||||||
{
|
{
|
||||||
LogToFile($"WebSocket forwarding error: {ex.Message}");
|
Console.WriteLine($"WebSocket forwarding error: {ex.Message}");
|
||||||
await destination.CloseOutputAsync(WebSocketCloseStatus.InternalServerError, "Error", cancellationToken);
|
try
|
||||||
|
{
|
||||||
|
await destination.CloseOutputAsync(WebSocketCloseStatus.InternalServerError, "Error", cancellationToken);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<system.webServer>
|
<system.webServer>
|
||||||
<!-- Enable WebSockets (may require unlocking at host level) -->
|
<!-- Enable WebSockets -->
|
||||||
<webSocket enabled="true" receiveBufferLimit="4194304" pingInterval="00:01:00" />
|
<webSocket enabled="true" receiveBufferLimit="4194304" pingInterval="00:01:00" />
|
||||||
|
|
||||||
<rewrite>
|
|
||||||
<rules>
|
|
||||||
<rule name="Proxy to Wrapper" stopProcessing="true">
|
|
||||||
<match url="^lst/(.*)" />
|
|
||||||
<conditions>
|
|
||||||
<add input="{HTTP_UPGRADE}" pattern="^WebSocket$" negate="true" />
|
|
||||||
</conditions>
|
|
||||||
<action type="Rewrite" url="http://localhost:4000/{R:1}" />
|
|
||||||
<serverVariables>
|
|
||||||
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
|
|
||||||
<set name="HTTP_X_REAL_IP" value="{REMOTE_ADDR}" />
|
|
||||||
</serverVariables>
|
|
||||||
</rule>
|
|
||||||
</rules>
|
|
||||||
</rewrite>
|
|
||||||
|
|
||||||
<staticContent>
|
<staticContent>
|
||||||
<remove fileExtension=".js" />
|
<remove fileExtension=".js" />
|
||||||
<mimeMap fileExtension=".js" mimeType="application/javascript" />
|
<mimeMap fileExtension=".js" mimeType="application/javascript" />
|
||||||
@@ -38,7 +22,7 @@
|
|||||||
|
|
||||||
<aspNetCore processPath="dotnet"
|
<aspNetCore processPath="dotnet"
|
||||||
arguments=".\lstWrapper.dll"
|
arguments=".\lstWrapper.dll"
|
||||||
stdoutLogEnabled="false"
|
stdoutLogEnabled="true"
|
||||||
stdoutLogFile=".\logs\stdout"
|
stdoutLogFile=".\logs\stdout"
|
||||||
hostingModel="inprocess" />
|
hostingModel="inprocess" />
|
||||||
</system.webServer>
|
</system.webServer>
|
||||||
|
|||||||
Reference in New Issue
Block a user