dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.27k stars 4.73k forks source link

[HttpListener] Calling Abort() or Dispose() on a server WebSocket originating from a HttpListener does nothing on Linux #58614

Open iconics-milan opened 3 years ago

iconics-milan commented 3 years ago

Description

Calling Abort() or Dispose() on a server WebSocket originating from a HttpListener does nothing with the underlying TCP connection, when run on Linux. The connection simply remains open. On Windows, it properly aborts the TCP connection.

Configuration

Tried on .NET Core 2.1, 3.1, 5.0 on Ubuntu - all with the same result. Works on Windows with all these versions.

Code

This code gets stuck on Linux, but finishes on Windows:

        using System;
        using System.IO;
        using System.Net;
        using System.Net.WebSockets;
        using System.Threading;
        using System.Threading.Tasks;

        namespace WebSocketCloseTest
        {
            public class Program
            {
                public static async Task Main(string[] args)
                {
                    var serverTask = Server("http://localhost:45000/test/ws/");
                    await Client("ws://localhost:45000/test/ws/");
                    await serverTask;
                }

                private static async Task Server(String serverAddress)
                {
                    var listener = new HttpListener();
                    listener.Prefixes.Add(serverAddress);
                    listener.Start();
                    HttpListenerContext ctx = await listener.GetContextAsync();
                    HttpListenerWebSocketContext wsCtx = await ctx.AcceptWebSocketAsync("subProtocol");
                    using (var webSocket = wsCtx.WebSocket)
                    {
                        webSocket.Abort();
                    }
                }

                private static async Task Client(String serverAddress)
                {
                    try
                    {
                        using (ClientWebSocket webSocket = new ClientWebSocket())
                        {
                            webSocket.Options.AddSubProtocol("subProtocol");
                            await webSocket.ConnectAsync(new Uri(serverAddress), CancellationToken.None).ConfigureAwait(false);
                            await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
                        }
                    }
                    catch (WebSocketException e)
                    {
                        Console.WriteLine($"Client WS exception: {e.Message}");
                    }
                    catch (IOException e)
                    {
                        Console.WriteLine($"Client WS exception: {e.Message}");
                    }
                }
            }
        }
ghost commented 3 years ago

Tagging subscribers to this area: @dotnet/ncl See info in area-owners.md if you want to be subscribed.

Issue Details
### Description Calling Abort() or Dispose() on a server WebSocket originating from a HttpListener does nothing with the underlying TCP connection, when run on Linux. The connection simply remains open. On Windows, it properly aborts the TCP connection. ### Configuration Tried on .NET Core 2.1, 3.1, 5.0 on Ubuntu - all with the same result. Works on Windows with all these versions. ### Code This code gets stuck on Linux, but finishes on Windows: using System; using System.IO; using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; namespace WebSocketCloseTest { public class Program { public static async Task Main(string[] args) { var serverTask = Server("http://localhost:45000/test/ws/"); await Client("ws://localhost:45000/test/ws/"); await serverTask; } private static async Task Server(String serverAddress) { var listener = new HttpListener(); listener.Prefixes.Add(serverAddress); listener.Start(); HttpListenerContext ctx = await listener.GetContextAsync(); HttpListenerWebSocketContext wsCtx = await ctx.AcceptWebSocketAsync("subProtocol"); using (var webSocket = wsCtx.WebSocket) { webSocket.Abort(); ctx.Response.Abort(); } } private static async Task Client(String serverAddress) { try { using (ClientWebSocket webSocket = new ClientWebSocket()) { webSocket.Options.AddSubProtocol("subProtocol"); await webSocket.ConnectAsync(new Uri(serverAddress), CancellationToken.None).ConfigureAwait(false); await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); } } catch (WebSocketException e) { Console.WriteLine($"Client WS exception: {e.Message}"); } catch (IOException e) { Console.WriteLine($"Client WS exception: {e.Message}"); } } } }
Author: iconics-milan
Assignees: -
Labels: `area-System.Net`, `untriaged`
Milestone: -
iconics-milan commented 3 years ago

After experimenting more, I found that my code was missing a ctx.Response.Close() when done with the WebSocket (ctx is the HttpListenerContext). With that call in place, the TCP connection gets torn down abruptly (as expected) when no CloseAsync() is called before. I guess there is no reason not to call Close() on WebSocket responses. This still means that Abort() does nothing, but it isn't an issue for me - I am simply not calling it anymore.

karelz commented 3 years ago

Triage: Windows works because it is implemented on top of http.sys Looks like something we might want to address in the managed implementation.

It might be as simple as make NetworkStream to own the Socket. We need to validate if HttpListener will be ok with that. We would appreciate help in the investigation here ...