diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 80b095d72..6594b5a0f 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -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, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode _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, IValueLinkListNode 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, IValueLinkListNode 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, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode 0) + { + DecodePacket(buffer, ref bytesWritten); + writer.Advance((uint)bytesWritten); + } + NextActivityCheck = Core.TickCount + 90000; } @@ -1090,7 +1108,6 @@ public partial class NetState : IComparable, IValueLinkListNode, IValueLinkListNode, IValueLinkListNode 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 { public static readonly IComparer Instance = new NetStateConnectingComparer(); diff --git a/Projects/Server/Network/Sockets/ISocket.cs b/Projects/Server/Network/Sockets/ISocket.cs new file mode 100644 index 000000000..3ff445283 --- /dev/null +++ b/Projects/Server/Network/Sockets/ISocket.cs @@ -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 buffer); + public int Receive(Span buffer, out bool forceClose); + public void Close(); + } +} diff --git a/Projects/Server/Network/Sockets/TcpSocket.cs b/Projects/Server/Network/Sockets/TcpSocket.cs new file mode 100644 index 000000000..d64dab399 --- /dev/null +++ b/Projects/Server/Network/Sockets/TcpSocket.cs @@ -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 buffer) => Socket.Send(buffer, SocketFlags.None); + + public int Receive(Span buffer, out bool forceClose) + { + int bytesRead = Socket.Receive(buffer, SocketFlags.None); + forceClose = bytesRead == 0; + + return bytesRead; + } + + public void Close() => Socket.Close(); + } +} diff --git a/Projects/Server/Network/Sockets/WebSocket.cs b/Projects/Server/Network/Sockets/WebSocket.cs new file mode 100644 index 000000000..698003c5f --- /dev/null +++ b/Projects/Server/Network/Sockets/WebSocket.cs @@ -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 buffer, out bool forceClose) + { + forceClose = false; + int available = Socket.Available; + + if (available < MinHeaderSize) + { + if (available == 0) + { + forceClose = true; + } + + return 0; + } + + Span header = stackalloc byte[MaxheaderSize]; + int bytesWritten = 0; + + while (available >= MinHeaderSize) + { + int iterationHeaderLength = Math.Min(available, MaxheaderSize); + Span 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 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 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 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 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 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 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 header, int available, Span 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 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 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 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 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 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 mask) + { + mask[..4].CopyTo(this); + } + } + + private enum WriteStatus + { + WaitingForHeader, + WaitingForMask, + WritingPayload, + } + } +} diff --git a/Projects/UOContent/Misc/ServerList.cs b/Projects/UOContent/Misc/ServerList.cs index 3a9c39cf8..eed605754 100644 --- a/Projects/UOContent/Misc/ServerList.cs +++ b/Projects/UOContent/Misc/ServerList.cs @@ -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;