-
Notifications
You must be signed in to change notification settings - Fork 113
Conversation
Upgraded .NET Framework to 4.6 Updated example to use web sockets on Kestrel
Hi @mkosieradzki, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution! The agreement was validated by .NET Foundation and real humans are currently evaluating your PR. TTYL, DNFBOT; |
Note some implementations of the ClientWebSocket don't support Win7. |
Too bad Ping and Pong messages can't be forwarded with this abstraction. You'll want some configuration for the ping/pong intervals. |
{ | ||
foreach (var kv in context.Request.Headers) | ||
{ | ||
if (!NotForwardedWebSocketHeaders.Contains(kv.Key)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrdinalIgnoreCase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
var wsScheme = string.Equals(_options.Scheme, "https", StringComparison.OrdinalIgnoreCase) ? "wss" : "ws"; | ||
var uriString = $"{wsScheme}://{_options.Host}:{_options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"; | ||
|
||
await client.ConnectAsync(new Uri(uriString), CancellationToken.None); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need some error handling. Not every WebSocket request is accepted. E.g. some get rejected with 401 for auth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the HttpContext.RequestAborted CancellationToken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree about error handling. Fixed it. However full implementation requires more than netstandard13 as WebException is not available there. So I created a generic implementation with 400 for start. After we figure out what to do with this WebException I can uncomment and add full error handling with entire HttpResponse forwarding.
var buffer = new byte[4096]; | ||
while (true) | ||
{ | ||
var res = await source.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HttpContext.RequestAborted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
var res = await source.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); | ||
if (res.MessageType == WebSocketMessageType.Close) | ||
{ | ||
await dest.CloseAsync(source.CloseStatus.Value, source.CloseStatusDescription, CancellationToken.None); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CloseOutputAsync.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was considering this one before. I am not 100% sure which one is correct here... Assuming that CloseOutputAsync is similar to socket shutdown this is the proper choice. But both options are not optimally documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CloseOutputAsync only sends a close. CloseAsync sends a close and then starts a read loop waiting for a close response. Since you have a separate thread handling the communication the other direction, you only want CloseOutputAsync in each thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That explains a lot (and complies with what I found in ManagedWebSocket source code) I got mislead by a .... stack overflow answer again. Shame on me :-(. Thank you Chris!
Added RequestAborted cancellation token support Added basic error handling for connect Replaced CloseAsync with CloseOutputAsync
@Tratcher Thank you very much for code review! I have addressed your concerns. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of things. Thanks for the contribution!
@@ -8,6 +8,9 @@ | |||
using Microsoft.AspNetCore.Builder; | |||
using Microsoft.AspNetCore.Http; | |||
using Microsoft.Extensions.Options; | |||
using System.Threading; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:Can you sort these using statements. Keeping the Microsoft namespaces after System though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
{ | ||
foreach (var kv in context.Request.Headers) | ||
{ | ||
if (!NotForwardedWebSocketHeaders.Contains(kv.Key, StringComparer.OrdinalIgnoreCase)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use a better iterator variable name. Maybe headerEntry or something else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
|
||
private static async Task PumpWebSocket(WebSocket source, WebSocket dest, CancellationToken ct) | ||
{ | ||
var buffer = new byte[4096]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Maybe we should think about making the buffer size configurable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Done.
Renamed iterator
@mikaelm12 Thank you for your code review. I have addressed your concerns. |
public int? WebSocketBufferSize | ||
{ | ||
get => _webSocketBufferSize; | ||
set => _webSocketBufferSize = value.HasValue && value.Value <= 0 ? throw new ArgumentOutOfRangeException(nameof(value)) : value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty unreadable. I'd split it across lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done - your project your rules :).
@davidfowl Thanks. I have rewritten this line. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One last thing regarding the AppVeyor failure.
@@ -1,7 +1,7 @@ | |||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So currently AppVeyor fails with a Missing Method Exception.
You can add
<PropertyGroup>
<!-- TODO remove when https://github.com/Microsoft/vstest/issues/428 is resolved -->
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>`
That's our current work around and that should fix it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. btw. Should we remove:
<!-- TODO remove when https://github.com/dotnet/sdk/issues/396 is resolved -->
<RuntimeIdentifier Condition=" '$(TargetFramework)' != 'netcoreapp1.1' ">win7-x64</RuntimeIdentifier>
as it seems to be already resolved? (Just asking not sure about this)
Done |
@@ -1,22 +1,28 @@ | |||
// Copyright (c) .NET Foundation. All rights reserved. | |||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
|
|||
using Microsoft.AspNetCore.Builder; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry if this wasn't clear earlier but we put the all System namespaces before the Microsoft namespaces. After this, it should be good to merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. It's my bad. My apologies. I've brainlessly used VS 2017 autosort refactoring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidfowl Thanks for that tip :). I've changed it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I kind of jumped the gun on the approval. I was about to merge until I realized that there are no tests. We're gonna need some test coverage before this goes in.
https://github.com/aspnet/WebSockets/ this is similar case. They use Autobahn Testsuite. |
@anurse Is there any chance you could help with porting automated tests from WebSockets repo? |
I can certainly help review it. The code in the WebSockets repo should migrate reasonably well. We use it in aspnet/SignalR as well and it migrated well. We may want to consider abstracting it into a separate assembly so that we don't have to keep duplicating it, but I don't really have the cycles to work on that right now :) |
I'd definitely suggest setting up some kind of simple smoke-test before merging this PR, but a full conformance test could go in a separate PR. |
@anurse Thanks a lot for your response. I understand your lack of "cycles" and appreciate your effort in SignalR and different projects :). Smoke-test here sounds like a good idea. I will try to implement such. Regarding full conformance tests I am VSTS-guy and have no hands-on experience with AppVeyor and I cannot commit to porting full test suite now or later, as it would take me way too much time. |
No worries @mkosieradzki 😄 . I'll merge this as soon as I get one more approval |
var buffer = new byte[_options.WebSocketBufferSize ?? DefaultWebSocketBufferSize]; | ||
while (true) | ||
{ | ||
var res = await source.ReceiveAsync(new ArraySegment<byte>(buffer), ct); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: result
instead of res
context.Response.StatusCode = 400; | ||
} | ||
|
||
private async Task PumpWebSocket(WebSocket source, WebSocket dest, CancellationToken ct) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: cancellationToken
instead of ct
var buffer = new byte[_options.WebSocketBufferSize ?? DefaultWebSocketBufferSize]; | ||
while (true) | ||
{ | ||
var res = await source.ReceiveAsync(new ArraySegment<byte>(buffer), ct); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be handling operation cancelled exceptions here? If the client should that really 500?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I partially agree: For receive error there should be websocket closure with InternalServerError instead of 500.
I have pushed fixes (I also have renamed some more variables to match your conventions).
@davidfowl I have ended up handling OperationCanceledException in the Receive. In that case I close the other socket with EndpointUnavailable (1001 https://tools.ietf.org/html/rfc6455#page-45) to ensure the closure sequence is initiated property with proper error. Alternatively we can send 1001 from proxy to server and 1011 from proxy to client if it makes more sense. I've decided not to handle it with during ConnectAsync as it will be (should be) handled by the infrastructure which induced AbortRequest. |
@mikaelm12 Do you need anything else from me here? |
I need to do some verification for this on my side. We had some other high pri tasks last week so this had to wait. But don't worry, this hasn't been forgotten 😄 |
Seems like the sample is no longer working... I'm looking into it |
@mikaelm12 You need to run it from Kestrel. There are some problems with IIS Express. |
if (result.MessageType == WebSocketMessageType.Close) | ||
{ | ||
await destination.CloseOutputAsync(source.CloseStatus.Value, source.CloseStatusDescription, cancellationToken); | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: this return and the following else are redundant, either way your at the end of the method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have fixed this in #46
} | ||
|
||
[Fact] | ||
public async Task ProxyWebSocketsSmokeTest() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tratcher @davidfowl Shouldn't this have a skip condition for Win7 and Windows Server 2008?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for reference, we also do this in the EndToEnd tests here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the reference, very helpful!
As Kestrel is not an edge server, we can't in good faith add a feature that only works with Kestrel. It needs to work with IIS before being merged. |
@mikaelm12 I have fixed broken example. Sample project works as before. Basic HTTP works properly with Kestrel, HttpSys (former WebListener) and IIS Express. BTW. HttpSys is a proper edge server. Known issue is #47 . Due to breaking changes in System.Net.HttpClient. When it receives Close message it kills the entire connection instead of completing proper close handshake. It seems to be an issue with AspNetCoreModule, it requires further investigation and after this PR is merged I will start some investigation and probably open an issue with AspNetCoreModule. However IIS-Express support might get broken later by the workaround for #47. |
OK. I did some investigation and have opened: aspnet/AspNetCoreModule#77 . |
Removed workarounds for aspnet#47
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested with a small app that sends websockets requests to the proxy which forwards to a websockets echo end point and it seems to work.
Added support for WebSockets
Addresses #38
Known issues: