feat: add monitoring HTTP endpoints and TLS support
Monitoring HTTP: - /varz, /connz, /healthz via Kestrel Minimal API - Pagination, sorting, subscription details on /connz - ServerStats atomic counters, CPU/memory sampling - CLI flags: -m, --http_port, --http_base_path, --https_port TLS Support: - 4-mode negotiation: no TLS, required, TLS-first, mixed - Certificate loading, pinning (SHA-256), client cert verification - PeekableStream for non-destructive TLS detection - Token-bucket rate limiter for TLS handshakes - CLI flags: --tls, --tlscert, --tlskey, --tlscacert, --tlsverify 29 new tests (78 → 107 total), all passing. # Conflicts: # src/NATS.Server.Host/Program.cs # src/NATS.Server/NATS.Server.csproj # src/NATS.Server/NatsClient.cs # src/NATS.Server/NatsOptions.cs # src/NATS.Server/NatsServer.cs # src/NATS.Server/Protocol/NatsProtocol.cs # tests/NATS.Server.Tests/ClientTests.cs
This commit is contained in:
@@ -41,7 +41,7 @@ public class ClientTests : IAsyncDisposable
|
||||
};
|
||||
|
||||
var authService = AuthService.Build(new NatsOptions());
|
||||
_natsClient = new NatsClient(1, _serverSocket, new NatsOptions(), serverInfo, authService, null, NullLogger.Instance);
|
||||
_natsClient = new NatsClient(1, new NetworkStream(_serverSocket, ownsSocket: false), _serverSocket, new NatsOptions(), serverInfo, authService, null, NullLogger.Instance, new ServerStats());
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -56,7 +56,7 @@ public class ClientTests : IAsyncDisposable
|
||||
{
|
||||
var runTask = _natsClient.RunAsync(_cts.Token);
|
||||
|
||||
// Read from client socket — should get INFO
|
||||
// Read from client socket -- should get INFO
|
||||
var buf = new byte[4096];
|
||||
var n = await _clientSocket.ReceiveAsync(buf, SocketFlags.None);
|
||||
var response = Encoding.ASCII.GetString(buf, 0, n);
|
||||
@@ -80,7 +80,7 @@ public class ClientTests : IAsyncDisposable
|
||||
// Send CONNECT then PING
|
||||
await _clientSocket.SendAsync(Encoding.ASCII.GetBytes("CONNECT {}\r\nPING\r\n"));
|
||||
|
||||
// Read response — should get PONG
|
||||
// Read response -- should get PONG
|
||||
var n = await _clientSocket.ReceiveAsync(buf, SocketFlags.None);
|
||||
var response = Encoding.ASCII.GetString(buf, 0, n);
|
||||
|
||||
@@ -128,7 +128,7 @@ public class ClientTests : IAsyncDisposable
|
||||
|
||||
response.ShouldBe("-ERR 'maximum connections exceeded'\r\n");
|
||||
|
||||
// Connection should be closed — next read returns 0
|
||||
// Connection should be closed -- next read returns 0
|
||||
n = await _clientSocket.ReceiveAsync(buf, SocketFlags.None, readCts.Token);
|
||||
n.ShouldBe(0);
|
||||
}
|
||||
|
||||
51
tests/NATS.Server.Tests/MonitorModelTests.cs
Normal file
51
tests/NATS.Server.Tests/MonitorModelTests.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Text.Json;
|
||||
using NATS.Server.Monitoring;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class MonitorModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Varz_serializes_with_go_field_names()
|
||||
{
|
||||
var varz = new Varz
|
||||
{
|
||||
Id = "TESTID", Name = "test-server", Version = "0.1.0",
|
||||
Host = "0.0.0.0", Port = 4222, InMsgs = 100, OutMsgs = 200,
|
||||
};
|
||||
var json = JsonSerializer.Serialize(varz);
|
||||
json.ShouldContain("\"server_id\":");
|
||||
json.ShouldContain("\"server_name\":");
|
||||
json.ShouldContain("\"in_msgs\":");
|
||||
json.ShouldContain("\"out_msgs\":");
|
||||
json.ShouldNotContain("\"InMsgs\"");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Connz_serializes_with_go_field_names()
|
||||
{
|
||||
var connz = new Connz
|
||||
{
|
||||
Id = "TESTID", Now = DateTime.UtcNow, NumConns = 1, Total = 1, Limit = 1024,
|
||||
Conns = [new ConnInfo { Cid = 1, Ip = "127.0.0.1", Port = 5555,
|
||||
InMsgs = 10, Uptime = "1s", Idle = "0s",
|
||||
Start = DateTime.UtcNow, LastActivity = DateTime.UtcNow }],
|
||||
};
|
||||
var json = JsonSerializer.Serialize(connz);
|
||||
json.ShouldContain("\"server_id\":");
|
||||
json.ShouldContain("\"num_connections\":");
|
||||
json.ShouldContain("\"in_msgs\":");
|
||||
json.ShouldContain("\"pending_bytes\":");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Varz_includes_nested_config_stubs()
|
||||
{
|
||||
var varz = new Varz { Id = "X", Name = "X", Version = "X", Host = "X" };
|
||||
var json = JsonSerializer.Serialize(varz);
|
||||
json.ShouldContain("\"cluster\":");
|
||||
json.ShouldContain("\"gateway\":");
|
||||
json.ShouldContain("\"leaf\":");
|
||||
json.ShouldContain("\"jetstream\":");
|
||||
}
|
||||
}
|
||||
274
tests/NATS.Server.Tests/MonitorTests.cs
Normal file
274
tests/NATS.Server.Tests/MonitorTests.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server.Monitoring;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class MonitorTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _natsPort;
|
||||
private readonly int _monitorPort;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly HttpClient _http = new();
|
||||
|
||||
public MonitorTests()
|
||||
{
|
||||
_natsPort = GetFreePort();
|
||||
_monitorPort = GetFreePort();
|
||||
_server = new NatsServer(
|
||||
new NatsOptions { Port = _natsPort, MonitorPort = _monitorPort },
|
||||
NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ = _server.StartAsync(_cts.Token);
|
||||
await _server.WaitForReadyAsync();
|
||||
// Wait for monitoring HTTP server to be ready
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz");
|
||||
if (response.IsSuccessStatusCode) break;
|
||||
}
|
||||
catch (HttpRequestException) { }
|
||||
await Task.Delay(50);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_http.Dispose();
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Healthz_returns_ok()
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Varz_returns_server_identity()
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var varz = await response.Content.ReadFromJsonAsync<Varz>();
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Id.ShouldNotBeNullOrEmpty();
|
||||
varz.Name.ShouldNotBeNullOrEmpty();
|
||||
varz.Version.ShouldBe("0.1.0");
|
||||
varz.Host.ShouldBe("0.0.0.0");
|
||||
varz.Port.ShouldBe(_natsPort);
|
||||
varz.MaxPayload.ShouldBe(1024 * 1024);
|
||||
varz.Uptime.ShouldNotBeNullOrEmpty();
|
||||
varz.Now.ShouldBeGreaterThan(DateTime.MinValue);
|
||||
varz.Mem.ShouldBeGreaterThan(0);
|
||||
varz.Cores.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Varz_tracks_connections_and_messages()
|
||||
{
|
||||
// Connect a client and send a message
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
||||
|
||||
var buf = new byte[4096];
|
||||
_ = await sock.ReceiveAsync(buf, SocketFlags.None); // Read INFO
|
||||
|
||||
var cmd = "CONNECT {}\r\nSUB test 1\r\nPUB test 5\r\nhello\r\n"u8.ToArray();
|
||||
await sock.SendAsync(cmd, SocketFlags.None);
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/varz");
|
||||
var varz = await response.Content.ReadFromJsonAsync<Varz>();
|
||||
|
||||
varz.ShouldNotBeNull();
|
||||
varz.Connections.ShouldBeGreaterThanOrEqualTo(1);
|
||||
varz.TotalConnections.ShouldBeGreaterThanOrEqualTo(1UL);
|
||||
varz.InMsgs.ShouldBeGreaterThanOrEqualTo(1L);
|
||||
varz.InBytes.ShouldBeGreaterThanOrEqualTo(5L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connz_returns_connections()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
||||
using var stream = new NetworkStream(sock);
|
||||
var buf = new byte[4096];
|
||||
_ = await stream.ReadAsync(buf);
|
||||
await stream.WriteAsync("CONNECT {\"name\":\"test-client\",\"lang\":\"csharp\",\"version\":\"1.0\"}\r\n"u8.ToArray());
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
|
||||
var connz = await response.Content.ReadFromJsonAsync<Connz>();
|
||||
connz.ShouldNotBeNull();
|
||||
connz.NumConns.ShouldBeGreaterThanOrEqualTo(1);
|
||||
connz.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
|
||||
var conn = connz.Conns.First(c => c.Name == "test-client");
|
||||
conn.Ip.ShouldNotBeNullOrEmpty();
|
||||
conn.Port.ShouldBeGreaterThan(0);
|
||||
conn.Lang.ShouldBe("csharp");
|
||||
conn.Version.ShouldBe("1.0");
|
||||
conn.Uptime.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connz_pagination()
|
||||
{
|
||||
var sockets = new List<Socket>();
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await s.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
||||
var ns = new NetworkStream(s);
|
||||
var buf = new byte[4096];
|
||||
_ = await ns.ReadAsync(buf);
|
||||
await ns.WriteAsync("CONNECT {}\r\n"u8.ToArray());
|
||||
sockets.Add(s);
|
||||
}
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?limit=2&offset=0");
|
||||
var connz = await response.Content.ReadFromJsonAsync<Connz>();
|
||||
|
||||
connz!.Conns.Length.ShouldBe(2);
|
||||
connz.Total.ShouldBeGreaterThanOrEqualTo(3);
|
||||
connz.Limit.ShouldBe(2);
|
||||
connz.Offset.ShouldBe(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var s in sockets) s.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connz_with_subscriptions()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _natsPort));
|
||||
using var stream = new NetworkStream(sock);
|
||||
var buf = new byte[4096];
|
||||
_ = await stream.ReadAsync(buf);
|
||||
await stream.WriteAsync("CONNECT {}\r\nSUB foo 1\r\nSUB bar 2\r\n"u8.ToArray());
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz?subs=true");
|
||||
var connz = await response.Content.ReadFromJsonAsync<Connz>();
|
||||
|
||||
var conn = connz!.Conns.First(c => c.NumSubs >= 2);
|
||||
conn.Subs.ShouldNotBeNull();
|
||||
conn.Subs.ShouldContain("foo");
|
||||
conn.Subs.ShouldContain("bar");
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
}
|
||||
|
||||
public class MonitorTlsTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _natsPort;
|
||||
private readonly int _monitorPort;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly HttpClient _http = new();
|
||||
private readonly string _certPath;
|
||||
private readonly string _keyPath;
|
||||
|
||||
public MonitorTlsTests()
|
||||
{
|
||||
_natsPort = GetFreePort();
|
||||
_monitorPort = GetFreePort();
|
||||
(_certPath, _keyPath) = TlsHelperTests.GenerateTestCertFiles();
|
||||
_server = new NatsServer(
|
||||
new NatsOptions
|
||||
{
|
||||
Port = _natsPort,
|
||||
MonitorPort = _monitorPort,
|
||||
TlsCert = _certPath,
|
||||
TlsKey = _keyPath,
|
||||
},
|
||||
NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ = _server.StartAsync(_cts.Token);
|
||||
await _server.WaitForReadyAsync();
|
||||
// Wait for monitoring HTTP server to be ready
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/healthz");
|
||||
if (response.IsSuccessStatusCode) break;
|
||||
}
|
||||
catch (HttpRequestException) { }
|
||||
await Task.Delay(50);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
_http.Dispose();
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
File.Delete(_certPath);
|
||||
File.Delete(_keyPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Connz_shows_tls_info_for_tls_client()
|
||||
{
|
||||
// Connect and upgrade to TLS
|
||||
using var tcp = new TcpClient();
|
||||
await tcp.ConnectAsync(IPAddress.Loopback, _natsPort);
|
||||
using var netStream = tcp.GetStream();
|
||||
var buf = new byte[4096];
|
||||
_ = await netStream.ReadAsync(buf); // Read INFO
|
||||
|
||||
using var ssl = new SslStream(netStream, false, (_, _, _, _) => true);
|
||||
await ssl.AuthenticateAsClientAsync("localhost");
|
||||
|
||||
await ssl.WriteAsync("CONNECT {}\r\n"u8.ToArray());
|
||||
await ssl.FlushAsync();
|
||||
await Task.Delay(200);
|
||||
|
||||
var response = await _http.GetAsync($"http://127.0.0.1:{_monitorPort}/connz");
|
||||
var connz = await response.Content.ReadFromJsonAsync<Connz>();
|
||||
|
||||
connz!.Conns.Length.ShouldBeGreaterThanOrEqualTo(1);
|
||||
var conn = connz.Conns[0];
|
||||
conn.TlsVersion.ShouldNotBeNullOrEmpty();
|
||||
conn.TlsCipherSuite.ShouldNotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
}
|
||||
84
tests/NATS.Server.Tests/ServerStatsTests.cs
Normal file
84
tests/NATS.Server.Tests/ServerStatsTests.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class ServerStatsTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _port;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public ServerStatsTests()
|
||||
{
|
||||
_port = GetFreePort();
|
||||
_server = new NatsServer(new NatsOptions { Port = _port }, NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ = _server.StartAsync(_cts.Token);
|
||||
await _server.WaitForReadyAsync();
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_cts.Cancel();
|
||||
_server.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Server_has_start_time()
|
||||
{
|
||||
_server.StartTime.ShouldNotBe(default);
|
||||
_server.StartTime.ShouldBeLessThanOrEqualTo(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Server_tracks_total_connections()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _port));
|
||||
await Task.Delay(100);
|
||||
_server.Stats.TotalConnections.ShouldBeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Server_stats_track_messages()
|
||||
{
|
||||
using var pub = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await pub.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _port));
|
||||
|
||||
var buf = new byte[4096];
|
||||
await pub.ReceiveAsync(buf, SocketFlags.None); // INFO
|
||||
|
||||
await pub.SendAsync("CONNECT {}\r\nSUB test 1\r\nPUB test 5\r\nhello\r\n"u8.ToArray());
|
||||
await Task.Delay(200);
|
||||
|
||||
_server.Stats.InMsgs.ShouldBeGreaterThanOrEqualTo(1);
|
||||
_server.Stats.InBytes.ShouldBeGreaterThanOrEqualTo(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Client_has_metadata()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _port));
|
||||
await Task.Delay(100);
|
||||
|
||||
var client = _server.GetClients().First();
|
||||
client.RemoteIp.ShouldNotBeNullOrEmpty();
|
||||
client.RemotePort.ShouldBeGreaterThan(0);
|
||||
client.StartTime.ShouldNotBe(default);
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
}
|
||||
254
tests/NATS.Server.Tests/TlsConnectionWrapperTests.cs
Normal file
254
tests/NATS.Server.Tests/TlsConnectionWrapperTests.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server;
|
||||
using NATS.Server.Protocol;
|
||||
using NATS.Server.Tls;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class TlsConnectionWrapperTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task NoTls_returns_plain_stream()
|
||||
{
|
||||
var (serverSocket, clientSocket) = await CreateSocketPairAsync();
|
||||
using var serverStream = new NetworkStream(serverSocket, ownsSocket: true);
|
||||
using var clientStream = new NetworkStream(clientSocket, ownsSocket: true);
|
||||
|
||||
var opts = new NatsOptions(); // No TLS configured
|
||||
var serverInfo = CreateServerInfo();
|
||||
|
||||
var (stream, infoSent) = await TlsConnectionWrapper.NegotiateAsync(
|
||||
serverSocket, serverStream, opts, null, serverInfo, NullLogger.Instance, CancellationToken.None);
|
||||
|
||||
stream.ShouldBe(serverStream); // Same stream, no wrapping
|
||||
infoSent.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TlsRequired_upgrades_to_ssl()
|
||||
{
|
||||
var (cert, _) = TlsHelperTests.GenerateTestCert();
|
||||
|
||||
var (serverSocket, clientSocket) = await CreateSocketPairAsync();
|
||||
using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true);
|
||||
|
||||
var opts = new NatsOptions { TlsCert = "dummy", TlsKey = "dummy" };
|
||||
var sslOpts = new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
};
|
||||
var serverInfo = CreateServerInfo();
|
||||
|
||||
// Client side: read INFO then start TLS
|
||||
var clientTask = Task.Run(async () =>
|
||||
{
|
||||
// Read INFO line
|
||||
var buf = new byte[4096];
|
||||
var read = await clientNetStream.ReadAsync(buf);
|
||||
var info = System.Text.Encoding.ASCII.GetString(buf, 0, read);
|
||||
info.ShouldStartWith("INFO ");
|
||||
|
||||
// Upgrade to TLS
|
||||
var sslClient = new SslStream(clientNetStream, true,
|
||||
(_, _, _, _) => true); // Trust all for testing
|
||||
await sslClient.AuthenticateAsClientAsync("localhost");
|
||||
return sslClient;
|
||||
});
|
||||
|
||||
var serverNetStream = new NetworkStream(serverSocket, ownsSocket: true);
|
||||
var (stream, infoSent) = await TlsConnectionWrapper.NegotiateAsync(
|
||||
serverSocket, serverNetStream, opts, sslOpts, serverInfo, NullLogger.Instance, CancellationToken.None);
|
||||
|
||||
stream.ShouldBeOfType<SslStream>();
|
||||
infoSent.ShouldBeTrue();
|
||||
|
||||
var clientSsl = await clientTask;
|
||||
|
||||
// Verify encrypted communication works
|
||||
await stream.WriteAsync("PING\r\n"u8.ToArray());
|
||||
await stream.FlushAsync();
|
||||
|
||||
var readBuf = new byte[64];
|
||||
var bytesRead = await clientSsl.ReadAsync(readBuf);
|
||||
var msg = System.Text.Encoding.ASCII.GetString(readBuf, 0, bytesRead);
|
||||
msg.ShouldBe("PING\r\n");
|
||||
|
||||
stream.Dispose();
|
||||
clientSsl.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MixedMode_allows_plaintext_when_AllowNonTls()
|
||||
{
|
||||
var (cert, _) = TlsHelperTests.GenerateTestCert();
|
||||
|
||||
var (serverSocket, clientSocket) = await CreateSocketPairAsync();
|
||||
using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true);
|
||||
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
TlsCert = "dummy",
|
||||
TlsKey = "dummy",
|
||||
AllowNonTls = true,
|
||||
TlsTimeout = TimeSpan.FromSeconds(2),
|
||||
};
|
||||
var sslOpts = new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
};
|
||||
var serverInfo = CreateServerInfo();
|
||||
|
||||
// Client side: read INFO then send plaintext (not TLS)
|
||||
var clientTask = Task.Run(async () =>
|
||||
{
|
||||
var buf = new byte[4096];
|
||||
var read = await clientNetStream.ReadAsync(buf);
|
||||
var info = System.Text.Encoding.ASCII.GetString(buf, 0, read);
|
||||
info.ShouldStartWith("INFO ");
|
||||
|
||||
// Send plaintext CONNECT (not a TLS handshake)
|
||||
var connectLine = System.Text.Encoding.ASCII.GetBytes("CONNECT {}\r\n");
|
||||
await clientNetStream.WriteAsync(connectLine);
|
||||
await clientNetStream.FlushAsync();
|
||||
});
|
||||
|
||||
var serverNetStream = new NetworkStream(serverSocket, ownsSocket: true);
|
||||
var (stream, infoSent) = await TlsConnectionWrapper.NegotiateAsync(
|
||||
serverSocket, serverNetStream, opts, sslOpts, serverInfo, NullLogger.Instance, CancellationToken.None);
|
||||
|
||||
await clientTask;
|
||||
|
||||
// In mixed mode with plaintext client, we get a PeekableStream, not SslStream
|
||||
stream.ShouldBeOfType<PeekableStream>();
|
||||
infoSent.ShouldBeTrue();
|
||||
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TlsRequired_rejects_plaintext()
|
||||
{
|
||||
var (cert, _) = TlsHelperTests.GenerateTestCert();
|
||||
|
||||
var (serverSocket, clientSocket) = await CreateSocketPairAsync();
|
||||
using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true);
|
||||
|
||||
var opts = new NatsOptions
|
||||
{
|
||||
TlsCert = "dummy",
|
||||
TlsKey = "dummy",
|
||||
AllowNonTls = false,
|
||||
TlsTimeout = TimeSpan.FromSeconds(2),
|
||||
};
|
||||
var sslOpts = new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
};
|
||||
var serverInfo = CreateServerInfo();
|
||||
|
||||
// Client side: read INFO then send plaintext
|
||||
var clientTask = Task.Run(async () =>
|
||||
{
|
||||
var buf = new byte[4096];
|
||||
var read = await clientNetStream.ReadAsync(buf);
|
||||
var info = System.Text.Encoding.ASCII.GetString(buf, 0, read);
|
||||
info.ShouldStartWith("INFO ");
|
||||
|
||||
// Send plaintext data (first byte is 'C', not 0x16 TLS marker)
|
||||
var connectLine = System.Text.Encoding.ASCII.GetBytes("CONNECT {}\r\n");
|
||||
await clientNetStream.WriteAsync(connectLine);
|
||||
await clientNetStream.FlushAsync();
|
||||
});
|
||||
|
||||
var serverNetStream = new NetworkStream(serverSocket, ownsSocket: true);
|
||||
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
{
|
||||
await TlsConnectionWrapper.NegotiateAsync(
|
||||
serverSocket, serverNetStream, opts, sslOpts, serverInfo, NullLogger.Instance, CancellationToken.None);
|
||||
});
|
||||
|
||||
await clientTask;
|
||||
serverNetStream.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TlsFirst_handshakes_before_sending_info()
|
||||
{
|
||||
var (cert, _) = TlsHelperTests.GenerateTestCert();
|
||||
|
||||
var (serverSocket, clientSocket) = await CreateSocketPairAsync();
|
||||
using var clientNetStream = new NetworkStream(clientSocket, ownsSocket: true);
|
||||
|
||||
var opts = new NatsOptions { TlsCert = "dummy", TlsKey = "dummy", TlsHandshakeFirst = true };
|
||||
var sslOpts = new SslServerAuthenticationOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
};
|
||||
var serverInfo = CreateServerInfo();
|
||||
|
||||
// Client side: immediately start TLS (no INFO first)
|
||||
var clientTask = Task.Run(async () =>
|
||||
{
|
||||
var sslClient = new SslStream(clientNetStream, true, (_, _, _, _) => true);
|
||||
await sslClient.AuthenticateAsClientAsync("localhost");
|
||||
|
||||
// After TLS, read INFO over encrypted stream
|
||||
var buf = new byte[4096];
|
||||
var read = await sslClient.ReadAsync(buf);
|
||||
var info = System.Text.Encoding.ASCII.GetString(buf, 0, read);
|
||||
info.ShouldStartWith("INFO ");
|
||||
|
||||
return sslClient;
|
||||
});
|
||||
|
||||
var serverNetStream = new NetworkStream(serverSocket, ownsSocket: true);
|
||||
var (stream, infoSent) = await TlsConnectionWrapper.NegotiateAsync(
|
||||
serverSocket, serverNetStream, opts, sslOpts, serverInfo, NullLogger.Instance, CancellationToken.None);
|
||||
|
||||
stream.ShouldBeOfType<SslStream>();
|
||||
infoSent.ShouldBeTrue();
|
||||
|
||||
var clientSsl = await clientTask;
|
||||
|
||||
// Verify encrypted communication works
|
||||
await stream.WriteAsync("PING\r\n"u8.ToArray());
|
||||
await stream.FlushAsync();
|
||||
|
||||
var readBuf = new byte[64];
|
||||
var bytesRead = await clientSsl.ReadAsync(readBuf);
|
||||
var msg = System.Text.Encoding.ASCII.GetString(readBuf, 0, bytesRead);
|
||||
msg.ShouldBe("PING\r\n");
|
||||
|
||||
stream.Dispose();
|
||||
clientSsl.Dispose();
|
||||
}
|
||||
|
||||
private static ServerInfo CreateServerInfo() => new()
|
||||
{
|
||||
ServerId = "TEST",
|
||||
ServerName = "test",
|
||||
Version = NatsProtocol.Version,
|
||||
Host = "127.0.0.1",
|
||||
Port = 4222,
|
||||
};
|
||||
|
||||
private static async Task<(Socket server, Socket client)> CreateSocketPairAsync()
|
||||
{
|
||||
using var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
listener.Listen(1);
|
||||
var port = ((IPEndPoint)listener.LocalEndPoint!).Port;
|
||||
|
||||
var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await client.ConnectAsync(new IPEndPoint(IPAddress.Loopback, port));
|
||||
var server = await listener.AcceptAsync();
|
||||
|
||||
return (server, client);
|
||||
}
|
||||
}
|
||||
110
tests/NATS.Server.Tests/TlsHelperTests.cs
Normal file
110
tests/NATS.Server.Tests/TlsHelperTests.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using NATS.Server;
|
||||
using NATS.Server.Tls;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class TlsHelperTests
|
||||
{
|
||||
[Fact]
|
||||
public void LoadCertificate_loads_pem_cert_and_key()
|
||||
{
|
||||
var (certPath, keyPath) = GenerateTestCertFiles();
|
||||
try
|
||||
{
|
||||
var cert = TlsHelper.LoadCertificate(certPath, keyPath);
|
||||
cert.ShouldNotBeNull();
|
||||
cert.HasPrivateKey.ShouldBeTrue();
|
||||
}
|
||||
finally { File.Delete(certPath); File.Delete(keyPath); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildServerAuthOptions_creates_valid_options()
|
||||
{
|
||||
var (certPath, keyPath) = GenerateTestCertFiles();
|
||||
try
|
||||
{
|
||||
var opts = new NatsOptions { TlsCert = certPath, TlsKey = keyPath };
|
||||
var authOpts = TlsHelper.BuildServerAuthOptions(opts);
|
||||
authOpts.ShouldNotBeNull();
|
||||
authOpts.ServerCertificate.ShouldNotBeNull();
|
||||
}
|
||||
finally { File.Delete(certPath); File.Delete(keyPath); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesPinnedCert_matches_correct_hash()
|
||||
{
|
||||
var (cert, _) = GenerateTestCert();
|
||||
var hash = TlsHelper.GetCertificateHash(cert);
|
||||
var pinned = new HashSet<string> { hash };
|
||||
TlsHelper.MatchesPinnedCert(cert, pinned).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesPinnedCert_rejects_wrong_hash()
|
||||
{
|
||||
var (cert, _) = GenerateTestCert();
|
||||
var pinned = new HashSet<string> { "0000000000000000000000000000000000000000000000000000000000000000" };
|
||||
TlsHelper.MatchesPinnedCert(cert, pinned).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PeekableStream_peeks_and_replays()
|
||||
{
|
||||
var data = "Hello, World!"u8.ToArray();
|
||||
using var ms = new MemoryStream(data);
|
||||
using var peekable = new PeekableStream(ms);
|
||||
|
||||
var peeked = await peekable.PeekAsync(1);
|
||||
peeked.Length.ShouldBe(1);
|
||||
peeked[0].ShouldBe((byte)'H');
|
||||
|
||||
var buf = new byte[data.Length];
|
||||
int total = 0;
|
||||
while (total < data.Length)
|
||||
{
|
||||
var read = await peekable.ReadAsync(buf.AsMemory(total));
|
||||
if (read == 0) break;
|
||||
total += read;
|
||||
}
|
||||
total.ShouldBe(data.Length);
|
||||
buf.ShouldBe(data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TlsRateLimiter_allows_within_limit()
|
||||
{
|
||||
using var limiter = new TlsRateLimiter(10);
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||
for (int i = 0; i < 5; i++)
|
||||
await limiter.WaitAsync(cts.Token);
|
||||
}
|
||||
|
||||
// Public helper methods used by other test classes
|
||||
public static (string certPath, string keyPath) GenerateTestCertFiles()
|
||||
{
|
||||
var (cert, key) = GenerateTestCert();
|
||||
var certPath = Path.GetTempFileName();
|
||||
var keyPath = Path.GetTempFileName();
|
||||
File.WriteAllText(certPath, cert.ExportCertificatePem());
|
||||
File.WriteAllText(keyPath, key.ExportPkcs8PrivateKeyPem());
|
||||
return (certPath, keyPath);
|
||||
}
|
||||
|
||||
public static (X509Certificate2 cert, RSA key) GenerateTestCert()
|
||||
{
|
||||
var key = RSA.Create(2048);
|
||||
var req = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddIpAddress(IPAddress.Loopback);
|
||||
sanBuilder.AddDnsName("localhost");
|
||||
req.CertificateExtensions.Add(sanBuilder.Build());
|
||||
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1));
|
||||
return (cert, key);
|
||||
}
|
||||
}
|
||||
225
tests/NATS.Server.Tests/TlsServerTests.cs
Normal file
225
tests/NATS.Server.Tests/TlsServerTests.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NATS.Server;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class TlsServerTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _port;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly string _certPath;
|
||||
private readonly string _keyPath;
|
||||
|
||||
public TlsServerTests()
|
||||
{
|
||||
_port = GetFreePort();
|
||||
(_certPath, _keyPath) = TlsHelperTests.GenerateTestCertFiles();
|
||||
_server = new NatsServer(
|
||||
new NatsOptions
|
||||
{
|
||||
Port = _port,
|
||||
TlsCert = _certPath,
|
||||
TlsKey = _keyPath,
|
||||
},
|
||||
NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ = _server.StartAsync(_cts.Token);
|
||||
await _server.WaitForReadyAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
File.Delete(_certPath);
|
||||
File.Delete(_keyPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tls_client_connects_and_receives_info()
|
||||
{
|
||||
using var tcp = new TcpClient();
|
||||
await tcp.ConnectAsync(IPAddress.Loopback, _port);
|
||||
using var netStream = tcp.GetStream();
|
||||
|
||||
// Read INFO (sent before TLS upgrade in Mode 2)
|
||||
var buf = new byte[4096];
|
||||
var read = await netStream.ReadAsync(buf);
|
||||
var info = Encoding.ASCII.GetString(buf, 0, read);
|
||||
info.ShouldStartWith("INFO ");
|
||||
info.ShouldContain("\"tls_required\":true");
|
||||
|
||||
// Upgrade to TLS
|
||||
using var sslStream = new SslStream(netStream, false, (_, _, _, _) => true);
|
||||
await sslStream.AuthenticateAsClientAsync("localhost");
|
||||
|
||||
// Send CONNECT + PING over TLS
|
||||
await sslStream.WriteAsync("CONNECT {}\r\nPING\r\n"u8.ToArray());
|
||||
await sslStream.FlushAsync();
|
||||
|
||||
// Read PONG
|
||||
var pongBuf = new byte[256];
|
||||
read = await sslStream.ReadAsync(pongBuf);
|
||||
var pong = Encoding.ASCII.GetString(pongBuf, 0, read);
|
||||
pong.ShouldContain("PONG");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tls_pubsub_works_over_encrypted_connection()
|
||||
{
|
||||
using var tcp1 = new TcpClient();
|
||||
await tcp1.ConnectAsync(IPAddress.Loopback, _port);
|
||||
using var ssl1 = await UpgradeToTlsAsync(tcp1);
|
||||
|
||||
using var tcp2 = new TcpClient();
|
||||
await tcp2.ConnectAsync(IPAddress.Loopback, _port);
|
||||
using var ssl2 = await UpgradeToTlsAsync(tcp2);
|
||||
|
||||
// Sub on client 1
|
||||
await ssl1.WriteAsync("CONNECT {}\r\nSUB test 1\r\nPING\r\n"u8.ToArray());
|
||||
await ssl1.FlushAsync();
|
||||
|
||||
// Wait for PONG to confirm subscription is registered
|
||||
var pongBuf = new byte[256];
|
||||
var pongRead = await ssl1.ReadAsync(pongBuf);
|
||||
var pongStr = Encoding.ASCII.GetString(pongBuf, 0, pongRead);
|
||||
pongStr.ShouldContain("PONG");
|
||||
|
||||
// Pub on client 2
|
||||
await ssl2.WriteAsync("CONNECT {}\r\nPUB test 5\r\nhello\r\nPING\r\n"u8.ToArray());
|
||||
await ssl2.FlushAsync();
|
||||
|
||||
// Client 1 should receive MSG (may arrive across multiple TLS records)
|
||||
var msg = await ReadUntilAsync(ssl1, "hello");
|
||||
msg.ShouldContain("MSG test 1 5");
|
||||
msg.ShouldContain("hello");
|
||||
}
|
||||
|
||||
private static async Task<string> ReadUntilAsync(Stream stream, string expected, int timeoutMs = 5000)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(timeoutMs);
|
||||
var sb = new StringBuilder();
|
||||
var buf = new byte[4096];
|
||||
while (!sb.ToString().Contains(expected))
|
||||
{
|
||||
var n = await stream.ReadAsync(buf, cts.Token);
|
||||
if (n == 0) break;
|
||||
sb.Append(Encoding.ASCII.GetString(buf, 0, n));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static async Task<SslStream> UpgradeToTlsAsync(TcpClient tcp)
|
||||
{
|
||||
var netStream = tcp.GetStream();
|
||||
var buf = new byte[4096];
|
||||
_ = await netStream.ReadAsync(buf); // Read INFO (discard)
|
||||
|
||||
var ssl = new SslStream(netStream, false, (_, _, _, _) => true);
|
||||
await ssl.AuthenticateAsClientAsync("localhost");
|
||||
return ssl;
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
}
|
||||
|
||||
public class TlsMixedModeTests : IAsyncLifetime
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
private readonly int _port;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly string _certPath;
|
||||
private readonly string _keyPath;
|
||||
|
||||
public TlsMixedModeTests()
|
||||
{
|
||||
_port = GetFreePort();
|
||||
(_certPath, _keyPath) = TlsHelperTests.GenerateTestCertFiles();
|
||||
_server = new NatsServer(
|
||||
new NatsOptions
|
||||
{
|
||||
Port = _port,
|
||||
TlsCert = _certPath,
|
||||
TlsKey = _keyPath,
|
||||
AllowNonTls = true,
|
||||
},
|
||||
NullLoggerFactory.Instance);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ = _server.StartAsync(_cts.Token);
|
||||
await _server.WaitForReadyAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
_server.Dispose();
|
||||
File.Delete(_certPath);
|
||||
File.Delete(_keyPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mixed_mode_accepts_plain_client()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
await sock.ConnectAsync(new IPEndPoint(IPAddress.Loopback, _port));
|
||||
using var stream = new NetworkStream(sock);
|
||||
|
||||
var buf = new byte[4096];
|
||||
var read = await stream.ReadAsync(buf);
|
||||
var info = Encoding.ASCII.GetString(buf, 0, read);
|
||||
info.ShouldContain("\"tls_available\":true");
|
||||
|
||||
await stream.WriteAsync("CONNECT {}\r\nPING\r\n"u8.ToArray());
|
||||
await stream.FlushAsync();
|
||||
|
||||
var pongBuf = new byte[64];
|
||||
read = await stream.ReadAsync(pongBuf);
|
||||
var pong = Encoding.ASCII.GetString(pongBuf, 0, read);
|
||||
pong.ShouldContain("PONG");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mixed_mode_accepts_tls_client()
|
||||
{
|
||||
using var tcp = new TcpClient();
|
||||
await tcp.ConnectAsync(IPAddress.Loopback, _port);
|
||||
using var netStream = tcp.GetStream();
|
||||
|
||||
var buf = new byte[4096];
|
||||
_ = await netStream.ReadAsync(buf); // Read INFO
|
||||
|
||||
using var ssl = new SslStream(netStream, false, (_, _, _, _) => true);
|
||||
await ssl.AuthenticateAsClientAsync("localhost");
|
||||
|
||||
await ssl.WriteAsync("CONNECT {}\r\nPING\r\n"u8.ToArray());
|
||||
await ssl.FlushAsync();
|
||||
|
||||
var pongBuf = new byte[64];
|
||||
var read = await ssl.ReadAsync(pongBuf);
|
||||
var pong = Encoding.ASCII.GetString(pongBuf, 0, read);
|
||||
pong.ShouldContain("PONG");
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
using var sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
sock.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||
return ((IPEndPoint)sock.LocalEndPoint!).Port;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user