Skip to content
This repository was archived by the owner on Sep 3, 2024. It is now read-only.

Commit 49ad184

Browse files
passing websockets through
1 parent fb813fc commit 49ad184

3 files changed

Lines changed: 212 additions & 61 deletions

File tree

Lines changed: 3 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
namespace SqlStreamStore.HAL.DevServer
22
{
33
using System;
4-
using System.Linq;
5-
using System.Net.Http;
6-
using System.Net.Http.Headers;
74
using System.Threading.Tasks;
85
using Microsoft.AspNetCore.Builder;
96
using Microsoft.AspNetCore.Hosting;
107
using Microsoft.AspNetCore.Http;
118
using Microsoft.Extensions.DependencyInjection;
12-
using Microsoft.Extensions.Primitives;
9+
using Serilog;
1310
using MidFunc = System.Func<
1411
Microsoft.AspNetCore.Http.HttpContext,
1512
System.Func<System.Threading.Tasks.Task>,
@@ -19,12 +16,10 @@
1916
internal class DevServerStartup : IStartup
2017
{
2118
private readonly IStreamStore _streamStore;
22-
private readonly HttpClient _httpClient;
2319

2420
public DevServerStartup(IStreamStore streamStore)
2521
{
2622
_streamStore = streamStore;
27-
_httpClient = new HttpClient();
2823
}
2924

3025
public IServiceProvider ConfigureServices(IServiceCollection services) => services
@@ -35,8 +30,7 @@ public void Configure(IApplicationBuilder app) => app
3530
.UseResponseCompression()
3631
.Use(VaryAccept)
3732
.Use(CatchAndDisplayErrors)
38-
.Use(SqlStreamStreamBrowserStatic)
39-
.Use(SqlStreamStreamBrowserHtml)
33+
.UseSqlStreamStoreBrowser()
4034
.UseSqlStreamStoreHal(_streamStore);
4135

4236
private static MidFunc CatchAndDisplayErrors => async (context, next) =>
@@ -47,7 +41,7 @@ public void Configure(IApplicationBuilder app) => app
4741
}
4842
catch(Exception ex)
4943
{
50-
Console.WriteLine(ex);
44+
Log.Warning(ex, "Error during request.");
5145
}
5246
};
5347

@@ -66,57 +60,5 @@ Task Vary(object state)
6660

6761
return next();
6862
};
69-
70-
private MidFunc SqlStreamStreamBrowserStatic => (context, next)
71-
=> context.Request.Path.StartsWithSegments(new PathString("/static"))
72-
|| context.Request.Path.StartsWithSegments(new PathString("/sockjs-node"))
73-
? ForwardToClientDevServer(
74-
context,
75-
context.Request.Path)
76-
: next();
77-
78-
private MidFunc SqlStreamStreamBrowserHtml => (context, next)
79-
=> GetAcceptHeaders(context.Request)
80-
.Any(header => header == "text/html")
81-
? ForwardToClientDevServer(context, context.Request.PathBase.ToUriComponent())
82-
: next();
83-
84-
private static string[] GetAcceptHeaders(HttpRequest contextRequest)
85-
=> Array.ConvertAll(
86-
contextRequest.Headers.GetCommaSeparatedValues("Accept"),
87-
value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header)
88-
? header.MediaType
89-
: null);
90-
91-
private Task RedirectToPathBase(HttpContext context, PathString path)
92-
{
93-
context.Response.Redirect(context.Request.PathBase + path);
94-
95-
return Task.CompletedTask;
96-
}
97-
98-
private async Task ForwardToClientDevServer(HttpContext context, PathString path)
99-
{
100-
using(var request = new HttpRequestMessage(
101-
new HttpMethod(context.Request.Method),
102-
new UriBuilder
103-
{
104-
Port = 3000,
105-
Host = "localhost",
106-
Path = path.ToUriComponent(),
107-
Query = context.Request.QueryString.ToUriComponent()
108-
}.Uri))
109-
using(var response = await _httpClient.SendAsync(request))
110-
using(var stream = await response.Content.ReadAsStreamAsync())
111-
{
112-
context.Response.StatusCode = (int) response.StatusCode;
113-
foreach(var header in response.Headers.Concat(response.Content.Headers))
114-
{
115-
context.Response.Headers.Add(header.Key, new StringValues(header.Value.ToArray()));
116-
}
117-
118-
await stream.CopyToAsync(context.Response.Body, 8192, context.RequestAborted);
119-
}
120-
}
12163
}
12264
}

src/SqlStreamStore.HAL.DevServer/SqlStreamStore.HAL.DevServer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.1.1" />
99
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.1.1" />
1010
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.1.1" />
11+
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.1.1" />
1112
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.1" />
1213
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.1.1" />
1314
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
namespace SqlStreamStore.HAL.DevServer
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Net.Http;
7+
using System.Net.Http.Headers;
8+
using System.Net.WebSockets;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.AspNetCore.Builder;
12+
using Microsoft.AspNetCore.Http;
13+
using Microsoft.Extensions.Primitives;
14+
using MidFunc = System.Func<
15+
Microsoft.AspNetCore.Http.HttpContext,
16+
System.Func<System.Threading.Tasks.Task>,
17+
System.Threading.Tasks.Task
18+
>;
19+
20+
internal static class SqlStreamStoreBrowserExtensions
21+
{
22+
public static IApplicationBuilder UseSqlStreamStoreBrowser(
23+
this IApplicationBuilder builder)
24+
{
25+
var httpClient = new HttpClient();
26+
27+
return builder
28+
.UseWebSockets()
29+
.Use(ForwardWebsockets(httpClient))
30+
.Use(ForwardAcceptHtml(httpClient))
31+
.Use(ForwardStaticFiles(httpClient));
32+
}
33+
34+
private static MidFunc ForwardWebsockets(HttpClient httpClient) => (context, next)
35+
=> context.WebSockets.IsWebSocketRequest
36+
? ForwardWebsocketToClientDevServer(context)
37+
: (context.Request.Path.StartsWithSegments(new PathString("/sockjs-node"))
38+
? ForwardToClientDevServer(httpClient, context)
39+
: next());
40+
41+
42+
private static MidFunc ForwardStaticFiles(HttpClient httpClient) => (context, next)
43+
=> context.Request.Path.StartsWithSegments(new PathString("/static"))
44+
|| context.Request.Path.StartsWithSegments("/__webpack_dev_server__")
45+
? ForwardToClientDevServer(
46+
httpClient,
47+
context)
48+
: next();
49+
50+
private static MidFunc ForwardAcceptHtml(HttpClient httpClient) => (context, next)
51+
=> GetAcceptHeaders(context.Request)
52+
.Any(header => header == "text/html")
53+
? ForwardToClientDevServer(
54+
httpClient,
55+
context,
56+
context.Request.PathBase.ToUriComponent())
57+
: next();
58+
59+
private static Task ForwardToClientDevServer(
60+
HttpClient httpClient,
61+
HttpContext context)
62+
=> ForwardToClientDevServer(httpClient, context, context.Request.Path);
63+
64+
private static async Task ForwardToClientDevServer(
65+
HttpClient httpClient,
66+
HttpContext context,
67+
PathString path)
68+
{
69+
using(var request = BuildRequest(context, path))
70+
using(var response = await httpClient.SendAsync(request))
71+
using(var stream = await response.Content.ReadAsStreamAsync())
72+
{
73+
context.Response.StatusCode = (int) response.StatusCode;
74+
75+
var headers = from header in response.Headers.Concat(
76+
response.Content?.Headers
77+
?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
78+
where !"transfer-encoding".Equals(header.Key, StringComparison.InvariantCultureIgnoreCase)
79+
select header;
80+
81+
foreach(var header in headers)
82+
{
83+
context.Response.Headers.Add(header.Key, new StringValues(header.Value.ToArray()));
84+
}
85+
86+
await stream.CopyToAsync(context.Response.Body, 8196, context.RequestAborted);
87+
88+
await context.Response.Body.FlushAsync(context.RequestAborted);
89+
}
90+
}
91+
92+
private static HttpRequestMessage BuildRequest(HttpContext context, PathString path)
93+
{
94+
var request = new HttpRequestMessage(
95+
new HttpMethod(context.Request.Method),
96+
new UriBuilder
97+
{
98+
Port = 3000,
99+
Path = path.ToUriComponent(),
100+
Query = context.Request.QueryString.ToUriComponent()
101+
}.Uri);
102+
103+
foreach(var header in context.Request.Headers)
104+
{
105+
var values = header.Value.ToArray();
106+
107+
request.Headers.TryAddWithoutValidation(header.Key, values);
108+
request.Content?.Headers.TryAddWithoutValidation(header.Key, values);
109+
}
110+
111+
return request;
112+
}
113+
114+
private static string[] GetAcceptHeaders(HttpRequest contextRequest)
115+
=> Array.ConvertAll(
116+
contextRequest.Headers.GetCommaSeparatedValues("Accept"),
117+
value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header)
118+
? header.MediaType
119+
: null);
120+
121+
static async Task ForwardWebsocketToClientDevServer(HttpContext context)
122+
{
123+
var socket = await context.WebSockets.AcceptWebSocketAsync();
124+
125+
using(var forwarder = new WebsocketForwarder(socket))
126+
{
127+
await forwarder.SendAndReceive(context.RequestAborted);
128+
}
129+
}
130+
131+
private class WebsocketForwarder : IDisposable
132+
{
133+
private readonly WebSocket _socket;
134+
private readonly ClientWebSocket _client;
135+
136+
public WebsocketForwarder(WebSocket socket)
137+
{
138+
_socket = socket;
139+
_client = new ClientWebSocket();
140+
}
141+
142+
public async Task SendAndReceive(CancellationToken ct)
143+
{
144+
await _client.ConnectAsync(new UriBuilder
145+
{
146+
Port = 3000
147+
}.Uri,
148+
ct);
149+
150+
await Task.WhenAll(Send(ct), Receive(ct));
151+
}
152+
153+
private async Task Send(CancellationToken ct)
154+
{
155+
var receiveBuffer = new byte[4096];
156+
157+
while(_socket.State == WebSocketState.Open)
158+
{
159+
var buffer = new ArraySegment<byte>(receiveBuffer);
160+
161+
var result = await _socket.ReceiveAsync(
162+
buffer,
163+
ct);
164+
165+
if(result.MessageType == WebSocketMessageType.Close)
166+
{
167+
await _client.CloseAsync(
168+
result.CloseStatus ?? WebSocketCloseStatus.Empty,
169+
result.CloseStatusDescription,
170+
ct);
171+
172+
return;
173+
}
174+
175+
await _client.SendAsync(buffer, result.MessageType, result.EndOfMessage, ct);
176+
}
177+
}
178+
179+
private async Task Receive(CancellationToken ct)
180+
{
181+
var sendBuffer = new byte[4096];
182+
183+
while(_socket.State == WebSocketState.Open)
184+
{
185+
var buffer = new ArraySegment<byte>(sendBuffer);
186+
187+
var result = await _client.ReceiveAsync(
188+
buffer,
189+
ct);
190+
191+
if(result.MessageType == WebSocketMessageType.Close)
192+
{
193+
await _socket.CloseAsync(
194+
result.CloseStatus ?? WebSocketCloseStatus.Empty,
195+
result.CloseStatusDescription,
196+
ct);
197+
198+
return;
199+
}
200+
201+
await _socket.SendAsync(buffer, result.MessageType, result.EndOfMessage, ct);
202+
}
203+
}
204+
205+
public void Dispose() => _client?.Dispose();
206+
}
207+
}
208+
}

0 commit comments

Comments
 (0)