Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Stefano Merotta
9491304b01 Ignore errors when try to close already closed WebSockets 2024-09-30 00:10:50 +02:00
Stefano Merotta
7362062556 Initial WebSocket support development 2024-09-29 23:53:00 +02:00
5 changed files with 490 additions and 14 deletions

View file

@ -19,6 +19,7 @@ using Server.HuePickers;
using Server.Items;
using Server.Logging;
using Server.Menus;
using Server.Network.Sockets;
using System;
using System.Buffers;
using System.Collections.Concurrent;
@ -29,6 +30,9 @@ using System.Net.Sockets;
using System.Network;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Text;
using System.Security.Cryptography;
namespace Server.Network;
@ -41,7 +45,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(NetState));
private static readonly TimeSpan ConnectingSocketIdleLimit = TimeSpan.FromMilliseconds(5000); // 5 seconds
private static readonly TimeSpan ConnectingSocketIdleLimit = TimeSpan.FromMilliseconds(50000); // 5 seconds
private const int RecvPipeSize = 1024 * 64;
private const int SendPipeSize = 1024 * 256;
private const int HuePickerCap = 512;
@ -121,7 +125,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
public NetState(Socket connection)
{
Connection = connection;
Connection = new TcpSocket(connection);
Seeded = false;
HuePickers = [];
Menus = [];
@ -147,7 +151,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
try
{
_pollGroup.Add(connection, _handle);
_pollGroup.Add(Connection.Socket, _handle);
}
catch (Exception ex)
{
@ -216,7 +220,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
public bool Running => _running;
public Socket Connection { get; private set; }
public ISocket Connection { get; private set; }
public bool CompressionEnabled { get; set; }
@ -577,6 +581,11 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
_protocolState = ProtocolState.LoginServer_AwaitingLogin;
}
}
else if (length > 3 && packetId == 0x47)
{
_parserState = ParserState.ProcessingPacket;
_parserState = HandleWebSocket(buffer) ? ParserState.AwaitingNextPacket : ParserState.Error;
}
else if (length >= 4)
{
int newSeed = (packetId << 24) | (packetReader.ReadByte() << 16) | (packetReader.ReadByte() << 8) | packetReader.ReadByte();
@ -651,7 +660,12 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
case ProtocolState.GameServer_AwaitingGameServerLogin:
{
if (packetId != 0x91 && packetId != 0x80)
if (length > 3 && packetId == 0x47)
{
_parserState = ParserState.ProcessingPacket;
_parserState = HandleWebSocket(buffer) ? ParserState.AwaitingNextPacket : ParserState.Error;
}
else if (packetId != 0x91 && packetId != 0x80)
{
HandleError(packetId, packetLength);
return;
@ -832,7 +846,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
try
{
bytesWritten = Connection.Send(buffer, SocketFlags.None);
bytesWritten = Connection.Send(buffer);
}
catch (SocketException ex)
{
@ -875,10 +889,11 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
}
var bytesWritten = 0;
var forceClose = false;
try
{
bytesWritten = Connection.Receive(buffer, SocketFlags.None);
bytesWritten = Connection.Receive(buffer, out forceClose);
}
catch (SocketException ex)
{
@ -895,15 +910,18 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
TraceException(ex);
}
if (bytesWritten <= 0)
if (bytesWritten < 0 || forceClose)
{
Disconnect(string.Empty);
return;
}
DecodePacket(buffer, ref bytesWritten);
writer.Advance((uint)bytesWritten);
if (bytesWritten > 0)
{
DecodePacket(buffer, ref bytesWritten);
writer.Advance((uint)bytesWritten);
}
NextActivityCheck = Core.TickCount + 90000;
}
@ -1090,7 +1108,6 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
}
_running = false;
_disconnectReason = reason;
_disposed.Enqueue(this);
}
@ -1145,7 +1162,7 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
try
{
_pollGroup.Remove(Connection, _handle);
_pollGroup.Remove(Connection.Socket, _handle);
}
catch (Exception ex)
{
@ -1173,6 +1190,42 @@ public partial class NetState : IComparable<NetState>, IValueLinkListNode<NetSta
LogInfo(a != null ? $"Disconnected. [{count} Online] [{a}]" : $"Disconnected. [{count} Online]");
}
private bool HandleWebSocket(Span<byte> buffer)
{
try
{
string s = Encoding.UTF8.GetString(buffer);
if (!s.StartsWith("GET", StringComparison.InvariantCultureIgnoreCase))
{
Disconnect("BOH");
return false;
}
string swk = Regex.Match(s, "Sec-WebSocket-Key: (.*)").Groups[1].Value.Trim();
string swka = swk + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
byte[] swkaSha1 = SHA1.HashData(Encoding.UTF8.GetBytes(swka));
string swkaSha1Base64 = Convert.ToBase64String(swkaSha1);
byte[] response = Encoding.UTF8.GetBytes(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Connection: Upgrade\r\n" +
"Upgrade: websocket\r\n" +
"Sec-WebSocket-Accept: " + swkaSha1Base64 + "\r\n\r\n");
var old = Connection;
Connection = new WebSocket(old.Socket);
old.Send(response);
}
catch
{
Disconnect("BOH2");
return false;
}
return true;
}
private class NetStateConnectingComparer : IComparer<NetState>
{
public static readonly IComparer<NetState> Instance = new NetStateConnectingComparer();

View file

@ -0,0 +1,18 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace Server.Network.Sockets
{
public interface ISocket
{
public Socket Socket { get; }
public bool Connected { get; }
public IPEndPoint? LocalEndPoint { get; }
public IPEndPoint? RemoteEndPoint { get; }
public int Send(ReadOnlySpan<byte> buffer);
public int Receive(Span<byte> buffer, out bool forceClose);
public void Close();
}
}

View file

@ -0,0 +1,31 @@
using System;
using System.Net;
using System.Net.Sockets;
namespace Server.Network.Sockets
{
public sealed class TcpSocket : ISocket
{
public Socket Socket { get; }
public bool Connected => Socket.Connected;
public IPEndPoint? LocalEndPoint => Socket.LocalEndPoint as IPEndPoint;
public IPEndPoint? RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
public TcpSocket(Socket socket)
{
Socket = socket;
}
public int Send(ReadOnlySpan<byte> buffer) => Socket.Send(buffer, SocketFlags.None);
public int Receive(Span<byte> buffer, out bool forceClose)
{
int bytesRead = Socket.Receive(buffer, SocketFlags.None);
forceClose = bytesRead == 0;
return bytesRead;
}
public void Close() => Socket.Close();
}
}

View file

@ -0,0 +1,374 @@
using System;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
namespace Server.Network.Sockets
{
public sealed class WebSocket : ISocket
{
private const int MinHeaderSize = 6; // 2 byte header + 4 byte mask
private const int MaxheaderSize = 14; // 2 byte header + 8 byte long length + 4 byte mask
private WriteStatus status;
private Mask mask;
private int headerLength;
private int remainingPayloadLength;
private int payloadMaskIndex;
public Socket Socket { get; }
public bool Connected => Socket.Connected;
public IPEndPoint LocalEndPoint => Socket.LocalEndPoint as IPEndPoint;
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
public WebSocket(Socket socket)
{
Socket = socket;
}
public int Receive(Span<byte> buffer, out bool forceClose)
{
forceClose = false;
int available = Socket.Available;
if (available < MinHeaderSize)
{
if (available == 0)
{
forceClose = true;
}
return 0;
}
Span<byte> header = stackalloc byte[MaxheaderSize];
int bytesWritten = 0;
while (available >= MinHeaderSize)
{
int iterationHeaderLength = Math.Min(available, MaxheaderSize);
Span<byte> iterationHeader = header[..iterationHeaderLength];
int bytesRead = Socket.Receive(iterationHeader, SocketFlags.Peek);
if (iterationHeaderLength != bytesRead)
{
throw new Exception("WebSocket header length mismatch");
}
int iterationBytesWritten = 0;
OpCode opCode = (OpCode)(header[0] & 0b0000_1111);
bool completed = opCode switch
{
OpCode.BinaryFrame => HandleDataFrame(header, available, buffer, out iterationBytesWritten),
OpCode.ContinueFrame => HandleDataFrame(header, available, buffer, out iterationBytesWritten),
OpCode.Ping => HandlePing(header, available),
OpCode.Pong => HandlePong(header, available),
OpCode.Close => HandleClose(header, available, out forceClose),
_ => throw new NotSupportedException($"WebSocket OpCode {opCode} not supported"),
};
bytesWritten += iterationBytesWritten;
if (!completed)
{
break;
}
available -= headerLength + iterationBytesWritten;
buffer = buffer[iterationBytesWritten..];
status = WriteStatus.WaitingForHeader;
payloadMaskIndex = 0;
}
return bytesWritten;
}
private bool HandlePing(Span<byte> header, int available)
{
// packets from client contains always a mask even for Control OpCodes
int totalLength = (byte)(header[1] & 0b0111_1111) + MinHeaderSize;
if (totalLength > available)
{
return false;
}
Span<byte> payload = stackalloc byte[totalLength];
int bytesRead = Socket.Receive(payload);
if (bytesRead != totalLength)
{
throw new Exception("WebSocket ping length mismatch");
}
const byte header1 = 0b1000_0000 | (byte)OpCode.Pong;
Socket.Send([header1, 0, .. payload[MinHeaderSize..]]);
return true;
}
private bool HandleClose(Span<byte> header, int available, out bool forceClose)
{
forceClose = true;
// packets from client contains always a mask even for Control OpCodes
int totalLength = (byte)(header[1] & 0b0111_1111) + MinHeaderSize;
if (totalLength > available)
{
return false;
}
Span<byte> payload = stackalloc byte[totalLength];
int bytesRead = Socket.Receive(payload);
if (bytesRead != totalLength)
{
throw new Exception("WebSocket close length mismatch");
}
const byte header1 = 0b1000_0000 | (byte)OpCode.Close;
Socket.Send([header1, 0, .. payload[MinHeaderSize..]]);
return true;
}
private bool HandlePong(Span<byte> header, int available)
{
// packets from client contains always a mask even for Control OpCodes
int totalLength = (byte)(header[1] & 0b0111_1111) + MinHeaderSize;
if (totalLength > available)
{
return false;
}
Span<byte> payload = stackalloc byte[totalLength];
int bytesRead = Socket.Receive(payload);
if (bytesRead != totalLength)
{
throw new Exception("WebSocket pong length mismatch");
}
return true;
}
private bool HandleDataFrame(Span<byte> header, int available, Span<byte> buffer, out int bytesWritten)
{
bytesWritten = 0;
if (status == WriteStatus.WaitingForHeader)
{
if (!ReadDataHeader(header))
{
return false;
}
}
if (status == WriteStatus.WaitingForMask)
{
if (!ReadMask(header))
{
return false;
}
}
if (status == WriteStatus.WritingPayload)
{
return WritePayload(buffer, available, out bytesWritten);
}
return false;
}
private bool ReadDataHeader(Span<byte> header)
{
bool masked = (header[1] & 0b1000_0000) != 0;
int length = (byte)(header[1] & 0b0111_1111);
if (!masked)
{
throw new NotSupportedException("WebSocket packet must be masked");
}
switch (length)
{
case <= 125:
{
headerLength = MinHeaderSize;
break;
}
case 126:
{
const int lengthSize = 2;
if (header.Length < lengthSize + 2)
{
return false;
}
length = BinaryPrimitives.ReadUInt16BigEndian(header[2..4]);
headerLength = lengthSize + MinHeaderSize;
break;
}
default:
{
const int lengthSize = 8;
if (header.Length < lengthSize + 2)
{
return false;
}
length = (int)BinaryPrimitives.ReadUInt64BigEndian(header[2..10]);
headerLength = lengthSize + MinHeaderSize;
break;
}
}
status = WriteStatus.WaitingForMask;
remainingPayloadLength = length;
return true;
}
private bool ReadMask(Span<byte> header)
{
if (header.Length < headerLength)
{
return false;
}
mask = new(header[(headerLength - 4)..]);
status = WriteStatus.WritingPayload;
int bytesRead = Socket.Receive(header[..headerLength]); // advance socket buffer to payload
if (bytesRead != headerLength)
{
throw new Exception("WebSocket header length mismatch");
}
return true;
}
private bool WritePayload(Span<byte> buffer, int available, out int bytesWritten)
{
int length = Math.Min(remainingPayloadLength, Math.Min(buffer.Length, available - headerLength));
if (length == 0)
{
bytesWritten = 0;
return false;
}
buffer = buffer[..length];
int bytesRead = Socket.Receive(buffer);
if (bytesRead != length)
{
throw new Exception("WebSocket payload length mismatch");
}
ApplyMask(buffer);
remainingPayloadLength -= bytesRead;
bytesWritten = bytesRead;
return remainingPayloadLength == 0;
}
private void ApplyMask(Span<byte> buffer)
{
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = (byte)(buffer[i] ^ mask[payloadMaskIndex++]);
if (payloadMaskIndex == 4)
{
payloadMaskIndex = 0;
}
}
}
public int Send(ReadOnlySpan<byte> buffer)
{
const byte header1 = 0b1000_0000 | (byte)OpCode.BinaryFrame;
switch (buffer.Length)
{
case <= 125:
{
Socket.Send([header1, (byte)buffer.Length, .. buffer]);
break;
}
case < ushort.MaxValue:
{
ushort length = (ushort)buffer.Length;
Socket.Send([header1, 126, (byte)(length >> 8), (byte)length, .. buffer]);
break;
}
default:
{
int length = buffer.Length;
Socket.Send([header1, 127, 0, 0, 0, 0, (byte)(length >> 24), (byte)(length >> 16),
(byte)(length >> 8), (byte)length, .. buffer]);
break;
}
}
return buffer.Length;
}
public void Close()
{
try
{
const byte header1 = 0b1000_0000 | (byte)OpCode.Close;
Socket.Send([header1, 0]);
Socket.Close();
}
catch
{
/* ignore already closed sockets */
}
}
private enum OpCode : byte
{
ContinueFrame = 0,
TextFrame = 1,
BinaryFrame = 2,
Close = 8,
Ping = 9,
Pong = 10,
}
[InlineArray(Size)]
private struct Mask
{
public const int Size = 4;
[SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Inline array")]
private byte element0;
public Mask(Span<byte> mask)
{
mask[..4].CopyTo(this);
}
}
private enum WriteStatus
{
WaitingForHeader,
WaitingForMask,
WritingPayload,
}
}
}

View file

@ -70,7 +70,7 @@ namespace Server.Misc
{
var ns = e.State;
var ipep = (IPEndPoint)ns.Connection?.LocalEndPoint;
var ipep = ns.Connection?.LocalEndPoint;
if (ipep == null)
{
return;