feat: port session 08 — Client Connection & PROXY Protocol
- ClientConnection: full connection lifecycle, string/identity helpers, SplitSubjectQueue, KindString, MsgParts, SetHeader, message header manipulation (GenHeader, RemoveHeader, SliceHeader, GetHeader) - ClientTypes: ClientConnectionType, ClientProtocol, ClientFlags, ReadCacheFlags, ClosedState, PmrFlags, DenyType, ClientOptions, ClientInfo, NbPool, RouteTarget, ClientKindHelpers - NatsMessageHeaders: complete header utility class (GenHeader, RemoveHeaderIfPrefixPresent, RemoveHeaderIfPresent, SliceHeader, GetHeader, SetHeader, GetHeaderKeyIndex) - ProxyProtocol: PROXY protocol v1/v2 parser (ReadV1Header, ParseV2Header, ReadProxyProtoHeader sync entry point) - ServerErrors: add ErrAuthorization sentinel - Tests: 32 standalone unit tests (proxy protocol: IDs 159-168, 171-178, 180-181; client: IDs 200-201, 247-256) - DB: 195 features → complete (387-581); 32 tests → complete; 81 server-dependent tests → n/a Features: 667 complete, 274 unit tests complete (17.2% overall)
This commit is contained in:
320
dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ClientTests.cs
Normal file
320
dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ClientTests.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
// Copyright 2012-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from server/client_test.go in the NATS server Go source.
|
||||
|
||||
using System.Text;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
using ZB.MOM.NatsNet.Server.Protocol;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Standalone unit tests for <see cref="ClientConnection"/> helper functions.
|
||||
/// Adapted from server/client_test.go.
|
||||
/// </summary>
|
||||
public sealed class ClientTests
|
||||
{
|
||||
// =========================================================================
|
||||
// TestSplitSubjectQueue — Test ID 200
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("foo", "foo", null, false)]
|
||||
[InlineData("foo bar", "foo", "bar", false)]
|
||||
[InlineData(" foo bar ", "foo", "bar", false)]
|
||||
[InlineData("foo bar", "foo", "bar", false)]
|
||||
[InlineData("foo bar fizz", null, null, true)]
|
||||
public void SplitSubjectQueue_TableDriven(
|
||||
string sq, string? wantSubject, string? wantQueue, bool wantErr)
|
||||
{
|
||||
if (wantErr)
|
||||
{
|
||||
Should.Throw<Exception>(() => ClientConnection.SplitSubjectQueue(sq));
|
||||
}
|
||||
else
|
||||
{
|
||||
var (subject, queue) = ClientConnection.SplitSubjectQueue(sq);
|
||||
subject.ShouldBe(wantSubject is null ? null : Encoding.ASCII.GetBytes(wantSubject));
|
||||
queue.ShouldBe(wantQueue is null ? null : Encoding.ASCII.GetBytes(wantQueue));
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestTypeString — Test ID 201
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(ClientKind.Client, "Client")]
|
||||
[InlineData(ClientKind.Router, "Router")]
|
||||
[InlineData(ClientKind.Gateway, "Gateway")]
|
||||
[InlineData(ClientKind.Leaf, "Leafnode")]
|
||||
[InlineData(ClientKind.JetStream, "JetStream")]
|
||||
[InlineData(ClientKind.Account, "Account")]
|
||||
[InlineData(ClientKind.System, "System")]
|
||||
[InlineData((ClientKind)(-1), "Unknown Type")]
|
||||
public void KindString_ReturnsExpectedString(ClientKind kind, string expected)
|
||||
{
|
||||
var c = new ClientConnection(kind);
|
||||
c.KindString().ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standalone unit tests for <see cref="NatsMessageHeaders"/> functions.
|
||||
/// Adapted from server/client_test.go (header utility tests).
|
||||
/// </summary>
|
||||
public sealed class NatsMessageHeadersTests
|
||||
{
|
||||
// =========================================================================
|
||||
// TestRemoveHeaderIfPrefixPresent — Test ID 247
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void RemoveHeaderIfPrefixPresent_RemovesMatchingHeaders()
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "a", "1");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedStream, "my-stream");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSeq, "22");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "b", "2");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastMsgId, "1");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "c", "3");
|
||||
|
||||
hdr = NatsMessageHeaders.RemoveHeaderIfPrefixPresent(hdr!, "Nats-Expected-");
|
||||
|
||||
var expected = Encoding.ASCII.GetBytes("NATS/1.0\r\na: 1\r\nb: 2\r\nc: 3\r\n\r\n");
|
||||
hdr.ShouldBe(expected);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestSliceHeader — Test ID 248
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void SliceHeader_ReturnsCorrectSlice()
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "a", "1");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedStream, "my-stream");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSeq, "22");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "b", "2");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastMsgId, "1");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "c", "3");
|
||||
|
||||
var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
|
||||
var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
|
||||
|
||||
sliced.ShouldNotBeNull();
|
||||
sliced!.Value.Length.ShouldBe(2); // "24" is 2 bytes
|
||||
copied.ShouldNotBeNull();
|
||||
sliced.Value.ToArray().ShouldBe(copied!);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestSliceHeaderOrderingPrefix — Test ID 249
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void SliceHeader_OrderingPrefix_LongerHeaderDoesNotPreemptShorter()
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
|
||||
|
||||
var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
|
||||
var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
|
||||
|
||||
sliced.ShouldNotBeNull();
|
||||
sliced!.Value.Length.ShouldBe(2);
|
||||
copied.ShouldNotBeNull();
|
||||
sliced.Value.ToArray().ShouldBe(copied!);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestSliceHeaderOrderingSuffix — Test ID 250
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void SliceHeader_OrderingSuffix_LongerHeaderDoesNotPreemptShorter()
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");
|
||||
|
||||
var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsMsgId, hdr!);
|
||||
var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsMsgId, hdr!);
|
||||
|
||||
sliced.ShouldNotBeNull();
|
||||
copied.ShouldNotBeNull();
|
||||
sliced!.Value.ToArray().ShouldBe(copied!);
|
||||
Encoding.ASCII.GetString(copied!).ShouldBe("control");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestRemoveHeaderIfPresentOrderingPrefix — Test ID 251
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void RemoveHeaderIfPresent_OrderingPrefix_OnlyRemovesExactKey()
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
|
||||
|
||||
hdr = NatsMessageHeaders.RemoveHeaderIfPresent(hdr!, NatsHeaderConstants.JsExpectedLastSubjSeq);
|
||||
var expected = NatsMessageHeaders.GenHeader(null, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
|
||||
hdr!.ShouldBe(expected);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestRemoveHeaderIfPresentOrderingSuffix — Test ID 252
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void RemoveHeaderIfPresent_OrderingSuffix_OnlyRemovesExactKey()
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");
|
||||
|
||||
hdr = NatsMessageHeaders.RemoveHeaderIfPresent(hdr!, NatsHeaderConstants.JsMsgId);
|
||||
var expected = NatsMessageHeaders.GenHeader(null, "Previous-Nats-Msg-Id", "user");
|
||||
hdr!.ShouldBe(expected);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestMsgPartsCapsHdrSlice — Test ID 253
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void MsgParts_HeaderSliceIsIsolatedCopy()
|
||||
{
|
||||
const string hdrContent = NatsHeaderConstants.HdrLine + "Key1: Val1\r\nKey2: Val2\r\n\r\n";
|
||||
const string msgBody = "hello\r\n";
|
||||
var buf = Encoding.ASCII.GetBytes(hdrContent + msgBody);
|
||||
|
||||
var c = new ClientConnection(ClientKind.Client);
|
||||
c.ParseCtx.Pa.HeaderSize = hdrContent.Length;
|
||||
|
||||
var (hdr, msg) = c.MsgParts(buf);
|
||||
|
||||
// Header and body should have correct content.
|
||||
Encoding.ASCII.GetString(hdr).ShouldBe(hdrContent);
|
||||
Encoding.ASCII.GetString(msg).ShouldBe(msgBody);
|
||||
|
||||
// hdr should be shorter than buf (cap(hdr) < cap(buf) in Go).
|
||||
hdr.Length.ShouldBeLessThan(buf.Length);
|
||||
|
||||
// Appending to hdr should not affect msg.
|
||||
var extended = hdr.Concat(Encoding.ASCII.GetBytes("test")).ToArray();
|
||||
Encoding.ASCII.GetString(extended).ShouldBe(hdrContent + "test");
|
||||
Encoding.ASCII.GetString(msg).ShouldBe("hello\r\n");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestSetHeaderDoesNotOverwriteUnderlyingBuffer — Test ID 254
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("Key1", "Val1Updated", "NATS/1.0\r\nKey1: Val1Updated\r\nKey2: Val2\r\n\r\n", true)]
|
||||
[InlineData("Key1", "v1", "NATS/1.0\r\nKey1: v1\r\nKey2: Val2\r\n\r\n", false)]
|
||||
[InlineData("Key3", "Val3", "NATS/1.0\r\nKey1: Val1\r\nKey2: Val2\r\nKey3: Val3\r\n\r\n", true)]
|
||||
public void SetHeader_DoesNotOverwriteUnderlyingBuffer(
|
||||
string key, string val, string expectedHdr, bool isNewBuf)
|
||||
{
|
||||
const string initialHdr = "NATS/1.0\r\nKey1: Val1\r\nKey2: Val2\r\n\r\n";
|
||||
const string msgBody = "this is the message body\r\n";
|
||||
|
||||
var buf = new byte[initialHdr.Length + msgBody.Length];
|
||||
Encoding.ASCII.GetBytes(initialHdr).CopyTo(buf, 0);
|
||||
Encoding.ASCII.GetBytes(msgBody).CopyTo(buf, initialHdr.Length);
|
||||
|
||||
var hdrSlice = buf[..initialHdr.Length];
|
||||
var msgSlice = buf[initialHdr.Length..];
|
||||
|
||||
var updatedHdr = NatsMessageHeaders.SetHeader(key, val, hdrSlice);
|
||||
|
||||
Encoding.ASCII.GetString(updatedHdr).ShouldBe(expectedHdr);
|
||||
Encoding.ASCII.GetString(msgSlice).ShouldBe(msgBody);
|
||||
|
||||
if (isNewBuf)
|
||||
{
|
||||
// New allocation: original buf's header portion must be unchanged.
|
||||
Encoding.ASCII.GetString(buf, 0, initialHdr.Length).ShouldBe(initialHdr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In-place update: C# array slices are copies (not views like Go), so buf
|
||||
// is unchanged. However, hdrSlice (the array passed to SetHeader) IS
|
||||
// modified in place via Buffer.BlockCopy.
|
||||
Encoding.ASCII.GetString(hdrSlice, 0, expectedHdr.Length).ShouldBe(expectedHdr);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestSetHeaderOrderingPrefix — Test ID 255
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void SetHeader_OrderingPrefix_LongerHeaderDoesNotPreemptShorter(bool withSpaces)
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
|
||||
if (!withSpaces)
|
||||
hdr = hdr!.Where(b => b != (byte)' ').ToArray();
|
||||
|
||||
hdr = NatsMessageHeaders.SetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, "12", hdr!);
|
||||
|
||||
byte[]? expected = null;
|
||||
expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
|
||||
expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsExpectedLastSubjSeq, "12");
|
||||
if (!withSpaces)
|
||||
expected = expected!.Where(b => b != (byte)' ').ToArray();
|
||||
|
||||
hdr!.ShouldBe(expected!);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TestSetHeaderOrderingSuffix — Test ID 256
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void SetHeader_OrderingSuffix_LongerHeaderDoesNotPreemptShorter(bool withSpaces)
|
||||
{
|
||||
byte[]? hdr = null;
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
|
||||
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");
|
||||
if (!withSpaces)
|
||||
hdr = hdr!.Where(b => b != (byte)' ').ToArray();
|
||||
|
||||
hdr = NatsMessageHeaders.SetHeader(NatsHeaderConstants.JsMsgId, "other", hdr!);
|
||||
|
||||
byte[]? expected = null;
|
||||
expected = NatsMessageHeaders.GenHeader(expected, "Previous-Nats-Msg-Id", "user");
|
||||
expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsMsgId, "other");
|
||||
if (!withSpaces)
|
||||
expected = expected!.Where(b => b != (byte)' ').ToArray();
|
||||
|
||||
hdr!.ShouldBe(expected!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
// Copyright 2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from server/client_proxyproto_test.go in the NATS server Go source.
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.NatsNet.Server.Protocol;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="ProxyProtocolParser"/>, <see cref="ProxyProtocolAddress"/>,
|
||||
/// and <see cref="ProxyProtocolConnection"/>.
|
||||
/// Adapted from server/client_proxyproto_test.go.
|
||||
/// </summary>
|
||||
[Collection("ProxyProtocol")]
|
||||
public sealed class ProxyProtocolTests
|
||||
{
|
||||
// =========================================================================
|
||||
// Test helpers — mirrors Go helper functions
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Builds a valid PROXY protocol v2 binary header.
|
||||
/// Mirrors Go <c>buildProxyV2Header</c>.
|
||||
/// </summary>
|
||||
private static byte[] BuildProxyV2Header(
|
||||
string srcIP, string dstIP, ushort srcPort, ushort dstPort, byte family)
|
||||
{
|
||||
using var buf = new MemoryStream();
|
||||
|
||||
// 12-byte signature
|
||||
const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
|
||||
foreach (char c in v2Sig)
|
||||
buf.WriteByte((byte)c);
|
||||
|
||||
// ver/cmd: version 2 (0x20) | PROXY command (0x01)
|
||||
buf.WriteByte(0x21); // proxyProtoV2Ver | proxyProtoCmdProxy
|
||||
|
||||
// fam/proto
|
||||
buf.WriteByte((byte)(family | 0x01)); // family | ProtoStream
|
||||
|
||||
var src = IPAddress.Parse(srcIP);
|
||||
var dst = IPAddress.Parse(dstIP);
|
||||
byte[] addrData;
|
||||
|
||||
if (family == 0x10) // FamilyInet
|
||||
{
|
||||
addrData = new byte[12]; // 4+4+2+2
|
||||
src.GetAddressBytes().CopyTo(addrData, 0);
|
||||
dst.GetAddressBytes().CopyTo(addrData, 4);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(8, 2), srcPort);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(10, 2), dstPort);
|
||||
}
|
||||
else if (family == 0x20) // FamilyInet6
|
||||
{
|
||||
addrData = new byte[36]; // 16+16+2+2
|
||||
src.GetAddressBytes().CopyTo(addrData, 0);
|
||||
dst.GetAddressBytes().CopyTo(addrData, 16);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(32, 2), srcPort);
|
||||
BinaryPrimitives.WriteUInt16BigEndian(addrData.AsSpan(34, 2), dstPort);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"unsupported family: {family}");
|
||||
}
|
||||
|
||||
// addr-len (big-endian 2 bytes)
|
||||
var lenBytes = new byte[2];
|
||||
BinaryPrimitives.WriteUInt16BigEndian(lenBytes, (ushort)addrData.Length);
|
||||
buf.Write(lenBytes);
|
||||
buf.Write(addrData);
|
||||
|
||||
return buf.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a PROXY protocol v2 LOCAL command header.
|
||||
/// Mirrors Go <c>buildProxyV2LocalHeader</c>.
|
||||
/// </summary>
|
||||
private static byte[] BuildProxyV2LocalHeader()
|
||||
{
|
||||
using var buf = new MemoryStream();
|
||||
const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
|
||||
foreach (char c in v2Sig)
|
||||
buf.WriteByte((byte)c);
|
||||
buf.WriteByte(0x20); // proxyProtoV2Ver | proxyProtoCmdLocal
|
||||
buf.WriteByte(0x00); // FamilyUnspec | ProtoUnspec
|
||||
buf.WriteByte(0);
|
||||
buf.WriteByte(0);
|
||||
return buf.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a PROXY protocol v1 text header.
|
||||
/// Mirrors Go <c>buildProxyV1Header</c>.
|
||||
/// </summary>
|
||||
private static byte[] BuildProxyV1Header(
|
||||
string protocol, string srcIP, string dstIP, ushort srcPort, ushort dstPort)
|
||||
{
|
||||
string line;
|
||||
if (protocol == "UNKNOWN")
|
||||
line = "PROXY UNKNOWN\r\n";
|
||||
else
|
||||
line = $"PROXY {protocol} {srcIP} {dstIP} {srcPort} {dstPort}\r\n";
|
||||
|
||||
return System.Text.Encoding.ASCII.GetBytes(line);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PROXY Protocol v2 Parse Tests
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>Test ID 159 — TestClientProxyProtoV2ParseIPv4</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_ParseIPv4_ReturnsCorrectAddresses()
|
||||
{
|
||||
var header = BuildProxyV2Header("192.168.1.50", "10.0.0.1", 12345, 4222, 0x10);
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
var addr = ProxyProtocolParser.ReadProxyProtoV2Header(stream);
|
||||
|
||||
addr.ShouldNotBeNull();
|
||||
addr!.SrcIp.ToString().ShouldBe("192.168.1.50");
|
||||
addr.SrcPort.ShouldBe((ushort)12345);
|
||||
addr.DstIp.ToString().ShouldBe("10.0.0.1");
|
||||
addr.DstPort.ShouldBe((ushort)4222);
|
||||
addr.String().ShouldBe("192.168.1.50:12345");
|
||||
addr.Network().ShouldBe("tcp4");
|
||||
}
|
||||
|
||||
/// <summary>Test ID 160 — TestClientProxyProtoV2ParseIPv6</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_ParseIPv6_ReturnsCorrectAddresses()
|
||||
{
|
||||
var header = BuildProxyV2Header("2001:db8::1", "2001:db8::2", 54321, 4222, 0x20);
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
var addr = ProxyProtocolParser.ReadProxyProtoV2Header(stream);
|
||||
|
||||
addr.ShouldNotBeNull();
|
||||
addr!.SrcIp.ToString().ShouldBe("2001:db8::1");
|
||||
addr.SrcPort.ShouldBe((ushort)54321);
|
||||
addr.DstIp.ToString().ShouldBe("2001:db8::2");
|
||||
addr.DstPort.ShouldBe((ushort)4222);
|
||||
addr.String().ShouldBe("[2001:db8::1]:54321");
|
||||
addr.Network().ShouldBe("tcp6");
|
||||
}
|
||||
|
||||
/// <summary>Test ID 161 — TestClientProxyProtoV2ParseLocalCommand</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_LocalCommand_ReturnsNull()
|
||||
{
|
||||
var header = BuildProxyV2LocalHeader();
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
var addr = ProxyProtocolParser.ReadProxyProtoV2Header(stream);
|
||||
|
||||
addr.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Test ID 162 — TestClientProxyProtoV2InvalidSignature</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_InvalidSignature_ThrowsInvalidData()
|
||||
{
|
||||
var header = new byte[16];
|
||||
System.Text.Encoding.ASCII.GetBytes("INVALID_SIG_").CopyTo(header, 0);
|
||||
header[12] = 0x20; header[13] = 0x11; header[14] = 0x00; header[15] = 0x0C;
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoV2Header(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 163 — TestClientProxyProtoV2InvalidVersion</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_InvalidVersion_ThrowsInvalidData()
|
||||
{
|
||||
using var buf = new MemoryStream();
|
||||
const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
|
||||
foreach (char c in v2Sig)
|
||||
buf.WriteByte((byte)c);
|
||||
buf.WriteByte(0x10 | 0x01); // version 1 instead of 2 — invalid
|
||||
buf.WriteByte(0x10 | 0x01); // FamilyInet | ProtoStream
|
||||
buf.WriteByte(0); buf.WriteByte(0);
|
||||
|
||||
using var stream = new MemoryStream(buf.ToArray());
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoV2Header(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 164 — TestClientProxyProtoV2UnsupportedFamily</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_UnixSocketFamily_ThrowsUnsupported()
|
||||
{
|
||||
using var buf = new MemoryStream();
|
||||
const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
|
||||
foreach (char c in v2Sig)
|
||||
buf.WriteByte((byte)c);
|
||||
buf.WriteByte(0x21); // v2 ver | proxy cmd
|
||||
buf.WriteByte(0x30 | 0x01); // FamilyUnix | ProtoStream
|
||||
buf.WriteByte(0); buf.WriteByte(0);
|
||||
|
||||
using var stream = new MemoryStream(buf.ToArray());
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoV2Header(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 165 — TestClientProxyProtoV2UnsupportedProtocol</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_UdpProtocol_ThrowsUnsupported()
|
||||
{
|
||||
using var buf = new MemoryStream();
|
||||
const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
|
||||
foreach (char c in v2Sig)
|
||||
buf.WriteByte((byte)c);
|
||||
buf.WriteByte(0x21); // v2 ver | proxy cmd
|
||||
buf.WriteByte(0x10 | 0x02); // FamilyInet | ProtoDatagram (UDP)
|
||||
buf.WriteByte(0); buf.WriteByte(12); // addr len = 12
|
||||
|
||||
using var stream = new MemoryStream(buf.ToArray());
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoV2Header(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 166 — TestClientProxyProtoV2TruncatedHeader</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_TruncatedHeader_ThrowsIOException()
|
||||
{
|
||||
var fullHeader = BuildProxyV2Header("192.168.1.50", "10.0.0.1", 12345, 4222, 0x10);
|
||||
// Only provide first 10 bytes — header is 16 bytes minimum
|
||||
using var stream = new MemoryStream(fullHeader[..10]);
|
||||
|
||||
Should.Throw<IOException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoV2Header(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 167 — TestClientProxyProtoV2ShortAddressData</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV2_ShortAddressData_ThrowsIOException()
|
||||
{
|
||||
using var buf = new MemoryStream();
|
||||
const string v2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
|
||||
foreach (char c in v2Sig)
|
||||
buf.WriteByte((byte)c);
|
||||
buf.WriteByte(0x21); // v2 ver | proxy cmd
|
||||
buf.WriteByte(0x10 | 0x01); // FamilyInet | ProtoStream
|
||||
buf.WriteByte(0); buf.WriteByte(12); // addr len = 12 but only 5 bytes follow
|
||||
buf.Write(new byte[] { 1, 2, 3, 4, 5 }); // only 5 bytes
|
||||
|
||||
using var stream = new MemoryStream(buf.ToArray());
|
||||
Should.Throw<IOException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoV2Header(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 168 — TestProxyConnRemoteAddr</summary>
|
||||
[Fact]
|
||||
public void ProxyConn_RemoteAddr_ReturnsProxiedAddress()
|
||||
{
|
||||
var proxyAddr = new ProxyProtocolAddress(
|
||||
IPAddress.Parse("10.0.0.50"), 12345,
|
||||
IPAddress.Parse("10.0.0.1"), 4222);
|
||||
|
||||
using var inner = new MemoryStream();
|
||||
var wrapped = new ProxyProtocolConnection(inner, proxyAddr);
|
||||
|
||||
wrapped.RemoteAddress.String().ShouldBe("10.0.0.50:12345");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PROXY Protocol v1 Parse Tests
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>Test ID 171 — TestClientProxyProtoV1ParseTCP4</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_ParseTCP4_ReturnsCorrectAddresses()
|
||||
{
|
||||
var header = BuildProxyV1Header("TCP4", "192.168.1.50", "10.0.0.1", 12345, 4222);
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
var addr = ProxyProtocolParser.ReadProxyProtoHeader(stream);
|
||||
|
||||
addr.ShouldNotBeNull();
|
||||
addr!.SrcIp.ToString().ShouldBe("192.168.1.50");
|
||||
addr.SrcPort.ShouldBe((ushort)12345);
|
||||
addr.DstIp.ToString().ShouldBe("10.0.0.1");
|
||||
addr.DstPort.ShouldBe((ushort)4222);
|
||||
}
|
||||
|
||||
/// <summary>Test ID 172 — TestClientProxyProtoV1ParseTCP6</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_ParseTCP6_ReturnsCorrectAddresses()
|
||||
{
|
||||
var header = BuildProxyV1Header("TCP6", "2001:db8::1", "2001:db8::2", 54321, 4222);
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
var addr = ProxyProtocolParser.ReadProxyProtoHeader(stream);
|
||||
|
||||
addr.ShouldNotBeNull();
|
||||
addr!.SrcIp.ToString().ShouldBe("2001:db8::1");
|
||||
addr.SrcPort.ShouldBe((ushort)54321);
|
||||
addr.DstIp.ToString().ShouldBe("2001:db8::2");
|
||||
addr.DstPort.ShouldBe((ushort)4222);
|
||||
}
|
||||
|
||||
/// <summary>Test ID 173 — TestClientProxyProtoV1ParseUnknown</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_UnknownProtocol_ReturnsNull()
|
||||
{
|
||||
var header = BuildProxyV1Header("UNKNOWN", "", "", 0, 0);
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
var addr = ProxyProtocolParser.ReadProxyProtoHeader(stream);
|
||||
|
||||
addr.ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Test ID 174 — TestClientProxyProtoV1InvalidFormat</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_MissingFields_ThrowsInvalidData()
|
||||
{
|
||||
var header = System.Text.Encoding.ASCII.GetBytes("PROXY TCP4 192.168.1.1\r\n");
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoHeader(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 175 — TestClientProxyProtoV1LineTooLong</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_LineTooLong_ThrowsInvalidData()
|
||||
{
|
||||
var longIp = new string('1', 120);
|
||||
var line = $"PROXY TCP4 {longIp} 10.0.0.1 12345 443\r\n";
|
||||
var header = System.Text.Encoding.ASCII.GetBytes(line);
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoHeader(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 176 — TestClientProxyProtoV1InvalidIP</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_InvalidIPAddress_ThrowsInvalidData()
|
||||
{
|
||||
var header = System.Text.Encoding.ASCII.GetBytes(
|
||||
"PROXY TCP4 not.an.ip.addr 10.0.0.1 12345 443\r\n");
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoHeader(stream));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 177 — TestClientProxyProtoV1MismatchedProtocol</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_TCP4WithIPv6Address_ThrowsInvalidData()
|
||||
{
|
||||
// TCP4 with IPv6 address
|
||||
var header = BuildProxyV1Header("TCP4", "2001:db8::1", "2001:db8::2", 12345, 443);
|
||||
using var stream = new MemoryStream(header);
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoHeader(stream));
|
||||
|
||||
// TCP6 with IPv4 address
|
||||
var header2 = BuildProxyV1Header("TCP6", "192.168.1.1", "10.0.0.1", 12345, 443);
|
||||
using var stream2 = new MemoryStream(header2);
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoHeader(stream2));
|
||||
}
|
||||
|
||||
/// <summary>Test ID 178 — TestClientProxyProtoV1InvalidPort</summary>
|
||||
[Fact]
|
||||
public void ProxyProtoV1_InvalidPort_ThrowsException()
|
||||
{
|
||||
var header = System.Text.Encoding.ASCII.GetBytes(
|
||||
"PROXY TCP4 192.168.1.1 10.0.0.1 99999 443\r\n");
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
Should.Throw<Exception>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoHeader(stream));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Mixed Protocol Version Tests
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>Test ID 180 — TestClientProxyProtoVersionDetection</summary>
|
||||
[Fact]
|
||||
public void ProxyProto_AutoDetect_HandlesV1AndV2()
|
||||
{
|
||||
// v1 detection
|
||||
var v1Header = BuildProxyV1Header("TCP4", "192.168.1.1", "10.0.0.1", 12345, 443);
|
||||
using var stream1 = new MemoryStream(v1Header);
|
||||
var addr1 = ProxyProtocolParser.ReadProxyProtoHeader(stream1);
|
||||
addr1.ShouldNotBeNull();
|
||||
addr1!.SrcIp.ToString().ShouldBe("192.168.1.1");
|
||||
|
||||
// v2 detection
|
||||
var v2Header = BuildProxyV2Header("192.168.1.2", "10.0.0.1", 54321, 443, 0x10);
|
||||
using var stream2 = new MemoryStream(v2Header);
|
||||
var addr2 = ProxyProtocolParser.ReadProxyProtoHeader(stream2);
|
||||
addr2.ShouldNotBeNull();
|
||||
addr2!.SrcIp.ToString().ShouldBe("192.168.1.2");
|
||||
}
|
||||
|
||||
/// <summary>Test ID 181 — TestClientProxyProtoUnrecognizedVersion</summary>
|
||||
[Fact]
|
||||
public void ProxyProto_UnrecognizedFormat_ThrowsInvalidData()
|
||||
{
|
||||
var header = System.Text.Encoding.ASCII.GetBytes("HELLO WORLD\r\n");
|
||||
using var stream = new MemoryStream(header);
|
||||
|
||||
Should.Throw<InvalidDataException>(() =>
|
||||
ProxyProtocolParser.ReadProxyProtoHeader(stream));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user