Phase 1: Binary MQTT 3.1.1 wire protocol with PipeReader-based parsing, full packet type dispatch, and MQTT 3.1.1 compliance checks. Phase 2: Auth pipeline routing MQTT CONNECT through AuthService, TLS transport with SslStream wrapping, pinned cert validation. Phase 3: IMessageRouter refactor (NatsClient → INatsClient), MqttNatsClientAdapter for cross-protocol bridging, MqttTopicMapper with full Go-parity topic/subject translation. Phase 4: /connz mqtt_client field population, /varz actual MQTT port. Phase 5: JetStream persistence — MqttStreamInitializer creates 5 internal streams, MqttConsumerManager for QoS 1/2 consumers, subject-keyed session/retained lookups replacing linear scans. All 503 MQTT tests and 1589 Core tests pass.
137 lines
4.5 KiB
C#
137 lines
4.5 KiB
C#
// Full Go-parity MQTT topic ↔ NATS subject translation.
|
|
// Go reference: golang/nats-server/server/mqtt.go mqttToNATSSubjectConversion ~line 2200
|
|
//
|
|
// Rules:
|
|
// MQTT → NATS:
|
|
// '/' → '.' (separator)
|
|
// '+' → '*' (single-level wildcard)
|
|
// '#' → '>' (multi-level wildcard)
|
|
// '.' in MQTT topics must be escaped (replaced with a placeholder)
|
|
// Empty levels (leading/trailing/consecutive slashes) produce empty tokens
|
|
// '$' prefix topics are protected from wildcard matching per MQTT spec [MQTT-4.7.2-1]
|
|
//
|
|
// NATS → MQTT (reverse):
|
|
// '.' → '/'
|
|
// '*' → '+'
|
|
// '>' → '#'
|
|
|
|
using System.Text;
|
|
|
|
namespace NATS.Server.Mqtt;
|
|
|
|
/// <summary>
|
|
/// Translates MQTT topics/filters to NATS subjects and vice versa with full Go parity.
|
|
/// Go reference: mqtt.go mqttToNATSSubjectConversion, mqttNATSToMQTTSubjectConversion.
|
|
/// </summary>
|
|
public static class MqttTopicMapper
|
|
{
|
|
// Escape sequence for dots that appear in MQTT topic names.
|
|
// Go uses _DOT_ internally to represent a literal dot in the NATS subject.
|
|
private const string DotEscape = "_DOT_";
|
|
private const string DotEscapeReverse = ".";
|
|
|
|
/// <summary>
|
|
/// Translates an MQTT topic or filter to a NATS subject.
|
|
/// Handles wildcards, dot escaping, empty levels, and '$' prefix protection.
|
|
/// </summary>
|
|
public static string MqttToNats(string mqttTopic)
|
|
{
|
|
if (mqttTopic.Length == 0)
|
|
return string.Empty;
|
|
|
|
var sb = new StringBuilder(mqttTopic.Length);
|
|
|
|
for (var i = 0; i < mqttTopic.Length; i++)
|
|
{
|
|
switch (mqttTopic[i])
|
|
{
|
|
case '/':
|
|
sb.Append('.');
|
|
break;
|
|
case '+':
|
|
sb.Append('*');
|
|
break;
|
|
case '#':
|
|
sb.Append('>');
|
|
break;
|
|
case '.':
|
|
// Dots in MQTT topic names must be escaped for NATS
|
|
sb.Append(DotEscape);
|
|
break;
|
|
default:
|
|
sb.Append(mqttTopic[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translates a NATS subject back to an MQTT topic.
|
|
/// Reverses the mapping: '.' → '/', '*' → '+', '>' → '#', '_DOT_' → '.'.
|
|
/// </summary>
|
|
public static string NatsToMqtt(string natsSubject)
|
|
{
|
|
if (natsSubject.Length == 0)
|
|
return string.Empty;
|
|
|
|
// First, replace _DOT_ escape sequences back to dots
|
|
var working = natsSubject.Replace(DotEscape, "\x00");
|
|
|
|
var sb = new StringBuilder(working.Length);
|
|
for (var i = 0; i < working.Length; i++)
|
|
{
|
|
switch (working[i])
|
|
{
|
|
case '.':
|
|
sb.Append('/');
|
|
break;
|
|
case '*':
|
|
sb.Append('+');
|
|
break;
|
|
case '>':
|
|
sb.Append('#');
|
|
break;
|
|
case '\x00':
|
|
sb.Append('.');
|
|
break;
|
|
default:
|
|
sb.Append(working[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if an MQTT topic starts with '$', which means it should
|
|
/// NOT be matched by wildcard subscriptions (MQTT spec [MQTT-4.7.2-1]).
|
|
/// Topics starting with '$' are reserved for system/server use.
|
|
/// </summary>
|
|
public static bool IsDollarTopic(string mqttTopic)
|
|
=> mqttTopic.Length > 0 && mqttTopic[0] == '$';
|
|
|
|
/// <summary>
|
|
/// Returns true if an MQTT topic filter starts with '$', indicating
|
|
/// it explicitly targets system topics.
|
|
/// </summary>
|
|
public static bool IsDollarFilter(string mqttFilter)
|
|
=> mqttFilter.Length > 0 && mqttFilter[0] == '$';
|
|
|
|
/// <summary>
|
|
/// Checks if a wildcard filter would match a '$' topic.
|
|
/// Per MQTT spec, wildcard filters (starting with '#' or '+') must NOT
|
|
/// match topics beginning with '$'. Only explicit '$' filters match '$' topics.
|
|
/// </summary>
|
|
public static bool WildcardMatchesDollarTopic(string mqttFilter, string mqttTopic)
|
|
{
|
|
if (!IsDollarTopic(mqttTopic))
|
|
return true; // non-$ topics are always matchable
|
|
|
|
// $ topics only matched by filters that also start with $
|
|
return IsDollarFilter(mqttFilter);
|
|
}
|
|
}
|