diff --git a/src/NATS.Server/WebSocket/WsConstants.cs b/src/NATS.Server/WebSocket/WsConstants.cs new file mode 100644 index 0000000..f0d392d --- /dev/null +++ b/src/NATS.Server/WebSocket/WsConstants.cs @@ -0,0 +1,78 @@ +namespace NATS.Server.WebSocket; + +/// +/// WebSocket protocol constants (RFC 6455). +/// Ported from golang/nats-server/server/websocket.go lines 41-106. +/// +public static class WsConstants +{ + // Opcodes (RFC 6455 Section 5.2) + public const int TextMessage = 1; + public const int BinaryMessage = 2; + public const int CloseMessage = 8; + public const int PingMessage = 9; + public const int PongMessage = 10; + public const int ContinuationFrame = 0; + + // Frame header bits + public const byte FinalBit = 0x80; // 1 << 7 + public const byte Rsv1Bit = 0x40; // 1 << 6 (compression, RFC 7692) + public const byte Rsv2Bit = 0x20; // 1 << 5 + public const byte Rsv3Bit = 0x10; // 1 << 4 + public const byte MaskBit = 0x80; // 1 << 7 (in second byte) + + // Frame size limits + public const int MaxFrameHeaderSize = 14; + public const int MaxControlPayloadSize = 125; + public const int FrameSizeForBrowsers = 4096; + public const int CompressThreshold = 64; + public const int CloseStatusSize = 2; + + // Close status codes (RFC 6455 Section 11.7) + public const int CloseStatusNormalClosure = 1000; + public const int CloseStatusGoingAway = 1001; + public const int CloseStatusProtocolError = 1002; + public const int CloseStatusUnsupportedData = 1003; + public const int CloseStatusNoStatusReceived = 1005; + public const int CloseStatusInvalidPayloadData = 1007; + public const int CloseStatusPolicyViolation = 1008; + public const int CloseStatusMessageTooBig = 1009; + public const int CloseStatusInternalSrvError = 1011; + public const int CloseStatusTlsHandshake = 1015; + + // Compression constants (RFC 7692) + public const string PmcExtension = "permessage-deflate"; + public const string PmcSrvNoCtx = "server_no_context_takeover"; + public const string PmcCliNoCtx = "client_no_context_takeover"; + public static readonly string PmcReqHeaderValue = $"{PmcExtension}; {PmcSrvNoCtx}; {PmcCliNoCtx}"; + public static readonly string PmcFullResponse = $"Sec-WebSocket-Extensions: {PmcExtension}; {PmcSrvNoCtx}; {PmcCliNoCtx}\r\n"; + + // Header names + public const string NoMaskingHeader = "Nats-No-Masking"; + public const string NoMaskingValue = "true"; + public static readonly string NoMaskingFullResponse = $"{NoMaskingHeader}: {NoMaskingValue}\r\n"; + public const string XForwardedForHeader = "X-Forwarded-For"; + + // Path routing + public const string ClientPath = "/"; + public const string LeafNodePath = "/leafnode"; + public const string MqttPath = "/mqtt"; + + // WebSocket GUID (RFC 6455 Section 1.3) + public static readonly byte[] WsGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"u8.ToArray(); + + // Compression trailer (RFC 7692 Section 7.2.2) + public static readonly byte[] CompressLastBlock = [0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff]; + + // Decompression trailer appended before decompressing + public static readonly byte[] DecompressTrailer = [0x00, 0x00, 0xff, 0xff]; + + public static bool IsControlFrame(int opcode) => opcode >= CloseMessage; +} + +public enum WsClientKind +{ + Client, + Leaf, + Mqtt, +} diff --git a/tests/NATS.Server.Tests/WebSocket/WsConstantsTests.cs b/tests/NATS.Server.Tests/WebSocket/WsConstantsTests.cs new file mode 100644 index 0000000..3dd0b33 --- /dev/null +++ b/tests/NATS.Server.Tests/WebSocket/WsConstantsTests.cs @@ -0,0 +1,53 @@ +using NATS.Server.WebSocket; +using Shouldly; + +namespace NATS.Server.Tests.WebSocket; + +public class WsConstantsTests +{ + [Fact] + public void OpCodes_MatchRfc6455() + { + WsConstants.TextMessage.ShouldBe(1); + WsConstants.BinaryMessage.ShouldBe(2); + WsConstants.CloseMessage.ShouldBe(8); + WsConstants.PingMessage.ShouldBe(9); + WsConstants.PongMessage.ShouldBe(10); + } + + [Fact] + public void FrameBits_MatchRfc6455() + { + WsConstants.FinalBit.ShouldBe((byte)0x80); + WsConstants.Rsv1Bit.ShouldBe((byte)0x40); + WsConstants.MaskBit.ShouldBe((byte)0x80); + } + + [Fact] + public void CloseStatusCodes_MatchRfc6455() + { + WsConstants.CloseStatusNormalClosure.ShouldBe(1000); + WsConstants.CloseStatusGoingAway.ShouldBe(1001); + WsConstants.CloseStatusProtocolError.ShouldBe(1002); + WsConstants.CloseStatusPolicyViolation.ShouldBe(1008); + WsConstants.CloseStatusMessageTooBig.ShouldBe(1009); + } + + [Theory] + [InlineData(WsConstants.CloseMessage)] + [InlineData(WsConstants.PingMessage)] + [InlineData(WsConstants.PongMessage)] + public void IsControlFrame_True(int opcode) + { + WsConstants.IsControlFrame(opcode).ShouldBeTrue(); + } + + [Theory] + [InlineData(WsConstants.TextMessage)] + [InlineData(WsConstants.BinaryMessage)] + [InlineData(0)] + public void IsControlFrame_False(int opcode) + { + WsConstants.IsControlFrame(opcode).ShouldBeFalse(); + } +}