diff --git a/Directory.Build.props b/Directory.Build.props index 20327e5b..06033624 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ latest enable enable - + @@ -21,18 +21,18 @@ 1.4.0 - - - + + + - - https://github.com/Aaronontheweb/TurboMqtt - - true - - true - snupkg - + + https://github.com/Aaronontheweb/TurboMqtt + + true + + true + snupkg + \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index a0d555ae..d8e9df7d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,9 +1,13 @@ -By accessing code under the [TurboMQTT GitHub Organization](https://github.com/Aaronontheweb/TurboMqtt) (Petabridge, LLC) here, you are agreeing to the following licensing terms. If you do not agree to these terms, do not access TurboMQTT code. +By accessing code under the [TurboMQTT GitHub Organization](https://github.com/Aaronontheweb/TurboMqtt) (Petabridge, +LLC) here, you are agreeing to the following licensing terms. If you do not agree to these terms, do not access +TurboMQTT code. -Your license to TurboMQTT source code and/or binaries is governed by the Reciprocal Public License 1.5 (RPL1.5) license as described here: +Your license to TurboMQTT source code and/or binaries is governed by the Reciprocal Public License 1.5 (RPL1.5) license +as described here: https://opensource.org/license/rpl-1-5/ -If you do not wish to release the source of software you build using TurboMQTT source code and/or binaries under the terms above, you may use TurboMQTT source code and/or binaries under the License Agreement described here: +If you do not wish to release the source of software you build using TurboMQTT source code and/or binaries under the +terms above, you may use TurboMQTT source code and/or binaries under the License Agreement described here: https://sdkbin.com/publisher/petabridge/product/phobos#license \ No newline at end of file diff --git a/README.md b/README.md index 0c15d992..389b9482 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,5 @@ This is a simple template designed to incorporate local [Akka.NET](https://getakka.net/) into a console application. -See https://github.com/akkadotnet/akkadotnet-templates/blob/dev/docs/ConsoleTemplate.md for complete and current documentation on this template. \ No newline at end of file +See https://github.com/akkadotnet/akkadotnet-templates/blob/dev/docs/ConsoleTemplate.md for complete and current +documentation on this template. \ No newline at end of file diff --git a/TurboMqtt.sln b/TurboMqtt.sln index 99c68e5f..a8e44b35 100644 --- a/TurboMqtt.sln +++ b/TurboMqtt.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TurboMqtt.App", "src\TurboMqtt.App\TurboMqtt.App.csproj", "{EDFEA62C-AF53-4056-A2F2-E37CEA04B24A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TurboMqtt.Core", "src\TurboMqtt.Core\TurboMqtt.Core.csproj", "{BE905781-3D96-44F5-A230-E06FD5213C1C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -14,9 +14,9 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EDFEA62C-AF53-4056-A2F2-E37CEA04B24A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EDFEA62C-AF53-4056-A2F2-E37CEA04B24A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EDFEA62C-AF53-4056-A2F2-E37CEA04B24A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EDFEA62C-AF53-4056-A2F2-E37CEA04B24A}.Release|Any CPU.Build.0 = Release|Any CPU + {BE905781-3D96-44F5-A230-E06FD5213C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE905781-3D96-44F5-A230-E06FD5213C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE905781-3D96-44F5-A230-E06FD5213C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE905781-3D96-44F5-A230-E06FD5213C1C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/TurboMqtt.sln.DotSettings b/TurboMqtt.sln.DotSettings new file mode 100644 index 00000000..f019c7dc --- /dev/null +++ b/TurboMqtt.sln.DotSettings @@ -0,0 +1,6 @@ + + ----------------------------------------------------------------------- +<copyright file="${File.FileName}" company="Petabridge, LLC"> + Copyright (C) ${File.CreatedYear} - ${CurrentDate.Year} Petabridge, LLC <https://petabridge.com> +</copyright> +----------------------------------------------------------------------- \ No newline at end of file diff --git a/src/TurboMqtt.App/HelloActor.cs b/src/TurboMqtt.App/HelloActor.cs deleted file mode 100644 index 9861a1fc..00000000 --- a/src/TurboMqtt.App/HelloActor.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace TurboMqtt.App; - -public class HelloActor : ReceiveActor -{ - private readonly ILoggingAdapter _log = Context.GetLogger(); - private int _helloCounter = 0; - - public HelloActor() - { - Receive(message => - { - _log.Info("{0} {1}", message, _helloCounter++); - }); - } -} \ No newline at end of file diff --git a/src/TurboMqtt.App/Program.cs b/src/TurboMqtt.App/Program.cs deleted file mode 100644 index afb39cc2..00000000 --- a/src/TurboMqtt.App/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Akka.Hosting; -using TurboMqtt.App; -using Microsoft.Extensions.Hosting; - -var hostBuilder = new HostBuilder(); - -hostBuilder.ConfigureServices((context, services) => -{ - services.AddAkka("MyActorSystem", (builder, sp) => - { - builder - .WithActors((system, registry, resolver) => - { - var helloActor = system.ActorOf(Props.Create(() => new HelloActor()), "hello-actor"); - registry.Register(helloActor); - }) - .WithActors((system, registry, resolver) => - { - var timerActorProps = - resolver.Props(); // uses Msft.Ext.DI to inject reference to helloActor - var timerActor = system.ActorOf(timerActorProps, "timer-actor"); - registry.Register(timerActor); - }); - }); -}); - -var host = hostBuilder.Build(); - -await host.RunAsync(); \ No newline at end of file diff --git a/src/TurboMqtt.App/TimerActor.cs b/src/TurboMqtt.App/TimerActor.cs deleted file mode 100644 index 121c7018..00000000 --- a/src/TurboMqtt.App/TimerActor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Akka.Hosting; - -namespace TurboMqtt.App; - -public class TimerActor : ReceiveActor, IWithTimers -{ - private readonly IActorRef _helloActor; - - public TimerActor(IRequiredActor helloActor) - { - _helloActor = helloActor.ActorRef; - Receive(message => - { - _helloActor.Tell(message); - }); - } - - protected override void PreStart() - { - Timers.StartPeriodicTimer("hello-key", "hello", TimeSpan.FromSeconds(1)); - } - - public ITimerScheduler Timers { get; set; } = null!; // gets set by Akka.NET -} \ No newline at end of file diff --git a/src/TurboMqtt.App/TurboMqtt.App.csproj b/src/TurboMqtt.App/TurboMqtt.App.csproj deleted file mode 100644 index 3ec2860f..00000000 --- a/src/TurboMqtt.App/TurboMqtt.App.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - - - diff --git a/src/TurboMqtt.App/Usings.cs b/src/TurboMqtt.App/Usings.cs deleted file mode 100644 index 15066e87..00000000 --- a/src/TurboMqtt.App/Usings.cs +++ /dev/null @@ -1,2 +0,0 @@ -global using Akka.Actor; -global using Akka.Event; \ No newline at end of file diff --git a/src/TurboMqtt.Core/ControlPacketHeaders.cs b/src/TurboMqtt.Core/ControlPacketHeaders.cs new file mode 100644 index 00000000..a71ee59f --- /dev/null +++ b/src/TurboMqtt.Core/ControlPacketHeaders.cs @@ -0,0 +1,34 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core; + +/// +/// The type of MQTT packet. +/// +/// +/// Aligns to the MQTT 3.1.1 specification: https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html +/// +/// Also supports MQTT 5.0. +/// +public enum MqttPacketType +{ + Connect = 0x10, + ConnAck = 0x20, + Publish = 0x30, + PubAck = 0x40, + PubRec = 0x50, + PubRel = 0x60, + PubComp = 0x70, + Subscribe = 0x80, + SubAck = 0x90, + Unsubscribe = 0xA0, + UnsubAck = 0xB0, + PingReq = 0xC0, + PingResp = 0xD0, + Disconnect = 0xE0, + Auth = 0xF0 +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/MqttPubAckReasonCode.cs b/src/TurboMqtt.Core/MqttPubAckReasonCode.cs new file mode 100644 index 00000000..fb7f34ef --- /dev/null +++ b/src/TurboMqtt.Core/MqttPubAckReasonCode.cs @@ -0,0 +1,44 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core; + +/// +/// All possible reason codes for the PubAck packet. +/// +public enum MqttPubAckReasonCode +{ + Success = 0x00, + NoMatchingSubscribers = 0x10, + UnspecifiedError = 0x80, + ImplementationSpecificError = 0x83, + NotAuthorized = 0x87, + TopicNameInvalid = 0x90, + PacketIdentifierInUse = 0x91, + QuotaExceeded = 0x97, + PayloadFormatInvalid = 0x99 +} + +// add a static helper method that can turn a MqttPubAckReason code into a hard-coded string representation +internal static class MqttPubAckHelpers +{ + public static string ReasonCodeToString(MqttPubAckReasonCode reasonCode) + { + return reasonCode switch + { + MqttPubAckReasonCode.Success => "Success", + MqttPubAckReasonCode.NoMatchingSubscribers => "NoMatchingSubscribers", + MqttPubAckReasonCode.UnspecifiedError => "UnspecifiedError", + MqttPubAckReasonCode.ImplementationSpecificError => "ImplementationSpecificError", + MqttPubAckReasonCode.NotAuthorized => "NotAuthorized", + MqttPubAckReasonCode.TopicNameInvalid => "TopicNameInvalid", + MqttPubAckReasonCode.PacketIdentifierInUse => "PacketIdentifierInUse", + MqttPubAckReasonCode.QuotaExceeded => "QuotaExceeded", + MqttPubAckReasonCode.PayloadFormatInvalid => "PayloadFormatInvalid", + _ => throw new ArgumentOutOfRangeException(nameof(reasonCode), reasonCode, null) + }; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/NonZeroUInt32.cs b/src/TurboMqtt.Core/NonZeroUInt32.cs new file mode 100644 index 00000000..04130644 --- /dev/null +++ b/src/TurboMqtt.Core/NonZeroUInt32.cs @@ -0,0 +1,31 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core; + +/// +/// Subscription and packet identifiers must be greater than 0. +/// +public readonly struct NonZeroUInt32 +{ + /// + /// The value of the identifier. + /// + public uint Value { get; } + + public NonZeroUInt32(uint value) + { + if (value == 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value must be greater than 0."); + } + + Value = value; + } + + public static implicit operator uint(NonZeroUInt32 value) => value.Value; + public static implicit operator NonZeroUInt32(uint value) => new(value); +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/AuthPacket.cs b/src/TurboMqtt.Core/PacketTypes/AuthPacket.cs new file mode 100644 index 00000000..76de7f8e --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/AuthPacket.cs @@ -0,0 +1,76 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used for authentication exchange or error reporting concerning authentication. +/// +/// +/// This packet is only applicable in MQTT 5.0 and is used both in the initial connection phase and for dynamic re-authentication. +/// +public sealed class AuthPacket(string authenticationMethod, AuthReasonCode reasonCode) : MqttPacket +{ + // turn the reason code into a ReasonString + + public override MqttPacketType PacketType => MqttPacketType.Auth; + + /// + /// The Reason Code for the AUTH packet, which indicates the status of the authentication or any authentication errors. + /// + public AuthReasonCode ReasonCode { get; } = reasonCode; + + // MQTT 5.0 - Optional Properties + /// + /// Authentication Method, used to specify the method of authentication. + /// + public string AuthenticationMethod { get; } = authenticationMethod; // Required if Auth Packet is used + + /// + /// Authentication Data, typically containing credentials or challenge/response data, depending on the auth method. + /// + public ReadOnlyMemory AuthenticationData { get; set; } + + /// + /// User Properties, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } + + /// + /// Reason String providing additional information about the authentication status. + /// + public string? ReasonString { get; set; } = reasonCode.ToReasonString(); + + public override string ToString() + { + return $"Auth: [ReasonCode={ReasonCode}]"; + } +} + +/// +/// Enumerates the reason codes applicable to the AUTH packet in MQTT 5.0. +/// +public enum AuthReasonCode +{ + Success = 0x00, + ContinueAuthentication = 0x18, + ReAuthenticate = 0x19 +} + +internal static class AuthReasonCodeHelpers +{ + public static string ToReasonString(this AuthReasonCode reasonCode) + { + return reasonCode switch + { + AuthReasonCode.Success => "Success", + AuthReasonCode.ContinueAuthentication => "Continue Authentication", + AuthReasonCode.ReAuthenticate => "Re-Authenticate", + _ => "Unknown Reason Code" + }; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/ConnAckPacket.cs b/src/TurboMqtt.Core/PacketTypes/ConnAckPacket.cs new file mode 100644 index 00000000..fd235b35 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/ConnAckPacket.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used by the broker to acknowledge a connection request from a client. +/// +public sealed class ConnAckPacket : MqttPacket +{ + public override MqttPacketType PacketType => MqttPacketType.ConnAck; + + public bool SessionPresent { get; set; } + public ConnAckReasonCode ReasonCode { get; set; } // Enum defined below + + // MQTT 5.0 - Optional Properties + public IReadOnlyDictionary? Properties { get; set; } + + public override string ToString() + { + return $"ConnAck: [SessionPresent={SessionPresent}] [ReasonCode={ReasonCode}]"; + } +} + +public enum ConnAckReasonCode +{ + Success = 0x00, + UnspecifiedError = 0x80, + MalformedPacket = 0x81, + ProtocolError = 0x82, + ImplementationSpecificError = 0x83, + UnsupportedProtocolVersion = 0x84, + ClientIdentifierNotValid = 0x85, + BadUsernameOrPassword = 0x86, + NotAuthorized = 0x87, + ServerUnavailable = 0x88, + ServerBusy = 0x89, + Banned = 0x8A, + BadAuthenticationMethod = 0x8C, + TopicNameInvalid = 0x90, + PacketTooLarge = 0x95, + QuotaExceeded = 0x97, + PayloadFormatInvalid = 0x99, + RetainNotSupported = 0x9A, + QoSNotSupported = 0x9B, + UseAnotherServer = 0x9C, + ServerMoved = 0x9D, + ConnectionRateExceeded = 0x9F +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/ConnectPacket.cs b/src/TurboMqtt.Core/PacketTypes/ConnectPacket.cs new file mode 100644 index 00000000..6a3a59a3 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/ConnectPacket.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used to initiate a connection to the MQTT broker. +/// +public class ConnectPacket(string clientId) : MqttPacket +{ + public override MqttPacketType PacketType => MqttPacketType.Connect; + + public string ClientId { get; } = clientId; + public bool CleanSession { get; set; } + public ushort KeepAlive { get; set; } + + // MQTT 5.0 - Optional Properties + public string? Username { get; set; } + public string? Password { get; set; } + + public bool? WillFlag { get; set; } + public string? WillTopic { get; set; } + public ReadOnlyMemory? WillMessage { get; set; } + public QualityOfService? WillQos { get; set; } + public bool? WillRetain { get; set; } + + public IReadOnlyDictionary? + Properties { get; set; } // Custom properties like Session Expiry Interval, Maximum Packet Size, etc. + + public override string ToString() + { + return $"Connect: [ClientId={ClientId}] [CleanSession={CleanSession}] [KeepAlive={KeepAlive}]"; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/DisconnectPacket.cs b/src/TurboMqtt.Core/PacketTypes/DisconnectPacket.cs new file mode 100644 index 00000000..da66a009 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/DisconnectPacket.cs @@ -0,0 +1,74 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used by a client to indicate that it will disconnect cleanly, or by the server to notify of a disconnect. +/// +public sealed class DisconnectPacket : MqttPacket +{ + public override MqttPacketType PacketType => MqttPacketType.Disconnect; + + // MQTT 5.0 - Optional Reason Code and Properties + public DisconnectReasonCode? ReasonCode { get; set; } // MQTT 5.0 only + + /// + /// User Properties, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + /// + /// The Server Reference property, available in MQTT 5.0. + /// This optional property can suggest another server for the client to use. + /// + public string? ServerReference { get; set; } // MQTT 5.0 only + + /// + /// Session Expiry Interval, available in MQTT 5.0. + /// This optional property can indicate the session expiry interval in seconds when the disconnect is initiated. + /// + public uint? SessionExpiryInterval { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return $"Disconnect: [ReasonCode={ReasonCode}]"; + } +} + +public enum DisconnectReasonCode +{ + NormalDisconnection = 0x00, + DisconnectWithWillMessage = 0x04, + UnspecifiedError = 0x80, + MalformedPacket = 0x81, + ProtocolError = 0x82, + ImplementationSpecificError = 0x83, + NotAuthorized = 0x87, + ServerBusy = 0x89, + ServerShuttingDown = 0x8B, + KeepAliveTimeout = 0x8D, + SessionTakenOver = 0x8E, + TopicFilterInvalid = 0x8F, + TopicNameInvalid = 0x90, + ReceiveMaximumExceeded = 0x93, + TopicAliasInvalid = 0x94, + PacketTooLarge = 0x95, + MessageRateTooHigh = 0x96, + QuotaExceeded = 0x97, + AdministrativeAction = 0x98, + PayloadFormatInvalid = 0x99, + RetainNotSupported = 0x9A, + QoSNotSupported = 0x9B, + UseAnotherServer = 0x9C, + ServerMoved = 0x9D, + SharedSubscriptionsNotSupported = 0x9E, + ConnectionRateExceeded = 0x9F, + MaximumConnectTime = 0xA0, + SubscriptionIdentifiersNotSupported = 0xA1, + WildcardSubscriptionsNotSupported = 0xA2 +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/MqttPacket.cs b/src/TurboMqtt.Core/PacketTypes/MqttPacket.cs new file mode 100644 index 00000000..5636482e --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/MqttPacket.cs @@ -0,0 +1,41 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Base for all MQTT packets. +/// +public abstract class MqttPacket +{ + public abstract MqttPacketType PacketType { get; } + + public virtual bool Duplicate => false; + + public virtual QualityOfService QualityOfService => QualityOfService.AtMostOnce; + + public virtual bool RetainRequested => false; + + public override string ToString() + { + return + $"{GetType().Name}[Type={PacketType}, QualityOfService={QualityOfService}, Duplicate={Duplicate}, Retain={RetainRequested}]"; + } +} + +/// +/// Base for MQTT packets that require a packet identifier. +/// +public abstract class MqttPacketWithId : MqttPacket +{ + /// + /// The unique identifier assigned to the packet. + /// + /// + /// Not all packets require an identifier. + /// + public NonZeroUInt32 PacketId { get; set; } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/PingReqPacket.cs b/src/TurboMqtt.Core/PacketTypes/PingReqPacket.cs new file mode 100644 index 00000000..2ba1015b --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/PingReqPacket.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Packet sent to the client by the server in response to a . +/// +/// +/// Used to keep the connection alive. +/// +public sealed class PingReqPacket : MqttPacket +{ + public static readonly PingReqPacket Instance = new PingReqPacket(); + + private PingReqPacket() + { + } + + public override MqttPacketType PacketType => MqttPacketType.PingReq; + + public override string ToString() + { + return "PingReq"; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/PingRespPacket.cs b/src/TurboMqtt.Core/PacketTypes/PingRespPacket.cs new file mode 100644 index 00000000..960ea03e --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/PingRespPacket.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Packet sent to the client by the server in response to a . +/// +public sealed class PingRespPacket : MqttPacket +{ + public static readonly PingRespPacket Instance = new PingRespPacket(); + + private PingRespPacket() + { + } + + public override MqttPacketType PacketType => MqttPacketType.PingResp; + + public override string ToString() + { + return "PingResp"; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/PubCompPacket.cs b/src/TurboMqtt.Core/PacketTypes/PubCompPacket.cs new file mode 100644 index 00000000..b2aca211 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/PubCompPacket.cs @@ -0,0 +1,47 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used to acknowledge the receipt of a from the client. +/// This is the final packet in the QoS 2 message flow. +/// +public sealed class PubCompPacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.PubComp; + + // MQTT 5.0 - Optional Reason Code and Properties + /// + /// The Reason Code for the PUBCOMP, available in MQTT 5.0. + /// + public PubCompReasonCode? ReasonCode { get; set; } // MQTT 5.0 only + + /// + /// The Reason String for the PUBREC, available in MQTT 5.0. + /// + public string ReasonString { get; set; } = string.Empty; + + /// + /// User Properties, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return $"PubComp: [PacketIdentifier={PacketId}], [ReasonCode={ReasonCode}], [ReasonString={ReasonString}]"; + } +} + +/// +/// Enum for PUBCOMP reason codes, using the same as PUBREC for simplicity and because MQTT 5.0 reuses these +/// +public enum PubCompReasonCode +{ + Success = 0x00, + PacketIdentifierNotFound = 0x92 +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/PubRecPacket.cs b/src/TurboMqtt.Core/PacketTypes/PubRecPacket.cs new file mode 100644 index 00000000..eeeaaa67 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/PubRecPacket.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used to acknowledge the receipt of a Publish packet with . +/// This packet type is part of the QoS 2 message flow. +/// +public sealed class PubRecPacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.PubRec; + + // MQTT 5.0 - Optional Reason Code and Properties + /// + /// The Reason Code for the PUBREC, available in MQTT 5.0. + /// + public PubRecReasonCode? ReasonCode { get; set; } // MQTT 5.0 only + + /// + /// The Reason String for the PUBREC, available in MQTT 5.0. + /// + public string ReasonString { get; set; } = string.Empty; + + /// + /// User Properties, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return $"PubRec: [PacketIdentifier={PacketId}], [ReasonCode={ReasonCode}], [ReasonString={ReasonString}]"; + } +} + +/// +/// Enum for PUBREC and PUBCOMP reason codes (as they share the same codes) +/// +public enum PubRecReasonCode +{ + Success = 0x00, + NoMatchingSubscribers = 0x10, + UnspecifiedError = 0x80, + ImplementationSpecificError = 0x83, + NotAuthorized = 0x87, + TopicNameInvalid = 0x90, + PacketIdentifierInUse = 0x91, + QuotaExceeded = 0x97, + PayloadFormatInvalid = 0x99 +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/PubRelPacket.cs b/src/TurboMqtt.Core/PacketTypes/PubRelPacket.cs new file mode 100644 index 00000000..786c041a --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/PubRelPacket.cs @@ -0,0 +1,47 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used to acknowledge the receipt of a from the broker. +/// This packet type is part of the QoS 2 message flow. +/// +public sealed class PubRelPacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.PubRel; + + // MQTT 5.0 - Optional Reason Code and Properties + /// + /// The Reason Code for the PUBREL, available in MQTT 5.0. + /// + public PubRelReasonCode? ReasonCode { get; set; } // MQTT 5.0 only + + /// + /// The Reason String for the PUBREC, available in MQTT 5.0. + /// + public string ReasonString { get; set; } = string.Empty; + + /// + /// User Properties, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return $"PubRel: [PacketIdentifier={PacketId}], [ReasonCode={ReasonCode}], [ReasonString={ReasonString}]"; + } +} + +/// +/// Enum for PUBREL reason codes (typically these would be simpler as successful flow is usually assumed) +/// +public enum PubRelReasonCode +{ + Success = 0x00, + PacketIdentifierNotFound = 0x92 +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/PublishAckPacket.cs b/src/TurboMqtt.Core/PacketTypes/PublishAckPacket.cs new file mode 100644 index 00000000..7e05d58a --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/PublishAckPacket.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +using static MqttPubAckHelpers; + +/// +/// Used to acknowledge the receipt of a Publish packet. +/// +public sealed class PublishAckPacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.PubAck; + + // MQTT 5.0 Optional Properties + + /// + /// Reason Code for the acknowledgment, available in MQTT 5.0. + /// This field is optional and provides more detailed acknowledgment information. + /// + public MqttPubAckReasonCode ReasonCode { get; set; } + + /// + /// User Properties, available in MQTT 5.0. + /// These are key-value pairs that can be sent to provide additional information in the acknowledgment. + /// + public string ReasonString => ReasonCodeToString(ReasonCode); + + public override string ToString() + { + return $"PubAck: [PacketIdentifier={PacketId}] [ReasonCode={ReasonCode}]"; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/PublishPacket.cs b/src/TurboMqtt.Core/PacketTypes/PublishPacket.cs new file mode 100644 index 00000000..639d3640 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/PublishPacket.cs @@ -0,0 +1,71 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used to send data to the server or client. +/// +/// The delivery guarantee for this packet. +/// Is this packet a duplicate? +/// Indicates whether or not this value has been retained by the MQTT broker. +public sealed class PublishPacket(QualityOfService Qos, bool Duplicate, bool RetainRequested) : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.Publish; + + public override bool Duplicate { get; } = Duplicate; + + public override QualityOfService QualityOfService { get; } = Qos; + + public override bool RetainRequested { get; } = RetainRequested; + + /// + /// Optional for + /// + public string? TopicName { get; set; } + + // Payload + public ReadOnlyMemory Payload { get; set; } = ReadOnlyMemory.Empty; + + // MQTT 3.1.1 and 5.0 - Optional Properties + + /// + /// The Content Type property, available in MQTT 5.0. + /// This property is optional and indicates the MIME type of the application message. + /// + public string? ContentType { get; set; } // MQTT 5.0 only + + /// + /// Response Topic property, available in MQTT 5.0. + /// It specifies the topic name for a response message. + /// + public string? ResponseTopic { get; set; } // MQTT 5.0 only + + /// + /// Correlation Data property, available in MQTT 5.0. + /// This property is used by the sender of the request message to identify which request the response message is for when it receives a response. + /// + public ReadOnlyMemory? CorrelationData { get; set; } // MQTT 5.0 only + + /// + /// User Property, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + /// + /// Subscription Identifiers, available in MQTT 5.0. + /// This property allows associating the publication with multiple subscriptions. + /// Each identifier corresponds to a different subscription that matches the published message. + /// + public IReadOnlyList? SubscriptionIdentifiers { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return + $"Publish: [Topic={TopicName}] [PayloadLength={Payload.Length}] [QoSLevel={QualityOfService}] [Dup={Duplicate}] [Retain={RetainRequested}] [PacketIdentifier={PacketId}]"; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/SubscribeAckPacket.cs b/src/TurboMqtt.Core/PacketTypes/SubscribeAckPacket.cs new file mode 100644 index 00000000..8d581d95 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/SubscribeAckPacket.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +public enum MqttSubscribeReasonCode +{ + // Common reason codes in MQTT 3.1.1 and earlier versions (implicitly used, typically not explicitly specified in these versions) + GrantedQoS0 = 0x00, // Maximum QoS 0, MQTT 3.0, 3.1.1 + GrantedQoS1 = 0x01, // Maximum QoS 1, MQTT 3.0, 3.1.1 + GrantedQoS2 = 0x02, // Maximum QoS 2, MQTT 3.0, 3.1.1 + + // MQTT 5.0 specific reason codes + UnspecifiedError = 0x80, // MQTT 5.0 + ImplementationSpecificError = 0x83, // MQTT 5.0 + NotAuthorized = 0x87, // MQTT 5.0 + TopicFilterInvalid = 0x8F, // MQTT 5.0 + PacketIdentifierInUse = 0x91, // MQTT 5.0 + QuotaExceeded = 0x97, // MQTT 5.0 + SharedSubscriptionsNotSupported = 0x9E, // MQTT 5.0 + SubscriptionIdentifiersNotSupported = 0xA1, // MQTT 5.0 + WildcardSubscriptionsNotSupported = 0xA2, // MQTT 5.0 +} + +public class SubscribeAckPacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.SubAck; + + /// + /// The reason codes for each topic subscription. + /// + public IReadOnlyList ReasonCodes { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/SubscribePacket.cs b/src/TurboMqtt.Core/PacketTypes/SubscribePacket.cs new file mode 100644 index 00000000..41d7610d --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/SubscribePacket.cs @@ -0,0 +1,135 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +public sealed class SubscribePacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.Subscribe; + + /// + /// The unique identity of this subscription for this client. + /// + /// + /// Must be a value greater than 0. + /// + public NonZeroUInt32 SubscriptionIdentifier { get; set; } + + /// + /// The set of topics we're subscribing to. + /// + public IReadOnlyList Topics { get; set; } = Array.Empty(); + + /// + /// User Property, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return + $"Subscribe: [PacketIdentifier={PacketId}] [SubscriptionIdentifier={SubscriptionIdentifier}] [Topics={string.Join(", ", Topics.Select(c => c))}]"; + } +} + +public sealed class TopicSubscription(string topic) +{ + /// + /// The topic to subscribe to. + /// + public string Topic { get; } = topic; + + /// + /// The subscription options - QoS, No Local, Retain As Published, Retain Handling. + /// + /// + /// Some of these are MQTT 5.0 features and will not be used in MQTT 3.1.1 or 3.0. + /// + public SubscriptionOptions Options { get; set; } + + public override string ToString() + { + return $"Topic: {Topic}, Options: {Options}"; + } +} + +public enum RetainHandlingOption +{ + SendAtSubscribe = 0, // 00 binary + SendAtSubscribeIfNew = 1, // 01 binary + DoNotSendAtSubscribe = 2 // 10 binary +} + +public struct SubscriptionOptions +{ + /// + /// Gets or sets the Quality of Service level to use when sending messages to the client. + /// + public QualityOfService QoS { get; set; } + + /// + /// MQTT 5.0 Feature: indicates whether or not the sender can receive its own messages. + /// + public bool NoLocal { get; set; } + + /// + /// MQTT 5.0 Feature: indicates whether or not the message should be retained by the broker. + /// + public bool RetainAsPublished { get; set; } + + /// + /// MQTT 5.0 Feature: indicates how the broker should handle retained messages. + /// + public RetainHandlingOption RetainHandling { get; set; } + + public override string ToString() + { + return + $"QoS: {QoS}, No Local: {NoLocal}, Retain As Published: {RetainAsPublished}, Retain Handling: {RetainHandling}"; + } +} + +internal static class SubscriptionOptionsHelpers +{ + public static byte ToByte(this SubscriptionOptions subscriptionOptions) + { + byte result = 0; + + // Set the QoS bits (bit 0 and 1) + result |= (byte)subscriptionOptions.QoS; + + // Set the No Local bit (bit 2) + if (subscriptionOptions.NoLocal) + { + result |= 1 << 2; + } + + // Set the Retain As Published bit (bit 3) + if (subscriptionOptions.RetainAsPublished) + { + result |= 1 << 3; + } + + // Set the Retain Handling bits (bit 4 and 5) + result |= (byte)((int)subscriptionOptions.RetainHandling << 4); + + return result; + } + + public static SubscriptionOptions ToSubscriptionOptions(this byte subscriptionOptions) + { + var result = new SubscriptionOptions + { + QoS = (QualityOfService)(subscriptionOptions & 0b11), + NoLocal = (subscriptionOptions & (1 << 2)) != 0, + RetainAsPublished = (subscriptionOptions & (1 << 3)) != 0, + RetainHandling = (RetainHandlingOption)((subscriptionOptions & 0b11000) >> 3) + }; + + return result; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/UnsubscribeAckPacket.cs b/src/TurboMqtt.Core/PacketTypes/UnsubscribeAckPacket.cs new file mode 100644 index 00000000..a55d7306 --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/UnsubscribeAckPacket.cs @@ -0,0 +1,63 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Unsubscribe Acknowledgment Reason Codes. +/// +/// +/// This is an MQTT 5.0 feature. +/// +public enum MqttUnsubscribeReasonCode +{ + // MQTT 5.0 specific reason codes + Success = 0x00, // The subscription is deleted successfully, MQTT 5.0 + NoSubscriptionExisted = 0x11, // No subscription existed for the specified topic filter, MQTT 5.0 + UnspecifiedError = 0x80, // The unsubscribe could not be completed and the reason is not specified, MQTT 5.0 + + ImplementationSpecificError = + 0x83, // The unsubscribe could not be completed due to an implementation-specific error, MQTT 5.0 + NotAuthorized = 0x87, // The client was not authorized to unsubscribe, MQTT 5.0 + TopicFilterInvalid = 0x8F, // The specified topic filter is invalid, MQTT 5.0 + PacketIdentifierInUse = 0x91, // The Packet Identifier is already in use, MQTT 5.0 +} + +/// +/// Used to acknowledge an unsubscribe request. +/// +public sealed class UnsubscribeAckPacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.UnsubAck; + + /// + /// Set of unsubscribe reason codes. + /// + /// + /// Available in MQTT 5.0. + /// + public IReadOnlyList ReasonCodes { get; set; } = + Array.Empty(); + + /// + /// Reason given by the server for the unsubscribe. + /// + /// + /// Available in MQTT 5.0. + /// + public string ReasonString { get; set; } = string.Empty; + + /// + /// User Property, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return $"Unsubscribe Ack: [PacketIdentifier={PacketId}] [ReasonCodes={string.Join(", ", ReasonCodes)}]"; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/PacketTypes/UnsubscribePacket.cs b/src/TurboMqtt.Core/PacketTypes/UnsubscribePacket.cs new file mode 100644 index 00000000..2f6254ea --- /dev/null +++ b/src/TurboMqtt.Core/PacketTypes/UnsubscribePacket.cs @@ -0,0 +1,31 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core.PacketTypes; + +/// +/// Used to unsubscribe from topics. +/// +public sealed class UnsubscribePacket : MqttPacketWithId +{ + public override MqttPacketType PacketType => MqttPacketType.Unsubscribe; + + /// + /// The set of topics we're unsubscribing from. + /// + public IReadOnlyList Topics { get; set; } = Array.Empty(); + + /// + /// User Property, available in MQTT 5.0. + /// This is a key-value pair that can be sent multiple times to convey additional information that is not covered by other means. + /// + public IReadOnlyDictionary? UserProperties { get; set; } // MQTT 5.0 only + + public override string ToString() + { + return $"Unsubscribe: [PacketIdentifier={PacketId}] [Topics={string.Join(", ", Topics)}]"; + } +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/QualityOfService.cs b/src/TurboMqtt.Core/QualityOfService.cs new file mode 100644 index 00000000..8a46a71b --- /dev/null +++ b/src/TurboMqtt.Core/QualityOfService.cs @@ -0,0 +1,17 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2024 - 2024 Petabridge, LLC +// +// ----------------------------------------------------------------------- + +namespace TurboMqtt.Core; + +/// +/// QoS value - corresponds to the MQTT specification. +/// +public enum QualityOfService +{ + AtMostOnce = 0, + AtLeastOnce = 1, + ExactlyOnce = 2 +} \ No newline at end of file diff --git a/src/TurboMqtt.Core/TurboMqtt.Core.csproj b/src/TurboMqtt.Core/TurboMqtt.Core.csproj new file mode 100644 index 00000000..3a635329 --- /dev/null +++ b/src/TurboMqtt.Core/TurboMqtt.Core.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + +