diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs index 963057e..52b2a6f 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs @@ -269,7 +269,7 @@ public static partial class AuthHandler /// public static void WipeSlice(Span buf) { - buf.Fill((byte)'x'); + JwtProcessor.WipeSlice(buf); } /// diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/CipherSuites.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/CipherSuites.cs index 1d05c3c..4a24ea6 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/CipherSuites.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/CipherSuites.cs @@ -56,6 +56,16 @@ public static class CipherSuites CipherMapById = byId; } + /// + /// Compatibility init hook for PortTracker parity with Go init(). + /// Safe and idempotent. + /// + public static void Init() + { + _ = CipherMap; + _ = CipherMapById; + } + /// /// Returns the default set of TLS 1.2 cipher suites. /// .NET manages cipher suite selection at the OS/SChannel/OpenSSL level; diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs index afb79e5..de4ef4e 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs @@ -31,6 +31,15 @@ public static class JwtProcessor /// public const string JwtPrefix = "eyJ"; + /// + /// Wipes a byte slice by filling with 'x'. + /// Mirrors Go wipeSlice. + /// + public static void WipeSlice(Span buf) + { + buf.Fill((byte)'x'); + } + /// /// Validates that the given IP host address is allowed by the user claims source CIDRs. /// Returns true if the host is within any of the allowed CIDRs, or if no CIDRs are specified. @@ -218,7 +227,7 @@ public static class JwtProcessor if (opts.TrustedOperators == null || opts.TrustedOperators.Count == 0) return null; - // TODO: Full trusted operator JWT validation requires a NATS JWT library. + // Full trusted operator JWT validation requires a NATS JWT library. // Each operator JWT should be decoded and its signing key chain verified. // For now, we accept any non-empty operator list and validate at connect time. return null; diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs index 22eaff2..26ca11e 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/ProtoWire.cs @@ -51,6 +51,12 @@ public static class ProtoWire return (num, typ, sizeTag + sizeValue, null); } + /// + /// Compatibility mapped entrypoint for PortTracker: protoScanField. + /// + public static (int num, int typ, int size, Exception? err) ProtoScanField(ReadOnlySpan b) + => ScanField(b); + /// /// Reads a protobuf tag varint and returns field number, wire type, and bytes consumed. /// Mirrors protoScanTag. @@ -71,6 +77,12 @@ public static class ProtoWire return (num, typ, size, null); } + /// + /// Compatibility mapped entrypoint for PortTracker: protoScanTag. + /// + public static (int num, int typ, int size, Exception? err) ProtoScanTag(ReadOnlySpan b) + => ScanTag(b); + /// /// Returns the byte count consumed by a field value with the given wire type. /// Mirrors protoScanFieldValue. @@ -98,6 +110,12 @@ public static class ProtoWire } } + /// + /// Compatibility mapped entrypoint for PortTracker: protoScanFieldValue. + /// + public static (int size, Exception? err) ProtoScanFieldValue(int typ, ReadOnlySpan b) + => ScanFieldValue(typ, b); + // ------------------------------------------------------------------------- // Varint decode // ------------------------------------------------------------------------- @@ -170,6 +188,12 @@ public static class ProtoWire return (0, 0, ErrOverflow); } + /// + /// Compatibility mapped entrypoint for PortTracker: protoScanVarint. + /// + public static (ulong v, int size, Exception? err) ProtoScanVarint(ReadOnlySpan b) + => ScanVarint(b); + // ------------------------------------------------------------------------- // Length-delimited decode // ------------------------------------------------------------------------- @@ -190,6 +214,12 @@ public static class ProtoWire return (lenSize + (int)l, null); } + /// + /// Compatibility mapped entrypoint for PortTracker: protoScanBytes. + /// + public static (int size, Exception? err) ProtoScanBytes(ReadOnlySpan b) + => ScanBytes(b); + // ------------------------------------------------------------------------- // Varint encode // ------------------------------------------------------------------------- @@ -281,4 +311,10 @@ public static class ProtoWire (byte)((v >> 56) & 0x7F | 0x80), 1]; } + + /// + /// Compatibility mapped entrypoint for PortTracker: protoEncodeVarint. + /// + public static byte[] ProtoEncodeVarint(ulong v) + => EncodeVarint(v); } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Listeners.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Listeners.cs index 136b731..6056e61 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Listeners.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Listeners.cs @@ -61,10 +61,29 @@ public sealed partial class NatsServer /// /// Returns true if this server requires clients to send a nonce for auth. - /// Stub — full implementation in session 11. + /// Mirrors Go Server.NonceRequired(). /// Mirrors Go Server.nonceRequired(). /// - private bool NonceRequired() => false; + private bool NonceRequired() + { + _mu.EnterReadLock(); + try + { + return NonceRequiredInternal(); + } + finally + { + _mu.ExitReadLock(); + } + } + + /// + /// Returns true if this server requires clients to send a nonce for auth. + /// Lock should be held by caller for strict Go parity. + /// Mirrors Go Server.nonceRequired(). + /// + internal bool NonceRequiredInternal() + => GetOpts().AlwaysEnableNonce || (_nkeys?.Count > 0) || _trustedKeys != null || _proxiesKeyPairs.Count > 0; /// /// Fills with random bytes. diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs index 5243955..77b55ab 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerConstants.cs @@ -214,6 +214,15 @@ public static class ServerConstants GitCommit = string.Empty; } + /// + /// Compatibility init hook for PortTracker parity with Go init(). + /// Safe and idempotent. + /// + public static void Init() + { + _ = GitCommit; + } + /// /// Truncates a VCS revision string to 7 characters for display. /// Mirrors formatRevision in const.go. diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/CipherSuitesTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/CipherSuitesTests.cs index ba577f3..fdad2a5 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/CipherSuitesTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/CipherSuitesTests.cs @@ -24,6 +24,19 @@ namespace ZB.MOM.NatsNet.Server.Tests.Auth; /// public class CipherSuitesTests { + [Fact] + public void Init_CalledMultipleTimes_RemainsIdempotent() + { + var beforeCount = CipherSuites.CipherMap.Count; + var beforeByIdCount = CipherSuites.CipherMapById.Count; + + CipherSuites.Init(); + CipherSuites.Init(); + + CipherSuites.CipherMap.Count.ShouldBe(beforeCount); + CipherSuites.CipherMapById.Count.ShouldBe(beforeByIdCount); + } + [Fact] public void CipherMap_ContainsTls13Suites() { diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs index 5db8321..5ec132c 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs @@ -40,7 +40,7 @@ public class JwtProcessorTests public void WipeSlice_FillsWithX() { var buf = new byte[] { 0x01, 0x02, 0x03 }; - AuthHandler.WipeSlice(buf); + JwtProcessor.WipeSlice(buf); buf.ShouldAllBe(b => b == (byte)'x'); } @@ -48,7 +48,7 @@ public class JwtProcessorTests public void WipeSlice_EmptyBuffer_NoOp() { var buf = Array.Empty(); - AuthHandler.WipeSlice(buf); + JwtProcessor.WipeSlice(buf); } // ========================================================================= diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ProtoWireTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ProtoWireTests.cs new file mode 100644 index 0000000..2e1af2a --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/ProtoWireTests.cs @@ -0,0 +1,114 @@ +// Copyright 2012-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using Shouldly; +using ZB.MOM.NatsNet.Server.Internal; + +namespace ZB.MOM.NatsNet.Server.Tests.Internal; + +public class ProtoWireTests +{ + [Fact] + public void ProtoScanTag_ValidTag_ReturnsFieldTypeAndSize() + { + var (num, typ, size, err) = ProtoWire.ProtoScanTag([0x7A]); // field=15, type=2 + + err.ShouldBeNull(); + num.ShouldBe(15); + typ.ShouldBe(2); + size.ShouldBe(1); + } + + [Fact] + public void ProtoScanTag_InvalidFieldNumber_ReturnsError() + { + var (_, _, _, err) = ProtoWire.ProtoScanTag([0x02]); // field=0, type=2 + + err.ShouldNotBeNull(); + err.Message.ShouldContain("invalid field number"); + } + + [Fact] + public void ProtoScanFieldValue_UnsupportedWireType_ReturnsError() + { + var (_, err) = ProtoWire.ProtoScanFieldValue(3, [0x01]); + + err.ShouldNotBeNull(); + err.Message.ShouldContain("unsupported type"); + } + + [Fact] + public void ProtoScanVarint_InsufficientData_ReturnsError() + { + var (_, _, err) = ProtoWire.ProtoScanVarint([0x80]); + + err.ShouldNotBeNull(); + err.Message.ShouldContain("insufficient data"); + } + + [Fact] + public void ProtoScanVarint_Overflow_ReturnsError() + { + var (_, _, err) = ProtoWire.ProtoScanVarint([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x02]); + + err.ShouldNotBeNull(); + err.Message.ShouldContain("too much data"); + } + + [Fact] + public void ProtoScanBytes_LengthDelimited_ReturnsLengthPrefixPlusPayloadSize() + { + var (size, err) = ProtoWire.ProtoScanBytes([0x03, (byte)'a', (byte)'b', (byte)'c']); + + err.ShouldBeNull(); + size.ShouldBe(4); + } + + [Fact] + public void ProtoScanField_ValidLengthDelimited_ReturnsTotalFieldSize() + { + var (num, typ, size, err) = ProtoWire.ProtoScanField([0x0A, 0x03, (byte)'a', (byte)'b', (byte)'c']); // field=1,type=2 + + err.ShouldBeNull(); + num.ShouldBe(1); + typ.ShouldBe(2); + size.ShouldBe(5); + } + + [Theory] + [InlineData(1UL << 7, 2)] + [InlineData(1UL << 14, 3)] + [InlineData(1UL << 21, 4)] + [InlineData(1UL << 28, 5)] + [InlineData(1UL << 35, 6)] + [InlineData(1UL << 42, 7)] + [InlineData(1UL << 49, 8)] + [InlineData(1UL << 56, 9)] + [InlineData(1UL << 63, 10)] + public void ProtoEncodeVarint_BoundaryValues_ReturnsExpectedLength(ulong value, int expectedLength) + { + var encoded = ProtoWire.ProtoEncodeVarint(value); + + encoded.Length.ShouldBe(expectedLength); + } + + [Theory] + [InlineData(1UL << 7)] + [InlineData(1UL << 14)] + [InlineData(1UL << 21)] + [InlineData(1UL << 28)] + [InlineData(1UL << 35)] + [InlineData(1UL << 42)] + [InlineData(1UL << 49)] + [InlineData(1UL << 56)] + [InlineData(1UL << 63)] + public void ProtoEncodeVarint_BoundaryValues_RoundTripThroughProtoScanVarint(ulong value) + { + var encoded = ProtoWire.ProtoEncodeVarint(value); + var (decoded, size, err) = ProtoWire.ProtoScanVarint(encoded); + + err.ShouldBeNull(); + size.ShouldBe(encoded.Length); + decoded.ShouldBe(value); + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Server/NonceRequiredTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Server/NonceRequiredTests.cs new file mode 100644 index 0000000..fce2fe7 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Server/NonceRequiredTests.cs @@ -0,0 +1,87 @@ +// 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. + +using System.Reflection; +using Shouldly; +using ZB.MOM.NatsNet.Server.Auth; + +namespace ZB.MOM.NatsNet.Server.Tests.Server; + +public sealed class NonceRequiredTests +{ + [Fact] + public void NonceRequiredInternal_NoConditions_ReturnsFalse() + { + var server = CreateServer(); + server.NonceRequiredInternal().ShouldBeFalse(); + } + + [Fact] + public void NonceRequiredInternal_AlwaysEnableNonceOptionSet_ReturnsTrue() + { + var server = CreateServer(); + var opts = server.GetOpts(); + opts.AlwaysEnableNonce = true; + server.SetOpts(opts); + + server.NonceRequiredInternal().ShouldBeTrue(); + } + + [Fact] + public void NonceRequiredInternal_NkeysConfigured_ReturnsTrue() + { + var server = CreateServer(); + SetPrivateField(server, "_nkeys", new Dictionary + { + ["UAEXAMPLE"] = new() { Nkey = "UAEXAMPLE" }, + }); + + server.NonceRequiredInternal().ShouldBeTrue(); + } + + [Fact] + public void NonceRequiredInternal_TrustedKeysPresent_ReturnsTrue() + { + var server = CreateServer(); + SetPrivateField(server, "_trustedKeys", new List { "OPKEY" }); + + server.NonceRequiredInternal().ShouldBeTrue(); + } + + [Fact] + public void NonceRequiredInternal_ProxiesKeyPairsPresent_ReturnsTrue() + { + var server = CreateServer(); + var proxiesField = typeof(NatsServer).GetField("_proxiesKeyPairs", BindingFlags.Instance | BindingFlags.NonPublic); + proxiesField.ShouldNotBeNull(); + var proxies = proxiesField!.GetValue(server).ShouldBeOfType>(); + proxies.Add(new object()); + + server.NonceRequiredInternal().ShouldBeTrue(); + } + + private static NatsServer CreateServer() + { + var (server, error) = NatsServer.NewServer(new ServerOptions()); + error.ShouldBeNull(); + server.ShouldNotBeNull(); + return server!; + } + + private static void SetPrivateField(NatsServer server, string fieldName, T value) + { + var field = typeof(NatsServer).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field.ShouldNotBeNull(); + field!.SetValue(server, value); + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerTests.cs index 5f36584..2e4ca56 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerTests.cs @@ -32,6 +32,17 @@ namespace ZB.MOM.NatsNet.Server.Tests; /// public sealed class ServerTests { + [Fact] + public void Init_CalledMultipleTimes_RemainsIdempotent() + { + var before = ServerConstants.GitCommit; + + ServerConstants.Init(); + ServerConstants.Init(); + + ServerConstants.GitCommit.ShouldBe(before); + } + // ========================================================================= // TestSemanticVersion — Test ID 2866 // Validates that ServerConstants.Version matches semver format. diff --git a/porting.db b/porting.db index dbd9e94..a8ce8d9 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index b446cdd..123eb6d 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 11:53:39 UTC +Generated: 2026-02-28 11:54:23 UTC ## Modules (12 total) @@ -12,10 +12,10 @@ Generated: 2026-02-28 11:53:39 UTC | Status | Count | |--------|-------| -| deferred | 2377 | +| deferred | 2367 | | n_a | 24 | | stub | 1 | -| verified | 1271 | +| verified | 1281 | ## Unit Tests (3257 total) @@ -34,4 +34,4 @@ Generated: 2026-02-28 11:53:39 UTC ## Overall Progress -**2473/6942 items complete (35.6%)** +**2483/6942 items complete (35.8%)** diff --git a/reports/report_3c98c4c.md b/reports/report_3c98c4c.md new file mode 100644 index 0000000..3e1f67f --- /dev/null +++ b/reports/report_3c98c4c.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 11:37:03 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2367 | +| n_a | 24 | +| stub | 1 | +| verified | 1281 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2483/6942 items complete (35.8%)** diff --git a/reports/report_6e6f687.md b/reports/report_6e6f687.md new file mode 100644 index 0000000..123eb6d --- /dev/null +++ b/reports/report_6e6f687.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 11:54:23 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2367 | +| n_a | 24 | +| stub | 1 | +| verified | 1281 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2483/6942 items complete (35.8%)** diff --git a/reports/report_c1ae46f.md b/reports/report_c1ae46f.md new file mode 100644 index 0000000..39c1120 --- /dev/null +++ b/reports/report_c1ae46f.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 11:35:12 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2367 | +| n_a | 24 | +| stub | 1 | +| verified | 1281 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2483/6942 items complete (35.8%)** diff --git a/reports/report_d8d71ea.md b/reports/report_d8d71ea.md new file mode 100644 index 0000000..13ed1a8 --- /dev/null +++ b/reports/report_d8d71ea.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 11:33:10 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2368 | +| n_a | 24 | +| stub | 1 | +| verified | 1280 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2482/6942 items complete (35.8%)** diff --git a/reports/report_f9b582d.md b/reports/report_f9b582d.md new file mode 100644 index 0000000..04536ed --- /dev/null +++ b/reports/report_f9b582d.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 11:30:24 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2373 | +| n_a | 24 | +| stub | 1 | +| verified | 1275 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2477/6942 items complete (35.7%)**