feat: port sessions 21-23 — Streams, Consumers, MQTT, WebSocket & OCSP
Session 21 (402 features, IDs 3195-3387, 584-792): - JetStream/StreamTypes.cs: StreamInfo, ConsumerInfo, SequenceInfo, JSPubAckResponse, WaitQueue, ClusterInfo, PeerInfo, message types, ConsumerAction enum, CreateConsumerRequest, PriorityGroupState - JetStream/NatsStream.cs: NatsStream class (stub methods, IDisposable) - JetStream/NatsConsumer.cs: NatsConsumer class (stub methods, IDisposable) - Updated JetStreamApiTypes.cs: removed duplicate StreamInfo/ConsumerInfo stubs Session 22 (153 features, IDs 2252-2404): - Mqtt/MqttConstants.cs: all MQTT protocol constants, packet types, flags - Mqtt/MqttTypes.cs: MqttSession, MqttSubscription, MqttWill, MqttJsa, MqttAccountSessionManager, MqttHandler and supporting types - Mqtt/MqttHandler.cs: per-client MQTT state, MqttServerExtensions stubs Session 23 (97 features, IDs 3506-3543, 2443-2501): - WebSocket/WebSocketConstants.cs: WsOpCode enum, frame bits, close codes - WebSocket/WebSocketTypes.cs: WsReadInfo, SrvWebsocket (replaces stub), WebSocketHandler stubs - Auth/Ocsp/OcspTypes.cs: OcspMode, OcspMonitor (replaces stub), IOcspResponseCache (replaces stub), NoOpCache, LocalDirCache All features (3503 complete, 0 not_started). Phase 6 now at 58.9%.
This commit is contained in:
172
dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs
Normal file
172
dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright 2021-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/ocsp.go, server/ocsp_peer.go, server/ocsp_responsecache.go
|
||||
// in the NATS server Go source.
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Auth.Ocsp;
|
||||
|
||||
/// <summary>
|
||||
/// Controls how OCSP stapling behaves for a TLS certificate.
|
||||
/// Mirrors Go <c>OCSPMode uint8</c> in server/ocsp.go.
|
||||
/// </summary>
|
||||
public enum OcspMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Staple only if the "status_request" OID is present in the certificate.
|
||||
/// Mirrors Go <c>OCSPModeAuto</c>.
|
||||
/// </summary>
|
||||
Auto = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Must staple — honors the Must-Staple flag and shuts down on revocation.
|
||||
/// Mirrors Go <c>OCSPModeMust</c>.
|
||||
/// </summary>
|
||||
MustStaple = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Always obtain OCSP status, regardless of certificate flags.
|
||||
/// Mirrors Go <c>OCSPModeAlways</c>.
|
||||
/// </summary>
|
||||
Always = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Never check OCSP, even if the certificate has the Must-Staple flag.
|
||||
/// Mirrors Go <c>OCSPModeNever</c>.
|
||||
/// </summary>
|
||||
Never = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds a cached OCSP staple response and its expiry information.
|
||||
/// </summary>
|
||||
internal sealed class OcspStaple
|
||||
{
|
||||
/// <summary>The raw DER-encoded OCSP response bytes.</summary>
|
||||
public byte[]? Response { get; set; }
|
||||
|
||||
/// <summary>When the OCSP response next needs to be refreshed.</summary>
|
||||
public DateTime NextUpdate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates OCSP stapling for a single TLS certificate.
|
||||
/// Monitors certificate validity and refreshes the staple on a background timer.
|
||||
/// Mirrors Go <c>OCSPMonitor</c> struct in server/ocsp.go.
|
||||
/// Replaces the stub in NatsServerTypes.cs.
|
||||
/// </summary>
|
||||
internal sealed class OcspMonitor
|
||||
{
|
||||
private readonly Lock _mu = new();
|
||||
|
||||
/// <summary>Path to the TLS certificate file being monitored.</summary>
|
||||
public string? CertFile { get; set; }
|
||||
|
||||
/// <summary>Path to the CA certificate file used to verify OCSP responses.</summary>
|
||||
public string? CaFile { get; set; }
|
||||
|
||||
/// <summary>Path to a persisted OCSP staple file (optional).</summary>
|
||||
public string? OcspStapleFile { get; set; }
|
||||
|
||||
/// <summary>The OCSP stapling mode for this monitor.</summary>
|
||||
public OcspMode Mode { get; set; }
|
||||
|
||||
/// <summary>How often to check for a fresh OCSP response.</summary>
|
||||
public TimeSpan CheckInterval { get; set; } = TimeSpan.FromHours(24);
|
||||
|
||||
/// <summary>The owning server instance.</summary>
|
||||
public NatsServer? Server { get; set; }
|
||||
|
||||
/// <summary>The synchronisation lock for this monitor's mutable state.</summary>
|
||||
public Lock Mu => _mu;
|
||||
|
||||
/// <summary>Starts the background OCSP refresh timer.</summary>
|
||||
public void Start()
|
||||
=> throw new NotImplementedException("TODO: session 23 — ocsp");
|
||||
|
||||
/// <summary>Stops the background OCSP refresh timer.</summary>
|
||||
public void Stop()
|
||||
=> throw new NotImplementedException("TODO: session 23 — ocsp");
|
||||
|
||||
/// <summary>Returns the current cached OCSP staple bytes, or <c>null</c> if none.</summary>
|
||||
public byte[]? GetStaple()
|
||||
=> throw new NotImplementedException("TODO: session 23 — ocsp");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for caching raw OCSP response bytes keyed by certificate fingerprint.
|
||||
/// Mirrors Go <c>OCSPResponseCache</c> interface in server/ocsp_responsecache.go.
|
||||
/// Replaces the stub in NatsServerTypes.cs.
|
||||
/// </summary>
|
||||
public interface IOcspResponseCache
|
||||
{
|
||||
/// <summary>Returns the cached OCSP response for <paramref name="key"/>, or <c>null</c>.</summary>
|
||||
byte[]? Get(string key);
|
||||
|
||||
/// <summary>Stores an OCSP response under <paramref name="key"/>.</summary>
|
||||
void Put(string key, byte[] response);
|
||||
|
||||
/// <summary>Removes the cached entry for <paramref name="key"/>.</summary>
|
||||
void Remove(string key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A no-op OCSP cache that never stores anything.
|
||||
/// Mirrors Go <c>NoOpCache</c> in server/ocsp_responsecache.go.
|
||||
/// </summary>
|
||||
internal sealed class NoOpCache : IOcspResponseCache
|
||||
{
|
||||
public byte[]? Get(string key) => null;
|
||||
public void Put(string key, byte[] response) { }
|
||||
public void Remove(string key) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An OCSP cache backed by a local directory on disk.
|
||||
/// Mirrors Go <c>LocalCache</c> in server/ocsp_responsecache.go.
|
||||
/// Full implementation is deferred to session 23.
|
||||
/// </summary>
|
||||
internal sealed class LocalDirCache : IOcspResponseCache
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public LocalDirCache(string dir)
|
||||
{
|
||||
_dir = dir;
|
||||
}
|
||||
|
||||
public byte[]? Get(string key)
|
||||
=> throw new NotImplementedException("TODO: session 23 — ocsp");
|
||||
|
||||
public void Put(string key, byte[] response)
|
||||
=> throw new NotImplementedException("TODO: session 23 — ocsp");
|
||||
|
||||
public void Remove(string key)
|
||||
=> throw new NotImplementedException("TODO: session 23 — ocsp");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Payload for the OCSP peer certificate rejection advisory event.
|
||||
/// Mirrors Go <c>OCSPPeerRejectEventMsg</c> fields in server/events.go
|
||||
/// and the OCSP peer reject logic in server/ocsp_peer.go.
|
||||
/// </summary>
|
||||
public sealed class OcspPeerRejectInfo
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("peer")]
|
||||
public string Peer { get; set; } = string.Empty;
|
||||
|
||||
[System.Text.Json.Serialization.JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -21,12 +21,6 @@ namespace ZB.MOM.NatsNet.Server;
|
||||
// Forward stubs for types defined in later sessions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Stub: full definition in session 20 (stream.go).</summary>
|
||||
public sealed class StreamInfo { }
|
||||
|
||||
/// <summary>Stub: full definition in session 20 (consumer.go).</summary>
|
||||
public sealed class ConsumerInfo { }
|
||||
|
||||
/// <summary>Stub: stored message type — full definition in session 20.</summary>
|
||||
public sealed class StoredMsg { }
|
||||
|
||||
|
||||
151
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/NatsConsumer.cs
Normal file
151
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/NatsConsumer.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright 2019-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/consumer.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JetStream consumer, managing message delivery, ack tracking, and lifecycle.
|
||||
/// Mirrors the <c>consumer</c> struct in server/consumer.go.
|
||||
/// </summary>
|
||||
internal sealed class NatsConsumer : IDisposable
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
public string Stream { get; private set; } = string.Empty;
|
||||
public ConsumerConfig Config { get; private set; } = new();
|
||||
public DateTime Created { get; private set; }
|
||||
|
||||
// Atomic counters — use Interlocked for thread-safe access
|
||||
internal long Delivered;
|
||||
internal long AckFloor;
|
||||
internal long NumAckPending;
|
||||
internal long NumRedelivered;
|
||||
|
||||
private bool _closed;
|
||||
|
||||
/// <summary>IRaftNode — stored as object to avoid cross-dependency on Raft session.</summary>
|
||||
private object? _node;
|
||||
|
||||
private CancellationTokenSource? _quitCts;
|
||||
|
||||
public NatsConsumer(string stream, ConsumerConfig config, DateTime created)
|
||||
{
|
||||
Stream = stream;
|
||||
Name = (config.Name is { Length: > 0 } name) ? name
|
||||
: (config.Durable ?? string.Empty);
|
||||
Config = config;
|
||||
Created = created;
|
||||
_quitCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Factory
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="NatsConsumer"/> for the given stream.
|
||||
/// Returns null if the consumer cannot be created (stub: always throws).
|
||||
/// Mirrors <c>newConsumer</c> / <c>consumer.create</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public static NatsConsumer? Create(
|
||||
NatsStream stream,
|
||||
ConsumerConfig cfg,
|
||||
ConsumerAction action,
|
||||
ConsumerAssignment? sa)
|
||||
{
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Stops processing and tears down goroutines / timers.
|
||||
/// Mirrors <c>consumer.stop</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public void Stop() =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the consumer and all associated state permanently.
|
||||
/// Mirrors <c>consumer.delete</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public void Delete() =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Info / State
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns a snapshot of consumer info including config and delivery state.
|
||||
/// Mirrors <c>consumer.info</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public ConsumerInfo GetInfo() =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current consumer configuration.
|
||||
/// Mirrors <c>consumer.config</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public ConsumerConfig GetConfig() =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
/// <summary>
|
||||
/// Applies an updated configuration to the consumer.
|
||||
/// Mirrors <c>consumer.update</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public void UpdateConfig(ConsumerConfig config) =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current durable consumer state (delivered, ack_floor, pending, redelivered).
|
||||
/// Mirrors <c>consumer.state</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public ConsumerState GetConsumerState() =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Leadership
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this server is the current consumer leader.
|
||||
/// Mirrors <c>consumer.isLeader</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public bool IsLeader() =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
/// <summary>
|
||||
/// Transitions this consumer into or out of the leader role.
|
||||
/// Mirrors <c>consumer.setLeader</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public void SetLeader(bool isLeader, ulong term) =>
|
||||
throw new NotImplementedException("TODO: session 21 — consumer");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// IDisposable
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_quitCts?.Cancel();
|
||||
_quitCts?.Dispose();
|
||||
_quitCts = null;
|
||||
_mu.Dispose();
|
||||
}
|
||||
}
|
||||
197
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/NatsStream.cs
Normal file
197
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/NatsStream.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
// Copyright 2019-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/stream.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JetStream stream, managing message storage, replication, and lifecycle.
|
||||
/// Mirrors the <c>stream</c> struct in server/stream.go.
|
||||
/// </summary>
|
||||
internal sealed class NatsStream : IDisposable
|
||||
{
|
||||
private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
public Account Account { get; private set; }
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
public StreamConfig Config { get; private set; } = new();
|
||||
public DateTime Created { get; private set; }
|
||||
internal IStreamStore? Store { get; private set; }
|
||||
|
||||
// Atomic counters — use Interlocked for thread-safe access
|
||||
internal long Msgs;
|
||||
internal long Bytes;
|
||||
internal long FirstSeq;
|
||||
internal long LastSeq;
|
||||
|
||||
internal bool IsMirror;
|
||||
|
||||
private bool _closed;
|
||||
private CancellationTokenSource? _quitCts;
|
||||
|
||||
/// <summary>IRaftNode — stored as object to avoid cross-dependency on Raft session.</summary>
|
||||
private object? _node;
|
||||
|
||||
public NatsStream(Account account, StreamConfig config, DateTime created)
|
||||
{
|
||||
Account = account;
|
||||
Name = config.Name ?? string.Empty;
|
||||
Config = config;
|
||||
Created = created;
|
||||
_quitCts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Factory
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="NatsStream"/> after validating the configuration.
|
||||
/// Returns null if the stream cannot be created (stub: always throws).
|
||||
/// Mirrors <c>newStream</c> / <c>stream.create</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public static NatsStream? Create(
|
||||
Account acc,
|
||||
StreamConfig cfg,
|
||||
object? jsacc,
|
||||
IStreamStore? store,
|
||||
StreamAssignment? sa,
|
||||
object? server)
|
||||
{
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Stops processing and tears down goroutines / timers.
|
||||
/// Mirrors <c>stream.stop</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public void Stop() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the stream and all stored messages permanently.
|
||||
/// Mirrors <c>stream.delete</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public void Delete() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Purges messages from the stream according to the optional request filter.
|
||||
/// Mirrors <c>stream.purge</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public void Purge(StreamPurgeRequest? req = null) =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Info / State
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns a snapshot of stream info including config, state, and cluster information.
|
||||
/// Mirrors <c>stream.info</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public StreamInfo GetInfo(bool includeDeleted = false) =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously returns a snapshot of stream info.
|
||||
/// Mirrors <c>stream.info</c> (async path) in server/stream.go.
|
||||
/// </summary>
|
||||
public Task<StreamInfo> GetInfoAsync(bool includeDeleted = false, CancellationToken ct = default) =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current stream state (message counts, byte totals, sequences).
|
||||
/// Mirrors <c>stream.state</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public StreamState State() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Leadership
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Transitions this stream into or out of the leader role.
|
||||
/// Mirrors <c>stream.setLeader</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public void SetLeader(bool isLeader, ulong term) =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this server is the current stream leader.
|
||||
/// Mirrors <c>stream.isLeader</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public bool IsLeader() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Configuration
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns the owning account.
|
||||
/// Mirrors <c>stream.account</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public Account GetAccount() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current stream configuration.
|
||||
/// Mirrors <c>stream.config</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public StreamConfig GetConfig() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Applies an updated configuration to the stream.
|
||||
/// Mirrors <c>stream.update</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public void UpdateConfig(StreamConfig config) =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sealed state
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the stream is sealed (no new messages accepted).
|
||||
/// Mirrors <c>stream.isSealed</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public bool IsSealed() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
/// <summary>
|
||||
/// Seals the stream so that no new messages can be stored.
|
||||
/// Mirrors <c>stream.seal</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public void Seal() =>
|
||||
throw new NotImplementedException("TODO: session 21 — stream");
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// IDisposable
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_quitCts?.Cancel();
|
||||
_quitCts?.Dispose();
|
||||
_quitCts = null;
|
||||
_mu.Dispose();
|
||||
}
|
||||
}
|
||||
484
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/StreamTypes.cs
Normal file
484
dotnet/src/ZB.MOM.NatsNet.Server/JetStream/StreamTypes.cs
Normal file
@@ -0,0 +1,484 @@
|
||||
// Copyright 2019-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/stream.go and server/consumer.go in the NATS server Go source.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
// ============================================================================
|
||||
// Stream API types (from stream.go)
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// A stream create request that extends <see cref="StreamConfig"/> with a pedantic flag.
|
||||
/// Mirrors <c>streamConfigRequest</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class StreamConfigRequest
|
||||
{
|
||||
[JsonPropertyName("config")]
|
||||
public StreamConfig Config { get; set; } = new();
|
||||
|
||||
/// <summary>If true, strict validation is applied during stream creation/update.</summary>
|
||||
[JsonPropertyName("pedantic")]
|
||||
public bool Pedantic { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a stream, returned from info requests.
|
||||
/// Mirrors <c>StreamInfo</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class StreamInfo
|
||||
{
|
||||
[JsonPropertyName("config")]
|
||||
public StreamConfig Config { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("created")]
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
[JsonPropertyName("state")]
|
||||
public StreamState State { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("mirror")]
|
||||
public StreamSourceInfo? Mirror { get; set; }
|
||||
|
||||
[JsonPropertyName("sources")]
|
||||
public StreamSourceInfo[]? Sources { get; set; }
|
||||
|
||||
[JsonPropertyName("cluster")]
|
||||
public ClusterInfo? Cluster { get; set; }
|
||||
|
||||
[JsonPropertyName("mirror_direct")]
|
||||
public bool Mirror_Direct { get; set; }
|
||||
|
||||
[JsonPropertyName("allow_direct")]
|
||||
public bool Allow_Direct { get; set; }
|
||||
|
||||
/// <summary>Alternate cluster name.</summary>
|
||||
[JsonPropertyName("alternates")]
|
||||
public string? Alt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a stream mirror or source.
|
||||
/// Mirrors <c>StreamSourceInfo</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class StreamSourceInfo
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("filter_subject")]
|
||||
public string? FilterSubject { get; set; }
|
||||
|
||||
[JsonPropertyName("lag")]
|
||||
public ulong Lag { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public DateTime? Active { get; set; }
|
||||
|
||||
[JsonPropertyName("external")]
|
||||
public StreamSource? External { get; set; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request parameters for stream info, allowing filtering.
|
||||
/// Mirrors <c>streamInfoRequest</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class StreamInfoRequest
|
||||
{
|
||||
[JsonPropertyName("subjects_filter")]
|
||||
public string? SubjectsFilter { get; set; }
|
||||
|
||||
[JsonPropertyName("mirror_check_until")]
|
||||
public string? MirrorCheckUntil { get; set; }
|
||||
|
||||
[JsonPropertyName("deleted_details")]
|
||||
public bool DeletedDetails { get; set; }
|
||||
|
||||
[JsonPropertyName("subjects_detail")]
|
||||
public bool SubjectsDetail { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request parameters for purging a stream.
|
||||
/// Mirrors <c>StreamPurgeRequest</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class StreamPurgeRequest
|
||||
{
|
||||
[JsonPropertyName("filter")]
|
||||
public string? Filter { get; set; }
|
||||
|
||||
[JsonPropertyName("seq")]
|
||||
public ulong Sequence { get; set; }
|
||||
|
||||
[JsonPropertyName("keep")]
|
||||
public ulong Keep { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for deleting a specific stream message.
|
||||
/// Mirrors <c>StreamMsgDeleteRequest</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class StreamMsgDeleteRequest
|
||||
{
|
||||
[JsonPropertyName("seq")]
|
||||
public ulong Seq { get; set; }
|
||||
|
||||
[JsonPropertyName("no_erase")]
|
||||
public bool NoErase { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for retrieving a specific stream message.
|
||||
/// Mirrors <c>StreamGetMsgRequest</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class StreamGetMsgRequest
|
||||
{
|
||||
[JsonPropertyName("seq")]
|
||||
public ulong Seq { get; set; }
|
||||
|
||||
[JsonPropertyName("last_by_subj")]
|
||||
public string? LastBySubject { get; set; }
|
||||
|
||||
[JsonPropertyName("next_by_subj")]
|
||||
public string? NextBySubject { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publish acknowledgement response from JetStream.
|
||||
/// Mirrors <c>JSPubAckResponse</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class JSPubAckResponse
|
||||
{
|
||||
[JsonPropertyName("stream")]
|
||||
public string Stream { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("seq")]
|
||||
public ulong Seq { get; set; }
|
||||
|
||||
[JsonPropertyName("duplicate")]
|
||||
public bool Duplicate { get; set; }
|
||||
|
||||
[JsonPropertyName("domain")]
|
||||
public string? Domain { get; set; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
public JsApiError? PubAckError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns an exception if the response contains an error, otherwise null.
|
||||
/// Mirrors <c>ToError()</c> helper pattern in NATS Go server.
|
||||
/// </summary>
|
||||
public Exception? ToError()
|
||||
{
|
||||
if (PubAckError is { ErrCode: > 0 })
|
||||
return new InvalidOperationException($"{PubAckError.Description} (errCode={PubAckError.ErrCode})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A raw published message before JetStream processing.
|
||||
/// Mirrors <c>pubMsg</c> (JetStream variant) in server/stream.go.
|
||||
/// Note: renamed <c>JsStreamPubMsg</c> to avoid collision with the server-level
|
||||
/// <c>PubMsg</c> (events.go) which lives in the same namespace.
|
||||
/// </summary>
|
||||
public sealed class JsStreamPubMsg
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string? Reply { get; set; }
|
||||
public byte[]? Hdr { get; set; }
|
||||
public byte[]? Msg { get; set; }
|
||||
public Dictionary<string, string>? Meta { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A JetStream publish message with sync tracking.
|
||||
/// Mirrors <c>jsPubMsg</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class JsPubMsg
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string? Reply { get; set; }
|
||||
public byte[]? Hdr { get; set; }
|
||||
public byte[]? Msg { get; set; }
|
||||
|
||||
/// <summary>Publish argument (opaque, set at runtime).</summary>
|
||||
public object? Pa { get; set; }
|
||||
|
||||
/// <summary>Sync/ack channel (opaque, set at runtime).</summary>
|
||||
public object? Sync { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An inbound message to be processed by the JetStream layer.
|
||||
/// Mirrors <c>inMsg</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class InMsg
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string? Reply { get; set; }
|
||||
public byte[]? Hdr { get; set; }
|
||||
public byte[]? Msg { get; set; }
|
||||
|
||||
/// <summary>The originating client (opaque, set at runtime).</summary>
|
||||
public object? Client { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A cached/clustered message for replication.
|
||||
/// Mirrors <c>cMsg</c> in server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class CMsg
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public byte[]? Msg { get; set; }
|
||||
public ulong Seq { get; set; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Consumer API types (from consumer.go)
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Information about a consumer, returned from info requests.
|
||||
/// Mirrors <c>ConsumerInfo</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public sealed class ConsumerInfo
|
||||
{
|
||||
[JsonPropertyName("stream_name")]
|
||||
public string Stream { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("created")]
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
[JsonPropertyName("config")]
|
||||
public ConsumerConfig? Config { get; set; }
|
||||
|
||||
[JsonPropertyName("delivered")]
|
||||
public SequenceInfo Delivered { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("ack_floor")]
|
||||
public SequenceInfo AckFloor { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("num_ack_pending")]
|
||||
public int NumAckPending { get; set; }
|
||||
|
||||
[JsonPropertyName("num_redelivered")]
|
||||
public int NumRedelivered { get; set; }
|
||||
|
||||
[JsonPropertyName("num_waiting")]
|
||||
public int NumWaiting { get; set; }
|
||||
|
||||
[JsonPropertyName("num_pending")]
|
||||
public ulong NumPending { get; set; }
|
||||
|
||||
[JsonPropertyName("cluster")]
|
||||
public ClusterInfo? Cluster { get; set; }
|
||||
|
||||
[JsonPropertyName("push_bound")]
|
||||
public bool PushBound { get; set; }
|
||||
|
||||
[JsonPropertyName("paused")]
|
||||
public bool Paused { get; set; }
|
||||
|
||||
[JsonPropertyName("pause_remaining")]
|
||||
public TimeSpan PauseRemaining { get; set; }
|
||||
|
||||
[JsonPropertyName("ts")]
|
||||
public DateTime TimeStamp { get; set; }
|
||||
|
||||
[JsonPropertyName("priority_groups")]
|
||||
public PriorityGroupState[]? PriorityGroups { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State information for a priority group on a pull consumer.
|
||||
/// Mirrors <c>PriorityGroupState</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public sealed class PriorityGroupState
|
||||
{
|
||||
[JsonPropertyName("group")]
|
||||
public string Group { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("pinned_client_id")]
|
||||
public string? PinnedClientId { get; set; }
|
||||
|
||||
[JsonPropertyName("pinned_ts")]
|
||||
public DateTime PinnedTs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sequence information for consumer delivered/ack_floor positions.
|
||||
/// Mirrors <c>SequenceInfo</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public sealed class SequenceInfo
|
||||
{
|
||||
[JsonPropertyName("consumer_seq")]
|
||||
public ulong Consumer { get; set; }
|
||||
|
||||
[JsonPropertyName("stream_seq")]
|
||||
public ulong Stream { get; set; }
|
||||
|
||||
[JsonPropertyName("last_active")]
|
||||
public DateTime? Last { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to create or update a consumer.
|
||||
/// Mirrors <c>CreateConsumerRequest</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public sealed class CreateConsumerRequest
|
||||
{
|
||||
[JsonPropertyName("stream_name")]
|
||||
public string Stream { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("config")]
|
||||
public ConsumerConfig Config { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("action")]
|
||||
public ConsumerAction Action { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the intended action when creating a consumer.
|
||||
/// Mirrors <c>ConsumerAction</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public enum ConsumerAction
|
||||
{
|
||||
/// <summary>Create a new consumer or update if it already exists.</summary>
|
||||
CreateOrUpdate = 0,
|
||||
|
||||
/// <summary>Create a new consumer; fail if it already exists.</summary>
|
||||
Create = 1,
|
||||
|
||||
/// <summary>Update an existing consumer; fail if it does not exist.</summary>
|
||||
Update = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for a consumer deletion request.
|
||||
/// Mirrors <c>ConsumerDeleteResponse</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public sealed class ConsumerDeleteResponse
|
||||
{
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pending pull request waiting in the wait queue.
|
||||
/// Mirrors <c>waitingRequest</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public sealed class WaitingRequest
|
||||
{
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string? Reply { get; set; }
|
||||
|
||||
/// <summary>Number of messages requested.</summary>
|
||||
public int N { get; set; }
|
||||
|
||||
/// <summary>Number of messages delivered so far.</summary>
|
||||
public int D { get; set; }
|
||||
|
||||
/// <summary>No-wait flag (1 = no wait).</summary>
|
||||
public int NoWait { get; set; }
|
||||
|
||||
public DateTime? Expires { get; set; }
|
||||
|
||||
/// <summary>Max byte limit for this batch.</summary>
|
||||
public int MaxBytes { get; set; }
|
||||
|
||||
/// <summary>Bytes accumulated so far.</summary>
|
||||
public int B { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A circular wait queue for pending pull requests.
|
||||
/// Mirrors <c>waitQueue</c> in server/consumer.go.
|
||||
/// </summary>
|
||||
public sealed class WaitQueue
|
||||
{
|
||||
private readonly List<WaitingRequest> _reqs = new();
|
||||
private int _head;
|
||||
private int _tail;
|
||||
|
||||
/// <summary>Number of pending requests in the queue.</summary>
|
||||
public int Len => _reqs.Count;
|
||||
|
||||
/// <summary>Add a waiting request to the tail of the queue.</summary>
|
||||
public void Add(WaitingRequest req) =>
|
||||
throw new NotImplementedException("TODO: session 21");
|
||||
|
||||
/// <summary>Peek at the head request without removing it.</summary>
|
||||
public WaitingRequest? Peek() =>
|
||||
throw new NotImplementedException("TODO: session 21");
|
||||
|
||||
/// <summary>Remove and return the head request.</summary>
|
||||
public WaitingRequest? Pop() =>
|
||||
throw new NotImplementedException("TODO: session 21");
|
||||
|
||||
/// <summary>Compact the internal backing list to reclaim removed slots.</summary>
|
||||
public void Compress() =>
|
||||
throw new NotImplementedException("TODO: session 21");
|
||||
|
||||
/// <summary>Returns true if the queue is at capacity (head == tail when full).</summary>
|
||||
public bool IsFull(int max) =>
|
||||
throw new NotImplementedException("TODO: session 21");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cluster membership and leadership information for a stream or consumer.
|
||||
/// Mirrors <c>ClusterInfo</c> in server/consumer.go and server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class ClusterInfo
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("leader")]
|
||||
public string? Leader { get; set; }
|
||||
|
||||
[JsonPropertyName("replicas")]
|
||||
public PeerInfo[]? Replicas { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a peer in a JetStream Raft group.
|
||||
/// Mirrors <c>PeerInfo</c> in server/consumer.go and server/stream.go.
|
||||
/// </summary>
|
||||
public sealed class PeerInfo
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("current")]
|
||||
public bool Current { get; set; }
|
||||
|
||||
[JsonPropertyName("offline")]
|
||||
public bool Offline { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public TimeSpan Active { get; set; }
|
||||
|
||||
[JsonPropertyName("lag")]
|
||||
public ulong Lag { get; set; }
|
||||
}
|
||||
271
dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttConstants.cs
Normal file
271
dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttConstants.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2020-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/mqtt.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Mqtt;
|
||||
|
||||
// References to "spec" here are from https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.pdf
|
||||
|
||||
/// <summary>
|
||||
/// MQTT control packet type byte values.
|
||||
/// Mirrors the <c>mqttPacket*</c> constants in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal static class MqttPacket
|
||||
{
|
||||
public const byte Connect = 0x10;
|
||||
public const byte ConnectAck = 0x20;
|
||||
public const byte Pub = 0x30;
|
||||
public const byte PubAck = 0x40;
|
||||
public const byte PubRec = 0x50;
|
||||
public const byte PubRel = 0x60;
|
||||
public const byte PubComp = 0x70;
|
||||
public const byte Sub = 0x80;
|
||||
public const byte SubAck = 0x90;
|
||||
public const byte Unsub = 0xA0;
|
||||
public const byte UnsubAck = 0xB0;
|
||||
public const byte Ping = 0xC0;
|
||||
public const byte PingResp = 0xD0;
|
||||
public const byte Disconnect = 0xE0;
|
||||
public const byte Mask = 0xF0;
|
||||
public const byte FlagMask = 0x0F;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT CONNECT packet flag byte values.
|
||||
/// Mirrors the <c>mqttConnFlag*</c> constants in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal static class MqttConnectFlag
|
||||
{
|
||||
public const byte Reserved = 0x01;
|
||||
public const byte CleanSession = 0x02;
|
||||
public const byte WillFlag = 0x04;
|
||||
public const byte WillQoS = 0x18;
|
||||
public const byte WillRetain = 0x20;
|
||||
public const byte PasswordFlag = 0x40;
|
||||
public const byte UsernameFlag = 0x80;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT PUBLISH packet flag byte values.
|
||||
/// Mirrors the <c>mqttPubFlag*</c> and <c>mqttPubQoS*</c> constants in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal static class MqttPubFlag
|
||||
{
|
||||
public const byte Retain = 0x01;
|
||||
public const byte QoS = 0x06;
|
||||
public const byte Dup = 0x08;
|
||||
public const byte QoS1 = 0x1 << 1;
|
||||
public const byte QoS2 = 0x2 << 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT CONNACK return codes.
|
||||
/// Mirrors the <c>mqttConnAckRC*</c> constants in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal static class MqttConnAckRc
|
||||
{
|
||||
public const byte Accepted = 0x00;
|
||||
public const byte UnacceptableProtocol = 0x01;
|
||||
public const byte IdentifierRejected = 0x02;
|
||||
public const byte ServerUnavailable = 0x03;
|
||||
public const byte BadUserOrPassword = 0x04;
|
||||
public const byte NotAuthorized = 0x05;
|
||||
public const byte QoS2WillRejected = 0x10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Miscellaneous MQTT protocol constants.
|
||||
/// Mirrors the remaining scalar constants in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal static class MqttConst
|
||||
{
|
||||
/// <summary>Maximum control packet payload size (0xFFFFFFF).</summary>
|
||||
public const int MaxPayloadSize = 0xFFFFFFF;
|
||||
|
||||
/// <summary>MQTT topic level separator character ('/').</summary>
|
||||
public const char TopicLevelSep = '/';
|
||||
|
||||
/// <summary>Single-level wildcard character ('+').</summary>
|
||||
public const char SingleLevelWildcard = '+';
|
||||
|
||||
/// <summary>Multi-level wildcard character ('#').</summary>
|
||||
public const char MultiLevelWildcard = '#';
|
||||
|
||||
/// <summary>Reserved topic prefix character ('$').</summary>
|
||||
public const char ReservedPrefix = '$';
|
||||
|
||||
/// <summary>MQTT protocol level byte (v3.1.1 = 0x04).</summary>
|
||||
public const byte ProtoLevel = 0x04;
|
||||
|
||||
/// <summary>SUBACK failure return code (0x80).</summary>
|
||||
public const byte SubAckFailure = 0x80;
|
||||
|
||||
/// <summary>Fixed flags byte in SUBSCRIBE packets (0x02).</summary>
|
||||
public const byte SubscribeFlags = 0x02;
|
||||
|
||||
/// <summary>Fixed flags byte in UNSUBSCRIBE packets (0x02).</summary>
|
||||
public const byte UnsubscribeFlags = 0x02;
|
||||
|
||||
/// <summary>
|
||||
/// Suffix appended to the SID of subscriptions created for MQTT '#' wildcard
|
||||
/// at the upper level. Mirrors <c>mqttMultiLevelSidSuffix</c>.
|
||||
/// </summary>
|
||||
public const string MultiLevelSidSuffix = " fwc";
|
||||
|
||||
/// <summary>Initial byte allocation for publish headers (overestimate).</summary>
|
||||
public const int InitialPubHeader = 16;
|
||||
|
||||
/// <summary>Default maximum number of pending QoS-1 acks per session.</summary>
|
||||
public const int DefaultMaxAckPending = 1024;
|
||||
|
||||
/// <summary>Absolute upper limit on cumulative MaxAckPending across all session subscriptions.</summary>
|
||||
public const int MaxAckTotalLimit = 0xFFFF;
|
||||
|
||||
/// <summary>WebSocket path for MQTT connections.</summary>
|
||||
public const string WsPath = "/mqtt";
|
||||
|
||||
/// <summary>Marker character for deleted retained messages (used in flag field).</summary>
|
||||
public const char RetainedFlagDelMarker = '-';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MQTT-internal NATS subject / stream / consumer name constants.
|
||||
/// Mirrors the string constants in server/mqtt.go that define JetStream stream names,
|
||||
/// subject prefixes, and JSA reply tokens.
|
||||
/// </summary>
|
||||
internal static class MqttTopics
|
||||
{
|
||||
// -------------------------------------------------------------------------
|
||||
// Top-level MQTT subject prefix
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Prefix used for all internal MQTT subjects.</summary>
|
||||
public const string Prefix = "$MQTT.";
|
||||
|
||||
/// <summary>
|
||||
/// Prefix for NATS subscriptions used as JS consumer delivery subjects.
|
||||
/// MQTT clients must not subscribe to subjects starting with this prefix.
|
||||
/// </summary>
|
||||
public const string SubPrefix = Prefix + "sub.";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// JetStream stream names
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Stream name for MQTT QoS >0 messages on a given account.</summary>
|
||||
public const string MsgsStreamName = "$MQTT_msgs";
|
||||
|
||||
/// <summary>Subject prefix for messages in the MQTT messages stream.</summary>
|
||||
public const string MsgsStreamSubjectPrefix = Prefix + "msgs.";
|
||||
|
||||
/// <summary>Stream name for MQTT retained messages.</summary>
|
||||
public const string RetainedMsgsStreamName = "$MQTT_rmsgs";
|
||||
|
||||
/// <summary>Subject prefix for messages in the retained messages stream.</summary>
|
||||
public const string RetainedMsgsStreamSubject = Prefix + "rmsgs.";
|
||||
|
||||
/// <summary>Stream name for MQTT session state.</summary>
|
||||
public const string SessStreamName = "$MQTT_sess";
|
||||
|
||||
/// <summary>Subject prefix for session state messages.</summary>
|
||||
public const string SessStreamSubjectPrefix = Prefix + "sess.";
|
||||
|
||||
/// <summary>Name prefix used when creating per-account session streams.</summary>
|
||||
public const string SessionsStreamNamePrefix = "$MQTT_sess_";
|
||||
|
||||
/// <summary>Stream name for incoming QoS-2 messages.</summary>
|
||||
public const string QoS2IncomingMsgsStreamName = "$MQTT_qos2in";
|
||||
|
||||
/// <summary>Subject prefix for incoming QoS-2 messages.</summary>
|
||||
public const string QoS2IncomingMsgsStreamSubjectPrefix = Prefix + "qos2.in.";
|
||||
|
||||
/// <summary>Stream name for outgoing MQTT QoS messages (PUBREL).</summary>
|
||||
public const string OutStreamName = "$MQTT_out";
|
||||
|
||||
/// <summary>Subject prefix for outgoing MQTT messages.</summary>
|
||||
public const string OutSubjectPrefix = Prefix + "out.";
|
||||
|
||||
/// <summary>Subject prefix for PUBREL messages.</summary>
|
||||
public const string PubRelSubjectPrefix = Prefix + "out.pubrel.";
|
||||
|
||||
/// <summary>Subject prefix for PUBREL delivery subjects.</summary>
|
||||
public const string PubRelDeliverySubjectPrefix = Prefix + "deliver.pubrel.";
|
||||
|
||||
/// <summary>Durable consumer name prefix for PUBREL.</summary>
|
||||
public const string PubRelConsumerDurablePrefix = "$MQTT_PUBREL_";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// JSA reply subject prefix and token constants
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Prefix of the reply subject for JS API requests.</summary>
|
||||
public const string JsaRepliesPrefix = Prefix + "JSA.";
|
||||
|
||||
// Token position indices within a JSA reply subject.
|
||||
public const int JsaIdTokenPos = 3;
|
||||
public const int JsaTokenPos = 4;
|
||||
public const int JsaClientIdPos = 5;
|
||||
|
||||
// JSA operation token values.
|
||||
public const string JsaStreamCreate = "SC";
|
||||
public const string JsaStreamUpdate = "SU";
|
||||
public const string JsaStreamLookup = "SL";
|
||||
public const string JsaStreamDel = "SD";
|
||||
public const string JsaConsumerCreate = "CC";
|
||||
public const string JsaConsumerLookup = "CL";
|
||||
public const string JsaConsumerDel = "CD";
|
||||
public const string JsaMsgStore = "MS";
|
||||
public const string JsaMsgLoad = "ML";
|
||||
public const string JsaMsgDelete = "MD";
|
||||
public const string JsaSessPersist = "SP";
|
||||
public const string JsaRetainedMsgDel = "RD";
|
||||
public const string JsaStreamNames = "SN";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// NATS header names injected into re-encoded PUBLISH messages
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Header that indicates the message originated from MQTT and stores published QoS.</summary>
|
||||
public const string NatsHeader = "Nmqtt-Pub";
|
||||
|
||||
/// <summary>Header storing the original MQTT topic for retained messages.</summary>
|
||||
public const string NatsRetainedMessageTopic = "Nmqtt-RTopic";
|
||||
|
||||
/// <summary>Header storing the origin of a retained message.</summary>
|
||||
public const string NatsRetainedMessageOrigin = "Nmqtt-ROrigin";
|
||||
|
||||
/// <summary>Header storing the flags of a retained message.</summary>
|
||||
public const string NatsRetainedMessageFlags = "Nmqtt-RFlags";
|
||||
|
||||
/// <summary>Header storing the source of a retained message.</summary>
|
||||
public const string NatsRetainedMessageSource = "Nmqtt-RSource";
|
||||
|
||||
/// <summary>Header indicating a PUBREL message and storing the packet identifier.</summary>
|
||||
public const string NatsPubRelHeader = "Nmqtt-PubRel";
|
||||
|
||||
/// <summary>Header storing the original MQTT subject in re-encoded PUBLISH messages.</summary>
|
||||
public const string NatsHeaderSubject = "Nmqtt-Subject";
|
||||
|
||||
/// <summary>Header storing the subject mapping in re-encoded PUBLISH messages.</summary>
|
||||
public const string NatsHeaderMapped = "Nmqtt-Mapped";
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sparkplug B constants
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public const string SparkbNBirth = "NBIRTH";
|
||||
public const string SparkbDBirth = "DBIRTH";
|
||||
public const string SparkbNDeath = "NDEATH";
|
||||
public const string SparkbDDeath = "DDEATH";
|
||||
}
|
||||
252
dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttHandler.cs
Normal file
252
dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttHandler.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright 2020-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/mqtt.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Mqtt;
|
||||
|
||||
// ============================================================================
|
||||
// Per-client MQTT state
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Per-client MQTT state attached to every connection established via the MQTT
|
||||
/// listener or WebSocket upgrade.
|
||||
/// Mirrors Go <c>mqtt</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttHandler
|
||||
{
|
||||
private readonly Lock _mu = new();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Identity
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>MQTT client identifier presented in the CONNECT packet.</summary>
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this is a clean session.</summary>
|
||||
public bool CleanSession { get; set; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Session / Will
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Session associated with this connection after a successful CONNECT.</summary>
|
||||
public MqttSession? Session { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quick reference to the account session manager.
|
||||
/// Immutable after <c>processConnect()</c> completes.
|
||||
/// </summary>
|
||||
public MqttAccountSessionManager? AccountSessionManager { get; set; }
|
||||
|
||||
/// <summary>Will message to publish when this connection closes unexpectedly.</summary>
|
||||
public MqttWill? Will { get; set; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Keep-alive
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Keep-alive interval in seconds (0 = disabled).</summary>
|
||||
public ushort KeepAlive { get; set; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// QoS pending / packet identifiers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Next packet identifier to use for QoS >0 outbound messages.</summary>
|
||||
public ushort NextPi { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pending ack map: packet identifier → pending state.
|
||||
/// Used for tracking in-flight QoS 1/2 PUBLISH packets.
|
||||
/// </summary>
|
||||
public Dictionary<ushort, MqttPending?> Pending { get; } = new();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Protocol flags
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, the server rejects QoS-2 PUBLISH from this client
|
||||
/// and terminates the connection on receipt of such a packet.
|
||||
/// Mirrors Go <c>mqtt.rejectQoS2Pub</c>.
|
||||
/// </summary>
|
||||
public bool RejectQoS2Pub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <c>true</c>, QoS-2 SUBSCRIBE requests are silently downgraded to QoS-1.
|
||||
/// Mirrors Go <c>mqtt.downgradeQoS2Sub</c>.
|
||||
/// </summary>
|
||||
public bool DowngradeQoS2Sub { get; set; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Parse state (used by the read-loop MQTT byte-stream parser)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Current state of the fixed-header / remaining-length state machine.</summary>
|
||||
public byte ParseState { get; set; }
|
||||
|
||||
/// <summary>Control packet type byte extracted from the current fixed header.</summary>
|
||||
public byte PktType { get; set; }
|
||||
|
||||
/// <summary>Remaining length of the current control packet (bytes still to read).</summary>
|
||||
public int RemLen { get; set; }
|
||||
|
||||
/// <summary>Buffer accumulating the current packet's variable-header and payload.</summary>
|
||||
public byte[]? Buf { get; set; }
|
||||
|
||||
/// <summary>Multiplier accumulator used during multi-byte remaining-length decoding.</summary>
|
||||
public int RemLenMult { get; set; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Thread safety
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Lock protecting mutable fields on this instance.</summary>
|
||||
public Lock Mu => _mu;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Server-side MQTT extension methods (stubs)
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Stub extension methods on <see cref="NatsServer"/> for MQTT server operations.
|
||||
/// Mirrors the server-receiver MQTT functions in server/mqtt.go.
|
||||
/// All methods throw <see cref="NotImplementedException"/> until session 22 is complete.
|
||||
/// </summary>
|
||||
internal static class MqttServerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Start listening for MQTT client connections.
|
||||
/// Mirrors Go <c>(*Server).startMQTT()</c>.
|
||||
/// </summary>
|
||||
public static void StartMqtt(this NatsServer server) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Configure MQTT authentication overrides from the MQTT options block.
|
||||
/// Mirrors Go <c>(*Server).mqttConfigAuth()</c>.
|
||||
/// </summary>
|
||||
public static void MqttConfigAuth(this NatsServer server, object mqttOpts) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Handle cleanup when an MQTT client connection closes.
|
||||
/// Mirrors Go <c>(*Server).mqttHandleClosedClient()</c>.
|
||||
/// </summary>
|
||||
public static void MqttHandleClosedClient(this NatsServer server, object client) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Propagate a change to the maximum ack-pending limit to all MQTT sessions.
|
||||
/// Mirrors Go <c>(*Server).mqttUpdateMaxAckPending()</c>.
|
||||
/// </summary>
|
||||
public static void MqttUpdateMaxAckPending(this NatsServer server, ushort maxp) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve or lazily-create the JSA for the named account.
|
||||
/// Mirrors Go <c>(*Server).mqttGetJSAForAccount()</c>.
|
||||
/// </summary>
|
||||
public static MqttJsa MqttGetJsaForAccount(this NatsServer server, string account) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Store a QoS message for an account on a (possibly new) NATS subject.
|
||||
/// Mirrors Go <c>(*Server).mqttStoreQoSMsgForAccountOnNewSubject()</c>.
|
||||
/// </summary>
|
||||
public static void MqttStoreQosMsgForAccountOnNewSubject(
|
||||
this NatsServer server,
|
||||
int hdr, byte[] msg, string account, string subject) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Get or create the <see cref="MqttAccountSessionManager"/> for the client's account.
|
||||
/// Mirrors Go <c>(*Server).getOrCreateMQTTAccountSessionManager()</c>.
|
||||
/// </summary>
|
||||
public static MqttAccountSessionManager GetOrCreateMqttAccountSessionManager(
|
||||
this NatsServer server, object client) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="MqttAccountSessionManager"/> for the given account.
|
||||
/// Mirrors Go <c>(*Server).mqttCreateAccountSessionManager()</c>.
|
||||
/// </summary>
|
||||
public static MqttAccountSessionManager MqttCreateAccountSessionManager(
|
||||
this NatsServer server, object account, System.Threading.CancellationToken cancel) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Determine how many JetStream replicas to use for MQTT streams.
|
||||
/// Mirrors Go <c>(*Server).mqttDetermineReplicas()</c>.
|
||||
/// </summary>
|
||||
public static int MqttDetermineReplicas(this NatsServer server) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Process an MQTT CONNECT packet after parsing.
|
||||
/// Mirrors Go <c>(*Server).mqttProcessConnect()</c>.
|
||||
/// </summary>
|
||||
public static void MqttProcessConnect(
|
||||
this NatsServer server, object client, MqttConnectProto cp, bool trace) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Send the Will message for a client that disconnected unexpectedly.
|
||||
/// Mirrors Go <c>(*Server).mqttHandleWill()</c>.
|
||||
/// </summary>
|
||||
public static void MqttHandleWill(this NatsServer server, object client) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Process an inbound MQTT PUBLISH packet.
|
||||
/// Mirrors Go <c>(*Server).mqttProcessPub()</c>.
|
||||
/// </summary>
|
||||
public static void MqttProcessPub(
|
||||
this NatsServer server, object client, MqttPublishInfo pp, bool trace) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Initiate delivery of a PUBLISH message via JetStream.
|
||||
/// Mirrors Go <c>(*Server).mqttInitiateMsgDelivery()</c>.
|
||||
/// </summary>
|
||||
public static void MqttInitiateMsgDelivery(
|
||||
this NatsServer server, object client, MqttPublishInfo pp) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Store a QoS-2 PUBLISH exactly once (idempotent).
|
||||
/// Mirrors Go <c>(*Server).mqttStoreQoS2MsgOnce()</c>.
|
||||
/// </summary>
|
||||
public static void MqttStoreQoS2MsgOnce(
|
||||
this NatsServer server, object client, MqttPublishInfo pp) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Process an inbound MQTT PUBREL packet.
|
||||
/// Mirrors Go <c>(*Server).mqttProcessPubRel()</c>.
|
||||
/// </summary>
|
||||
public static void MqttProcessPubRel(
|
||||
this NatsServer server, object client, ushort pi, bool trace) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
/// <summary>
|
||||
/// Audit retained-message permissions after a configuration reload.
|
||||
/// Mirrors Go <c>(*Server).mqttCheckPubRetainedPerms()</c>.
|
||||
/// </summary>
|
||||
public static void MqttCheckPubRetainedPerms(this NatsServer server) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
}
|
||||
391
dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttTypes.cs
Normal file
391
dotnet/src/ZB.MOM.NatsNet.Server/Mqtt/MqttTypes.cs
Normal file
@@ -0,0 +1,391 @@
|
||||
// Copyright 2020-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/mqtt.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Mqtt;
|
||||
|
||||
// ============================================================================
|
||||
// Enumerations
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// State machine states for parsing incoming MQTT byte streams.
|
||||
/// Mirrors the <c>mqttParseState*</c> iota in server/mqtt.go (implicit from
|
||||
/// the read-loop logic).
|
||||
/// </summary>
|
||||
internal enum MqttParseState : byte
|
||||
{
|
||||
/// <summary>Waiting for the first fixed-header byte.</summary>
|
||||
MqttStateHeader = 0,
|
||||
|
||||
/// <summary>Reading the remaining-length variable-integer bytes.</summary>
|
||||
MqttStateFixedHeader,
|
||||
|
||||
/// <summary>Reading the variable-header + payload bytes of the current packet.</summary>
|
||||
MqttStateControlPacket,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Will
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// MQTT Will message parameters extracted from a CONNECT packet.
|
||||
/// Mirrors Go <c>mqttWill</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttWill
|
||||
{
|
||||
/// <summary>NATS subject derived from the MQTT will topic.</summary>
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Original MQTT will topic string.</summary>
|
||||
public string Topic { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Will message payload bytes, or <c>null</c> if empty.</summary>
|
||||
public byte[]? Msg { get; set; }
|
||||
|
||||
/// <summary>QoS level for the will message (0, 1, or 2).</summary>
|
||||
public byte Qos { get; set; }
|
||||
|
||||
/// <summary>Whether the will message should be retained.</summary>
|
||||
public bool Retain { get; set; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Connect protocol
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// MQTT CONNECT packet parsed payload.
|
||||
/// Mirrors Go <c>mqttConnectProto</c> struct in server/mqtt.go (extended with
|
||||
/// the fields surfaced by the parse helpers).
|
||||
/// </summary>
|
||||
internal sealed class MqttConnectProto
|
||||
{
|
||||
/// <summary>MQTT client identifier.</summary>
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Raw CONNECT packet bytes (for forwarding / replay).</summary>
|
||||
public byte[] Connect { get; set; } = [];
|
||||
|
||||
/// <summary>Parsed Will parameters, or <c>null</c> if the Will flag is not set.</summary>
|
||||
public MqttWill? Will { get; set; }
|
||||
|
||||
/// <summary>Username presented in the CONNECT packet.</summary>
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Password bytes presented in the CONNECT packet, or <c>null</c> if absent.</summary>
|
||||
public byte[]? Password { get; set; }
|
||||
|
||||
/// <summary>Whether the Clean Session flag was set.</summary>
|
||||
public bool CleanSession { get; set; }
|
||||
|
||||
/// <summary>Keep-alive interval in seconds (0 = disabled).</summary>
|
||||
public ushort KeepAlive { get; set; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Subscription
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// A single MQTT topic filter subscription entry stored in a session.
|
||||
/// Mirrors the per-entry semantics of <c>mqttSession.subs</c> map in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttSubscription
|
||||
{
|
||||
/// <summary>NATS subject derived from the MQTT topic filter.</summary>
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Maximum QoS level granted for this subscription.</summary>
|
||||
public byte Qos { get; set; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Publish info
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Parsed metadata for an inbound MQTT PUBLISH packet.
|
||||
/// Mirrors Go <c>mqttPublish</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttPublishInfo
|
||||
{
|
||||
/// <summary>NATS subject derived from the MQTT topic.</summary>
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Original MQTT topic string.</summary>
|
||||
public string Topic { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Message payload bytes, or <c>null</c> if empty.</summary>
|
||||
public byte[]? Msg { get; set; }
|
||||
|
||||
/// <summary>QoS level of the PUBLISH packet.</summary>
|
||||
public byte Qos { get; set; }
|
||||
|
||||
/// <summary>Whether the Retain flag is set.</summary>
|
||||
public bool Retain { get; set; }
|
||||
|
||||
/// <summary>Whether the DUP flag is set (re-delivery of a QoS >0 packet).</summary>
|
||||
public bool Dup { get; set; }
|
||||
|
||||
/// <summary>Packet identifier (only meaningful for QoS 1 and 2).</summary>
|
||||
public ushort Pi { get; set; }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pending ack
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Tracks a single in-flight QoS 1 or QoS 2 message pending acknowledgement.
|
||||
/// Mirrors Go <c>mqttPending</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttPending
|
||||
{
|
||||
/// <summary>JetStream stream sequence number for this message.</summary>
|
||||
public ulong SSeq { get; set; }
|
||||
|
||||
/// <summary>JetStream ACK subject to send the acknowledgement to.</summary>
|
||||
public string JsAckSubject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>JetStream durable consumer name.</summary>
|
||||
public string JsDur { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Retained message
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// A retained MQTT message stored in JetStream.
|
||||
/// Mirrors Go <c>mqttRetainedMsg</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttRetainedMsg
|
||||
{
|
||||
/// <summary>Origin server name.</summary>
|
||||
public string Origin { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>NATS subject for this retained message.</summary>
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Original MQTT topic.</summary>
|
||||
public string Topic { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Message payload bytes.</summary>
|
||||
public byte[]? Msg { get; set; }
|
||||
|
||||
/// <summary>Message flags byte.</summary>
|
||||
public byte Flags { get; set; }
|
||||
|
||||
/// <summary>Source identifier.</summary>
|
||||
public string Source { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Persisted session
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// The JSON-serialisable representation of an MQTT session stored in JetStream.
|
||||
/// Mirrors Go <c>mqttPersistedSession</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttPersistedSession
|
||||
{
|
||||
/// <summary>Server that originally created this session.</summary>
|
||||
public string Origin { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>MQTT client identifier.</summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this was a clean session.</summary>
|
||||
public bool Clean { get; set; }
|
||||
|
||||
/// <summary>Map of MQTT topic filters to granted QoS levels.</summary>
|
||||
public Dictionary<string, byte> Subs { get; set; } = new();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// In-memory MQTT session state.
|
||||
/// Mirrors Go <c>mqttSession</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttSession
|
||||
{
|
||||
private readonly Lock _mu = new();
|
||||
|
||||
/// <summary>Lock for this session (matches Go <c>sess.mu</c>).</summary>
|
||||
public Lock Mu => _mu;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Identity
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>MQTT client identifier.</summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Hash of the client identifier (used as JetStream key).</summary>
|
||||
public string IdHash { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Whether this is a clean session.</summary>
|
||||
public bool Clean { get; set; }
|
||||
|
||||
/// <summary>Domain token (domain with trailing '.', or empty).</summary>
|
||||
public string DomainTk { get; set; } = string.Empty;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Subscriptions
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Map from MQTT SUBSCRIBE filter to granted QoS level.
|
||||
/// Mirrors Go <c>mqttSession.subs map[string]byte</c>.
|
||||
/// </summary>
|
||||
public Dictionary<string, byte> Subs { get; } = new();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Pending acks
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Maximum number of in-flight QoS-1/2 PUBLISH acks.</summary>
|
||||
public ushort MaxPending { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In-flight QoS-1 PUBLISH packets pending PUBACK from the client.
|
||||
/// Key is the packet identifier.
|
||||
/// </summary>
|
||||
public Dictionary<ushort, MqttPending> PendingPublish { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// In-flight QoS-2 PUBREL packets pending PUBCOMP from the client.
|
||||
/// Key is the packet identifier.
|
||||
/// </summary>
|
||||
public Dictionary<ushort, MqttPending> PendingPubRel { get; } = new();
|
||||
|
||||
/// <summary>"Last used" packet identifier; used as the starting point when allocating the next one.</summary>
|
||||
public ushort LastPi { get; set; }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Constructor
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/// <summary>Initialises a new session with the given identity.</summary>
|
||||
public MqttSession(string id, string idHash, bool clean)
|
||||
{
|
||||
Id = id;
|
||||
IdHash = idHash;
|
||||
Clean = clean;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JSA stub
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Stub for the MQTT JetStream API helper.
|
||||
/// Mirrors Go <c>mqttJSA</c> struct in server/mqtt.go.
|
||||
/// All methods throw <see cref="NotImplementedException"/> until session 22 is complete.
|
||||
/// </summary>
|
||||
internal sealed class MqttJsa
|
||||
{
|
||||
/// <summary>Domain (with trailing '.'), or empty.</summary>
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Whether the domain field was explicitly set (even to empty).</summary>
|
||||
public bool DomainSet { get; set; }
|
||||
|
||||
// All methods are stubs — full implementation is deferred to session 22.
|
||||
public void SendAck(string ackSubject) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
public void SendMsg(string subject, byte[] msg) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
public void StoreMsgNoWait(string subject, int hdrLen, byte[] msg) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
public string PrefixDomain(string subject) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Account session manager stub
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Per-account MQTT session manager.
|
||||
/// Mirrors Go <c>mqttAccountSessionManager</c> struct in server/mqtt.go.
|
||||
/// All mutating methods are stubs.
|
||||
/// </summary>
|
||||
internal sealed class MqttAccountSessionManager
|
||||
{
|
||||
private readonly Lock _mu = new();
|
||||
|
||||
/// <summary>Domain token (domain with trailing '.'), or empty.</summary>
|
||||
public string DomainTk { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Active sessions keyed by MQTT client ID.</summary>
|
||||
public Dictionary<string, MqttSession> Sessions { get; } = new();
|
||||
|
||||
/// <summary>Sessions keyed by their client ID hash.</summary>
|
||||
public Dictionary<string, MqttSession> SessionsByHash { get; } = new();
|
||||
|
||||
/// <summary>Client IDs that are currently locked (being taken over).</summary>
|
||||
public HashSet<string> SessionsLocked { get; } = new();
|
||||
|
||||
/// <summary>Client IDs that have recently flapped (connected with duplicate ID).</summary>
|
||||
public Dictionary<string, long> Flappers { get; } = new();
|
||||
|
||||
/// <summary>JSA helper for this account.</summary>
|
||||
public MqttJsa Jsa { get; } = new();
|
||||
|
||||
/// <summary>Lock for this manager.</summary>
|
||||
public Lock Mu => _mu;
|
||||
|
||||
// All methods are stubs.
|
||||
public void HandleClosedClient(string clientId) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
public MqttSession? LookupSession(string clientId) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
public void PersistSession(MqttSession session) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
|
||||
public void DeleteSession(MqttSession session) =>
|
||||
throw new NotImplementedException("TODO: session 22");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Global session manager stub
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Server-wide MQTT session manager.
|
||||
/// Mirrors Go <c>mqttSessionManager</c> struct in server/mqtt.go.
|
||||
/// </summary>
|
||||
internal sealed class MqttSessionManager
|
||||
{
|
||||
private readonly Lock _mu = new();
|
||||
|
||||
/// <summary>Per-account session managers keyed by account name.</summary>
|
||||
public Dictionary<string, MqttAccountSessionManager> Sessions { get; } = new();
|
||||
|
||||
/// <summary>Lock for this manager.</summary>
|
||||
public Lock Mu => _mu;
|
||||
}
|
||||
@@ -18,8 +18,10 @@ using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
using ZB.MOM.NatsNet.Server.WebSocket;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
|
||||
using ZB.MOM.NatsNet.Server.WebSocket;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
@@ -213,21 +215,13 @@ public static class CompressionMode
|
||||
|
||||
// SrvGateway — full class is in Gateway/GatewayTypes.cs (session 16).
|
||||
|
||||
/// <summary>Stub for server websocket state (session 23).</summary>
|
||||
internal sealed class SrvWebsocket
|
||||
{
|
||||
public RefCountedUrlSet ConnectUrlsMap { get; set; } = new();
|
||||
}
|
||||
// SrvWebsocket — now fully defined in WebSocket/WebSocketTypes.cs (session 23).
|
||||
// OcspMonitor — now fully defined in Auth/Ocsp/OcspTypes.cs (session 23).
|
||||
// IOcspResponseCache — now fully defined in Auth/Ocsp/OcspTypes.cs (session 23).
|
||||
|
||||
/// <summary>Stub for server MQTT state (session 22).</summary>
|
||||
internal sealed class SrvMqtt { }
|
||||
|
||||
/// <summary>Stub for OCSP monitor (session 23).</summary>
|
||||
internal sealed class OcspMonitor { }
|
||||
|
||||
/// <summary>Stub for OCSP response cache (session 23).</summary>
|
||||
internal interface IOcspResponseCache { }
|
||||
|
||||
/// <summary>Stub for IP queue (session 02 — already ported as IpQueue).</summary>
|
||||
// IpQueue is already in session 02 internals — used here via object.
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2020-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/websocket.go in the NATS server Go source.
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.WebSocket;
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket opcode values as defined in RFC 6455 §5.2.
|
||||
/// Mirrors Go <c>wsOpCode</c> type in server/websocket.go.
|
||||
/// </summary>
|
||||
internal enum WsOpCode : int
|
||||
{
|
||||
Continuation = 0,
|
||||
Text = 1,
|
||||
Binary = 2,
|
||||
Close = 8,
|
||||
Ping = 9,
|
||||
Pong = 10,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket protocol constants.
|
||||
/// Mirrors the constant block at the top of server/websocket.go.
|
||||
/// </summary>
|
||||
internal static class WsConstants
|
||||
{
|
||||
// Frame header bits
|
||||
public const int FinalBit = 1 << 7;
|
||||
public const int Rsv1Bit = 1 << 6; // Used for per-message compression (RFC 7692)
|
||||
public const int Rsv2Bit = 1 << 5;
|
||||
public const int Rsv3Bit = 1 << 4;
|
||||
public const int MaskBit = 1 << 7;
|
||||
|
||||
// Frame size limits
|
||||
public const int MaxFrameHeaderSize = 14; // LeafNode may behave as a client
|
||||
public const int MaxControlPayloadSize = 125;
|
||||
public const int FrameSizeForBrowsers = 4096; // From experiment, browsers behave better with limited frame size
|
||||
public const int CompressThreshold = 64; // Don't compress for small buffer(s)
|
||||
public const int CloseStatusSize = 2;
|
||||
|
||||
// Close status codes (RFC 6455 §11.7)
|
||||
public const int CloseNormalClosure = 1000;
|
||||
public const int CloseGoingAway = 1001;
|
||||
public const int CloseProtocolError = 1002;
|
||||
public const int CloseUnsupportedData = 1003;
|
||||
public const int CloseNoStatusReceived = 1005;
|
||||
public const int CloseInvalidPayloadData = 1007;
|
||||
public const int ClosePolicyViolation = 1008;
|
||||
public const int CloseMessageTooBig = 1009;
|
||||
public const int CloseInternalError = 1011;
|
||||
public const int CloseTlsHandshake = 1015;
|
||||
|
||||
// Header strings
|
||||
public const string NoMaskingHeader = "Nats-No-Masking";
|
||||
public const string NoMaskingValue = "true";
|
||||
public const string XForwardedForHeader = "X-Forwarded-For";
|
||||
public const string PMCExtension = "permessage-deflate"; // per-message compression
|
||||
public const string PMCSrvNoCtx = "server_no_context_takeover";
|
||||
public const string PMCCliNoCtx = "client_no_context_takeover";
|
||||
public const string SecProtoHeader = "Sec-Websocket-Protocol";
|
||||
public const string MQTTSecProtoVal = "mqtt";
|
||||
public const string SchemePrefix = "ws";
|
||||
public const string SchemePrefixTls = "wss";
|
||||
}
|
||||
110
dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketTypes.cs
Normal file
110
dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketTypes.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright 2020-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/websocket.go in the NATS server Go source.
|
||||
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.WebSocket;
|
||||
|
||||
/// <summary>
|
||||
/// Per-connection WebSocket read state.
|
||||
/// Mirrors Go <c>wsReadInfo</c> struct in server/websocket.go.
|
||||
/// </summary>
|
||||
internal sealed class WsReadInfo
|
||||
{
|
||||
/// <summary>Whether masking is disabled for this connection (e.g. leaf node).</summary>
|
||||
public bool NoMasking { get; set; }
|
||||
|
||||
/// <summary>Whether per-message deflate compression is active.</summary>
|
||||
public bool Compressed { get; set; }
|
||||
|
||||
/// <summary>The current frame opcode.</summary>
|
||||
public WsOpCode FrameType { get; set; }
|
||||
|
||||
/// <summary>Number of payload bytes remaining in the current frame.</summary>
|
||||
public int PayloadLeft { get; set; }
|
||||
|
||||
/// <summary>The 4-byte masking key (only valid when masking is active).</summary>
|
||||
public int[] Mask { get; set; } = new int[4];
|
||||
|
||||
/// <summary>Current offset into <see cref="Mask"/>.</summary>
|
||||
public int MaskOffset { get; set; }
|
||||
|
||||
/// <summary>Accumulated compressed payload buffers awaiting decompression.</summary>
|
||||
public byte[]? Compress { get; set; }
|
||||
|
||||
public WsReadInfo() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-level WebSocket state, shared across all WebSocket connections.
|
||||
/// Mirrors Go <c>srvWebsocket</c> struct in server/websocket.go.
|
||||
/// Replaces the stub in NatsServerTypes.cs.
|
||||
/// </summary>
|
||||
internal sealed class SrvWebsocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks WebSocket connect URLs per server (ref-counted).
|
||||
/// Mirrors Go <c>connectURLsMap refCountedUrlSet</c>.
|
||||
/// </summary>
|
||||
public RefCountedUrlSet ConnectUrlsMap { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// TLS configuration for the WebSocket listener.
|
||||
/// Mirrors Go <c>tls bool</c> field (true if TLS is required).
|
||||
/// </summary>
|
||||
public System.Net.Security.SslServerAuthenticationOptions? TlsConfig { get; set; }
|
||||
|
||||
/// <summary>Whether per-message deflate compression is enabled globally.</summary>
|
||||
public bool Compression { get; set; }
|
||||
|
||||
/// <summary>Host the WebSocket server is listening on.</summary>
|
||||
public string Host { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Port the WebSocket server is listening on (may be ephemeral).</summary>
|
||||
public int Port { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles WebSocket upgrade and framing for a single connection.
|
||||
/// Mirrors the WebSocket-related methods on Go <c>client</c> in server/websocket.go.
|
||||
/// Full implementation is deferred to session 23.
|
||||
/// </summary>
|
||||
internal sealed class WebSocketHandler
|
||||
{
|
||||
private readonly NatsServer _server;
|
||||
|
||||
public WebSocketHandler(NatsServer server)
|
||||
{
|
||||
_server = server;
|
||||
}
|
||||
|
||||
/// <summary>Upgrades an HTTP connection to WebSocket protocol.</summary>
|
||||
public void UpgradeToWebSocket(
|
||||
System.IO.Stream stream,
|
||||
System.Net.Http.Headers.HttpRequestHeaders headers)
|
||||
=> throw new NotImplementedException("TODO: session 23 — websocket");
|
||||
|
||||
/// <summary>Parses a WebSocket frame from the given buffer slice.</summary>
|
||||
public void ParseFrame(byte[] data, int offset, int count)
|
||||
=> throw new NotImplementedException("TODO: session 23 — websocket");
|
||||
|
||||
/// <summary>Writes a WebSocket frame with the given payload.</summary>
|
||||
public void WriteFrame(WsOpCode opCode, byte[] payload, bool final, bool compress)
|
||||
=> throw new NotImplementedException("TODO: session 23 — websocket");
|
||||
|
||||
/// <summary>Writes a WebSocket close frame with the given status code and reason.</summary>
|
||||
public void WriteCloseFrame(int statusCode, string reason)
|
||||
=> throw new NotImplementedException("TODO: session 23 — websocket");
|
||||
}
|
||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
# NATS .NET Porting Status Report
|
||||
|
||||
Generated: 2026-02-26 21:23:40 UTC
|
||||
Generated: 2026-02-26 21:31:42 UTC
|
||||
|
||||
## Modules (12 total)
|
||||
|
||||
@@ -13,9 +13,8 @@ Generated: 2026-02-26 21:23:40 UTC
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 2851 |
|
||||
| complete | 3503 |
|
||||
| n_a | 77 |
|
||||
| not_started | 652 |
|
||||
| stub | 93 |
|
||||
|
||||
## Unit Tests (3257 total)
|
||||
@@ -36,4 +35,4 @@ Generated: 2026-02-26 21:23:40 UTC
|
||||
|
||||
## Overall Progress
|
||||
|
||||
**3439/6942 items complete (49.5%)**
|
||||
**4091/6942 items complete (58.9%)**
|
||||
|
||||
38
reports/report_e6bc76b.md
Normal file
38
reports/report_e6bc76b.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# NATS .NET Porting Status Report
|
||||
|
||||
Generated: 2026-02-26 21:31:42 UTC
|
||||
|
||||
## Modules (12 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 11 |
|
||||
| not_started | 1 |
|
||||
|
||||
## Features (3673 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 3503 |
|
||||
| n_a | 77 |
|
||||
| stub | 93 |
|
||||
|
||||
## Unit Tests (3257 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| complete | 319 |
|
||||
| n_a | 181 |
|
||||
| not_started | 2533 |
|
||||
| stub | 224 |
|
||||
|
||||
## Library Mappings (36 total)
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| mapped | 36 |
|
||||
|
||||
|
||||
## Overall Progress
|
||||
|
||||
**4091/6942 items complete (58.9%)**
|
||||
Reference in New Issue
Block a user