Add ParsePub, ParseSubsOrUnsubs, ProcessPub (QoS 0), ProcessSubs, ProcessUnsubs, EnqueueSubAck, and EnqueueUnsubAck to MqttPacketHandlers. Wire PUB/SUB/UNSUB dispatch cases in MqttParser. Add ReadSlice to MqttReader for raw payload extraction. 18 new unit tests covering parsing, flags, error cases, QoS downgrade, and full flow. 1 new integration test verifying SUBSCRIBE handshake over TCP.
181 lines
6.7 KiB
C#
181 lines
6.7 KiB
C#
// 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 mqttParse() in the NATS server Go source.
|
|
|
|
namespace ZB.MOM.NatsNet.Server.Mqtt;
|
|
|
|
/// <summary>
|
|
/// MQTT binary packet parser and dispatch.
|
|
/// Reads packets from a byte buffer using <see cref="MqttReader"/> and dispatches
|
|
/// to the appropriate handler based on packet type.
|
|
/// Mirrors Go <c>mqttParse()</c> in server/mqtt.go.
|
|
/// </summary>
|
|
internal static class MqttParser
|
|
{
|
|
/// <summary>PINGRESP packet bytes: 0xD0 0x00.</summary>
|
|
private static readonly byte[] PingRespPacket = [MqttPacket.PingResp, 0x00];
|
|
|
|
/// <summary>
|
|
/// Parses MQTT packets from <paramref name="buf"/> and dispatches to handlers.
|
|
/// Returns null on success, or an exception describing the parse/dispatch error.
|
|
/// Handles partial packets by saving state in the client's <see cref="MqttReader"/>.
|
|
/// Mirrors Go <c>mqttParse(r *mqttReader, c *client, ...)</c>.
|
|
/// </summary>
|
|
public static Exception? Parse(ClientConnection c, byte[] buf, int len)
|
|
{
|
|
var mqtt = c.Mqtt!;
|
|
var r = mqtt.Reader;
|
|
|
|
// Slice buffer to actual length if needed.
|
|
if (len < buf.Length)
|
|
{
|
|
var tmp = new byte[len];
|
|
Buffer.BlockCopy(buf, 0, tmp, 0, len);
|
|
buf = tmp;
|
|
}
|
|
|
|
r.Reset(buf);
|
|
|
|
var connected = (c.Flags & ClientFlags.ConnectReceived) != 0;
|
|
Exception? err = null;
|
|
|
|
while (err == null && r.HasMore())
|
|
{
|
|
r.PacketStart = r.Position;
|
|
|
|
// Read packet type + flags byte.
|
|
byte b;
|
|
try { b = r.ReadByte("packet type"); }
|
|
catch (Exception ex) { err = ex; break; }
|
|
|
|
var pt = (byte)(b & MqttPacket.Mask);
|
|
|
|
// CONNECT must be the first packet.
|
|
if (!connected && pt != MqttPacket.Connect)
|
|
{
|
|
err = new InvalidOperationException(
|
|
$"the first packet should be a CONNECT (0x{MqttPacket.Connect:X2}), got 0x{pt:X2}");
|
|
break;
|
|
}
|
|
|
|
// Read remaining length (variable-length encoding).
|
|
int pl;
|
|
bool complete;
|
|
try
|
|
{
|
|
(pl, complete) = r.ReadPacketLen();
|
|
}
|
|
catch (Exception ex) { err = ex; break; }
|
|
|
|
if (!complete)
|
|
break; // Partial packet — state saved in reader.
|
|
|
|
// Dispatch based on packet type.
|
|
switch (pt)
|
|
{
|
|
case MqttPacket.Pub:
|
|
var pubFlags = (byte)(b & MqttPacket.FlagMask);
|
|
var (pp, pubErr) = MqttPacketHandlers.ParsePub(r, pl, pubFlags, mqtt.RejectQoS2Pub);
|
|
if (pubErr != null) { err = pubErr; break; }
|
|
err = MqttPacketHandlers.ProcessPub(c, pp!);
|
|
break;
|
|
|
|
case MqttPacket.PubAck:
|
|
// TODO: Task 5 — process PUBACK
|
|
err = new NotImplementedException("PUBACK not yet implemented");
|
|
break;
|
|
|
|
case MqttPacket.PubRec:
|
|
// TODO: Task 5 — process PUBREC
|
|
err = new NotImplementedException("PUBREC not yet implemented");
|
|
break;
|
|
|
|
case MqttPacket.PubRel:
|
|
// TODO: Task 5 — process PUBREL
|
|
err = new NotImplementedException("PUBREL not yet implemented");
|
|
break;
|
|
|
|
case MqttPacket.PubComp:
|
|
// TODO: Task 5 — process PUBCOMP
|
|
err = new NotImplementedException("PUBCOMP not yet implemented");
|
|
break;
|
|
|
|
case MqttPacket.Sub:
|
|
var (subPi, subFilters, subErr) = MqttPacketHandlers.ParseSubsOrUnsubs(r, b, pl, isSub: true);
|
|
if (subErr != null) { err = subErr; break; }
|
|
err = MqttPacketHandlers.ProcessSubs(c, subPi, subFilters!);
|
|
break;
|
|
|
|
case MqttPacket.Unsub:
|
|
var (unsubPi, unsubFilters, unsubErr) = MqttPacketHandlers.ParseSubsOrUnsubs(r, b, pl, isSub: false);
|
|
if (unsubErr != null) { err = unsubErr; break; }
|
|
err = MqttPacketHandlers.ProcessUnsubs(c, unsubPi, unsubFilters!);
|
|
break;
|
|
|
|
case MqttPacket.Ping:
|
|
HandlePingReq(c);
|
|
break;
|
|
|
|
case MqttPacket.Connect:
|
|
if (connected)
|
|
{
|
|
err = new InvalidOperationException("second CONNECT packet not allowed");
|
|
break;
|
|
}
|
|
var (rc, cp, parseErr) = MqttPacketHandlers.ParseConnect(r);
|
|
if (parseErr != null)
|
|
{
|
|
// Send CONNACK with error code if we have one, then close.
|
|
if (rc != MqttConnAckRc.Accepted)
|
|
MqttPacketHandlers.EnqueueConnAck(c, rc, false);
|
|
err = parseErr;
|
|
break;
|
|
}
|
|
if (rc != MqttConnAckRc.Accepted)
|
|
{
|
|
MqttPacketHandlers.EnqueueConnAck(c, rc, false);
|
|
err = new InvalidOperationException($"CONNECT rejected with code 0x{rc:X2}");
|
|
break;
|
|
}
|
|
err = MqttPacketHandlers.ProcessConnect(c, cp!);
|
|
if (err == null)
|
|
connected = true;
|
|
break;
|
|
|
|
case MqttPacket.Disconnect:
|
|
MqttPacketHandlers.HandleDisconnect(c);
|
|
return null; // Connection closed, exit parse loop.
|
|
|
|
default:
|
|
err = new InvalidOperationException($"unknown MQTT packet type: 0x{pt:X2}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles PINGREQ by enqueueing a PINGRESP packet.
|
|
/// Mirrors Go <c>mqttEnqueuePingResp()</c>.
|
|
/// </summary>
|
|
private static void HandlePingReq(ClientConnection c)
|
|
{
|
|
lock (c)
|
|
{
|
|
c.EnqueueProto(PingRespPacket);
|
|
}
|
|
}
|
|
}
|