mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
272 lines
9.5 KiB
C#
272 lines
9.5 KiB
C#
using Esiur.Data;
|
|
using System;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace Esiur.Net.Packets.WebSocket;
|
|
|
|
public class WebsocketPacket : Packet
|
|
{
|
|
private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
|
|
|
|
public enum WSOpcode : byte
|
|
{
|
|
ContinuationFrame = 0x0,
|
|
TextFrame = 0x1,
|
|
BinaryFrame = 0x2,
|
|
ConnectionClose = 0x8,
|
|
Ping = 0x9,
|
|
Pong = 0xA,
|
|
}
|
|
|
|
public const ulong DefaultMaximumPayloadLength = 8 * 1024 * 1024;
|
|
|
|
public bool FIN;
|
|
public bool RSV1;
|
|
public bool RSV2;
|
|
public bool RSV3;
|
|
public WSOpcode Opcode;
|
|
public bool Mask;
|
|
public long PayloadLength;
|
|
public byte[] MaskKey;
|
|
public byte[] Message;
|
|
|
|
/// <summary>
|
|
/// Maximum accepted or composed payload. Set to zero to disable the limit.
|
|
/// </summary>
|
|
public ulong MaximumPayloadLength { get; set; } = DefaultMaximumPayloadLength;
|
|
|
|
/// <summary>
|
|
/// Expected mask bit for an incoming frame. Servers set this to <c>true</c>,
|
|
/// clients set it to <c>false</c>, and standalone packet parsing can leave it
|
|
/// <c>null</c> to accept either direction.
|
|
/// </summary>
|
|
public bool? ExpectedMask { get; set; }
|
|
|
|
public override string ToString()
|
|
=> $"WebsocketPacket\n\tFIN: {FIN}\n\tOpcode: {Opcode}\n\tPayload: {PayloadLength}" +
|
|
$"\n\tMaskKey: {MaskKey}\n\tMessage: {(Message == null ? "NULL" : Message.Length.ToString())}";
|
|
|
|
public override bool Compose()
|
|
{
|
|
var message = Message ?? Array.Empty<byte>();
|
|
ValidateFrame(Opcode, FIN, (ulong)message.LongLength);
|
|
ValidateApplicationPayload(Opcode, FIN, message);
|
|
EnsureWithinLimit((ulong)message.LongLength);
|
|
|
|
var extendedLengthSize = message.Length <= 125
|
|
? 0
|
|
: message.Length <= ushort.MaxValue ? 2 : 8;
|
|
var headerLength = 2 + extendedLengthSize + (Mask ? 4 : 0);
|
|
Data = new byte[checked(headerLength + message.Length)];
|
|
|
|
var offset = 0;
|
|
Data[offset++] = (byte)((FIN ? 0x80 : 0) |
|
|
(RSV1 ? 0x40 : 0) |
|
|
(RSV2 ? 0x20 : 0) |
|
|
(RSV3 ? 0x10 : 0) |
|
|
(byte)Opcode);
|
|
|
|
if (extendedLengthSize == 0)
|
|
{
|
|
Data[offset++] = (byte)((Mask ? 0x80 : 0) | message.Length);
|
|
}
|
|
else if (extendedLengthSize == 2)
|
|
{
|
|
Data[offset++] = (byte)((Mask ? 0x80 : 0) | 126);
|
|
Data[offset++] = (byte)(message.Length >> 8);
|
|
Data[offset++] = (byte)message.Length;
|
|
}
|
|
else
|
|
{
|
|
Data[offset++] = (byte)((Mask ? 0x80 : 0) | 127);
|
|
var length = (ulong)message.LongLength;
|
|
for (var shift = 56; shift >= 0; shift -= 8)
|
|
Data[offset++] = (byte)(length >> shift);
|
|
}
|
|
|
|
if (Mask)
|
|
{
|
|
if (MaskKey == null || MaskKey.Length != 4)
|
|
{
|
|
MaskKey = new byte[4];
|
|
using (var random = RandomNumberGenerator.Create())
|
|
random.GetBytes(MaskKey);
|
|
}
|
|
|
|
Buffer.BlockCopy(MaskKey, 0, Data, offset, MaskKey.Length);
|
|
offset += MaskKey.Length;
|
|
|
|
for (var i = 0; i < message.Length; i++)
|
|
Data[offset + i] = (byte)(message[i] ^ MaskKey[i & 3]);
|
|
}
|
|
else if (message.Length > 0)
|
|
{
|
|
Buffer.BlockCopy(message, 0, Data, offset, message.Length);
|
|
}
|
|
|
|
PayloadLength = message.LongLength;
|
|
return true;
|
|
}
|
|
|
|
public override long Parse(byte[] data, uint offset, uint ends)
|
|
{
|
|
ValidateBounds(data, offset, ends);
|
|
var originalOffset = offset;
|
|
|
|
if (TryGetMissingBytes(offset, ends, 2, out var incomplete))
|
|
return incomplete;
|
|
|
|
var first = data[offset++];
|
|
var second = data[offset++];
|
|
|
|
FIN = (first & 0x80) != 0;
|
|
RSV1 = (first & 0x40) != 0;
|
|
RSV2 = (first & 0x20) != 0;
|
|
RSV3 = (first & 0x10) != 0;
|
|
Opcode = (WSOpcode)(first & 0x0F);
|
|
Mask = (second & 0x80) != 0;
|
|
|
|
if (ExpectedMask.HasValue && Mask != ExpectedMask.Value)
|
|
throw new InvalidDataException(ExpectedMask.Value
|
|
? "WebSocket clients must mask every frame sent to a server."
|
|
: "WebSocket servers must not mask frames sent to a client.");
|
|
|
|
if (RSV1 || RSV2 || RSV3)
|
|
throw new InvalidDataException("WebSocket extensions are not enabled for this connection.");
|
|
|
|
ulong payloadLength = (byte)(second & 0x7F);
|
|
if (payloadLength == 126)
|
|
{
|
|
if (TryGetMissingBytes(offset, ends, 2, out incomplete))
|
|
return incomplete;
|
|
|
|
payloadLength = (uint)(data[offset] << 8 | data[offset + 1]);
|
|
offset += 2;
|
|
|
|
if (payloadLength < 126)
|
|
throw new InvalidDataException("WebSocket payload length is not minimally encoded.");
|
|
}
|
|
else if (payloadLength == 127)
|
|
{
|
|
if (TryGetMissingBytes(offset, ends, 8, out incomplete))
|
|
return incomplete;
|
|
if ((data[offset] & 0x80) != 0)
|
|
throw new InvalidDataException("WebSocket payload length exceeds the protocol limit.");
|
|
|
|
payloadLength = 0;
|
|
for (var i = 0; i < 8; i++)
|
|
payloadLength = payloadLength << 8 | data[offset++];
|
|
|
|
if (payloadLength <= ushort.MaxValue)
|
|
throw new InvalidDataException("WebSocket payload length is not minimally encoded.");
|
|
}
|
|
|
|
ValidateFrame(Opcode, FIN, payloadLength);
|
|
EnsureWithinLimit(payloadLength);
|
|
if (payloadLength > int.MaxValue)
|
|
throw new ParserLimitException("WebSocket payload cannot fit in a managed byte array.");
|
|
|
|
if (Mask)
|
|
{
|
|
if (TryGetMissingBytes(offset, ends, 4, out incomplete))
|
|
return incomplete;
|
|
|
|
MaskKey = new byte[4];
|
|
Buffer.BlockCopy(data, (int)offset, MaskKey, 0, MaskKey.Length);
|
|
offset += 4;
|
|
}
|
|
else
|
|
{
|
|
MaskKey = null;
|
|
}
|
|
|
|
var availablePayload = ends - offset;
|
|
if ((ulong)availablePayload < payloadLength)
|
|
return -(long)(payloadLength - availablePayload);
|
|
|
|
Message = new byte[(int)payloadLength];
|
|
if (Mask)
|
|
{
|
|
for (var i = 0; i < Message.Length; i++)
|
|
Message[i] = (byte)(data[offset + i] ^ MaskKey[i & 3]);
|
|
}
|
|
else if (Message.Length > 0)
|
|
{
|
|
Buffer.BlockCopy(data, (int)offset, Message, 0, Message.Length);
|
|
}
|
|
|
|
offset += (uint)payloadLength;
|
|
PayloadLength = (long)payloadLength;
|
|
ValidateApplicationPayload(Opcode, FIN, Message);
|
|
return offset - originalOffset;
|
|
}
|
|
|
|
private void EnsureWithinLimit(ulong payloadLength)
|
|
{
|
|
if (MaximumPayloadLength > 0 && payloadLength > MaximumPayloadLength)
|
|
throw new ParserLimitException(
|
|
$"WebSocket payload of {payloadLength} bytes exceeds the {MaximumPayloadLength}-byte limit.");
|
|
}
|
|
|
|
private static void ValidateFrame(WSOpcode opcode, bool final, ulong payloadLength)
|
|
{
|
|
var isControl = opcode == WSOpcode.ConnectionClose ||
|
|
opcode == WSOpcode.Ping ||
|
|
opcode == WSOpcode.Pong;
|
|
var isData = opcode == WSOpcode.ContinuationFrame ||
|
|
opcode == WSOpcode.TextFrame ||
|
|
opcode == WSOpcode.BinaryFrame;
|
|
|
|
if (!isControl && !isData)
|
|
throw new InvalidDataException($"Unsupported WebSocket opcode: 0x{(byte)opcode:X}.");
|
|
if (isControl && (!final || payloadLength > 125))
|
|
throw new InvalidDataException("WebSocket control frames must be final and at most 125 bytes.");
|
|
if (opcode == WSOpcode.ConnectionClose && payloadLength == 1)
|
|
throw new InvalidDataException("A WebSocket close frame cannot contain a one-byte payload.");
|
|
}
|
|
|
|
private static void ValidateApplicationPayload(WSOpcode opcode, bool final, byte[] payload)
|
|
{
|
|
if (opcode == WSOpcode.TextFrame && final)
|
|
ValidateTextPayload(payload);
|
|
else if (opcode == WSOpcode.ConnectionClose)
|
|
ValidateClosePayload(payload);
|
|
}
|
|
|
|
internal static void ValidateTextPayload(byte[] payload)
|
|
=> ValidateTextPayload(payload ?? Array.Empty<byte>(), 0, payload?.Length ?? 0);
|
|
|
|
private static void ValidateTextPayload(byte[] payload, int offset, int count)
|
|
{
|
|
try
|
|
{
|
|
_ = StrictUtf8.GetCharCount(payload, offset, count);
|
|
}
|
|
catch (DecoderFallbackException exception)
|
|
{
|
|
throw new InvalidDataException("WebSocket text payload is not valid UTF-8.", exception);
|
|
}
|
|
}
|
|
|
|
private static void ValidateClosePayload(byte[] payload)
|
|
{
|
|
if (payload == null || payload.Length < 2)
|
|
return;
|
|
|
|
var statusCode = payload[0] << 8 | payload[1];
|
|
var isDefinedProtocolCode = statusCode >= 1000 && statusCode <= 1014
|
|
&& statusCode != 1004
|
|
&& statusCode != 1005
|
|
&& statusCode != 1006;
|
|
var isApplicationCode = statusCode >= 3000 && statusCode <= 4999;
|
|
|
|
if (!isDefinedProtocolCode && !isApplicationCode)
|
|
throw new InvalidDataException($"Invalid WebSocket close status code: {statusCode}.");
|
|
|
|
if (payload.Length > 2)
|
|
ValidateTextPayload(payload, 2, payload.Length - 2);
|
|
}
|
|
}
|