doghappy / socket.io-client-csharp

socket.io-client implemention for .NET
MIT License
721 stars 124 forks source link

ConnectAsync stuck in .NET 4.x framework when ExtraHeaders in SocketIOOptions are used #300

Closed useraccessdenied closed 1 year ago

useraccessdenied commented 1 year ago

Hi @doghappy,

I found an issue with this library when used with .NET framework 4.x series (tested on 4.6 - 4.8). The ConnectAsync function seems to be stuck in an infinite loop when I pass ExtraHeaders to SocketIO constructor options. The client does not even seem to initiate handshake to the server. Without ExtraHeaders in SocketIOOptions, the ConnectAsync function establishes a connection to the server as expected. None of the SocketIO events are triggered on client and server side when supplying ExtraHeaders. I am unable to debug where the code is stuck as debugger does not seem to provide any useful information.

This issue seems to be affected in .NET Framework 4.x series only. The below code works without any change in .net6.0 and others.

Here is my client code

// C#
using System;
using SocketIOClient;

var socketIOOptions = new SocketIOOptions
{
    //EIO = 4,
    Auth = new Dictionary<string, string> { { "user", "test" }, { "token", "test" }, },
    //ConnectionTimeout = TimeSpan.FromSeconds(10),
    Reconnection = true,
    ReconnectionAttempts = 5,
    Transport = SocketIOClient.Transport.TransportProtocol.WebSocket,
    ExtraHeaders = new Dictionary<string, string> { { "User-Agent", "dotnet-socketio[client]/socket" } }
};

var io = new SocketIO("http://localhost:8000", socketIOOptions);

io.On("seq-num", e => Console.WriteLine(e));
io.OnConnected += async (s, e) => Console.WriteLine("Connected!");
io.OnDisconnected += async (s, e) => Console.WriteLine("Disconnected!");
io.OnError += async (s, e) => Console.WriteLine("Error!");
io.OnPing += async (s, e) => Console.WriteLine("Ping!");
io.OnPong += async (s, e) => Console.WriteLine("Pong!");
io.OnReconnectAttempt += async (s, e) => Console.WriteLine("Reconnecting!");
io.OnReconnected += async (s, e) => Console.WriteLine("Reconnected!");
io.OnReconnectError += async (s, e) => Console.WriteLine("Reconnect Error!");
io.OnReconnectFailed += async (s, e) => Console.WriteLine("Reconnect Failed!");

await io.ConnectAsync();

await Task.Delay(100000);

And here is the server code that I used to test it.

// JS
const
    {Server} = require("socket.io"),
    server = new Server(8000);

let
    sequenceNumberByClient = new Map();

// event fired every time a new client connects:
server.use( (socket, next) => {
  console.log(socket.handshake);
  if (socket.handshake.auth && socket.handshake.auth.user && socket.handshake.auth.user == "test") {
    next();
  } else {
    next(new Error("Invalid User!"));
  }
})
.on("connection", (socket) => {
    console.info(`Client connected [id=${socket.id}]`);
    // initialize this client's sequence number
    sequenceNumberByClient.set(socket, 1);

    // when socket disconnects, remove it from the list:
    socket.on("disconnect", () => {
        sequenceNumberByClient.delete(socket);
        console.info(`Client gone [id=${socket.id}]`);
    });
});

// sends each client its current sequence number
setInterval(() => {
    for (const [client, sequenceNumber] of sequenceNumberByClient.entries()) {
        client.emit("seq-num", sequenceNumber);
        sequenceNumberByClient.set(client, sequenceNumber + 1);
    }
}, 3000);

My project file

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net462</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
      <LangVersion>10.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="SocketIOClient" Version="3.0.6" />
  </ItemGroup>

</Project>
useraccessdenied commented 1 year ago

I think I have pinpointed the issue.

The issue is with this line. ExtraHeaders = new Dictionary<string, string> { { "User-Agent", "dotnet-socketio[client]/socket" } } and specifically with the key User-Agent.

For whatever reasons, either a bug in dotnet framework 4.x or something other, my program loops infinitely on header key User-Agent. It works flawlessly on UserAgent, User_Agent and User--Agent. There seems to be some kind of parsing issue within dotnet framework and User-Agent key is clashing with something in there.

doghappy commented 1 year ago

Reason: Some common headers are considered restricted and are either exposed directly by the API (such as User-Agent) or protected by the system and cannot be changed.

here is a good solution: https://stackoverflow.com/a/15976436/7771913

I will fix the issue

doghappy commented 1 year ago

failed to hack AddWithoutValidate via reflection, ConnectAsync will throw same exception.

Looks like this is a bug in .NET Framework https://github.com/dotnet/corefx/issues/26627#issuecomment-391472613

useraccessdenied commented 1 year ago

How about this solution? (StackOverflow.com) Modify HeaderInfoTable with reflection

Here, read-only IsRequestRestricted is modified using reflection to false and then it won't throw error on setting property as usual. This will work for both ClientWebSocket and HTTPWebRequest.

doghappy commented 1 year ago

Yeah, I am tested, it works

useraccessdenied commented 1 year ago

Good to hear!

doghappy commented 1 year ago

fixed

useraccessdenied commented 1 year ago

Thanks for the effort!

One more thing. Issue #290 seems to be the same as this. The user also sets User-Agent there and he might not know it was due to .net framework 4.x.