Skip to content
This repository has been archived by the owner on Oct 10, 2018. It is now read-only.

Added support for WebSockets #41

Merged
merged 18 commits into from
Mar 1, 2017
Merged

Added support for WebSockets #41

merged 18 commits into from
Mar 1, 2017

Conversation

mkosieradzki
Copy link
Contributor

Added support for WebSockets

  • Bumped .NET Framework version to 4.6 (required by System.Net.WebSockets.Client)
  • Updated sample project to be HttpUpgradeFeature-compatible

Addresses #38

Known issues:

  • Connection closing handshake crashes when using IIS Express (It works correctly with standalone Kestrel and WebListener)
  • Lacking tests - it would be good idea to test it using Autobahn Testsuite

Upgraded .NET Framework to 4.6
Updated example to use web sockets on Kestrel
@dnfclas
Copy link

dnfclas commented Feb 22, 2017

Hi @mkosieradzki, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by .NET Foundation and real humans are currently evaluating your PR.

TTYL, DNFBOT;

@Tratcher
Copy link
Member

Note some implementations of the ClientWebSocket don't support Win7.

@Tratcher
Copy link
Member

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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrdinalIgnoreCase

Copy link
Contributor Author

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);
Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpContext.RequestAborted

Copy link
Contributor Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloseOutputAsync.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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
@mkosieradzki
Copy link
Contributor Author

@Tratcher Thank you very much for code review! I have addressed your concerns.

Copy link
Contributor

@mikaelm12 mikaelm12 left a 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;
Copy link
Contributor

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

Copy link
Contributor Author

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))
Copy link
Contributor

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

Copy link
Contributor Author

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];
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Done.

@mkosieradzki
Copy link
Contributor Author

@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;
Copy link
Member

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.

Copy link
Contributor Author

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 :).

@mkosieradzki
Copy link
Contributor Author

@davidfowl Thanks. I have rewritten this line.

Copy link
Contributor

@mikaelm12 mikaelm12 left a 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">

Copy link
Contributor

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.

Copy link
Contributor Author

@mkosieradzki mkosieradzki Feb 23, 2017

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)

@mkosieradzki
Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default settings are bad... Not your fault. You can change them here:

image

Copy link
Contributor Author

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.

Copy link
Contributor

@mikaelm12 mikaelm12 left a 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.

@mkosieradzki
Copy link
Contributor Author

https://github.com/aspnet/WebSockets/ this is similar case. They use Autobahn Testsuite.

@mkosieradzki
Copy link
Contributor Author

@anurse Is there any chance you could help with porting automated tests from WebSockets repo?
Or maybe somehow integrate testing between those two repos.
@mikaelm12 Would you agree that porting entire testing infrastructure from WebSockets repo would deserve a separate PR?

@analogrelay
Copy link
Contributor

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 :)

@analogrelay
Copy link
Contributor

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.

@mkosieradzki
Copy link
Contributor Author

mkosieradzki commented Feb 24, 2017

@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.

@mikaelm12
Copy link
Contributor

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);
Copy link
Member

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)
Copy link
Member

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);
Copy link
Member

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?

Copy link
Contributor Author

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).

@mkosieradzki
Copy link
Contributor Author

mkosieradzki commented Feb 24, 2017

@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.

@mkosieradzki
Copy link
Contributor Author

@mikaelm12 Do you need anything else from me here?

@mikaelm12
Copy link
Contributor

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 😄
And thanks for the contributions!

@mikaelm12
Copy link
Contributor

Seems like the sample is no longer working... I'm looking into it

@mkosieradzki
Copy link
Contributor Author

@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;
Copy link
Member

@Tratcher Tratcher Feb 28, 2017

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.

Copy link
Contributor Author

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()
Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor Author

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!

@mikaelm12
Copy link
Contributor

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.

@mkosieradzki
Copy link
Contributor Author

mkosieradzki commented Mar 1, 2017

@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.
Another known issue is that IIS Express does not seem to correctly perform WebSockets closing.

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.

@mkosieradzki
Copy link
Contributor Author

OK. I did some investigation and have opened: aspnet/AspNetCoreModule#77 .

mkosieradzki added a commit to mkosieradzki/Proxy that referenced this pull request Mar 1, 2017
Removed workarounds for aspnet#47
Copy link
Contributor

@mikaelm12 mikaelm12 left a 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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants