From 5f153c94f61585ba76b2bb65554355d3abaa716d Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 26 Aug 2024 16:40:43 +0200 Subject: [PATCH 001/202] beginning of mqtt-poc, ping --- shard.lock | 4 ++ shard.yml | 2 + spec/mqtt_spec.cr | 51 ++++++++++++++ spec/spec_helper.cr | 14 +++- src/lavinmq/config.cr | 2 + src/lavinmq/http/handler/websocket.cr | 2 +- src/lavinmq/http/http_server.cr | 2 +- src/lavinmq/launcher.cr | 11 ++- src/lavinmq/mqtt/client.cr | 96 ++++++++++++++++++++++++++ src/lavinmq/mqtt/connection_factory.cr | 47 +++++++++++++ src/lavinmq/mqtt/protocol.cr | 7 ++ src/lavinmq/server.cr | 45 +++++++----- static/js/connections.js | 17 ++--- 13 files changed, 268 insertions(+), 32 deletions(-) create mode 100644 spec/mqtt_spec.cr create mode 100644 src/lavinmq/mqtt/client.cr create mode 100644 src/lavinmq/mqtt/connection_factory.cr create mode 100644 src/lavinmq/mqtt/protocol.cr diff --git a/shard.lock b/shard.lock index 182e0db6a9..19ac85c652 100644 --- a/shard.lock +++ b/shard.lock @@ -16,6 +16,10 @@ shards: git: https://github.com/84codes/lz4.cr.git version: 1.0.0+git.commit.96d714f7593c66ca7425872fd26c7b1286806d3d + mqtt-protocol: + git: https://github.com/84codes/mqtt-protocol.cr.git + version: 0.2.0+git.commit.3f82ee85d029e6d0505cbe261b108e156df4e598 + systemd: git: https://github.com/84codes/systemd.cr.git version: 2.0.0 diff --git a/shard.yml b/shard.yml index 6e2eb48079..8ddc2105af 100644 --- a/shard.yml +++ b/shard.yml @@ -32,6 +32,8 @@ dependencies: github: 84codes/systemd.cr lz4: github: 84codes/lz4.cr + mqtt-protocol: + github: 84codes/mqtt-protocol.cr development_dependencies: ameba: diff --git a/spec/mqtt_spec.cr b/spec/mqtt_spec.cr new file mode 100644 index 0000000000..c6e73d2835 --- /dev/null +++ b/spec/mqtt_spec.cr @@ -0,0 +1,51 @@ +require "spec" +require "socket" +require "./spec_helper" +require "mqtt-protocol" +require "../src/lavinmq/mqtt/connection_factory" + + +def setup_connection(s, pass) + left, right = UNIXSocket.pair + io = MQTT::Protocol::IO.new(left) + s.users.create("usr", "pass", [LavinMQ::Tag::Administrator]) + MQTT::Protocol::Connect.new("abc", false, 60u16, "usr", pass.to_slice, nil).to_io(io) + connection_factory = LavinMQ::MQTT::ConnectionFactory.new(right, + LavinMQ::ConnectionInfo.local, + s.users, + s.vhosts["/"]) + { connection_factory.start, io } +end + +describe LavinMQ do + src = "127.0.0.1" + dst = "127.0.0.1" + + it "MQTT connection should pass authentication" do + with_amqp_server do |s| + client, io = setup_connection(s, "pass") + client.should be_a(LavinMQ::MQTT::Client) + # client.close + MQTT::Protocol::Disconnect.new.to_io(io) + end + end + + it "unauthorized MQTT connection should not pass authentication" do + with_amqp_server do |s| + client, io = setup_connection(s, "pa&ss") + client.should_not be_a(LavinMQ::MQTT::Client) + # client.close + MQTT::Protocol::Disconnect.new.to_io(io) + end + end + + it "should handle a Ping" do + with_amqp_server do |s| + client, io = setup_connection(s, "pass") + client.should be_a(LavinMQ::MQTT::Client) + MQTT::Protocol::PingReq.new.to_io(io) + MQTT::Protocol::Packet.from_io(io).should be_a(MQTT::Protocol::Connack) + MQTT::Protocol::Packet.from_io(io).should be_a(MQTT::Protocol::PingResp) + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index f4bf0652e1..8a1ae84ed1 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -80,9 +80,9 @@ def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.n ctx = OpenSSL::SSL::Context::Server.new ctx.certificate_chain = "spec/resources/server_certificate.pem" ctx.private_key = "spec/resources/server_key.pem" - spawn(name: "amqp tls listen") { s.listen_tls(tcp_server, ctx) } + spawn(name: "amqp tls listen") { s.listen_tls(tcp_server, ctx, "amqp") } else - spawn(name: "amqp tcp listen") { s.listen(tcp_server) } + spawn(name: "amqp tcp listen") { s.listen(tcp_server, "amqp") } end Fiber.yield yield s @@ -92,6 +92,16 @@ def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.n end end +#do i need to do this? +# def with_mqtt_server(tls = false, & : LavinMQ::Server -> Nil) +# tcp_server = TCPServer.new("localhost", 0) +# s = LavinMQ::Server.new(LavinMQ::Config.instance.data_dir, replicator) +# begin +# if tls +# end + +# end + def with_http_server(&) with_amqp_server do |s| h = LavinMQ::HTTP::Server.new(s) diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index 0f2a347d63..fed630a317 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -17,6 +17,8 @@ module LavinMQ property amqp_bind = "127.0.0.1" property amqp_port = 5672 property amqps_port = -1 + property mqtt_port = 1883 + property mqtt_bind = "127.0.0.1" property unix_path = "" property unix_proxy_protocol = 1_u8 # PROXY protocol version on unix domain socket connections property tcp_proxy_protocol = 0_u8 # PROXY protocol version on amqp tcp connections diff --git a/src/lavinmq/http/handler/websocket.cr b/src/lavinmq/http/handler/websocket.cr index 4a8fb131bd..b749807cb1 100644 --- a/src/lavinmq/http/handler/websocket.cr +++ b/src/lavinmq/http/handler/websocket.cr @@ -11,7 +11,7 @@ module LavinMQ Socket::IPAddress.new("127.0.0.1", 0) # Fake when UNIXAddress connection_info = ConnectionInfo.new(remote_address, local_address) io = WebSocketIO.new(ws) - spawn amqp_server.handle_connection(io, connection_info), name: "HandleWSconnection #{remote_address}" + spawn amqp_server.handle_connection(io, connection_info, "amqp"), name: "HandleWSconnection #{remote_address}" end end end diff --git a/src/lavinmq/http/http_server.cr b/src/lavinmq/http/http_server.cr index 856b78ab42..d7da4caf90 100644 --- a/src/lavinmq/http/http_server.cr +++ b/src/lavinmq/http/http_server.cr @@ -24,7 +24,7 @@ module LavinMQ ViewsController.new, ApiErrorHandler.new, AuthHandler.new(@amqp_server), - PrometheusController.new(@amqp_server), + # PrometheusController.new(@amqp_server), ApiDefaultsHandler.new, MainController.new(@amqp_server), DefinitionsController.new(@amqp_server), diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index ec1ada68e3..f2e433f814 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -116,13 +116,13 @@ module LavinMQ private def listen if @config.amqp_port > 0 - spawn @amqp_server.listen(@config.amqp_bind, @config.amqp_port), + spawn @amqp_server.listen(@config.amqp_bind, @config.amqp_port, :amqp), name: "AMQP listening on #{@config.amqp_port}" end if @config.amqps_port > 0 if ctx = @tls_context - spawn @amqp_server.listen_tls(@config.amqp_bind, @config.amqps_port, ctx), + spawn @amqp_server.listen_tls(@config.amqp_bind, @config.amqps_port, ctx, :amqp), name: "AMQPS listening on #{@config.amqps_port}" end end @@ -132,7 +132,7 @@ module LavinMQ end unless @config.unix_path.empty? - spawn @amqp_server.listen_unix(@config.unix_path), name: "AMQP listening at #{@config.unix_path}" + spawn @amqp_server.listen_unix(@config.unix_path, :amqp), name: "AMQP listening at #{@config.unix_path}" end if @config.http_port > 0 @@ -151,6 +151,11 @@ module LavinMQ spawn(name: "HTTP listener") do @http_server.not_nil!.listen end + + if @config.mqtt_port > 0 + spawn @amqp_server.listen(@config.mqtt_bind, @config.mqtt_port, :mqtt), + name: "MQTT listening on #{@config.mqtt_port}" + end end private def dump_debug_info diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr new file mode 100644 index 0000000000..333f6a6e60 --- /dev/null +++ b/src/lavinmq/mqtt/client.cr @@ -0,0 +1,96 @@ +require "openssl" +require "socket" +require "../client" +require "../error" + +module LavinMQ + module MQTT + class Client < LavinMQ::Client + include Stats + include SortableJSON + + getter vhost, channels, log, name, user + Log = ::Log.for "MQTT.client" + rate_stats({"send_oct", "recv_oct"}) + + def initialize(@socket : ::IO, + @connection_info : ConnectionInfo, + @vhost : VHost, + @user : User) + @io = MQTT::IO.new(@socket) + @lock = Mutex.new + @remote_address = @connection_info.src + @local_address = @connection_info.dst + @metadata = ::Log::Metadata.new(nil, {vhost: @vhost.name, address: @remote_address.to_s}) + @log = Logger.new(Log, @metadata) + @channels = Hash(UInt16, Client::Channel).new + @vhost.add_connection(self) + spawn read_loop + connection_name = "#{@remote_address} -> #{@local_address}" + @name = "#{@remote_address} -> #{@local_address}" + end + + private def read_loop + loop do + Log.trace { "waiting for packet" } + packet = read_and_handle_packet + # The disconnect packet has been handled and the socket has been closed. + # If we dont breakt the loop here we'll get a IO/Error on next read. + break if packet.is_a?(MQTT::Disconnect) + end + rescue ex : MQTT::Error::Connect + Log.warn { "Connect error #{ex.inspect}" } + ensure + @socket.close + @vhost.rm_connection(self) + end + + def read_and_handle_packet + packet : MQTT::Packet = MQTT::Packet.from_io(@io) + Log.info { "recv #{packet.inspect}" } + + case packet + when MQTT::Publish then pp "publish" + when MQTT::PubAck then pp "puback" + when MQTT::Subscribe then pp "subscribe" + when MQTT::Unsubscribe then pp "unsubscribe" + when MQTT::PingReq then receive_pingreq(packet) + when MQTT::Disconnect then return packet + else raise "invalid packet type for client to send" + end + packet + end + + private def send(packet) + @lock.synchronize do + packet.to_io(@io) + @socket.flush + end + # @broker.increment_bytes_sent(packet.bytesize) + # @broker.increment_messages_sent + # @broker.increment_publish_sent if packet.is_a?(MQTT::Protocol::Publish) + end + + def receive_pingreq(packet : MQTT::PingReq) + send(MQTT::PingResp.new) + end + + def details_tuple + { + vhost: @vhost.name, + user: @user.name, + protocol: "MQTT", + }.merge(stats_details) + end + + def update_rates + end + + def close(reason) + end + + def force_close + end + end + end +end diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr new file mode 100644 index 0000000000..af458850f1 --- /dev/null +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -0,0 +1,47 @@ +require "socket" +require "./protocol" +require "log" +require "./client" +require "../vhost" +require "../user" + +module LavinMQ + module MQTT + class ConnectionFactory + def initialize(@socket : ::IO, + @connection_info : ConnectionInfo, + @users : UserStore, + @vhost : VHost) + end + + def start + io = ::MQTT::Protocol::IO.new(@socket) + if packet = MQTT::Packet.from_io(@socket).as?(MQTT::Connect) + Log.trace { "recv #{packet.inspect}" } + if user = authenticate(io, packet, @users) + ::MQTT::Protocol::Connack.new(false, ::MQTT::Protocol::Connack::ReturnCode::Accepted).to_io(io) + io.flush + return LavinMQ::MQTT::Client.new(@socket, @connection_info, @vhost, user) + end + end + rescue ex + Log.warn { "Recieved the wrong packet" } + @socket.close + end + + def authenticate(io, packet, users) + return nil unless (username = packet.username) && (password = packet.password) + user = users[username]? + return user if user && user.password && user.password.not_nil!.verify(String.new(password)) + #probably not good to differentiate between user not found and wrong password + if user.nil? + Log.warn { "User \"#{username}\" not found" } + else + Log.warn { "Authentication failure for user \"#{username}\"" } + end + ::MQTT::Protocol::Connack.new(false, ::MQTT::Protocol::Connack::ReturnCode::NotAuthorized).to_io(io) + nil + end + end + end +end diff --git a/src/lavinmq/mqtt/protocol.cr b/src/lavinmq/mqtt/protocol.cr new file mode 100644 index 0000000000..9349f9560f --- /dev/null +++ b/src/lavinmq/mqtt/protocol.cr @@ -0,0 +1,7 @@ +require "mqtt-protocol" + +module LavinMQ + module MQTT + include ::MQTT::Protocol + end +end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 69b729079d..b2d3d771a0 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -2,6 +2,7 @@ require "socket" require "openssl" require "systemd" require "./amqp" +require "./mqtt/protocol" require "./rough_time" require "../stdlib/*" require "./vhost_store" @@ -15,6 +16,7 @@ require "./proxy_protocol" require "./client/client" require "./client/connection_factory" require "./amqp/connection_factory" +require "./mqtt/connection_factory" require "./stats" module LavinMQ @@ -74,8 +76,8 @@ module LavinMQ Iterator(Client).chain(@vhosts.each_value.map(&.connections.each)) end - def listen(s : TCPServer) - @listeners[s] = :amqp + def listen(s : TCPServer, protocol) + @listeners[s] = :protocol Log.info { "Listening on #{s.local_address}" } loop do client = s.accept? || break @@ -85,7 +87,7 @@ module LavinMQ set_socket_options(client) set_buffer_size(client) conn_info = extract_conn_info(client) - handle_connection(client, conn_info) + handle_connection(client, conn_info, protocol) rescue ex Log.warn(exception: ex) { "Error accepting connection from #{remote_address}" } client.close rescue nil @@ -120,8 +122,8 @@ module LavinMQ end end - def listen(s : UNIXServer) - @listeners[s] = :amqp + def listen(s : UNIXServer, protocol) + @listeners[s] = :protocol Log.info { "Listening on #{s.local_address}" } loop do # do not try to use while client = s.accept? || break @@ -135,7 +137,7 @@ module LavinMQ when 2 then ProxyProtocol::V2.parse(client) else ConnectionInfo.local # TODO: use unix socket address, don't fake local end - handle_connection(client, conn_info) + handle_connection(client, conn_info, protocol) rescue ex Log.warn(exception: ex) { "Error accepting connection from #{remote_address}" } client.close rescue nil @@ -147,13 +149,13 @@ module LavinMQ @listeners.delete(s) end - def listen(bind = "::", port = 5672) + def listen(bind = "::", port = 5672, protocol = :amqp) s = TCPServer.new(bind, port) - listen(s) + listen(s, protocol) end - def listen_tls(s : TCPServer, context) - @listeners[s] = :amqps + def listen_tls(s : TCPServer, context, protocol) + @listeners[s] = :protocol Log.info { "Listening on #{s.local_address} (TLS)" } loop do # do not try to use while client = s.accept? || break @@ -168,7 +170,7 @@ module LavinMQ conn_info.ssl = true conn_info.ssl_version = ssl_client.tls_version conn_info.ssl_cipher = ssl_client.cipher - handle_connection(ssl_client, conn_info) + handle_connection(ssl_client, conn_info, protocol) rescue ex Log.warn(exception: ex) { "Error accepting TLS connection from #{remote_addr}" } client.close rescue nil @@ -180,15 +182,15 @@ module LavinMQ @listeners.delete(s) end - def listen_tls(bind, port, context) - listen_tls(TCPServer.new(bind, port), context) + def listen_tls(bind, port, context, protocol) + listen_tls(TCPServer.new(bind, port), context, protocol) end - def listen_unix(path : String) + def listen_unix(path : String, protocol) File.delete?(path) s = UNIXServer.new(path) File.chmod(path, 0o666) - listen(s) + listen(s, protocol) end def listen_clustering(bind, port) @@ -244,8 +246,17 @@ module LavinMQ end end - def handle_connection(socket, connection_info) - client = @amqp_connection_factory.start(socket, connection_info, @vhosts, @users) + def handle_connection(socket, connection_info, protocol) + case protocol + when :amqp + client = @amqp_connection_factory.start(socket, connection_info, @vhosts, @users) + when :mqtt + client = MQTT::ConnectionFactory.new(socket, connection_info, @users, @vhosts["/"]).start + else + Log.warn { "Unknown protocol '#{protocol}'" } + socket.close + end + ensure socket.close if client.nil? end diff --git a/static/js/connections.js b/static/js/connections.js index 611d4c5065..06b58c87b1 100644 --- a/static/js/connections.js +++ b/static/js/connections.js @@ -19,13 +19,14 @@ Table.renderTable('table', tableOptions, function (tr, item, all) { if (all) { const connectionLink = document.createElement('a') connectionLink.href = `connection#name=${encodeURIComponent(item.name)}` - if (item.client_properties.connection_name) { - connectionLink.appendChild(document.createElement('span')).textContent = item.name - connectionLink.appendChild(document.createElement('br')) - connectionLink.appendChild(document.createElement('small')).textContent = item.client_properties.connection_name - } else { + console.log(item) + // if (item.client_properties.connection_name) { + // connectionLink.appendChild(document.createElement('span')).textContent = item.name + // connectionLink.appendChild(document.createElement('br')) + // connectionLink.appendChild(document.createElement('small')).textContent = item.client_properties.connection_name + // } else { connectionLink.textContent = item.name - } + // } Table.renderCell(tr, 0, item.vhost) Table.renderCell(tr, 1, connectionLink) Table.renderCell(tr, 2, item.user) @@ -37,9 +38,9 @@ Table.renderTable('table', tableOptions, function (tr, item, all) { Table.renderCell(tr, 10, item.timeout, 'right') // Table.renderCell(tr, 8, item.auth_mechanism) const clientDiv = document.createElement('span') - clientDiv.textContent = `${item.client_properties.product} / ${item.client_properties.platform || ''}` + // clientDiv.textContent = `${item.client_properties.product} / ${item.client_properties.platform || ''}` clientDiv.appendChild(document.createElement('br')) - clientDiv.appendChild(document.createElement('small')).textContent = item.client_properties.version + // clientDiv.appendChild(document.createElement('small')).textContent = item.client_properties.version Table.renderCell(tr, 11, clientDiv) Table.renderCell(tr, 12, new Date(item.connected_at).toLocaleString(), 'center') } From 343cfb6020870a99f5ac6c76a8938c1dae1b8387 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 27 Aug 2024 14:57:42 +0200 Subject: [PATCH 002/202] add send/recieve oct_count, make prometheus controller compile --- src/lavinmq/http/http_server.cr | 2 +- src/lavinmq/mqtt/client.cr | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/lavinmq/http/http_server.cr b/src/lavinmq/http/http_server.cr index d7da4caf90..856b78ab42 100644 --- a/src/lavinmq/http/http_server.cr +++ b/src/lavinmq/http/http_server.cr @@ -24,7 +24,7 @@ module LavinMQ ViewsController.new, ApiErrorHandler.new, AuthHandler.new(@amqp_server), - # PrometheusController.new(@amqp_server), + PrometheusController.new(@amqp_server), ApiDefaultsHandler.new, MainController.new(@amqp_server), DefinitionsController.new(@amqp_server), diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 333f6a6e60..944543ad98 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -10,8 +10,10 @@ module LavinMQ include SortableJSON getter vhost, channels, log, name, user - Log = ::Log.for "MQTT.client" + + @channels = Hash(UInt16, Client::Channel).new rate_stats({"send_oct", "recv_oct"}) + Log = ::Log.for "MQTT.client" def initialize(@socket : ::IO, @connection_info : ConnectionInfo, @@ -21,13 +23,13 @@ module LavinMQ @lock = Mutex.new @remote_address = @connection_info.src @local_address = @connection_info.dst + @name = "#{@remote_address} -> #{@local_address}" + connection_name = @name @metadata = ::Log::Metadata.new(nil, {vhost: @vhost.name, address: @remote_address.to_s}) @log = Logger.new(Log, @metadata) - @channels = Hash(UInt16, Client::Channel).new @vhost.add_connection(self) + @log.info { "Connection established for user=#{@user.name}" } spawn read_loop - connection_name = "#{@remote_address} -> #{@local_address}" - @name = "#{@remote_address} -> #{@local_address}" end private def read_loop @@ -48,6 +50,7 @@ module LavinMQ def read_and_handle_packet packet : MQTT::Packet = MQTT::Packet.from_io(@io) Log.info { "recv #{packet.inspect}" } + @recv_oct_count += packet.bytesize case packet when MQTT::Publish then pp "publish" @@ -62,13 +65,11 @@ module LavinMQ end private def send(packet) - @lock.synchronize do - packet.to_io(@io) - @socket.flush - end - # @broker.increment_bytes_sent(packet.bytesize) - # @broker.increment_messages_sent - # @broker.increment_publish_sent if packet.is_a?(MQTT::Protocol::Publish) + @lock.synchronize do + packet.to_io(@io) + @socket.flush + end + @send_oct_count += packet.bytesize end def receive_pingreq(packet : MQTT::PingReq) From a690eb2af4fbb573938bbcb23da865fe5217a9bc Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 28 Aug 2024 10:10:05 +0200 Subject: [PATCH 003/202] client stores client_id and quick solution for UI --- spec/spec_helper.cr | 10 ---------- src/lavinmq/config.cr | 2 +- src/lavinmq/mqtt/client.cr | 7 ++++--- src/lavinmq/mqtt/connection_factory.cr | 2 +- static/js/connections.js | 23 ++++++++++++----------- 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 8a1ae84ed1..48e5a873f2 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -92,16 +92,6 @@ def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.n end end -#do i need to do this? -# def with_mqtt_server(tls = false, & : LavinMQ::Server -> Nil) -# tcp_server = TCPServer.new("localhost", 0) -# s = LavinMQ::Server.new(LavinMQ::Config.instance.data_dir, replicator) -# begin -# if tls -# end - -# end - def with_http_server(&) with_amqp_server do |s| h = LavinMQ::HTTP::Server.new(s) diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index fed630a317..a5bd680c70 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -17,8 +17,8 @@ module LavinMQ property amqp_bind = "127.0.0.1" property amqp_port = 5672 property amqps_port = -1 - property mqtt_port = 1883 property mqtt_bind = "127.0.0.1" + property mqtt_port = 1883 property unix_path = "" property unix_proxy_protocol = 1_u8 # PROXY protocol version on unix domain socket connections property tcp_proxy_protocol = 0_u8 # PROXY protocol version on amqp tcp connections diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 944543ad98..2ea84cbac7 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -9,7 +9,7 @@ module LavinMQ include Stats include SortableJSON - getter vhost, channels, log, name, user + getter vhost, channels, log, name, user, client_id @channels = Hash(UInt16, Client::Channel).new rate_stats({"send_oct", "recv_oct"}) @@ -18,13 +18,13 @@ module LavinMQ def initialize(@socket : ::IO, @connection_info : ConnectionInfo, @vhost : VHost, - @user : User) + @user : User, + @client_id : String) @io = MQTT::IO.new(@socket) @lock = Mutex.new @remote_address = @connection_info.src @local_address = @connection_info.dst @name = "#{@remote_address} -> #{@local_address}" - connection_name = @name @metadata = ::Log::Metadata.new(nil, {vhost: @vhost.name, address: @remote_address.to_s}) @log = Logger.new(Log, @metadata) @vhost.add_connection(self) @@ -81,6 +81,7 @@ module LavinMQ vhost: @vhost.name, user: @user.name, protocol: "MQTT", + client_id: @client_id, }.merge(stats_details) end diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index af458850f1..a2affcbd19 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -21,7 +21,7 @@ module LavinMQ if user = authenticate(io, packet, @users) ::MQTT::Protocol::Connack.new(false, ::MQTT::Protocol::Connack::ReturnCode::Accepted).to_io(io) io.flush - return LavinMQ::MQTT::Client.new(@socket, @connection_info, @vhost, user) + return LavinMQ::MQTT::Client.new(@socket, @connection_info, @vhost, user, packet.client_id) end end rescue ex diff --git a/static/js/connections.js b/static/js/connections.js index 06b58c87b1..90fd633c48 100644 --- a/static/js/connections.js +++ b/static/js/connections.js @@ -19,14 +19,13 @@ Table.renderTable('table', tableOptions, function (tr, item, all) { if (all) { const connectionLink = document.createElement('a') connectionLink.href = `connection#name=${encodeURIComponent(item.name)}` - console.log(item) - // if (item.client_properties.connection_name) { - // connectionLink.appendChild(document.createElement('span')).textContent = item.name - // connectionLink.appendChild(document.createElement('br')) - // connectionLink.appendChild(document.createElement('small')).textContent = item.client_properties.connection_name - // } else { + if (item.protocol !== 'MQTT' && item.client_properties.connection_name) { + connectionLink.appendChild(document.createElement('span')).textContent = item.name + connectionLink.appendChild(document.createElement('br')) + connectionLink.appendChild(document.createElement('small')).textContent = item.client_properties.connection_name + } else { connectionLink.textContent = item.name - // } + } Table.renderCell(tr, 0, item.vhost) Table.renderCell(tr, 1, connectionLink) Table.renderCell(tr, 2, item.user) @@ -36,11 +35,13 @@ Table.renderTable('table', tableOptions, function (tr, item, all) { Table.renderCell(tr, 7, item.protocol, 'center') Table.renderCell(tr, 9, item.channel_max, 'right') Table.renderCell(tr, 10, item.timeout, 'right') - // Table.renderCell(tr, 8, item.auth_mechanism) + Table.renderCell(tr, 8, item.auth_mechanism) const clientDiv = document.createElement('span') - // clientDiv.textContent = `${item.client_properties.product} / ${item.client_properties.platform || ''}` - clientDiv.appendChild(document.createElement('br')) - // clientDiv.appendChild(document.createElement('small')).textContent = item.client_properties.version + if (item.protocol !== 'MQTT') { + clientDiv.textContent = `${item.client_properties.product} / ${item.client_properties.platform || ''}` + clientDiv.appendChild(document.createElement('br')) + clientDiv.appendChild(document.createElement('small')).textContent = item.client_properties.version + } Table.renderCell(tr, 11, clientDiv) Table.renderCell(tr, 12, new Date(item.connected_at).toLocaleString(), 'center') } From eadfb6a5d747db9526e75be050bcfcda8c2285d0 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 6 Sep 2024 13:18:26 +0200 Subject: [PATCH 004/202] amqp publish --- src/lavinmq/amqp/channel.cr | 1 + src/lavinmq/mqtt/channel.cr | 0 src/lavinmq/mqtt/client.cr | 53 ++++++++++++++++++++++++-- src/lavinmq/mqtt/connection_factory.cr | 19 +++++---- src/lavinmq/mqtt/session.cr | 11 ++++++ src/lavinmq/mqtt/session_store.cr | 15 ++++++++ src/lavinmq/server.cr | 3 +- src/lavinmq/vhost.cr | 21 +++++++++- 8 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 src/lavinmq/mqtt/channel.cr create mode 100644 src/lavinmq/mqtt/session.cr create mode 100644 src/lavinmq/mqtt/session_store.cr diff --git a/src/lavinmq/amqp/channel.cr b/src/lavinmq/amqp/channel.cr index 192d2d8665..94bc6f6655 100644 --- a/src/lavinmq/amqp/channel.cr +++ b/src/lavinmq/amqp/channel.cr @@ -247,6 +247,7 @@ module LavinMQ end confirm do + #here ok = @client.vhost.publish msg, @next_publish_immediate, @visited, @found_queues basic_return(msg, @next_publish_mandatory, @next_publish_immediate) unless ok rescue e : LavinMQ::Error::PreconditionFailed diff --git a/src/lavinmq/mqtt/channel.cr b/src/lavinmq/mqtt/channel.cr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 2ea84cbac7..59b3cfa968 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -2,6 +2,7 @@ require "openssl" require "socket" require "../client" require "../error" +require "./session" module LavinMQ module MQTT @@ -10,8 +11,8 @@ module LavinMQ include SortableJSON getter vhost, channels, log, name, user, client_id - @channels = Hash(UInt16, Client::Channel).new + @session : MQTT::Session | Nil rate_stats({"send_oct", "recv_oct"}) Log = ::Log.for "MQTT.client" @@ -19,7 +20,8 @@ module LavinMQ @connection_info : ConnectionInfo, @vhost : VHost, @user : User, - @client_id : String) + @client_id : String, + @clean_session = false) @io = MQTT::IO.new(@socket) @lock = Mutex.new @remote_address = @connection_info.src @@ -28,7 +30,9 @@ module LavinMQ @metadata = ::Log::Metadata.new(nil, {vhost: @vhost.name, address: @remote_address.to_s}) @log = Logger.new(Log, @metadata) @vhost.add_connection(self) + @session = start_session(self) @log.info { "Connection established for user=#{@user.name}" } + pp "spawn" spawn read_loop end @@ -43,6 +47,10 @@ module LavinMQ rescue ex : MQTT::Error::Connect Log.warn { "Connect error #{ex.inspect}" } ensure + + if @clean_session + disconnect_session(self) + end @socket.close @vhost.rm_connection(self) end @@ -53,7 +61,7 @@ module LavinMQ @recv_oct_count += packet.bytesize case packet - when MQTT::Publish then pp "publish" + when MQTT::Publish then recieve_publish(packet) when MQTT::PubAck then pp "puback" when MQTT::Subscribe then pp "subscribe" when MQTT::Unsubscribe then pp "unsubscribe" @@ -76,6 +84,29 @@ module LavinMQ send(MQTT::PingResp.new) end + def recieve_publish(packet) + msg = Message.new("mqtt", packet.topic, packet.payload.to_s, AMQ::Protocol::Properties.new) + @vhost.publish(msg) + # @session = start_session(self) unless @session + # @session.publish(msg) + # if packet.qos > 0 && (packet_id = packet.packet_id) + # send(MQTT::PubAck.new(packet_id)) + # end + end + + def recieve_puback(packet) + end + + #let prefetch = 1 + def recieve_subscribe(packet) + # exclusive conusmer + # + end + + def recieve_unsubscribe(packet) + + end + def details_tuple { vhost: @vhost.name, @@ -85,6 +116,21 @@ module LavinMQ }.merge(stats_details) end + def start_session(client) : MQTT::Session + if @clean_session + pp "clear session" + @vhost.clear_session(client) + end + pp "start session" + @vhost.start_session(client) + end + + def disconnect_session(client) + pp "disconnect session" + @vhost.clear_session(client) + end + + def update_rates end @@ -93,6 +139,7 @@ module LavinMQ def force_close end + end end end diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index a2affcbd19..30ac98ab91 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -8,30 +8,29 @@ require "../user" module LavinMQ module MQTT class ConnectionFactory - def initialize(@socket : ::IO, - @connection_info : ConnectionInfo, + def initialize( @users : UserStore, @vhost : VHost) end - def start - io = ::MQTT::Protocol::IO.new(@socket) - if packet = MQTT::Packet.from_io(@socket).as?(MQTT::Connect) + def start(socket : ::IO, connection_info : ConnectionInfo) + io = ::MQTT::Protocol::IO.new(socket) + if packet = MQTT::Packet.from_io(socket).as?(MQTT::Connect) Log.trace { "recv #{packet.inspect}" } - if user = authenticate(io, packet, @users) + if user = authenticate(io, packet) ::MQTT::Protocol::Connack.new(false, ::MQTT::Protocol::Connack::ReturnCode::Accepted).to_io(io) io.flush - return LavinMQ::MQTT::Client.new(@socket, @connection_info, @vhost, user, packet.client_id) + return LavinMQ::MQTT::Client.new(socket, connection_info, @vhost, user, packet.client_id, packet.clean_session?) end end rescue ex Log.warn { "Recieved the wrong packet" } - @socket.close + socket.close end - def authenticate(io, packet, users) + def authenticate(io, packet) return nil unless (username = packet.username) && (password = packet.password) - user = users[username]? + user = @users[username]? return user if user && user.password && user.password.not_nil!.verify(String.new(password)) #probably not good to differentiate between user not found and wrong password if user.nil? diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr new file mode 100644 index 0000000000..e7a7828c13 --- /dev/null +++ b/src/lavinmq/mqtt/session.cr @@ -0,0 +1,11 @@ +module LavinMQ + module MQTT + class Session < Queue + def initialize(@vhost : VHost, @name : String, @exclusive = true, @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) + super + end + + #rm_consumer override for clean_session + end + end +end diff --git a/src/lavinmq/mqtt/session_store.cr b/src/lavinmq/mqtt/session_store.cr new file mode 100644 index 0000000000..b60aff58dd --- /dev/null +++ b/src/lavinmq/mqtt/session_store.cr @@ -0,0 +1,15 @@ +#holds all sessions in a vhost +require "./session" +module LavinMQ + module MQTT + class SessionStore + getter vhost, sessions + def initialize(@vhost : VHost) + @sessions = Hash(String, MQTT::Session).new + end + + forward_missing_to @sessions + + end + end +end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index b2d3d771a0..74eef07def 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -39,6 +39,7 @@ module LavinMQ @vhosts = VHostStore.new(@data_dir, @users, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) @amqp_connection_factory = LavinMQ::AMQP::ConnectionFactory.new + @mqtt_connection_factory = MQTT::ConnectionFactory.new(@users, @vhosts["/"]) apply_parameter spawn stats_loop, name: "Server#stats_loop" end @@ -251,7 +252,7 @@ module LavinMQ when :amqp client = @amqp_connection_factory.start(socket, connection_info, @vhosts, @users) when :mqtt - client = MQTT::ConnectionFactory.new(socket, connection_info, @users, @vhosts["/"]).start + client = @mqtt_connection_factory.start(socket, connection_info) else Log.warn { "Unknown protocol '#{protocol}'" } socket.close diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index 98e3332564..80df2bd95a 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -14,6 +14,7 @@ require "./schema" require "./event_type" require "./stats" require "./queue_factory" +require "./mqtt/session_store" module LavinMQ class VHost @@ -25,7 +26,7 @@ module LavinMQ "redeliver", "reject", "consumer_added", "consumer_removed"}) getter name, exchanges, queues, data_dir, operator_policies, policies, parameters, shovels, - direct_reply_consumers, connections, dir, users + direct_reply_consumers, connections, dir, users, sessions property? flow = true getter? closed = false property max_connections : Int32? @@ -36,6 +37,7 @@ module LavinMQ @direct_reply_consumers = Hash(String, Client::Channel).new @shovels : ShovelStore? @upstreams : Federation::UpstreamStore? + @sessions : MQTT::SessionStore? @connections = Array(Client).new(512) @definitions_file : File @definitions_lock = Mutex.new(:reentrant) @@ -58,6 +60,7 @@ module LavinMQ @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator, vhost: @name) @shovels = ShovelStore.new(self) @upstreams = Federation::UpstreamStore.new(self) + @sessions = MQTT::SessionStore.new(self) load! spawn check_consumer_timeouts_loop, name: "Consumer timeouts loop" end @@ -336,6 +339,18 @@ module LavinMQ @connections.delete client end + def start_session(client : Client) + client_id = client.client_id + session = MQTT::Session.new(self, client_id) + sessions[client_id] = session + @queues[client_id] = session + end + + def clear_session(client : Client) + sessions.delete client.client_id + @queues.delete client.client_id + end + SHOVEL = "shovel" FEDERATION_UPSTREAM = "federation-upstream" FEDERATION_UPSTREAM_SET = "federation-upstream-set" @@ -653,6 +668,10 @@ module LavinMQ @shovels.not_nil! end + def sessions + @sessions.not_nil! + end + def event_tick(event_type) case event_type in EventType::ChannelClosed then @channel_closed_count += 1 From a77824933fa8347bc334d9ec267efc0ae4ea148b Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Fri, 6 Sep 2024 13:50:00 +0200 Subject: [PATCH 005/202] mqtt integration spec: ping --- spec/mqtt/integrations/ping_spec.cr | 14 ++++ spec/mqtt/spec_helper.cr | 34 ++++++++ spec/mqtt/spec_helper/mqtt_client.cr | 112 ++++++++++++++++++++++++++ spec/mqtt/spec_helper/mqtt_helpers.cr | 71 ++++++++++++++++ spec/spec_helper.cr | 14 +++- src/lavinmq/server.cr | 7 +- 6 files changed, 245 insertions(+), 7 deletions(-) create mode 100644 spec/mqtt/integrations/ping_spec.cr create mode 100644 spec/mqtt/spec_helper.cr create mode 100644 spec/mqtt/spec_helper/mqtt_client.cr create mode 100644 spec/mqtt/spec_helper/mqtt_helpers.cr diff --git a/spec/mqtt/integrations/ping_spec.cr b/spec/mqtt/integrations/ping_spec.cr new file mode 100644 index 0000000000..a7428cf825 --- /dev/null +++ b/spec/mqtt/integrations/ping_spec.cr @@ -0,0 +1,14 @@ +require "../spec_helper" + +describe "ping" do + it "responds to ping [MQTT-3.12.4-1]" do + with_mqtt_server do |server| + with_client_io(server) do |io| + connect(io) + ping(io) + resp = read_packet(io) + resp.should be_a(MQTT::Protocol::PingResp) + end + end + end +end diff --git a/spec/mqtt/spec_helper.cr b/spec/mqtt/spec_helper.cr new file mode 100644 index 0000000000..9465e68393 --- /dev/null +++ b/spec/mqtt/spec_helper.cr @@ -0,0 +1,34 @@ +require "../spec_helper" +require "./spec_helper/mqtt_helpers" + +def with_client_socket(server, &) + listener = server.listeners.find { |l| l[:protocol] == :mqtt }.as(NamedTuple(ip_address: String, protocol: Symbol, port: Int32) + ) + + socket = TCPSocket.new( + listener[:ip_address], + listener[:port], + connect_timeout: 30) + socket.keepalive = true + socket.tcp_nodelay = false + socket.tcp_keepalive_idle = 60 + socket.tcp_keepalive_count = 3 + socket.tcp_keepalive_interval = 10 + socket.sync = true + socket.read_buffering = true + socket.buffer_size = 16384 + socket.read_timeout = 60.seconds + yield socket +ensure + socket.try &.close +end + +def with_client_io(server, &) + with_client_socket(server) do |socket| + yield MQTT::Protocol::IO.new(socket), socket + end +end + +def with_mqtt_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) + with_server(:mqtt, tls, replicator, &blk) +end diff --git a/spec/mqtt/spec_helper/mqtt_client.cr b/spec/mqtt/spec_helper/mqtt_client.cr new file mode 100644 index 0000000000..4e8a0c842b --- /dev/null +++ b/spec/mqtt/spec_helper/mqtt_client.cr @@ -0,0 +1,112 @@ +require "mqtt-protocol" +require "./mqtt_helpers" + +module Specs + class MqttClient + def next_packet_id + @packet_id_generator.next.as(UInt16) + end + + @packet_id_generator : Iterator(UInt16) + + getter client_id + + def initialize(io : IO) + @client_id = "" + @io = MQTT::Protocol::IO.new(io) + @packet_id_generator = (0u16..).each + end + + def connect( + expect_response = true, + username = "valid_user", + password = "valid_password", + client_id = "spec_client", + keepalive = 30u16, + will = nil, + clean_session = true, + **args + ) + connect_args = { + client_id: client_id, + clean_session: clean_session, + keepalive: keepalive, + will: will, + username: username, + password: password.to_slice, + }.merge(args) + @client_id = connect_args.fetch(:client_id, "").to_s + MQTT::Protocol::Connect.new(**connect_args).to_io(@io) + read_packet if expect_response + end + + def disconnect + MQTT::Protocol::Disconnect.new.to_io(@io) + true + rescue IO::Error + false + end + + def subscribe(topic : String, qos : UInt8 = 0u8, expect_response = true) + filter = MQTT::Protocol::Subscribe::TopicFilter.new(topic, qos) + MQTT::Protocol::Subscribe.new([filter], packet_id: next_packet_id).to_io(@io) + read_packet if expect_response + end + + def unsubscribe(*topics : String, expect_response = true) + MQTT::Protocol::Unsubscribe.new(topics.to_a, next_packet_id).to_io(@io) + read_packet if expect_response + end + + def publish( + topic : String, + payload : String, + qos = 0, + retain = false, + packet_id : UInt16? = next_packet_id, + expect_response = true + ) + pub_args = { + packet_id: packet_id, + payload: payload.to_slice, + topic: topic, + dup: false, + qos: qos.to_u8, + retain: retain, + } + MQTT::Protocol::Publish.new(**pub_args).to_io(@io) + read_packet if pub_args[:qos].positive? && expect_response + end + + def puback(packet_id : UInt16?) + return if packet_id.nil? + MQTT::Protocol::PubAck.new(packet_id).to_io(@io) + end + + def puback(packet : MQTT::Protocol::Publish) + if packet_id = packet.packet_id + MQTT::Protocol::PubAck.new(packet_id).to_io(@io) + end + end + + def ping(expect_response = true) + MQTT::Protocol::PingReq.new.to_io(@io) + read_packet if expect_response + end + + def read_packet + MQTT::Protocol::Packet.from_io(@io) + rescue ex : IO::Error + @io.close + raise ex + end + + def close + @io.close + end + + def closed? + @io.closed? + end + end +end diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr new file mode 100644 index 0000000000..d4841f590d --- /dev/null +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -0,0 +1,71 @@ +require "mqtt-protocol" +require "./mqtt_client" + +def packet_id_generator + (0u16..).each +end + +def next_packet_id + packet_id_generator.next.as(UInt16) +end + +def connect(io, expect_response = true, **args) + MQTT::Protocol::Connect.new(**{ + client_id: "client_id", + clean_session: false, + keepalive: 30u16, + username: "guest", + password: "guest".to_slice, + will: nil, + }.merge(args)).to_io(io) + MQTT::Protocol::Packet.from_io(io) if expect_response +end + +def disconnect(io) + MQTT::Protocol::Disconnect.new.to_io(io) +end + +def mk_topic_filters(*args) : Array(MQTT::Protocol::Subscribe::TopicFilter) + ret = Array(MQTT::Protocol::Subscribe::TopicFilter).new + args.each { |topic, qos| ret << subtopic(topic, qos) } + ret +end + +def subscribe(io, expect_response = true, **args) + MQTT::Protocol::Subscribe.new(**{packet_id: next_packet_id}.merge(args)).to_io(io) + MQTT::Protocol::Packet.from_io(io) if expect_response +end + +def unsubscribe(io, topics : Array(String), expect_response = true, packet_id = next_packet_id) + MQTT::Protocol::Unsubscribe.new(topics, packet_id).to_io(io) + MQTT::Protocol::Packet.from_io(io) if expect_response +end + +def subtopic(topic : String, qos = 0) + MQTT::Protocol::Subscribe::TopicFilter.new(topic, qos.to_u8) +end + +def publish(io, expect_response = true, **args) + pub_args = { + packet_id: next_packet_id, + payload: "data".to_slice, + dup: false, + qos: 0u8, + retain: false, + }.merge(args) + MQTT::Protocol::Publish.new(**pub_args).to_io(io) + MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response +end + +def puback(io, packet_id : UInt16?) + return if packet_id.nil? + MQTT::Protocol::PubAck.new(packet_id).to_io(io) +end + +def ping(io) + MQTT::Protocol::PingReq.new.to_io(io) +end + +def read_packet(io) + MQTT::Protocol::Packet.from_io(io) +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 48e5a873f2..129e8dd144 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -72,7 +72,7 @@ def test_headers(headers = nil) req_hdrs end -def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) +def with_server(protocol, tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) tcp_server = TCPServer.new("localhost", 0) s = LavinMQ::Server.new(LavinMQ::Config.instance.data_dir, replicator) begin @@ -80,9 +80,9 @@ def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.n ctx = OpenSSL::SSL::Context::Server.new ctx.certificate_chain = "spec/resources/server_certificate.pem" ctx.private_key = "spec/resources/server_key.pem" - spawn(name: "amqp tls listen") { s.listen_tls(tcp_server, ctx, "amqp") } + spawn(name: "#{protocol} tls listen") { s.listen_tls(tcp_server, ctx, protocol) } else - spawn(name: "amqp tcp listen") { s.listen(tcp_server, "amqp") } + spawn(name: "#{protocol} tcp listen") { s.listen(tcp_server, protocol) } end Fiber.yield yield s @@ -92,6 +92,14 @@ def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.n end end +def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) + with_server(:amqp, tls, replicator, &blk) +end + +def with_mqtt_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) + with_server(:mqtt, tls, replicator, &blk) +end + def with_http_server(&) with_amqp_server do |s| h = LavinMQ::HTTP::Server.new(s) diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 74eef07def..a78c71d78c 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -78,7 +78,7 @@ module LavinMQ end def listen(s : TCPServer, protocol) - @listeners[s] = :protocol + @listeners[s] = protocol Log.info { "Listening on #{s.local_address}" } loop do client = s.accept? || break @@ -124,7 +124,7 @@ module LavinMQ end def listen(s : UNIXServer, protocol) - @listeners[s] = :protocol + @listeners[s] = protocol Log.info { "Listening on #{s.local_address}" } loop do # do not try to use while client = s.accept? || break @@ -156,7 +156,7 @@ module LavinMQ end def listen_tls(s : TCPServer, context, protocol) - @listeners[s] = :protocol + @listeners[s] = protocol Log.info { "Listening on #{s.local_address} (TLS)" } loop do # do not try to use while client = s.accept? || break @@ -257,7 +257,6 @@ module LavinMQ Log.warn { "Unknown protocol '#{protocol}'" } socket.close end - ensure socket.close if client.nil? end From 403dfcfe7c2af84b4a1ce6cd9a47d8857652a368 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 6 Sep 2024 13:57:55 +0200 Subject: [PATCH 006/202] fixup! amqp publish --- static/js/connections.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/connections.js b/static/js/connections.js index 90fd633c48..949f0c05d1 100644 --- a/static/js/connections.js +++ b/static/js/connections.js @@ -19,7 +19,7 @@ Table.renderTable('table', tableOptions, function (tr, item, all) { if (all) { const connectionLink = document.createElement('a') connectionLink.href = `connection#name=${encodeURIComponent(item.name)}` - if (item.protocol !== 'MQTT' && item.client_properties.connection_name) { + if (item?.client_properties?.connection_name) { connectionLink.appendChild(document.createElement('span')).textContent = item.name connectionLink.appendChild(document.createElement('br')) connectionLink.appendChild(document.createElement('small')).textContent = item.client_properties.connection_name @@ -37,7 +37,7 @@ Table.renderTable('table', tableOptions, function (tr, item, all) { Table.renderCell(tr, 10, item.timeout, 'right') Table.renderCell(tr, 8, item.auth_mechanism) const clientDiv = document.createElement('span') - if (item.protocol !== 'MQTT') { + if (item?.client_properties) { clientDiv.textContent = `${item.client_properties.product} / ${item.client_properties.platform || ''}` clientDiv.appendChild(document.createElement('br')) clientDiv.appendChild(document.createElement('small')).textContent = item.client_properties.version From 1c198bc6131d3f3d85367ceab5f2e955cb5a16b5 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Fri, 6 Sep 2024 14:53:50 +0200 Subject: [PATCH 007/202] int-specs --- spec/mqtt/integrations/ping_spec.cr | 19 ++-- spec/mqtt/spec_helper.cr | 32 ------ spec/mqtt/spec_helper/mqtt_helpers.cr | 150 ++++++++++++++++--------- spec/mqtt_spec.cr | 13 +-- spec/spec_helper.cr | 12 +- src/lavinmq/mqtt/client.cr | 30 ++--- src/lavinmq/mqtt/connection_factory.cr | 11 +- 7 files changed, 137 insertions(+), 130 deletions(-) diff --git a/spec/mqtt/integrations/ping_spec.cr b/spec/mqtt/integrations/ping_spec.cr index a7428cf825..35a2c80eb2 100644 --- a/spec/mqtt/integrations/ping_spec.cr +++ b/spec/mqtt/integrations/ping_spec.cr @@ -1,13 +1,16 @@ require "../spec_helper" -describe "ping" do - it "responds to ping [MQTT-3.12.4-1]" do - with_mqtt_server do |server| - with_client_io(server) do |io| - connect(io) - ping(io) - resp = read_packet(io) - resp.should be_a(MQTT::Protocol::PingResp) +module MqttSpecs + extend MqttHelpers + describe "ping" do + it "responds to ping [MQTT-3.12.4-1]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + ping(io) + resp = read_packet(io) + resp.should be_a(MQTT::Protocol::PingResp) + end end end end diff --git a/spec/mqtt/spec_helper.cr b/spec/mqtt/spec_helper.cr index 9465e68393..9c7fbca83c 100644 --- a/spec/mqtt/spec_helper.cr +++ b/spec/mqtt/spec_helper.cr @@ -1,34 +1,2 @@ require "../spec_helper" require "./spec_helper/mqtt_helpers" - -def with_client_socket(server, &) - listener = server.listeners.find { |l| l[:protocol] == :mqtt }.as(NamedTuple(ip_address: String, protocol: Symbol, port: Int32) - ) - - socket = TCPSocket.new( - listener[:ip_address], - listener[:port], - connect_timeout: 30) - socket.keepalive = true - socket.tcp_nodelay = false - socket.tcp_keepalive_idle = 60 - socket.tcp_keepalive_count = 3 - socket.tcp_keepalive_interval = 10 - socket.sync = true - socket.read_buffering = true - socket.buffer_size = 16384 - socket.read_timeout = 60.seconds - yield socket -ensure - socket.try &.close -end - -def with_client_io(server, &) - with_client_socket(server) do |socket| - yield MQTT::Protocol::IO.new(socket), socket - end -end - -def with_mqtt_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) - with_server(:mqtt, tls, replicator, &blk) -end diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr index d4841f590d..40478360d3 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -1,71 +1,109 @@ require "mqtt-protocol" require "./mqtt_client" +require "../../spec_helper" -def packet_id_generator - (0u16..).each -end +module MqttHelpers + def with_client_socket(server, &) + listener = server.listeners.find { |l| l[:protocol] == :mqtt } + tcp_listener = listener.as(NamedTuple(ip_address: String, protocol: Symbol, port: Int32)) -def next_packet_id - packet_id_generator.next.as(UInt16) -end + socket = TCPSocket.new( + tcp_listener[:ip_address], + tcp_listener[:port], + connect_timeout: 30) + socket.keepalive = true + socket.tcp_nodelay = false + socket.tcp_keepalive_idle = 60 + socket.tcp_keepalive_count = 3 + socket.tcp_keepalive_interval = 10 + socket.sync = true + socket.read_buffering = true + socket.buffer_size = 16384 + socket.read_timeout = 60.seconds + yield socket + ensure + socket.try &.close + end -def connect(io, expect_response = true, **args) - MQTT::Protocol::Connect.new(**{ - client_id: "client_id", - clean_session: false, - keepalive: 30u16, - username: "guest", - password: "guest".to_slice, - will: nil, - }.merge(args)).to_io(io) - MQTT::Protocol::Packet.from_io(io) if expect_response -end + def with_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) + with_server(:mqtt, tls, replicator) do |s| + yield s + end + end -def disconnect(io) - MQTT::Protocol::Disconnect.new.to_io(io) -end + def with_client_io(server, &) + with_client_socket(server) do |socket| + io = MQTT::Protocol::IO.new(socket) + with MqttHelpers yield io + end + end -def mk_topic_filters(*args) : Array(MQTT::Protocol::Subscribe::TopicFilter) - ret = Array(MQTT::Protocol::Subscribe::TopicFilter).new - args.each { |topic, qos| ret << subtopic(topic, qos) } - ret -end + def packet_id_generator + (0u16..).each + end -def subscribe(io, expect_response = true, **args) - MQTT::Protocol::Subscribe.new(**{packet_id: next_packet_id}.merge(args)).to_io(io) - MQTT::Protocol::Packet.from_io(io) if expect_response -end + def next_packet_id + packet_id_generator.next.as(UInt16) + end -def unsubscribe(io, topics : Array(String), expect_response = true, packet_id = next_packet_id) - MQTT::Protocol::Unsubscribe.new(topics, packet_id).to_io(io) - MQTT::Protocol::Packet.from_io(io) if expect_response -end + def connect(io, expect_response = true, **args) + MQTT::Protocol::Connect.new(**{ + client_id: "client_id", + clean_session: false, + keepalive: 30u16, + username: "guest", + password: "guest".to_slice, + will: nil, + }.merge(args)).to_io(io) + MQTT::Protocol::Packet.from_io(io) if expect_response + end -def subtopic(topic : String, qos = 0) - MQTT::Protocol::Subscribe::TopicFilter.new(topic, qos.to_u8) -end + def disconnect(io) + MQTT::Protocol::Disconnect.new.to_io(io) + end -def publish(io, expect_response = true, **args) - pub_args = { - packet_id: next_packet_id, - payload: "data".to_slice, - dup: false, - qos: 0u8, - retain: false, - }.merge(args) - MQTT::Protocol::Publish.new(**pub_args).to_io(io) - MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response -end + def mk_topic_filters(*args) : Array(MQTT::Protocol::Subscribe::TopicFilter) + ret = Array(MQTT::Protocol::Subscribe::TopicFilter).new + args.each { |topic, qos| ret << subtopic(topic, qos) } + ret + end -def puback(io, packet_id : UInt16?) - return if packet_id.nil? - MQTT::Protocol::PubAck.new(packet_id).to_io(io) -end + def subscribe(io, expect_response = true, **args) + MQTT::Protocol::Subscribe.new(**{packet_id: next_packet_id}.merge(args)).to_io(io) + MQTT::Protocol::Packet.from_io(io) if expect_response + end -def ping(io) - MQTT::Protocol::PingReq.new.to_io(io) -end + def unsubscribe(io, topics : Array(String), expect_response = true, packet_id = next_packet_id) + MQTT::Protocol::Unsubscribe.new(topics, packet_id).to_io(io) + MQTT::Protocol::Packet.from_io(io) if expect_response + end + + def subtopic(topic : String, qos = 0) + MQTT::Protocol::Subscribe::TopicFilter.new(topic, qos.to_u8) + end + + def publish(io, expect_response = true, **args) + pub_args = { + packet_id: next_packet_id, + payload: "data".to_slice, + dup: false, + qos: 0u8, + retain: false, + }.merge(args) + MQTT::Protocol::Publish.new(**pub_args).to_io(io) + MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response + end + + def puback(io, packet_id : UInt16?) + return if packet_id.nil? + MQTT::Protocol::PubAck.new(packet_id).to_io(io) + end + + def ping(io) + MQTT::Protocol::PingReq.new.to_io(io) + end -def read_packet(io) - MQTT::Protocol::Packet.from_io(io) + def read_packet(io) + MQTT::Protocol::Packet.from_io(io) + end end diff --git a/spec/mqtt_spec.cr b/spec/mqtt_spec.cr index c6e73d2835..a7e15c05ba 100644 --- a/spec/mqtt_spec.cr +++ b/spec/mqtt_spec.cr @@ -4,23 +4,18 @@ require "./spec_helper" require "mqtt-protocol" require "../src/lavinmq/mqtt/connection_factory" - def setup_connection(s, pass) left, right = UNIXSocket.pair io = MQTT::Protocol::IO.new(left) s.users.create("usr", "pass", [LavinMQ::Tag::Administrator]) MQTT::Protocol::Connect.new("abc", false, 60u16, "usr", pass.to_slice, nil).to_io(io) - connection_factory = LavinMQ::MQTT::ConnectionFactory.new(right, - LavinMQ::ConnectionInfo.local, - s.users, - s.vhosts["/"]) - { connection_factory.start, io } + connection_factory = LavinMQ::MQTT::ConnectionFactory.new( + s.users, + s.vhosts["/"]) + {connection_factory.start(right, LavinMQ::ConnectionInfo.local), io} end describe LavinMQ do - src = "127.0.0.1" - dst = "127.0.0.1" - it "MQTT connection should pass authentication" do with_amqp_server do |s| client, io = setup_connection(s, "pass") diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 129e8dd144..c7c42649d3 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -92,12 +92,16 @@ def with_server(protocol, tls = false, replicator = LavinMQ::Clustering::NoopSer end end -def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) - with_server(:amqp, tls, replicator, &blk) +def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) + with_server(:amqp, tls, replicator) do |s| + yield s + end end -def with_mqtt_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) - with_server(:mqtt, tls, replicator, &blk) +def with_mqtt_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) + with_server(:mqtt, tls, replicator) do |s| + yield s + end end def with_http_server(&) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 59b3cfa968..d9a24aefb9 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -32,10 +32,13 @@ module LavinMQ @vhost.add_connection(self) @session = start_session(self) @log.info { "Connection established for user=#{@user.name}" } - pp "spawn" spawn read_loop end + def client_name + "mqtt-client" + end + private def read_loop loop do Log.trace { "waiting for packet" } @@ -44,10 +47,11 @@ module LavinMQ # If we dont breakt the loop here we'll get a IO/Error on next read. break if packet.is_a?(MQTT::Disconnect) end - rescue ex : MQTT::Error::Connect + rescue ex : MQTT::Error::Connect Log.warn { "Connect error #{ex.inspect}" } - ensure - + rescue ex : ::IO::EOFError + Log.info { "eof #{ex.inspect}" } + ensure if @clean_session disconnect_session(self) end @@ -67,7 +71,7 @@ module LavinMQ when MQTT::Unsubscribe then pp "unsubscribe" when MQTT::PingReq then receive_pingreq(packet) when MQTT::Disconnect then return packet - else raise "invalid packet type for client to send" + else raise "invalid packet type for client to send" end packet end @@ -97,22 +101,21 @@ module LavinMQ def recieve_puback(packet) end - #let prefetch = 1 + # let prefetch = 1 def recieve_subscribe(packet) # exclusive conusmer # end def recieve_unsubscribe(packet) - end def details_tuple { - vhost: @vhost.name, - user: @user.name, - protocol: "MQTT", - client_id: @client_id, + vhost: @vhost.name, + user: @user.name, + protocol: "MQTT", + client_id: @client_id, }.merge(stats_details) end @@ -121,7 +124,6 @@ module LavinMQ pp "clear session" @vhost.clear_session(client) end - pp "start session" @vhost.start_session(client) end @@ -130,16 +132,14 @@ module LavinMQ @vhost.clear_session(client) end - def update_rates end - def close(reason) + def close(reason = "") end def force_close end - end end end diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 30ac98ab91..c434cd95e1 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -8,8 +8,7 @@ require "../user" module LavinMQ module MQTT class ConnectionFactory - def initialize( - @users : UserStore, + def initialize(@users : UserStore, @vhost : VHost) end @@ -23,16 +22,16 @@ module LavinMQ return LavinMQ::MQTT::Client.new(socket, connection_info, @vhost, user, packet.client_id, packet.clean_session?) end end - rescue ex - Log.warn { "Recieved the wrong packet" } - socket.close + rescue ex + Log.warn { "Recieved the wrong packet" } + socket.close end def authenticate(io, packet) return nil unless (username = packet.username) && (password = packet.password) user = @users[username]? return user if user && user.password && user.password.not_nil!.verify(String.new(password)) - #probably not good to differentiate between user not found and wrong password + # probably not good to differentiate between user not found and wrong password if user.nil? Log.warn { "User \"#{username}\" not found" } else From 702d6fb479f1c94cb307daf7932b2ce03e19fe66 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Fri, 6 Sep 2024 15:01:31 +0200 Subject: [PATCH 008/202] cleanup --- spec/mqtt/spec_helper.cr | 1 - spec/mqtt/spec_helper/mqtt_client.cr | 1 - 2 files changed, 2 deletions(-) diff --git a/spec/mqtt/spec_helper.cr b/spec/mqtt/spec_helper.cr index 9c7fbca83c..75c4e8cd10 100644 --- a/spec/mqtt/spec_helper.cr +++ b/spec/mqtt/spec_helper.cr @@ -1,2 +1 @@ -require "../spec_helper" require "./spec_helper/mqtt_helpers" diff --git a/spec/mqtt/spec_helper/mqtt_client.cr b/spec/mqtt/spec_helper/mqtt_client.cr index 4e8a0c842b..20d0702c3e 100644 --- a/spec/mqtt/spec_helper/mqtt_client.cr +++ b/spec/mqtt/spec_helper/mqtt_client.cr @@ -1,5 +1,4 @@ require "mqtt-protocol" -require "./mqtt_helpers" module Specs class MqttClient From 880d521ab3c7c246e4b479843049a47bb28055c1 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 10 Sep 2024 13:20:42 +0200 Subject: [PATCH 009/202] connect specs (pending) --- spec/mqtt/integrations/connect_spec.cr | 268 +++++++++++++++++++++++++ spec/mqtt/spec_helper.cr | 2 + spec/mqtt/spec_helper/mqtt_helpers.cr | 2 +- spec/mqtt/spec_helper/mqtt_matchers.cr | 25 +++ spec/mqtt/spec_helper/mqtt_protocol.cr | 12 ++ spec/spec_helper.cr | 6 - 6 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 spec/mqtt/integrations/connect_spec.cr create mode 100644 spec/mqtt/spec_helper/mqtt_matchers.cr create mode 100644 spec/mqtt/spec_helper/mqtt_protocol.cr diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr new file mode 100644 index 0000000000..11d65a9aac --- /dev/null +++ b/spec/mqtt/integrations/connect_spec.cr @@ -0,0 +1,268 @@ +require "../spec_helper" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + describe "connect [MQTT-3.1.4-1]" do + describe "when client already connected", tags: "first" do + pending "should replace the already connected client [MQTT-3.1.4-2]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + with_client_io(server) do |io2| + connect(io2) + io.should be_closed + end + end + end + end + end + + describe "receives connack" do + describe "with expected flags set" do + pending "no session present when reconnecting a non-clean session with a clean session [MQTT-3.1.2-6]" do + with_server do |server| + with_client_io(server) do |io| + connect(io, clean_session: false) + + # Myra won't save sessions without subscriptions + subscribe(io, + topic_filters: [subtopic("a/topic", 0u8)], + packet_id: 1u16 + ) + disconnect(io) + end + with_client_io(server) do |io| + connack = connect(io, clean_session: true) + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + connack.session_present?.should be_false + end + end + end + + pending "no session present when reconnecting a clean session with a non-clean session [MQTT-3.1.2-6]" do + with_server do |server| + with_client_io(server) do |io| + connect(io, clean_session: true) + subscribe(io, + topic_filters: [subtopic("a/topic", 0u8)], + packet_id: 1u16 + ) + disconnect(io) + end + with_client_io(server) do |io| + connack = connect(io, clean_session: false) + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + connack.session_present?.should be_false + end + end + end + + pending "no session present when reconnecting a clean session [MQTT-3.1.2-6]" do + with_server do |server| + with_client_io(server) do |io| + connect(io, clean_session: true) + subscribe(io, + topic_filters: [subtopic("a/topic", 0u8)], + packet_id: 1u16 + ) + disconnect(io) + end + with_client_io(server) do |io| + connack = connect(io, clean_session: true) + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + connack.session_present?.should be_false + end + end + end + + pending "session present when reconnecting a non-clean session [MQTT-3.1.2-4]" do + with_server do |server| + with_client_io(server) do |io| + connect(io, clean_session: false) + subscribe(io, + topic_filters: [subtopic("a/topic", 0u8)], + packet_id: 1u16 + ) + disconnect(io) + end + with_client_io(server) do |io| + connack = connect(io, clean_session: false) + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + connack.session_present?.should be_true + end + end + end + end + + describe "with expected return code" do + pending "for valid credentials [MQTT-3.1.4-4]" do + with_server do |server| + with_client_io(server) do |io| + connack = connect(io) + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + pp connack.return_code + connack.return_code.should eq(MQTT::Protocol::Connack::ReturnCode::Accepted) + end + end + end + + # pending "for invalid credentials" do + # auth = SpecAuth.new({"a" => {password: "b", acls: ["a", "a/b", "/", "/a"] of String}}) + # with_server(auth: auth) do |server| + # with_client_io(server) do |io| + # connack = connect(io, username: "nouser") + + # connack.should be_a(MQTT::Protocol::Connack) + # connack = connack.as(MQTT::Protocol::Connack) + # connack.return_code.should eq(MQTT::Protocol::Connack::ReturnCode::NotAuthorized) + # # Verify that connection is closed [MQTT-3.1.4-1] + # io.should be_closed + # end + # end + # end + + pending "for invalid protocol version [MQTT-3.1.2-2]" do + with_server do |server| + with_client_io(server) do |io| + temp_io = IO::Memory.new + temp_mqtt_io = MQTT::Protocol::IO.new(temp_io) + connect(temp_mqtt_io, expect_response: false) + temp_io.rewind + connect_pkt = temp_io.to_slice + # This will overwrite the protocol level byte + connect_pkt[8] = 9u8 + io.write_bytes_raw connect_pkt + + connack = MQTT::Protocol::Packet.from_io(io) + + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + connack.return_code.should eq(MQTT::Protocol::Connack::ReturnCode::UnacceptableProtocolVersion) + # Verify that connection is closed [MQTT-3.1.4-1] + io.should be_closed + end + end + end + + pending "for empty client id with non-clean session [MQTT-3.1.3-8]" do + with_server do |server| + with_client_io(server) do |io| + connack = connect(io, client_id: "", clean_session: false) + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + connack.return_code.should eq(MQTT::Protocol::Connack::ReturnCode::IdentifierRejected) + # Verify that connection is closed [MQTT-3.1.4-1] + io.should be_closed + end + end + end + + pending "for password flag set without username flag set [MQTT-3.1.2-22]" do + with_server do |server| + with_client_io(server) do |io| + connect = MQTT::Protocol::Connect.new( + client_id: "client_id", + clean_session: true, + keepalive: 30u16, + username: nil, + password: "valid_password".to_slice, + will: nil + ).to_slice + # Set password flag + connect[9] |= 0b0100_0000 + io.write_bytes_raw connect + + # Verify that connection is closed [MQTT-3.1.4-1] + io.should be_closed + end + end + end + end + + describe "tcp socket is closed [MQTT-3.1.4-1]" do + pending "if first packet is not a CONNECT [MQTT-3.1.0-1]" do + with_server do |server| + with_client_io(server) do |io| + ping(io) + io.should be_closed + end + end + end + + pending "for a second CONNECT packet [MQTT-3.1.0-2]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + connect(io, expect_response: false) + + io.should be_closed + end + end + end + + pending "for invalid client id [MQTT-3.1.3-4]." do + with_server do |server| + with_client_io(server) do |io| + MQTT::Protocol::Connect.new( + client_id: "client\u0000_id", + clean_session: true, + keepalive: 30u16, + username: "valid_user", + password: "valid_user".to_slice, + will: nil + ).to_io(io) + + io.should be_closed + end + end + end + + pending "for invalid protocol name [MQTT-3.1.2-1]" do + with_server do |server| + with_client_io(server) do |io| + connect = MQTT::Protocol::Connect.new( + client_id: "client_id", + clean_session: true, + keepalive: 30u16, + username: "valid_user", + password: "valid_password".to_slice, + will: nil + ).to_slice + + # This will overwrite the last "T" in MQTT + connect[7] = 'x'.ord.to_u8 + io.write_bytes_raw connect + + io.should be_closed + end + end + end + + pending "for reserved bit set [MQTT-3.1.2-3]" do + with_server do |server| + with_client_io(server) do |io| + connect = MQTT::Protocol::Connect.new( + client_id: "client_id", + clean_session: true, + keepalive: 30u16, + username: "valid_user", + password: "valid_password".to_slice, + will: nil + ).to_slice + connect[9] |= 0b0000_0001 + io.write_bytes_raw connect + + io.should be_closed + end + end + end + end + end + end +end diff --git a/spec/mqtt/spec_helper.cr b/spec/mqtt/spec_helper.cr index 75c4e8cd10..ae011c16b6 100644 --- a/spec/mqtt/spec_helper.cr +++ b/spec/mqtt/spec_helper.cr @@ -1 +1,3 @@ require "./spec_helper/mqtt_helpers" +require "./spec_helper/mqtt_matchers" +require "./spec_helper/mqtt_protocol" diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr index 40478360d3..3491352b9c 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -19,7 +19,7 @@ module MqttHelpers socket.sync = true socket.read_buffering = true socket.buffer_size = 16384 - socket.read_timeout = 60.seconds + socket.read_timeout = 1.seconds yield socket ensure socket.try &.close diff --git a/spec/mqtt/spec_helper/mqtt_matchers.cr b/spec/mqtt/spec_helper/mqtt_matchers.cr new file mode 100644 index 0000000000..ecb77ac92c --- /dev/null +++ b/spec/mqtt/spec_helper/mqtt_matchers.cr @@ -0,0 +1,25 @@ +module MqttMatchers + struct ClosedExpectation + include MqttHelpers + + def match(actual : MQTT::Protocol::IO) + return true if actual.closed? + read_packet(actual) + false + rescue e : IO::Error + true + end + + def failure_message(actual_value) + "Expected #{actual_value.pretty_inspect} to be closed" + end + + def negative_failure_message(actual_value) + "Expected #{actual_value.pretty_inspect} to be open" + end + end + + def be_closed + ClosedExpectation.new + end +end diff --git a/spec/mqtt/spec_helper/mqtt_protocol.cr b/spec/mqtt/spec_helper/mqtt_protocol.cr new file mode 100644 index 0000000000..5bf73e4ba9 --- /dev/null +++ b/spec/mqtt/spec_helper/mqtt_protocol.cr @@ -0,0 +1,12 @@ +module MQTT + module Protocol + abstract struct Packet + def to_slice + io = ::IO::Memory.new + self.to_io(IO.new(io)) + io.rewind + io.to_slice + end + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index c7c42649d3..40501c6323 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -98,12 +98,6 @@ def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.n end end -def with_mqtt_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) - with_server(:mqtt, tls, replicator) do |s| - yield s - end -end - def with_http_server(&) with_amqp_server do |s| h = LavinMQ::HTTP::Server.new(s) From 8f2d9cb3e2d8810157d0368b8ee0b0be6f7c7167 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 10 Sep 2024 13:58:18 +0200 Subject: [PATCH 010/202] all integration specs (pending) --- spec/mqtt/integrations/connect_spec.cr | 2 +- .../integrations/duplicate_message_spec.cr | 90 +++++++ spec/mqtt/integrations/message_qos_spec.cr | 248 ++++++++++++++++++ .../integrations/retained_messages_spec.cr | 79 ++++++ spec/mqtt/integrations/subscribe_spec.cr | 104 ++++++++ spec/mqtt/integrations/unsubscribe_spec.cr | 96 +++++++ spec/mqtt/integrations/will_spec.cr | 166 ++++++++++++ spec/mqtt/spec_helper/mqtt_matchers.cr | 22 ++ 8 files changed, 806 insertions(+), 1 deletion(-) create mode 100644 spec/mqtt/integrations/duplicate_message_spec.cr create mode 100644 spec/mqtt/integrations/message_qos_spec.cr create mode 100644 spec/mqtt/integrations/retained_messages_spec.cr create mode 100644 spec/mqtt/integrations/subscribe_spec.cr create mode 100644 spec/mqtt/integrations/unsubscribe_spec.cr create mode 100644 spec/mqtt/integrations/will_spec.cr diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 11d65a9aac..5a3d6e7d0a 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -4,7 +4,7 @@ module MqttSpecs extend MqttHelpers extend MqttMatchers describe "connect [MQTT-3.1.4-1]" do - describe "when client already connected", tags: "first" do + describe "when client already connected" do pending "should replace the already connected client [MQTT-3.1.4-2]" do with_server do |server| with_client_io(server) do |io| diff --git a/spec/mqtt/integrations/duplicate_message_spec.cr b/spec/mqtt/integrations/duplicate_message_spec.cr new file mode 100644 index 0000000000..70ccf10349 --- /dev/null +++ b/spec/mqtt/integrations/duplicate_message_spec.cr @@ -0,0 +1,90 @@ +require "../spec_helper.cr" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + + describe "duplicate messages" do + pending "dup must not be set if qos is 0 [MQTT-3.3.1-2]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + # Subscribe with qos=0 means downgrade messages to qos=0 + topic_filter = MQTT::Protocol::Subscribe::TopicFilter.new("a/b", 0u8) + subscribe(io, topic_filters: [topic_filter]) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + publish(publisher_io, topic: "a/b", qos: 0u8) + publish(publisher_io, topic: "a/b", qos: 1u8) + disconnect(publisher_io) + end + + pub1 = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub1.qos.should eq(0u8) + pub1.dup?.should be_false + pub2 = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub2.qos.should eq(0u8) + pub2.dup?.should be_false + + disconnect(io) + end + end + end + + pending "dup is set when a message is being redelivered [MQTT-3.3.1.-1]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filter = MQTT::Protocol::Subscribe::TopicFilter.new("a/b", 1u8) + subscribe(io, topic_filters: [topic_filter]) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + publish(publisher_io, topic: "a/b", qos: 1u8) + disconnect(publisher_io) + end + + pub = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub.dup?.should be_false + disconnect(io) + end + + with_client_io(server) do |io| + connect(io) + pub = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub.dup?.should be_true + disconnect(io) + end + end + end + + pending "dup on incoming messages is not propagated to other clients [MQTT-3.3.1-3]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + # Subscribe with qos=0 means downgrade messages to qos=0 + topic_filter = MQTT::Protocol::Subscribe::TopicFilter.new("a/b", 1u8) + subscribe(io, topic_filters: [topic_filter]) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + publish(publisher_io, topic: "a/b", qos: 1u8, dup: true) + publish(publisher_io, topic: "a/b", qos: 1u8, dup: true) + disconnect(publisher_io) + end + + pub1 = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub1.dup?.should be_false + pub2 = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub2.dup?.should be_false + + puback(io, pub1.packet_id) + puback(io, pub2.packet_id) + + disconnect(io) + end + end + end + end +end diff --git a/spec/mqtt/integrations/message_qos_spec.cr b/spec/mqtt/integrations/message_qos_spec.cr new file mode 100644 index 0000000000..871c886c8c --- /dev/null +++ b/spec/mqtt/integrations/message_qos_spec.cr @@ -0,0 +1,248 @@ +require "../spec_helper.cr" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + describe "message qos" do + pending "both qos bits can't be set [MQTT-3.3.1-4]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + temp_io = IO::Memory.new + publish(MQTT::Protocol::IO.new(temp_io), topic: "a/b", qos: 1u8, expect_response: false) + pub_pkt = temp_io.to_slice + pub_pkt[0] |= 0b0000_0110u8 + io.write pub_pkt + + io.should be_closed + end + end + end + + pending "qos is set according to subscription qos [MYRA non-normative]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + # Subscribe with qos=0 means downgrade messages to qos=0 + topic_filters = mk_topic_filters({"a/b", 0u8}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + publish(publisher_io, topic: "a/b", qos: 0u8) + publish(publisher_io, topic: "a/b", qos: 1u8) + disconnect(publisher_io) + end + + pub1 = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub1.qos.should eq(0u8) + pub1.dup?.should be_false + pub2 = MQTT::Protocol::Packet.from_io(io).as(MQTT::Protocol::Publish) + pub2.qos.should eq(0u8) + pub2.dup?.should be_false + + disconnect(io) + end + end + end + + pending "qos1 messages are stored for offline sessions [MQTT-3.1.2-5]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"a/b", 1u8}) + subscribe(io, topic_filters: topic_filters) + disconnect(io) + end + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + 100.times do + # qos doesnt matter here + publish(publisher_io, topic: "a/b", qos: 0u8) + end + disconnect(publisher_io) + end + + with_client_io(server) do |io| + connect(io) + 100.times do + pkt = read_packet(io) + pkt.should be_a(MQTT::Protocol::Publish) + if pub = pkt.as?(MQTT::Protocol::Publish) + puback(io, pub.packet_id) + end + end + disconnect(io) + end + end + end + + pending "acked qos1 message won't be sent again" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"a/b", 1u8}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + publish(publisher_io, topic: "a/b", payload: "1".to_slice, qos: 0u8) + publish(publisher_io, topic: "a/b", payload: "2".to_slice, qos: 0u8) + disconnect(publisher_io) + end + + pkt = read_packet(io) + if pub = pkt.as?(MQTT::Protocol::Publish) + pub.payload.should eq("1".to_slice) + puback(io, pub.packet_id) + end + disconnect(io) + end + + with_client_io(server) do |io| + connect(io) + pkt = read_packet(io) + if pub = pkt.as?(MQTT::Protocol::Publish) + pub.payload.should eq("2".to_slice) + puback(io, pub.packet_id) + end + disconnect(io) + end + end + end + + pending "acks must not be ordered" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"a/b", 1u8}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + 10.times do |i| + publish(publisher_io, topic: "a/b", payload: "#{i}".to_slice, qos: 0u8) + end + disconnect(publisher_io) + end + + pubs = Array(MQTT::Protocol::Publish).new(9) + # Read all but one + 9.times do + pubs << read_packet(io).as(MQTT::Protocol::Publish) + end + [1, 3, 4, 0, 2, 7, 5, 6, 8].each do |i| + puback(io, pubs[i].packet_id) + end + disconnect(io) + end + with_client_io(server) do |io| + connect(io) + pub = read_packet(io).as(MQTT::Protocol::Publish) + pub.dup?.should be_true + pub.payload.should eq("9".to_slice) + disconnect(io) + end + end + end + + pending "cannot ack invalid packet id" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + puback(io, 123u16) + + expect_raises(IO::Error) do + read_packet(io) + end + end + end + end + + pending "cannot ack a message twice" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"a/b", 1u8}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + publish(publisher_io, topic: "a/b", qos: 0u8) + disconnect(publisher_io) + end + + pub = read_packet(io).as(MQTT::Protocol::Publish) + + puback(io, pub.packet_id) + + # Sending the second ack make the server close the connection + puback(io, pub.packet_id) + + io.should be_closed + end + end + end + + # TODO: Not yet migrated due to the Spectator::Synchronizer + pending "qos1 unacked messages re-sent in the initial order [MQTT-4.6.0-1]" do + max_inflight_messages = 5 # TODO value was previous "MyraMQ::Config.settings.max_inflight_messages" + # We'll only ACK odd packet ids, and the first id is 1, so if we don't + # do -1 the last packet (id=20) won't be sent because we've reached max + # inflight with all odd ids. + number_of_messages = (max_inflight_messages * 2 - 1).to_u16 + with_server do |server| + with_client_io(server) do |io| + connect(io, client_id: "subscriber") + topic_filters = mk_topic_filters({"a/b", 1u8}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |publisher_io| + connect(publisher_io, client_id: "publisher") + number_of_messages.times do |i| + data = Bytes.new(sizeof(UInt16)) + IO::ByteFormat::SystemEndian.encode(i, data) + # qos doesnt matter here + publish(publisher_io, topic: "a/b", payload: data, qos: 0u8) + end + disconnect(publisher_io) + end + + # Read all messages, but only ack every second + sync = Spectator::Synchronizer.new + spawn(name: "read msgs") do + number_of_messages.times do |i| + pkt = read_packet(io) + pub = pkt.should be_a(MQTT::Protocol::Publish) + # We only ack odd packet ids + puback(io, pub.packet_id) if (i % 2) > 0 + end + sync.done + end + sync.synchronize(timeout: 3.second, msg: "Timeout first read") + disconnect(io) + end + + # We should now get the 50 messages we didn't ack previously, and in order + with_client_io(server) do |io| + connect(io, client_id: "subscriber") + sync = Spectator::Synchronizer.new + spawn(name: "read msgs") do + (number_of_messages // 2).times do |i| + pkt = read_packet(io) + pkt.should be_a(MQTT::Protocol::Publish) + pub = pkt.as(MQTT::Protocol::Publish) + puback(io, pub.packet_id) + data = IO::ByteFormat::SystemEndian.decode(UInt16, pub.payload) + data.should eq(i * 2) + end + sync.done + end + sync.synchronize(timeout: 3.second, msg: "Timeout second read") + disconnect(io) + end + end + end + end +end diff --git a/spec/mqtt/integrations/retained_messages_spec.cr b/spec/mqtt/integrations/retained_messages_spec.cr new file mode 100644 index 0000000000..fda6d2c356 --- /dev/null +++ b/spec/mqtt/integrations/retained_messages_spec.cr @@ -0,0 +1,79 @@ +require "../spec_helper.cr" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + describe "retained messages" do + pending "retained messages are received on subscribe" do + with_server do |server| + with_client_io(server) do |io| + connect(io, client_id: "publisher") + publish(io, topic: "a/b", qos: 0u8, retain: true) + disconnect(io) + end + + with_client_io(server) do |io| + connect(io, client_id: "subscriber") + subscribe(io, topic_filters: [subtopic("a/b")]) + pub = read_packet(io).as(MQTT::Protocol::Publish) + pub.topic.should eq("a/b") + pub.retain?.should eq(true) + disconnect(io) + end + end + end + + pending "retained messages are redelivered for subscriptions with qos1" do + with_server do |server| + with_client_io(server) do |io| + connect(io, client_id: "publisher") + publish(io, topic: "a/b", qos: 0u8, retain: true) + disconnect(io) + end + + with_client_io(server) do |io| + connect(io, client_id: "subscriber") + subscribe(io, topic_filters: [subtopic("a/b", 1u8)]) + # Dont ack + pub = read_packet(io).as(MQTT::Protocol::Publish) + pub.qos.should eq(1u8) + pub.topic.should eq("a/b") + pub.retain?.should eq(true) + pub.dup?.should eq(false) + end + + with_client_io(server) do |io| + connect(io, client_id: "subscriber") + pub = read_packet(io).as(MQTT::Protocol::Publish) + pub.qos.should eq(1u8) + pub.topic.should eq("a/b") + pub.retain?.should eq(true) + pub.dup?.should eq(true) + puback(io, pub.packet_id) + end + end + end + + pending "retain is set in PUBLISH for retained messages" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + publish(io, topic: "a/b", qos: 0u8, retain: true) + disconnect(io) + end + + with_client_io(server) do |io| + connect(io) + # Subscribe with qos=0 means downgrade messages to qos=0 + topic_filters = mk_topic_filters({"a/b", 0u8}) + subscribe(io, topic_filters: topic_filters) + + pub = read_packet(io).as(MQTT::Protocol::Publish) + pub.retain?.should be(true) + + disconnect(io) + end + end + end + end +end diff --git a/spec/mqtt/integrations/subscribe_spec.cr b/spec/mqtt/integrations/subscribe_spec.cr new file mode 100644 index 0000000000..3f96f182bb --- /dev/null +++ b/spec/mqtt/integrations/subscribe_spec.cr @@ -0,0 +1,104 @@ +require "../spec_helper" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + describe "subscribe" do + pending "bits 3,2,1,0 must be set to 0,0,1,0 [MQTT-3.8.1-1]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + temp_io = IO::Memory.new + topic_filters = mk_topic_filters({"a/b", 0}) + subscribe(MQTT::Protocol::IO.new(temp_io), topic_filters: topic_filters, expect_response: false) + temp_io.rewind + subscribe_pkt = temp_io.to_slice + # This will overwrite the protocol level byte + subscribe_pkt[0] |= 0b0000_1010u8 + io.write_bytes_raw subscribe_pkt + + # Verify that connection is closed + io.should be_closed + end + end + end + + pending "must contain at least one topic filter [MQTT-3.8.3-3]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + topic_filters = mk_topic_filters({"a/b", 0}) + temp_io = IO::Memory.new + subscribe(MQTT::Protocol::IO.new(temp_io), topic_filters: topic_filters, expect_response: false) + temp_io.rewind + sub_pkt = temp_io.to_slice + sub_pkt[1] = 2u8 # Override remaning length + io.write_bytes_raw sub_pkt + + # Verify that connection is closed + io.should be_closed + end + end + end + + pending "should not allow any payload reserved bits to be set [MQTT-3-8.3-4]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + topic_filters = mk_topic_filters({"a/b", 0}) + temp_io = IO::Memory.new + subscribe(MQTT::Protocol::IO.new(temp_io), topic_filters: topic_filters, expect_response: false) + temp_io.rewind + sub_pkt = temp_io.to_slice + sub_pkt[sub_pkt.size - 1] |= 0b1010_0100u8 + io.write_bytes_raw sub_pkt + + # Verify that connection is closed + io.should be_closed + end + end + end + + pending "should replace old subscription with new [MQTT-3.8.4-3]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + topic_filters = mk_topic_filters({"a/b", 0}) + suback = subscribe(io, topic_filters: topic_filters) + suback.should be_a(MQTT::Protocol::SubAck) + suback = suback.as(MQTT::Protocol::SubAck) + # Verify that we subscribed as qos0 + suback.return_codes.first.should eq(MQTT::Protocol::SubAck::ReturnCode::QoS0) + + # Publish something to the topic we're subscribed to... + publish(io, topic: "a/b", payload: "a".to_slice, qos: 1u8) + # ... consume it... + pub = read_packet(io).as(MQTT::Protocol::Publish) + # ... and verify it be qos0 (i.e. our subscribe is correct) + pub.qos.should eq(0u8) + + # Now do a second subscribe with another qos and do the same verification + topic_filters = mk_topic_filters({"a/b", 1}) + suback = subscribe(io, topic_filters: topic_filters) + suback.should be_a(MQTT::Protocol::SubAck) + suback = suback.as(MQTT::Protocol::SubAck) + # Verify that we subscribed as qos1 + suback.return_codes.should eq([MQTT::Protocol::SubAck::ReturnCode::QoS1]) + + # Publish something to the topic we're subscribed to... + publish(io, topic: "a/b", payload: "a".to_slice, qos: 1u8) + # ... consume it... + pub = read_packet(io).as(MQTT::Protocol::Publish) + # ... and verify it be qos0 (i.e. our subscribe is correct) + pub.qos.should eq(1u8) + + io.should be_drained + end + end + end + end +end diff --git a/spec/mqtt/integrations/unsubscribe_spec.cr b/spec/mqtt/integrations/unsubscribe_spec.cr new file mode 100644 index 0000000000..94b3d0cf6e --- /dev/null +++ b/spec/mqtt/integrations/unsubscribe_spec.cr @@ -0,0 +1,96 @@ +require "../spec_helper" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + + describe "unsubscribe" do + pending "bits 3,2,1,0 must be set to 0,0,1,0 [MQTT-3.10.1-1]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + temp_io = IO::Memory.new + unsubscribe(MQTT::Protocol::IO.new(temp_io), topics: ["a/b"], expect_response: false) + temp_io.rewind + unsubscribe_pkt = temp_io.to_slice + # This will overwrite the protocol level byte + unsubscribe_pkt[0] |= 0b0000_1010u8 + io.write_bytes_raw unsubscribe_pkt + + io.should be_closed + end + end + end + + pending "must contain at least one topic filter [MQTT-3.10.3-2]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + temp_io = IO::Memory.new + unsubscribe(MQTT::Protocol::IO.new(temp_io), topics: ["a/b"], expect_response: false) + temp_io.rewind + unsubscribe_pkt = temp_io.to_slice + # Overwrite remaining length + unsubscribe_pkt[1] = 2u8 + io.write_bytes_raw unsubscribe_pkt + + io.should be_closed + end + end + end + + pending "must stop adding any new messages for delivery to the Client, but completes delivery of previous messages [MQTT-3.10.4-2] and [MQTT-3.10.4-3]" do + with_server do |server| + with_client_io(server) do |pubio| + connect(pubio, client_id: "publisher") + + # Create a non-clean session with an active subscription + with_client_io(server) do |io| + connect(io, clean_session: false) + topics = mk_topic_filters({"a/b", 1}) + subscribe(io, topic_filters: topics) + disconnect(io) + end + + # Publish messages that will be stored for the subscriber + 2.times { |i| publish(pubio, topic: "a/b", payload: i.to_s.to_slice, qos: 0u8) } + + # Let the subscriber connect and read the messages, but don't ack. Then unsubscribe. + # We must read the Publish packets before unsubscribe, else the "suback" will be stuck. + with_client_io(server) do |io| + connect(io, clean_session: false) + 2.times do + pkt = read_packet(io) + pkt.should be_a(MQTT::Protocol::Publish) + # dont ack + end + + unsubscribe(io, topics: ["a/b"]) + disconnect(io) + end + + # Publish more messages + 2.times { |i| publish(pubio, topic: "a/b", payload: (2 + i).to_s.to_slice, qos: 0u8) } + + # Now, if unsubscribed worked, the last two publish packets shouldn't be held for the + # session. Read the two we expect, then test that there is nothing more to read. + with_client_io(server) do |io| + connect(io, clean_session: false) + 2.times do |i| + pkt = read_packet(io) + pkt.should be_a(MQTT::Protocol::Publish) + pkt = pkt.as(MQTT::Protocol::Publish) + pkt.payload.should eq(i.to_s.to_slice) + end + + io.should be_drained + disconnect(io) + end + disconnect(pubio) + end + end + end + end +end diff --git a/spec/mqtt/integrations/will_spec.cr b/spec/mqtt/integrations/will_spec.cr new file mode 100644 index 0000000000..e4eec3337b --- /dev/null +++ b/spec/mqtt/integrations/will_spec.cr @@ -0,0 +1,166 @@ +require "../spec_helper" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + + describe "client will" do + pending "will is not delivered on graceful disconnect [MQTT-3.14.4-3]" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"#", 0}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |io2| + will = MQTT::Protocol::Will.new( + topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: false) + connect(io2, client_id: "will_client", will: will, keepalive: 1u16) + disconnect(io2) + end + + # If the will has been published it should be received before this + publish(io, topic: "a/b", payload: "alive".to_slice) + + pub = read_packet(io) + + pub.should be_a(MQTT::Protocol::Publish) + pub = pub.as(MQTT::Protocol::Publish) + pub.payload.should eq("alive".to_slice) + pub.topic.should eq("a/b") + + disconnect(io) + end + end + end + + pending "will is delivered on ungraceful disconnect" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"will/t", 0}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |io2| + will = MQTT::Protocol::Will.new( + topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: false) + connect(io2, client_id: "will_client", will: will, keepalive: 1u16) + end + + pub = read_packet(io) + + pub.should be_a(MQTT::Protocol::Publish) + pub = pub.as(MQTT::Protocol::Publish) + pub.payload.should eq("dead".to_slice) + pub.topic.should eq("will/t") + + disconnect(io) + end + end + end + + pending "will can be retained [MQTT-3.1.2-17]" do + with_server do |server| + with_client_io(server) do |io2| + will = MQTT::Protocol::Will.new( + topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: true) + connect(io2, client_id: "will_client", will: will, keepalive: 1u16) + end + + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"will/t", 0}) + subscribe(io, topic_filters: topic_filters) + + pub = read_packet(io) + + pub.should be_a(MQTT::Protocol::Publish) + pub = pub.as(MQTT::Protocol::Publish) + pub.payload.should eq("dead".to_slice) + pub.topic.should eq("will/t") + pub.retain?.should eq(true) + + disconnect(io) + end + end + end + + pending "will won't be published if missing permission" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"topic-without-permission/t", 0}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |io2| + will = MQTT::Protocol::Will.new( + topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: false) + connect(io2, client_id: "will_client", will: will, keepalive: 1u16) + end + + # Send a ping to ensure we can read at least one packet, so we're not stuck + # waiting here (since this spec verifies that nothing is sent) + ping(io) + + pkt = read_packet(io) + pkt.should be_a(MQTT::Protocol::PingResp) + + disconnect(io) + end + end + end + + pending "will qos can't be set of will flag is unset [MQTT-3.1.2-13]" do + with_server do |server| + with_client_io(server) do |io| + temp_io = IO::Memory.new + connect(MQTT::Protocol::IO.new(temp_io), client_id: "will_client", keepalive: 1u16, expect_response: false) + temp_io.rewind + connect_pkt = temp_io.to_slice + connect_pkt[9] |= 0b0001_0000u8 + io.write connect_pkt + + expect_raises(IO::Error) do + read_packet(io) + end + end + end + end + + pending "will qos must not be 3 [MQTT-3.1.2-14]" do + with_server do |server| + with_client_io(server) do |io| + temp_io = IO::Memory.new + will = MQTT::Protocol::Will.new( + topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: false) + connect(MQTT::Protocol::IO.new(temp_io), will: will, client_id: "will_client", keepalive: 1u16, expect_response: false) + temp_io.rewind + connect_pkt = temp_io.to_slice + connect_pkt[9] |= 0b0001_1000u8 + io.write connect_pkt + + expect_raises(IO::Error) do + read_packet(io) + end + end + end + end + + pending "will retain can't be set of will flag is unset [MQTT-3.1.2-15]" do + with_server do |server| + with_client_io(server) do |io| + temp_io = IO::Memory.new + connect(MQTT::Protocol::IO.new(temp_io), client_id: "will_client", keepalive: 1u16, expect_response: false) + temp_io.rewind + connect_pkt = temp_io.to_slice + connect_pkt[9] |= 0b0010_0000u8 + io.write connect_pkt + + expect_raises(IO::Error) do + read_packet(io) + end + end + end + end + end +end diff --git a/spec/mqtt/spec_helper/mqtt_matchers.cr b/spec/mqtt/spec_helper/mqtt_matchers.cr index ecb77ac92c..3018de7f14 100644 --- a/spec/mqtt/spec_helper/mqtt_matchers.cr +++ b/spec/mqtt/spec_helper/mqtt_matchers.cr @@ -22,4 +22,26 @@ module MqttMatchers def be_closed ClosedExpectation.new end + + struct EmptyMatcher + include MqttHelpers + + def match(actual) + ping(actual) + resp = read_packet(actual) + resp.is_a?(MQTT::Protocol::PingResp) + end + + def failure_message(actual_value) + "Expected #{actual_value.pretty_inspect} to be drained" + end + + def negative_failure_message(actual_value) + "Expected #{actual_value.pretty_inspect} to not be drained" + end + end + + def be_drained + EmptyMatcher.new + end end From e24cdf1c9ad860e9a8b7661c159c7d25c7ff7f9d Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 10 Sep 2024 14:23:50 +0200 Subject: [PATCH 011/202] Synchronizer -> channel --- spec/mqtt/integrations/message_qos_spec.cr | 29 ++++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/spec/mqtt/integrations/message_qos_spec.cr b/spec/mqtt/integrations/message_qos_spec.cr index 871c886c8c..467da52e2b 100644 --- a/spec/mqtt/integrations/message_qos_spec.cr +++ b/spec/mqtt/integrations/message_qos_spec.cr @@ -185,9 +185,8 @@ module MqttSpecs end end - # TODO: Not yet migrated due to the Spectator::Synchronizer pending "qos1 unacked messages re-sent in the initial order [MQTT-4.6.0-1]" do - max_inflight_messages = 5 # TODO value was previous "MyraMQ::Config.settings.max_inflight_messages" + max_inflight_messages = 10 # We'll only ACK odd packet ids, and the first id is 1, so if we don't # do -1 the last packet (id=20) won't be sent because we've reached max # inflight with all odd ids. @@ -210,7 +209,8 @@ module MqttSpecs end # Read all messages, but only ack every second - sync = Spectator::Synchronizer.new + # sync = Spectator::Synchronizer.new + sync = Channel(Bool).new(1) spawn(name: "read msgs") do number_of_messages.times do |i| pkt = read_packet(io) @@ -218,16 +218,23 @@ module MqttSpecs # We only ack odd packet ids puback(io, pub.packet_id) if (i % 2) > 0 end - sync.done + sync.send true + # sync.done end - sync.synchronize(timeout: 3.second, msg: "Timeout first read") + select + when sync.receive + when timeout(3.seconds) + fail "Timeout first read" + end + # sync.synchronize(timeout: 3.second, msg: "Timeout first read") disconnect(io) end # We should now get the 50 messages we didn't ack previously, and in order with_client_io(server) do |io| connect(io, client_id: "subscriber") - sync = Spectator::Synchronizer.new + # sync = Spectator::Synchronizer.new + sync = Channel(Bool).new(1) spawn(name: "read msgs") do (number_of_messages // 2).times do |i| pkt = read_packet(io) @@ -237,9 +244,15 @@ module MqttSpecs data = IO::ByteFormat::SystemEndian.decode(UInt16, pub.payload) data.should eq(i * 2) end - sync.done + sync.send true + # sync.done + end + select + when sync.receive + when timeout(3.seconds) + puts "Timeout second read" end - sync.synchronize(timeout: 3.second, msg: "Timeout second read") + # sync.synchronize(timeout: 3.second, msg: "Timeout second read") disconnect(io) end end From 9f908db01445a2b90606db3d221941d28a8fa7a0 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Wed, 11 Sep 2024 21:52:37 +0200 Subject: [PATCH 012/202] spec: publish mqtt ends up in amqp queue --- spec/mqtt/integrations/connect_spec.cr | 2 +- spec/mqtt/integrations/publish_spec.cr | 38 ++++++++++++++++++++++++++ spec/mqtt/spec_helper/mqtt_helpers.cr | 13 +++++++-- spec/mqtt/spec_helper/mqtt_matchers.cr | 8 +++--- spec/spec_helper.cr | 19 +++++++------ src/lavinmq/mqtt/client.cr | 4 ++- src/lavinmq/server.cr | 7 ++++- 7 files changed, 73 insertions(+), 18 deletions(-) create mode 100644 spec/mqtt/integrations/publish_spec.cr diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 5a3d6e7d0a..0dfbdf483b 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -8,7 +8,7 @@ module MqttSpecs pending "should replace the already connected client [MQTT-3.1.4-2]" do with_server do |server| with_client_io(server) do |io| - connect(io) + connect(io, false) with_client_io(server) do |io2| connect(io2) io.should be_closed diff --git a/spec/mqtt/integrations/publish_spec.cr b/spec/mqtt/integrations/publish_spec.cr new file mode 100644 index 0000000000..e36b975ee6 --- /dev/null +++ b/spec/mqtt/integrations/publish_spec.cr @@ -0,0 +1,38 @@ +require "../spec_helper" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + + describe "publish" do + it "should put the message in a queue" do + with_server do |server| + with_channel(server) do |ch| + x = ch.exchange("mqtt", "direct") + q = ch.queue("test") + q.bind(x.name, q.name) + + with_client_io(server) do |io| + connect(io) + + payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + publish(io, topic: "test", payload: payload) + pub = read_packet(io) + pub.should be_a(MQTT::Protocol::Publish) + pub = pub.as(MQTT::Protocol::Publish) + pub.payload.should eq(payload) + pub.topic.should eq("test") + + body = q.get(no_ack: true).try do |v| + s = Slice(UInt8).new(payload.size) + v.body_io.read(s) + s + end + body.should eq(payload) + disconnect(io) + end + end + end + end + end +end diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr index 3491352b9c..147830d84a 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -25,9 +25,18 @@ module MqttHelpers socket.try &.close end - def with_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, &blk : LavinMQ::Server -> Nil) - with_server(:mqtt, tls, replicator) do |s| + def with_server(& : LavinMQ::Server -> Nil) + mqtt_server = TCPServer.new("localhost", 0) + amqp_server = TCPServer.new("localhost", 0) + s = LavinMQ::Server.new(LavinMQ::Config.instance.data_dir, LavinMQ::Clustering::NoopServer.new) + begin + spawn(name: "amqp tcp listen") { s.listen(amqp_server, :amqp) } + spawn(name: "mqtt tcp listen") { s.listen(mqtt_server, :mqtt) } + Fiber.yield yield s + ensure + s.close + FileUtils.rm_rf(LavinMQ::Config.instance.data_dir) end end diff --git a/spec/mqtt/spec_helper/mqtt_matchers.cr b/spec/mqtt/spec_helper/mqtt_matchers.cr index 3018de7f14..2789744e16 100644 --- a/spec/mqtt/spec_helper/mqtt_matchers.cr +++ b/spec/mqtt/spec_helper/mqtt_matchers.cr @@ -11,11 +11,11 @@ module MqttMatchers end def failure_message(actual_value) - "Expected #{actual_value.pretty_inspect} to be closed" + "Expected socket to be closed" end def negative_failure_message(actual_value) - "Expected #{actual_value.pretty_inspect} to be open" + "Expected socket to be open" end end @@ -33,11 +33,11 @@ module MqttMatchers end def failure_message(actual_value) - "Expected #{actual_value.pretty_inspect} to be drained" + "Expected socket to be drained" end def negative_failure_message(actual_value) - "Expected #{actual_value.pretty_inspect} to not be drained" + "Expected socket to not be drained" end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 40501c6323..d78553b34f 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -29,6 +29,13 @@ end def with_channel(s : LavinMQ::Server, file = __FILE__, line = __LINE__, **args, &) name = "lavinmq-spec-#{file}:#{line}" + port = s.@listeners + .select { |k, v| k.is_a?(TCPServer) && v == :amqp } + .keys + .select(TCPServer) + .first + .local_address + .port args = {port: amqp_port(s), name: name}.merge(args) conn = AMQP::Client.new(**args).connect ch = conn.channel @@ -72,7 +79,7 @@ def test_headers(headers = nil) req_hdrs end -def with_server(protocol, tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) +def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) tcp_server = TCPServer.new("localhost", 0) s = LavinMQ::Server.new(LavinMQ::Config.instance.data_dir, replicator) begin @@ -80,9 +87,9 @@ def with_server(protocol, tls = false, replicator = LavinMQ::Clustering::NoopSer ctx = OpenSSL::SSL::Context::Server.new ctx.certificate_chain = "spec/resources/server_certificate.pem" ctx.private_key = "spec/resources/server_key.pem" - spawn(name: "#{protocol} tls listen") { s.listen_tls(tcp_server, ctx, protocol) } + spawn(name: "amqp tls listen") { s.listen_tls(tcp_server, ctx, :amqp) } else - spawn(name: "#{protocol} tcp listen") { s.listen(tcp_server, protocol) } + spawn(name: "amqp tcp listen") { s.listen(tcp_server, :amqp) } end Fiber.yield yield s @@ -92,12 +99,6 @@ def with_server(protocol, tls = false, replicator = LavinMQ::Clustering::NoopSer end end -def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.new, & : LavinMQ::Server -> Nil) - with_server(:amqp, tls, replicator) do |s| - yield s - end -end - def with_http_server(&) with_amqp_server do |s| h = LavinMQ::HTTP::Server.new(s) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index d9a24aefb9..506919520f 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -89,8 +89,10 @@ module LavinMQ end def recieve_publish(packet) - msg = Message.new("mqtt", packet.topic, packet.payload.to_s, AMQ::Protocol::Properties.new) + # TODO: String.new around payload.. should be stored as Bytes + msg = Message.new("mqtt", packet.topic, String.new(packet.payload), AMQ::Protocol::Properties.new) @vhost.publish(msg) + send packet # TODO: Ok to send back same packet? # @session = start_session(self) unless @session # @session.publish(msg) # if packet.qos > 0 && (packet_id = packet.packet_id) diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index a78c71d78c..7f818ade29 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -49,7 +49,12 @@ module LavinMQ end def amqp_url - addr = @listeners.each_key.select(TCPServer).first.local_address + addr = @listeners + .select { |k, v| k.is_a?(TCPServer) && v == :amqp } + .keys + .select(TCPServer) + .first + .local_address "amqp://#{addr}" end From d3ae0cc6ae1895aee29edb5da8da6c6675b12498 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Sep 2024 09:25:05 +0200 Subject: [PATCH 013/202] send puback after publish if qos > 0 --- src/lavinmq/mqtt/client.cr | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 506919520f..b30f177a5d 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -85,19 +85,18 @@ module LavinMQ end def receive_pingreq(packet : MQTT::PingReq) - send(MQTT::PingResp.new) + send MQTT::PingResp.new end def recieve_publish(packet) # TODO: String.new around payload.. should be stored as Bytes msg = Message.new("mqtt", packet.topic, String.new(packet.payload), AMQ::Protocol::Properties.new) @vhost.publish(msg) - send packet # TODO: Ok to send back same packet? - # @session = start_session(self) unless @session - # @session.publish(msg) - # if packet.qos > 0 && (packet_id = packet.packet_id) - # send(MQTT::PubAck.new(packet_id)) - # end + # send packet # TODO: Ok to send back same packet? + # Ok to not send anything if qos = 0 (at most once delivery) + if packet.qos > 0 && (packet_id = packet.packet_id) + send(MQTT::PubAck.new(packet_id)) + end end def recieve_puback(packet) From 1ae27bcb3b66870f1f757a79a8dee681744b7dc6 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Sep 2024 09:27:43 +0200 Subject: [PATCH 014/202] publish will to session WIP --- src/lavinmq/mqtt/client.cr | 21 ++++++++++++++++++--- src/lavinmq/mqtt/connection_factory.cr | 8 ++++---- src/lavinmq/mqtt/session.cr | 1 + 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index b30f177a5d..f1a0431c19 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -12,7 +12,7 @@ module LavinMQ getter vhost, channels, log, name, user, client_id @channels = Hash(UInt16, Client::Channel).new - @session : MQTT::Session | Nil + session : MQTT::Session rate_stats({"send_oct", "recv_oct"}) Log = ::Log.for "MQTT.client" @@ -21,7 +21,8 @@ module LavinMQ @vhost : VHost, @user : User, @client_id : String, - @clean_session = false) + @clean_session = false, + @will : MQTT::Will? = nil) @io = MQTT::IO.new(@socket) @lock = Mutex.new @remote_address = @connection_info.src @@ -30,7 +31,7 @@ module LavinMQ @metadata = ::Log::Metadata.new(nil, {vhost: @vhost.name, address: @remote_address.to_s}) @log = Logger.new(Log, @metadata) @vhost.add_connection(self) - @session = start_session(self) + session = start_session(self) @log.info { "Connection established for user=#{@user.name}" } spawn read_loop end @@ -52,6 +53,7 @@ module LavinMQ rescue ex : ::IO::EOFError Log.info { "eof #{ex.inspect}" } ensure + # publish_will if @clean_session disconnect_session(self) end @@ -133,6 +135,19 @@ module LavinMQ @vhost.clear_session(client) end + # TODO: WIP + # private def publish_will + # if will = @will + # Log.debug { "publishing will" } + # msg = Message.new("mqtt", will.topic, will.payload.to_s, AMQ::Protocol::Properties.new) + # pp "publish will to session" + # session = start_session(self) + # # session.publish(msg) + # end + # rescue ex + # Log.warn { "Failed to publish will: #{ex.message}" } + # end + def update_rates end diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index c434cd95e1..2deabb9610 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -13,13 +13,13 @@ module LavinMQ end def start(socket : ::IO, connection_info : ConnectionInfo) - io = ::MQTT::Protocol::IO.new(socket) + io = MQTT::IO.new(socket) if packet = MQTT::Packet.from_io(socket).as?(MQTT::Connect) Log.trace { "recv #{packet.inspect}" } if user = authenticate(io, packet) - ::MQTT::Protocol::Connack.new(false, ::MQTT::Protocol::Connack::ReturnCode::Accepted).to_io(io) + MQTT::Connack.new(false, MQTT::Connack::ReturnCode::Accepted).to_io(io) io.flush - return LavinMQ::MQTT::Client.new(socket, connection_info, @vhost, user, packet.client_id, packet.clean_session?) + return LavinMQ::MQTT::Client.new(socket, connection_info, @vhost, user, packet.client_id, packet.clean_session?, packet.will) end end rescue ex @@ -37,7 +37,7 @@ module LavinMQ else Log.warn { "Authentication failure for user \"#{username}\"" } end - ::MQTT::Protocol::Connack.new(false, ::MQTT::Protocol::Connack::ReturnCode::NotAuthorized).to_io(io) + MQTT::Connack.new(false, MQTT::Connack::ReturnCode::NotAuthorized).to_io(io) nil end end diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index e7a7828c13..2c1681fd8b 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -5,6 +5,7 @@ module LavinMQ super end + #if sub comes in with clean_session, set auto_delete on session #rm_consumer override for clean_session end end From 8a3fa3488fcd9199e678548d985266a33c10073e Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 12 Sep 2024 10:04:24 +0200 Subject: [PATCH 015/202] publish returns PubAck --- spec/mqtt/integrations/publish_spec.cr | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/mqtt/integrations/publish_spec.cr b/spec/mqtt/integrations/publish_spec.cr index e36b975ee6..599f2eb0fb 100644 --- a/spec/mqtt/integrations/publish_spec.cr +++ b/spec/mqtt/integrations/publish_spec.cr @@ -16,12 +16,8 @@ module MqttSpecs connect(io) payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] - publish(io, topic: "test", payload: payload) - pub = read_packet(io) - pub.should be_a(MQTT::Protocol::Publish) - pub = pub.as(MQTT::Protocol::Publish) - pub.payload.should eq(payload) - pub.topic.should eq("test") + ack = publish(io, topic: "test", payload: payload, qos: 1u8) + ack.should be_a(MQTT::Protocol::PubAck) body = q.get(no_ack: true).try do |v| s = Slice(UInt8).new(payload.size) From 86b944b0567081f9d769258f515b9e9fa378b882 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 16 Sep 2024 10:51:55 +0200 Subject: [PATCH 016/202] cleanup will preparation --- src/lavinmq/mqtt/client.cr | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index f1a0431c19..e03d06459c 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -53,10 +53,8 @@ module LavinMQ rescue ex : ::IO::EOFError Log.info { "eof #{ex.inspect}" } ensure - # publish_will - if @clean_session - disconnect_session(self) - end + publish_will if @will + disconnect_session(self) if @clean_session @socket.close @vhost.rm_connection(self) end @@ -135,18 +133,14 @@ module LavinMQ @vhost.clear_session(client) end - # TODO: WIP - # private def publish_will - # if will = @will - # Log.debug { "publishing will" } - # msg = Message.new("mqtt", will.topic, will.payload.to_s, AMQ::Protocol::Properties.new) - # pp "publish will to session" - # session = start_session(self) - # # session.publish(msg) - # end - # rescue ex - # Log.warn { "Failed to publish will: #{ex.message}" } - # end + # TODO: actually publish will to session + private def publish_will + if will = @will + pp "Publish will to session" + end + rescue ex + Log.warn { "Failed to publish will: #{ex.message}" } + end def update_rates end From c715857daf8e59ec467455309d74cfeb14b9fa50 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 16 Sep 2024 13:59:31 +0200 Subject: [PATCH 017/202] Got first working subscribe But a lot of things missing :) --- spec/mqtt/integrations/publish_spec.cr | 27 +++++- spec/mqtt/integrations/subscribe_spec.cr | 42 +++++++++ spec/mqtt/spec_helper/mqtt_helpers.cr | 14 ++- src/lavinmq/mqtt/client.cr | 113 +++++++++++++++++++++-- 4 files changed, 179 insertions(+), 17 deletions(-) diff --git a/spec/mqtt/integrations/publish_spec.cr b/spec/mqtt/integrations/publish_spec.cr index 599f2eb0fb..68fad531ac 100644 --- a/spec/mqtt/integrations/publish_spec.cr +++ b/spec/mqtt/integrations/publish_spec.cr @@ -5,10 +5,33 @@ module MqttSpecs extend MqttMatchers describe "publish" do + it "should return PubAck for QoS=1" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + ack = publish(io, topic: "test", payload: payload, qos: 1u8) + ack.should be_a(MQTT::Protocol::PubAck) + end + end + end + + it "shouldn't return anything for QoS=0" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + ack = publish(io, topic: "test", payload: payload, qos: 0u8) + ack.should be_nil + end + end + end + it "should put the message in a queue" do with_server do |server| with_channel(server) do |ch| - x = ch.exchange("mqtt", "direct") + x = ch.exchange("amq.topic", "topic") q = ch.queue("test") q.bind(x.name, q.name) @@ -17,7 +40,7 @@ module MqttSpecs payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] ack = publish(io, topic: "test", payload: payload, qos: 1u8) - ack.should be_a(MQTT::Protocol::PubAck) + ack.should_not be_nil body = q.get(no_ack: true).try do |v| s = Slice(UInt8).new(payload.size) diff --git a/spec/mqtt/integrations/subscribe_spec.cr b/spec/mqtt/integrations/subscribe_spec.cr index 3f96f182bb..79c8f89817 100644 --- a/spec/mqtt/integrations/subscribe_spec.cr +++ b/spec/mqtt/integrations/subscribe_spec.cr @@ -4,6 +4,30 @@ module MqttSpecs extend MqttHelpers extend MqttMatchers describe "subscribe" do + it "pub/sub" do + with_server do |server| + with_client_io(server) do |sub_io| + connect(sub_io, client_id: "sub") + + topic_filters = mk_topic_filters({"test", 0}) + subscribe(sub_io, topic_filters: topic_filters) + + with_client_io(server) do |pub_io| + connect(pub_io, client_id: "pub") + + payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + ack = publish(pub_io, topic: "test", payload: payload, qos: 0u8) + ack.should be_nil + + msg = read_packet(sub_io) + msg.should be_a(MQTT::Protocol::Publish) + msg = msg.as(MQTT::Protocol::Publish) + msg.payload.should eq payload + end + end + end + end + pending "bits 3,2,1,0 must be set to 0,0,1,0 [MQTT-3.8.1-1]" do with_server do |server| with_client_io(server) do |io| @@ -101,4 +125,22 @@ module MqttSpecs end end end + + describe "amqp" do + pending "should create a queue and subscribe queue to amq.topic" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + + topic_filters = mk_topic_filters({"a/b", 0}) + suback = subscribe(io, topic_filters: topic_filters) + suback.should be_a(MQTT::Protocol::SubAck) + + q = server.vhosts["/"].queues["mqtt.client_id"] + binding = q.bindings.find { |a, b| a.is_a?(LavinMQ::TopicExchange) && b[0] == "a.b" } + binding.should_not be_nil + end + end + end + end end diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr index 147830d84a..16212398d0 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -3,6 +3,12 @@ require "./mqtt_client" require "../../spec_helper" module MqttHelpers + GENERATOR = (0u16..).each + + def next_packet_id + GENERATOR.next.as(UInt16) + end + def with_client_socket(server, &) listener = server.listeners.find { |l| l[:protocol] == :mqtt } tcp_listener = listener.as(NamedTuple(ip_address: String, protocol: Symbol, port: Int32)) @@ -47,14 +53,6 @@ module MqttHelpers end end - def packet_id_generator - (0u16..).each - end - - def next_packet_id - packet_id_generator.next.as(UInt16) - end - def connect(io, expect_response = true, **args) MQTT::Protocol::Connect.new(**{ client_id: "client_id", diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index e03d06459c..95bbdab579 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -6,6 +6,90 @@ require "./session" module LavinMQ module MQTT + class MqttConsumer < LavinMQ::Client::Channel::Consumer + getter unacked = 0_u32 + getter tag : String = "mqtt" + property prefetch_count = 1 + + def initialize(@client : Client, @queue : Queue) + @has_capacity.try_send? true + spawn deliver_loop, name: "Consumer deliver loop", same_thread: true + end + + private def deliver_loop + queue = @queue + i = 0 + loop do + queue.consume_get(self) do |env| + deliver(env.message, env.segment_position, env.redelivered) + end + Fiber.yield if (i &+= 1) % 32768 == 0 + end + rescue ex + puts "deliver loop exiting: #{ex.inspect}" + end + + def details_tuple + { + queue: { + name: "mqtt.client_id", + vhost: "mqtt", + }, + } + end + + def no_ack? + true + end + + def accepts? : Bool + true + end + + def deliver(msg, sp, redelivered = false, recover = false) + # pp "deliver", msg, sp + pub_args = { + packet_id: 123u16, # next_packet_id, + payload: msg.body, + dup: false, + qos: 0u8, + retain: false, + topic: "test", + } # .merge(args) + @client.send(::MQTT::Protocol::Publish.new(**pub_args)) + # MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response + end + + def exclusive? + true + end + + def cancel + end + + def close + end + + def closed? + false + end + + def flow(active : Bool) + end + + getter has_capacity = ::Channel(Bool).new + + def ack(sp) + end + + def reject(sp, requeue = false) + end + + def priority + 0 + end + end + class Client < LavinMQ::Client include Stats include SortableJSON @@ -67,7 +151,7 @@ module LavinMQ case packet when MQTT::Publish then recieve_publish(packet) when MQTT::PubAck then pp "puback" - when MQTT::Subscribe then pp "subscribe" + when MQTT::Subscribe then recieve_subscribe(packet) when MQTT::Unsubscribe then pp "unsubscribe" when MQTT::PingReq then receive_pingreq(packet) when MQTT::Disconnect then return packet @@ -76,7 +160,7 @@ module LavinMQ packet end - private def send(packet) + def send(packet) @lock.synchronize do packet.to_io(@io) @socket.flush @@ -90,7 +174,8 @@ module LavinMQ def recieve_publish(packet) # TODO: String.new around payload.. should be stored as Bytes - msg = Message.new("mqtt", packet.topic, String.new(packet.payload), AMQ::Protocol::Properties.new) + + msg = Message.new("amq.topic", packet.topic, String.new(packet.payload), AMQ::Protocol::Properties.new) @vhost.publish(msg) # send packet # TODO: Ok to send back same packet? # Ok to not send anything if qos = 0 (at most once delivery) @@ -102,10 +187,24 @@ module LavinMQ def recieve_puback(packet) end - # let prefetch = 1 - def recieve_subscribe(packet) - # exclusive conusmer - # + def recieve_subscribe(packet : MQTT::Subscribe) + name = "mqtt.#{@client_id}" + durable = false + auto_delete = true + tbl = AMQP::Table.new + q = @vhost.declare_queue(name, durable, auto_delete, tbl) + packet.topic_filters.each do |tf| + rk = topicfilter_to_routingkey(tf) + @vhost.bind_queue(name, "amq.topic", rk) + end + queue = @vhost.queues[name] + consumer = MqttConsumer.new(self, queue) + queue.add_consumer(consumer) + send(MQTT::SubAck.new([::MQTT::Protocol::SubAck::ReturnCode::QoS0], packet.packet_id)) + end + + def topicfilter_to_routingkey(tf) : String + tf.topic.gsub("/") { "." } end def recieve_unsubscribe(packet) From 9140c7dab0b18e79501b61a05ee8f7baee302344 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 16 Sep 2024 15:41:55 +0200 Subject: [PATCH 018/202] subscribe specs --- spec/mqtt/integrations/subscribe_spec.cr | 6 +++--- src/lavinmq/mqtt/client.cr | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/mqtt/integrations/subscribe_spec.cr b/spec/mqtt/integrations/subscribe_spec.cr index 79c8f89817..c022ff585c 100644 --- a/spec/mqtt/integrations/subscribe_spec.cr +++ b/spec/mqtt/integrations/subscribe_spec.cr @@ -28,7 +28,7 @@ module MqttSpecs end end - pending "bits 3,2,1,0 must be set to 0,0,1,0 [MQTT-3.8.1-1]" do + it "bits 3,2,1,0 must be set to 0,0,1,0 [MQTT-3.8.1-1]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -48,7 +48,7 @@ module MqttSpecs end end - pending "must contain at least one topic filter [MQTT-3.8.3-3]" do + it "must contain at least one topic filter [MQTT-3.8.3-3]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -67,7 +67,7 @@ module MqttSpecs end end - pending "should not allow any payload reserved bits to be set [MQTT-3-8.3-4]" do + it "should not allow any payload reserved bits to be set [MQTT-3-8.3-4]" do with_server do |server| with_client_io(server) do |io| connect(io) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 95bbdab579..c7d5ea3f63 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -132,6 +132,8 @@ module LavinMQ # If we dont breakt the loop here we'll get a IO/Error on next read. break if packet.is_a?(MQTT::Disconnect) end + rescue ex : ::MQTT::Protocol::Error::PacketDecode + @socket.close rescue ex : MQTT::Error::Connect Log.warn { "Connect error #{ex.inspect}" } rescue ex : ::IO::EOFError From c0be2dbe0a755cfb1524da0458b4ae16917b8483 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 16 Sep 2024 22:28:00 +0200 Subject: [PATCH 019/202] packet_id stuff --- spec/mqtt/integrations/subscribe_spec.cr | 9 ++++++++- src/lavinmq/mqtt/client.cr | 20 +++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/spec/mqtt/integrations/subscribe_spec.cr b/spec/mqtt/integrations/subscribe_spec.cr index c022ff585c..6f69a83af1 100644 --- a/spec/mqtt/integrations/subscribe_spec.cr +++ b/spec/mqtt/integrations/subscribe_spec.cr @@ -16,13 +16,20 @@ module MqttSpecs connect(pub_io, client_id: "pub") payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] - ack = publish(pub_io, topic: "test", payload: payload, qos: 0u8) + packet_id = next_packet_id + ack = publish(pub_io, + topic: "test", + payload: payload, + qos: 0u8, + packet_id: packet_id + ) ack.should be_nil msg = read_packet(sub_io) msg.should be_a(MQTT::Protocol::Publish) msg = msg.as(MQTT::Protocol::Publish) msg.payload.should eq payload + msg.packet_id.should be_nil # QoS=0 end end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index c7d5ea3f63..670a9af3f2 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -25,6 +25,7 @@ module LavinMQ end Fiber.yield if (i &+= 1) % 32768 == 0 end + rescue LavinMQ::Queue::ClosedError rescue ex puts "deliver loop exiting: #{ex.inspect}" end @@ -47,15 +48,18 @@ module LavinMQ end def deliver(msg, sp, redelivered = false, recover = false) - # pp "deliver", msg, sp + packet_id = nil + if message_id = msg.properties.message_id + packet_id = message_id.to_u16 unless message_id.empty? + end pub_args = { - packet_id: 123u16, # next_packet_id, + packet_id: packet_id, payload: msg.body, dup: false, qos: 0u8, retain: false, topic: "test", - } # .merge(args) + } @client.send(::MQTT::Protocol::Publish.new(**pub_args)) # MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response end @@ -175,11 +179,13 @@ module LavinMQ end def recieve_publish(packet) + rk = topicfilter_to_routingkey(packet.topic) + props = AMQ::Protocol::Properties.new( + message_id: packet.packet_id.to_s + ) # TODO: String.new around payload.. should be stored as Bytes - - msg = Message.new("amq.topic", packet.topic, String.new(packet.payload), AMQ::Protocol::Properties.new) + msg = Message.new("amq.topic", rk, String.new(packet.payload), props) @vhost.publish(msg) - # send packet # TODO: Ok to send back same packet? # Ok to not send anything if qos = 0 (at most once delivery) if packet.qos > 0 && (packet_id = packet.packet_id) send(MQTT::PubAck.new(packet_id)) @@ -206,7 +212,7 @@ module LavinMQ end def topicfilter_to_routingkey(tf) : String - tf.topic.gsub("/") { "." } + tf.gsub("/", ".") end def recieve_unsubscribe(packet) From fa42c196c96a8fcde58f586b325d6bd200154dea Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 17 Sep 2024 16:55:17 +0200 Subject: [PATCH 020/202] pass more connect specs --- spec/mqtt/integrations/connect_spec.cr | 12 ++++++------ src/lavinmq/mqtt/connection_factory.cr | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 0dfbdf483b..32e5fb86a3 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -20,7 +20,7 @@ module MqttSpecs describe "receives connack" do describe "with expected flags set" do - pending "no session present when reconnecting a non-clean session with a clean session [MQTT-3.1.2-6]" do + it "no session present when reconnecting a non-clean session with a clean session [MQTT-3.1.2-6]" do with_server do |server| with_client_io(server) do |io| connect(io, clean_session: false) @@ -41,7 +41,7 @@ module MqttSpecs end end - pending "no session present when reconnecting a clean session with a non-clean session [MQTT-3.1.2-6]" do + it "no session present when reconnecting a clean session with a non-clean session [MQTT-3.1.2-6]" do with_server do |server| with_client_io(server) do |io| connect(io, clean_session: true) @@ -60,7 +60,7 @@ module MqttSpecs end end - pending "no session present when reconnecting a clean session [MQTT-3.1.2-6]" do + it "no session present when reconnecting a clean session [MQTT-3.1.2-6]" do with_server do |server| with_client_io(server) do |io| connect(io, clean_session: true) @@ -100,7 +100,7 @@ module MqttSpecs end describe "with expected return code" do - pending "for valid credentials [MQTT-3.1.4-4]" do + it "for valid credentials [MQTT-3.1.4-4]" do with_server do |server| with_client_io(server) do |io| connack = connect(io) @@ -127,7 +127,7 @@ module MqttSpecs # end # end - pending "for invalid protocol version [MQTT-3.1.2-2]" do + it "for invalid protocol version [MQTT-3.1.2-2]" do with_server do |server| with_client_io(server) do |io| temp_io = IO::Memory.new @@ -150,7 +150,7 @@ module MqttSpecs end end - pending "for empty client id with non-clean session [MQTT-3.1.3-8]" do + it "for empty client id with non-clean session [MQTT-3.1.3-8]" do with_server do |server| with_client_io(server) do |io| connack = connect(io, client_id: "", clean_session: false) diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 2deabb9610..df75e4d978 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -22,6 +22,13 @@ module LavinMQ return LavinMQ::MQTT::Client.new(socket, connection_info, @vhost, user, packet.client_id, packet.clean_session?, packet.will) end end + rescue ex : MQTT::Error::Connect + Log.warn { "Connect error #{ex.inspect}" } + if io + MQTT::Connack.new(false, MQTT::Connack::ReturnCode.new(ex.return_code)).to_io(io) + end + socket.close + rescue ex Log.warn { "Recieved the wrong packet" } socket.close From 7db85e0ad6676ca6456030e02104d78bc6d355ae Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 17 Sep 2024 16:56:33 +0200 Subject: [PATCH 021/202] work on subscriber specs --- spec/mqtt/integrations/subscribe_spec.cr | 8 ++++---- spec/mqtt/spec_helper/mqtt_helpers.cr | 2 ++ src/lavinmq/mqtt/client.cr | 7 +++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/spec/mqtt/integrations/subscribe_spec.cr b/spec/mqtt/integrations/subscribe_spec.cr index 6f69a83af1..ee9b84382d 100644 --- a/spec/mqtt/integrations/subscribe_spec.cr +++ b/spec/mqtt/integrations/subscribe_spec.cr @@ -108,9 +108,9 @@ module MqttSpecs # Publish something to the topic we're subscribed to... publish(io, topic: "a/b", payload: "a".to_slice, qos: 1u8) # ... consume it... - pub = read_packet(io).as(MQTT::Protocol::Publish) + packet = read_packet(io).as(MQTT::Protocol::Publish) # ... and verify it be qos0 (i.e. our subscribe is correct) - pub.qos.should eq(0u8) + packet.qos.should eq(0u8) # Now do a second subscribe with another qos and do the same verification topic_filters = mk_topic_filters({"a/b", 1}) @@ -123,9 +123,9 @@ module MqttSpecs # Publish something to the topic we're subscribed to... publish(io, topic: "a/b", payload: "a".to_slice, qos: 1u8) # ... consume it... - pub = read_packet(io).as(MQTT::Protocol::Publish) + packet = read_packet(io).as(MQTT::Protocol::Publish) # ... and verify it be qos0 (i.e. our subscribe is correct) - pub.qos.should eq(1u8) + packet.qos.should eq(1u8) io.should be_drained end diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr index 16212398d0..dd428479a2 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -112,5 +112,7 @@ module MqttHelpers def read_packet(io) MQTT::Protocol::Packet.from_io(io) + rescue IO::TimeoutError + fail "Did not get packet on time" end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 670a9af3f2..c6a18b9a2a 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -200,15 +200,18 @@ module LavinMQ durable = false auto_delete = true tbl = AMQP::Table.new + # TODO: declare Session instead q = @vhost.declare_queue(name, durable, auto_delete, tbl) + qos = Array(MQTT::SubAck::ReturnCode).new packet.topic_filters.each do |tf| - rk = topicfilter_to_routingkey(tf) + qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) + rk = topicfilter_to_routingkey(tf.topic) @vhost.bind_queue(name, "amq.topic", rk) end queue = @vhost.queues[name] consumer = MqttConsumer.new(self, queue) queue.add_consumer(consumer) - send(MQTT::SubAck.new([::MQTT::Protocol::SubAck::ReturnCode::QoS0], packet.packet_id)) + send(MQTT::SubAck.new(qos, packet.packet_id)) end def topicfilter_to_routingkey(tf) : String From a92f090dc7eed589200c8f528ef048db7b354331 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 17 Sep 2024 16:57:23 +0200 Subject: [PATCH 022/202] start working on broker for clients --- src/lavinmq/mqtt/broker.cr | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/lavinmq/mqtt/broker.cr diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr new file mode 100644 index 0000000000..a3bc23a4e3 --- /dev/null +++ b/src/lavinmq/mqtt/broker.cr @@ -0,0 +1,6 @@ +module LavinMQ + module MQTT + class Broker + end + end +end From 2a29c97b3250fd69924b5f13b18e35612d0d4124 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 23 Sep 2024 10:13:17 +0200 Subject: [PATCH 023/202] wrap vhost in broker for mqtt::client --- spec/mqtt/integrations/connect_spec.cr | 2 +- src/lavinmq/config.cr | 2 +- src/lavinmq/http/controller/queues.cr | 32 ++-- src/lavinmq/mqtt/broker.cr | 24 +++ src/lavinmq/mqtt/channel.cr | 0 src/lavinmq/mqtt/client.cr | 211 +++++++++++++------------ src/lavinmq/mqtt/connection_factory.cr | 8 +- src/lavinmq/mqtt/session.cr | 13 +- src/lavinmq/server.cr | 2 +- src/lavinmq/vhost.cr | 10 -- 10 files changed, 164 insertions(+), 140 deletions(-) delete mode 100644 src/lavinmq/mqtt/channel.cr diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 32e5fb86a3..b556b86209 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -79,7 +79,7 @@ module MqttSpecs end end - pending "session present when reconnecting a non-clean session [MQTT-3.1.2-4]" do + it "session present when reconnecting a non-clean session [MQTT-3.1.2-4]" do with_server do |server| with_client_io(server) do |io| connect(io, clean_session: false) diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index a5bd680c70..ae8fe4e526 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -8,7 +8,7 @@ require "./in_memory_backend" module LavinMQ class Config - DEFAULT_LOG_LEVEL = ::Log::Severity::Info + DEFAULT_LOG_LEVEL = ::Log::Severity::Trace property data_dir : String = ENV.fetch("STATE_DIRECTORY", "/var/lib/lavinmq") property config_file = File.exists?(File.join(ENV.fetch("CONFIGURATION_DIRECTORY", "/etc/lavinmq"), "lavinmq.ini")) ? File.join(ENV.fetch("CONFIGURATION_DIRECTORY", "/etc/lavinmq"), "lavinmq.ini") : "" diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index 7c7f054b18..c375d96a2c 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -42,22 +42,22 @@ module LavinMQ end end - get "/api/queues/:vhost/:name/unacked" do |context, params| - with_vhost(context, params) do |vhost| - refuse_unless_management(context, user(context), vhost) - q = queue(context, params, vhost) - unacked_messages = q.consumers.each.flat_map do |c| - c.unacked_messages.each.compact_map do |u| - next unless u.queue == q - if consumer = u.consumer - UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) - end - end - end - unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) - page(context, unacked_messages) - end - end + # get "/api/queues/:vhost/:name/unacked" do |context, params| + # with_vhost(context, params) do |vhost| + # refuse_unless_management(context, user(context), vhost) + # q = queue(context, params, vhost) + # # unacked_messages = q.consumers.each.flat_map do |c| + # # c.unacked_messages.each.compact_map do |u| + # # next unless u.queue == q + # # if consumer = u.consumer + # # UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) + # # end + # # end + # # end + # # unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) + # # page(context, unacked_messages) + # end + # end put "/api/queues/:vhost/:name" do |context, params| with_vhost(context, params) do |vhost| diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index a3bc23a4e3..fa66ee0917 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -1,6 +1,30 @@ module LavinMQ module MQTT class Broker + + getter vhost + def initialize(@vhost : VHost) + @queues = Hash(String, Session).new + @sessions = Hash(String, Session).new + end + + def start_session(client : Client) + client_id = client.client_id + session = MQTT::Session.new(self, client_id) + @sessions[client_id] = session + @queues[client_id] = session + end + + def clear_session(client : Client) + @sessions.delete client.client_id + @queues.delete client.client_id + end + + # def connected(client) : MQTT::Session + # session = Session.new(client.vhost, client.client_id) + # session.connect(client) + # session + # end end end end diff --git a/src/lavinmq/mqtt/channel.cr b/src/lavinmq/mqtt/channel.cr deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index c6a18b9a2a..d966a03a6d 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -6,120 +6,33 @@ require "./session" module LavinMQ module MQTT - class MqttConsumer < LavinMQ::Client::Channel::Consumer - getter unacked = 0_u32 - getter tag : String = "mqtt" - property prefetch_count = 1 - - def initialize(@client : Client, @queue : Queue) - @has_capacity.try_send? true - spawn deliver_loop, name: "Consumer deliver loop", same_thread: true - end - - private def deliver_loop - queue = @queue - i = 0 - loop do - queue.consume_get(self) do |env| - deliver(env.message, env.segment_position, env.redelivered) - end - Fiber.yield if (i &+= 1) % 32768 == 0 - end - rescue LavinMQ::Queue::ClosedError - rescue ex - puts "deliver loop exiting: #{ex.inspect}" - end - - def details_tuple - { - queue: { - name: "mqtt.client_id", - vhost: "mqtt", - }, - } - end - - def no_ack? - true - end - - def accepts? : Bool - true - end - - def deliver(msg, sp, redelivered = false, recover = false) - packet_id = nil - if message_id = msg.properties.message_id - packet_id = message_id.to_u16 unless message_id.empty? - end - pub_args = { - packet_id: packet_id, - payload: msg.body, - dup: false, - qos: 0u8, - retain: false, - topic: "test", - } - @client.send(::MQTT::Protocol::Publish.new(**pub_args)) - # MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response - end - - def exclusive? - true - end - - def cancel - end - - def close - end - - def closed? - false - end - - def flow(active : Bool) - end - - getter has_capacity = ::Channel(Bool).new - - def ack(sp) - end - - def reject(sp, requeue = false) - end - - def priority - 0 - end - end - class Client < LavinMQ::Client include Stats include SortableJSON - getter vhost, channels, log, name, user, client_id + getter vhost, channels, log, name, user, client_id, socket @channels = Hash(UInt16, Client::Channel).new - session : MQTT::Session + @session : MQTT::Session? rate_stats({"send_oct", "recv_oct"}) Log = ::Log.for "MQTT.client" def initialize(@socket : ::IO, @connection_info : ConnectionInfo, - @vhost : VHost, @user : User, + @vhost : VHost, + @broker : MQTT::Broker, @client_id : String, @clean_session = false, - @will : MQTT::Will? = nil) + @will : MQTT::Will? = nil + ) @io = MQTT::IO.new(@socket) @lock = Mutex.new @remote_address = @connection_info.src @local_address = @connection_info.dst @name = "#{@remote_address} -> #{@local_address}" - @metadata = ::Log::Metadata.new(nil, {vhost: @vhost.name, address: @remote_address.to_s}) + @metadata = ::Log::Metadata.new(nil, {vhost: @broker.vhost.name, address: @remote_address.to_s}) @log = Logger.new(Log, @metadata) - @vhost.add_connection(self) - session = start_session(self) + # @session = @broker.connected(self) @log.info { "Connection established for user=#{@user.name}" } spawn read_loop end @@ -146,7 +59,7 @@ module LavinMQ publish_will if @will disconnect_session(self) if @clean_session @socket.close - @vhost.rm_connection(self) + @broker.vhost.rm_connection(self) end def read_and_handle_packet @@ -178,14 +91,14 @@ module LavinMQ send MQTT::PingResp.new end - def recieve_publish(packet) + def recieve_publish(packet : MQTT::Publish) rk = topicfilter_to_routingkey(packet.topic) props = AMQ::Protocol::Properties.new( message_id: packet.packet_id.to_s ) # TODO: String.new around payload.. should be stored as Bytes msg = Message.new("amq.topic", rk, String.new(packet.payload), props) - @vhost.publish(msg) + @broker.vhost.publish(msg) # Ok to not send anything if qos = 0 (at most once delivery) if packet.qos > 0 && (packet_id = packet.packet_id) send(MQTT::PubAck.new(packet_id)) @@ -201,14 +114,14 @@ module LavinMQ auto_delete = true tbl = AMQP::Table.new # TODO: declare Session instead - q = @vhost.declare_queue(name, durable, auto_delete, tbl) + q = @broker.vhost.declare_queue(name, durable, auto_delete, tbl) qos = Array(MQTT::SubAck::ReturnCode).new packet.topic_filters.each do |tf| qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) rk = topicfilter_to_routingkey(tf.topic) - @vhost.bind_queue(name, "amq.topic", rk) + @broker.vhost.bind_queue(name, "amq.topic", rk) end - queue = @vhost.queues[name] + queue = @broker.vhost.queues[name] consumer = MqttConsumer.new(self, queue) queue.add_consumer(consumer) send(MQTT::SubAck.new(qos, packet.packet_id)) @@ -223,7 +136,7 @@ module LavinMQ def details_tuple { - vhost: @vhost.name, + vhost: @broker.vhost.name, user: @user.name, protocol: "MQTT", client_id: @client_id, @@ -233,14 +146,14 @@ module LavinMQ def start_session(client) : MQTT::Session if @clean_session pp "clear session" - @vhost.clear_session(client) + @broker.clear_session(client) end - @vhost.start_session(client) + @broker.start_session(client) end def disconnect_session(client) pp "disconnect session" - @vhost.clear_session(client) + @broker.clear_session(client) end # TODO: actually publish will to session @@ -261,5 +174,93 @@ module LavinMQ def force_close end end + + class MqttConsumer < LavinMQ::Client::Channel::Consumer + getter unacked = 0_u32 + getter tag : String = "mqtt" + property prefetch_count = 1 + + def initialize(@client : Client, @queue : Queue) + @has_capacity.try_send? true + spawn deliver_loop, name: "Consumer deliver loop", same_thread: true + end + + private def deliver_loop + queue = @queue + i = 0 + loop do + queue.consume_get(self) do |env| + deliver(env.message, env.segment_position, env.redelivered) + end + Fiber.yield if (i &+= 1) % 32768 == 0 + end + rescue LavinMQ::Queue::ClosedError + rescue ex + puts "deliver loop exiting: #{ex.inspect}" + end + + def details_tuple + { + queue: { + name: "mqtt.client_id", + vhost: "mqtt", + }, + } + end + + def no_ack? + true + end + + def accepts? : Bool + true + end + + def deliver(msg, sp, redelivered = false, recover = false) + packet_id = nil + if message_id = msg.properties.message_id + packet_id = message_id.to_u16 unless message_id.empty? + end + pub_args = { + packet_id: packet_id, + payload: msg.body, + dup: false, + qos: 0u8, + retain: false, + topic: "test", + } + @client.send(::MQTT::Protocol::Publish.new(**pub_args)) + # MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response + end + + def exclusive? + true + end + + def cancel + end + + def close + end + + def closed? + false + end + + def flow(active : Bool) + end + + getter has_capacity = ::Channel(Bool).new + + def ack(sp) + end + + def reject(sp, requeue = false) + end + + def priority + 0 + end + end end end diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index df75e4d978..a3af16459b 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -4,22 +4,25 @@ require "log" require "./client" require "../vhost" require "../user" +require "./broker" module LavinMQ module MQTT class ConnectionFactory def initialize(@users : UserStore, - @vhost : VHost) + @vhost : VHost, + @broker : MQTT::Broker) end def start(socket : ::IO, connection_info : ConnectionInfo) + io = MQTT::IO.new(socket) if packet = MQTT::Packet.from_io(socket).as?(MQTT::Connect) Log.trace { "recv #{packet.inspect}" } if user = authenticate(io, packet) MQTT::Connack.new(false, MQTT::Connack::ReturnCode::Accepted).to_io(io) io.flush - return LavinMQ::MQTT::Client.new(socket, connection_info, @vhost, user, packet.client_id, packet.clean_session?, packet.will) + return LavinMQ::MQTT::Client.new(socket, connection_info, user, @vhost, @broker, packet.client_id, packet.clean_session?, packet.will) end end rescue ex : MQTT::Error::Connect @@ -28,7 +31,6 @@ module LavinMQ MQTT::Connack.new(false, MQTT::Connack::ReturnCode.new(ex.return_code)).to_io(io) end socket.close - rescue ex Log.warn { "Recieved the wrong packet" } socket.close diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 2c1681fd8b..1802e4a773 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,12 +1,19 @@ module LavinMQ module MQTT class Session < Queue - def initialize(@vhost : VHost, @name : String, @exclusive = true, @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) + def initialize(@vhost : VHost, + @name : String, + @exclusive = true, + @auto_delete = false, + arguments : ::AMQ::Protocol::Table = AMQP::Table.new) super end - #if sub comes in with clean_session, set auto_delete on session - #rm_consumer override for clean_session + + #TODO: implement subscribers array and session_present? and send instead of false + def connect(client) + client.send(MQTT::Connack.new(false, MQTT::Connack::ReturnCode::Accepted)) + end end end end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 7f818ade29..57e39f2976 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -39,7 +39,7 @@ module LavinMQ @vhosts = VHostStore.new(@data_dir, @users, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) @amqp_connection_factory = LavinMQ::AMQP::ConnectionFactory.new - @mqtt_connection_factory = MQTT::ConnectionFactory.new(@users, @vhosts["/"]) + @mqtt_connection_factory = MQTT::ConnectionFactory.new(@users, @vhosts["/"], MQTT::Broker.new(@vhosts["/"])) apply_parameter spawn stats_loop, name: "Server#stats_loop" end diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index 80df2bd95a..ad7d4b6c56 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -339,17 +339,7 @@ module LavinMQ @connections.delete client end - def start_session(client : Client) - client_id = client.client_id - session = MQTT::Session.new(self, client_id) - sessions[client_id] = session - @queues[client_id] = session - end - def clear_session(client : Client) - sessions.delete client.client_id - @queues.delete client.client_id - end SHOVEL = "shovel" FEDERATION_UPSTREAM = "federation-upstream" From 383cee7cb875f8c05413d2e84ddd3669d234467f Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 23 Sep 2024 11:47:39 +0200 Subject: [PATCH 024/202] pass more specs and handle session_present --- spec/mqtt/integrations/connect_spec.cr | 2 +- src/lavinmq/mqtt/broker.cr | 25 +++++++++++++++++++------ src/lavinmq/mqtt/client.cr | 8 ++++---- src/lavinmq/mqtt/connection_factory.cr | 4 +++- src/lavinmq/mqtt/session.cr | 5 +++++ 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index b556b86209..8522b8ab6f 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -25,7 +25,7 @@ module MqttSpecs with_client_io(server) do |io| connect(io, clean_session: false) - # Myra won't save sessions without subscriptions + # LavinMQ won't save sessions without subscriptions subscribe(io, topic_filters: [subtopic("a/topic", 0u8)], packet_id: 1u16 diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index fa66ee0917..1788c15464 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -8,16 +8,29 @@ module LavinMQ @sessions = Hash(String, Session).new end - def start_session(client : Client) - client_id = client.client_id - session = MQTT::Session.new(self, client_id) + def clean_session?(client_id : String) : Bool + session = @sessions[client_id]? + return false if session.nil? + session.set_clean_session + end + + def session_present?(client_id : String, clean_session) : Bool + session = @sessions[client_id]? + clean_session?(client_id) + return false if session.nil? || clean_session + true + end + + def start_session(client_id, clean_session) + session = MQTT::Session.new(@vhost, client_id) + session.clean_session if clean_session @sessions[client_id] = session @queues[client_id] = session end - def clear_session(client : Client) - @sessions.delete client.client_id - @queues.delete client.client_id + def clear_session(client_id) + @sessions.delete client_id + @queues.delete client_id end # def connected(client) : MQTT::Session diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index d966a03a6d..3d56a6b5b6 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -57,7 +57,7 @@ module LavinMQ Log.info { "eof #{ex.inspect}" } ensure publish_will if @will - disconnect_session(self) if @clean_session + @broker.clear_session(client_id) if @clean_session @socket.close @broker.vhost.rm_connection(self) end @@ -114,6 +114,7 @@ module LavinMQ auto_delete = true tbl = AMQP::Table.new # TODO: declare Session instead + @broker.start_session(@client_id, @clean_session) q = @broker.vhost.declare_queue(name, durable, auto_delete, tbl) qos = Array(MQTT::SubAck::ReturnCode).new packet.topic_filters.each do |tf| @@ -145,21 +146,20 @@ module LavinMQ def start_session(client) : MQTT::Session if @clean_session - pp "clear session" + Log.trace { "clear session" } @broker.clear_session(client) end @broker.start_session(client) end def disconnect_session(client) - pp "disconnect session" + Log.trace { "disconnect session" } @broker.clear_session(client) end # TODO: actually publish will to session private def publish_will if will = @will - pp "Publish will to session" end rescue ex Log.warn { "Failed to publish will: #{ex.message}" } diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index a3af16459b..b8dda71112 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -20,7 +20,9 @@ module LavinMQ if packet = MQTT::Packet.from_io(socket).as?(MQTT::Connect) Log.trace { "recv #{packet.inspect}" } if user = authenticate(io, packet) - MQTT::Connack.new(false, MQTT::Connack::ReturnCode::Accepted).to_io(io) + session_present = @broker.session_present?(packet.client_id, packet.clean_session?) + pp "in connection_factory: #{session_present}" + MQTT::Connack.new(session_present, MQTT::Connack::ReturnCode::Accepted).to_io(io) io.flush return LavinMQ::MQTT::Client.new(socket, connection_info, user, @vhost, @broker, packet.client_id, packet.clean_session?, packet.will) end diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 1802e4a773..673e123753 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,6 +1,8 @@ module LavinMQ module MQTT class Session < Queue + @clean_session : Bool = false + getter clean_session def initialize(@vhost : VHost, @name : String, @exclusive = true, @@ -9,6 +11,9 @@ module LavinMQ super end + def set_clean_session + @clean_session = true + end #TODO: implement subscribers array and session_present? and send instead of false def connect(client) From 660f2f97dc401443c234aab61d37a6e048964a79 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 23 Sep 2024 16:05:36 +0200 Subject: [PATCH 025/202] all connect specs working except replace client with new connection --- spec/mqtt/integrations/connect_spec.cr | 15 ++++++++------- src/lavinmq/mqtt/broker.cr | 22 ++++++++++++++-------- src/lavinmq/mqtt/client.cr | 4 ++++ src/lavinmq/mqtt/connection_factory.cr | 4 +--- src/lavinmq/mqtt/session.cr | 8 ++++++++ 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 8522b8ab6f..d2bf9a9f14 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -5,7 +5,7 @@ module MqttSpecs extend MqttMatchers describe "connect [MQTT-3.1.4-1]" do describe "when client already connected" do - pending "should replace the already connected client [MQTT-3.1.4-2]" do + it "should replace the already connected client [MQTT-3.1.4-2]" do with_server do |server| with_client_io(server) do |io| connect(io, false) @@ -163,7 +163,8 @@ module MqttSpecs end end - pending "for password flag set without username flag set [MQTT-3.1.2-22]" do + # TODO: rescue and log error + it "for password flag set without username flag set [MQTT-3.1.2-22]" do with_server do |server| with_client_io(server) do |io| connect = MQTT::Protocol::Connect.new( @@ -186,7 +187,7 @@ module MqttSpecs end describe "tcp socket is closed [MQTT-3.1.4-1]" do - pending "if first packet is not a CONNECT [MQTT-3.1.0-1]" do + it "if first packet is not a CONNECT [MQTT-3.1.0-1]" do with_server do |server| with_client_io(server) do |io| ping(io) @@ -195,7 +196,7 @@ module MqttSpecs end end - pending "for a second CONNECT packet [MQTT-3.1.0-2]" do + it "for a second CONNECT packet [MQTT-3.1.0-2]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -206,7 +207,7 @@ module MqttSpecs end end - pending "for invalid client id [MQTT-3.1.3-4]." do + it "for invalid client id [MQTT-3.1.3-4]." do with_server do |server| with_client_io(server) do |io| MQTT::Protocol::Connect.new( @@ -223,7 +224,7 @@ module MqttSpecs end end - pending "for invalid protocol name [MQTT-3.1.2-1]" do + it "for invalid protocol name [MQTT-3.1.2-1]" do with_server do |server| with_client_io(server) do |io| connect = MQTT::Protocol::Connect.new( @@ -244,7 +245,7 @@ module MqttSpecs end end - pending "for reserved bit set [MQTT-3.1.2-3]" do + it "for reserved bit set [MQTT-3.1.2-3]" do with_server do |server| with_client_io(server) do |io| connect = MQTT::Protocol::Connect.new( diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 1788c15464..1dd334590a 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -2,28 +2,34 @@ module LavinMQ module MQTT class Broker - getter vhost + getter vhost, sessions def initialize(@vhost : VHost) @queues = Hash(String, Session).new @sessions = Hash(String, Session).new + @clients = Hash(String, Client).new end - def clean_session?(client_id : String) : Bool - session = @sessions[client_id]? - return false if session.nil? - session.set_clean_session + def connect_client(socket, connection_info, user, vhost, packet) + if prev_client = @clients[packet.client_id]? + Log.trace { "Found previous client connected with client_id: #{packet.client_id}, closing" } + pp "rev client" + prev_client.close + end + client = MQTT::Client.new(socket, connection_info, user, vhost, self, packet.client_id, packet.clean_session?, packet.will) + @clients[packet.client_id] = client + client end def session_present?(client_id : String, clean_session) : Bool session = @sessions[client_id]? - clean_session?(client_id) - return false if session.nil? || clean_session + pp "session_present? #{session.inspect}" + return false if session.nil? || clean_session && session.set_clean_session true end def start_session(client_id, clean_session) session = MQTT::Session.new(@vhost, client_id) - session.clean_session if clean_session + session.set_clean_session if clean_session @sessions[client_id] = session @queues[client_id] = session end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 3d56a6b5b6..5258a4f698 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -56,6 +56,7 @@ module LavinMQ rescue ex : ::IO::EOFError Log.info { "eof #{ex.inspect}" } ensure + pp "ensuring" publish_will if @will @broker.clear_session(client_id) if @clean_session @socket.close @@ -169,6 +170,9 @@ module LavinMQ end def close(reason = "") + Log.trace { "Client#close" } + @closed = true + @socket.close end def force_close diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index b8dda71112..1d8574a090 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -15,16 +15,14 @@ module LavinMQ end def start(socket : ::IO, connection_info : ConnectionInfo) - io = MQTT::IO.new(socket) if packet = MQTT::Packet.from_io(socket).as?(MQTT::Connect) Log.trace { "recv #{packet.inspect}" } if user = authenticate(io, packet) session_present = @broker.session_present?(packet.client_id, packet.clean_session?) - pp "in connection_factory: #{session_present}" MQTT::Connack.new(session_present, MQTT::Connack::ReturnCode::Accepted).to_io(io) io.flush - return LavinMQ::MQTT::Client.new(socket, connection_info, user, @vhost, @broker, packet.client_id, packet.clean_session?, packet.will) + return @broker.connect_client(socket, connection_info, user, @vhost, packet) end end rescue ex : MQTT::Error::Connect diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 673e123753..e07fb8f144 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -12,9 +12,17 @@ module LavinMQ end def set_clean_session + pp "Setting clean session" + clear_session @clean_session = true end + + #Maybe use something other than purge? + def clear_session + purge + end + #TODO: implement subscribers array and session_present? and send instead of false def connect(client) client.send(MQTT::Connack.new(false, MQTT::Connack::ReturnCode::Accepted)) From 10cb2615dbf5190c68d01ce64ea3afb53cd5f834 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 24 Sep 2024 13:55:25 +0200 Subject: [PATCH 026/202] fix logs in client and dot expect connack in specs --- spec/mqtt/integrations/connect_spec.cr | 2 +- src/lavinmq/mqtt/broker.cr | 2 +- src/lavinmq/mqtt/client.cr | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index d2bf9a9f14..f81f0cb6ee 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -8,7 +8,7 @@ module MqttSpecs it "should replace the already connected client [MQTT-3.1.4-2]" do with_server do |server| with_client_io(server) do |io| - connect(io, false) + connect(io) with_client_io(server) do |io2| connect(io2) io.should be_closed diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 1dd334590a..37b38da2d6 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -23,7 +23,7 @@ module LavinMQ def session_present?(client_id : String, clean_session) : Bool session = @sessions[client_id]? pp "session_present? #{session.inspect}" - return false if session.nil? || clean_session && session.set_clean_session + return false if session.nil? || ( clean_session && session.set_clean_session ) true end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 5258a4f698..8278cb72d6 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -43,7 +43,7 @@ module LavinMQ private def read_loop loop do - Log.trace { "waiting for packet" } + @log.trace { "waiting for packet" } packet = read_and_handle_packet # The disconnect packet has been handled and the socket has been closed. # If we dont breakt the loop here we'll get a IO/Error on next read. @@ -52,9 +52,9 @@ module LavinMQ rescue ex : ::MQTT::Protocol::Error::PacketDecode @socket.close rescue ex : MQTT::Error::Connect - Log.warn { "Connect error #{ex.inspect}" } - rescue ex : ::IO::EOFError - Log.info { "eof #{ex.inspect}" } + @log.warn { "Connect error #{ex.inspect}" } + rescue ex : ::IO::Error + @log.warn(exception: ex) { "Read Loop error" } ensure pp "ensuring" publish_will if @will @@ -65,7 +65,7 @@ module LavinMQ def read_and_handle_packet packet : MQTT::Packet = MQTT::Packet.from_io(@io) - Log.info { "recv #{packet.inspect}" } + @log.info { "recv #{packet.inspect}" } @recv_oct_count += packet.bytesize case packet @@ -147,14 +147,14 @@ module LavinMQ def start_session(client) : MQTT::Session if @clean_session - Log.trace { "clear session" } + @log.trace { "clear session" } @broker.clear_session(client) end @broker.start_session(client) end def disconnect_session(client) - Log.trace { "disconnect session" } + @log.trace { "disconnect session" } @broker.clear_session(client) end @@ -163,14 +163,14 @@ module LavinMQ if will = @will end rescue ex - Log.warn { "Failed to publish will: #{ex.message}" } + @log.warn { "Failed to publish will: #{ex.message}" } end def update_rates end def close(reason = "") - Log.trace { "Client#close" } + @log.trace { "Client#close" } @closed = true @socket.close end From d4e0008a2690333b2463292cecdbb8c04c870b6c Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 25 Sep 2024 10:33:44 +0200 Subject: [PATCH 027/202] add sessions to broker, and handle incoming subscribe with session --- spec/mqtt/integrations/connect_spec.cr | 1 - src/lavinmq/mqtt/broker.cr | 61 ++++++++++++++++++-------- src/lavinmq/mqtt/client.cr | 35 +++++---------- src/lavinmq/mqtt/session.cr | 15 +++---- src/lavinmq/queue_factory.cr | 10 ++++- 5 files changed, 66 insertions(+), 56 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index f81f0cb6ee..262eff3393 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -163,7 +163,6 @@ module MqttSpecs end end - # TODO: rescue and log error it "for password flag set without username flag set [MQTT-3.1.2-22]" do with_server do |server| with_client_io(server) do |io| diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 37b38da2d6..680e33a790 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -1,18 +1,47 @@ module LavinMQ module MQTT + struct Sessions + + @queues : Hash(String, Queue) + + def initialize( @vhost : VHost) + @queues = @vhost.queues + end + + def []?(client_id : String) : Session? + @queues["amq.mqtt-#{client_id}"]?.try &.as(Session) + end + + def [](client_id : String) : Session + @queues["amq.mqtt-#{client_id}"].as(Session) + end + + def declare(client_id : String, clean_session : Bool) + if session = self[client_id]? + return session + end + @vhost.declare_queue("amq.mqtt-#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) + return self[client_id] + end + + def delete(client_id : String) + @vhost.delete_queue("amq.mqtt-#{client_id}") + end + end + class Broker getter vhost, sessions + def initialize(@vhost : VHost) - @queues = Hash(String, Session).new - @sessions = Hash(String, Session).new + @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new end + #remember to remove the old client entry form the hash if you replace a client. (maybe it already does?) def connect_client(socket, connection_info, user, vhost, packet) if prev_client = @clients[packet.client_id]? Log.trace { "Found previous client connected with client_id: #{packet.client_id}, closing" } - pp "rev client" prev_client.close end client = MQTT::Client.new(socket, connection_info, user, vhost, self, packet.client_id, packet.clean_session?, packet.will) @@ -20,30 +49,24 @@ module LavinMQ client end + def subscribe(client, packet) + name = "amq.mqtt-#{client.client_id}" + durable = false + auto_delete = false + pp "clean_session: #{client.@clean_session}" + @sessions.declare(client.client_id, client.@clean_session) + # Handle bindings, packet.topics + end + def session_present?(client_id : String, clean_session) : Bool session = @sessions[client_id]? - pp "session_present? #{session.inspect}" - return false if session.nil? || ( clean_session && session.set_clean_session ) + return false if session.nil? || clean_session true end - def start_session(client_id, clean_session) - session = MQTT::Session.new(@vhost, client_id) - session.set_clean_session if clean_session - @sessions[client_id] = session - @queues[client_id] = session - end - def clear_session(client_id) @sessions.delete client_id - @queues.delete client_id end - - # def connected(client) : MQTT::Session - # session = Session.new(client.vhost, client.client_id) - # session.connect(client) - # session - # end end end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 8278cb72d6..5828bc4383 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -32,7 +32,7 @@ module LavinMQ @name = "#{@remote_address} -> #{@local_address}" @metadata = ::Log::Metadata.new(nil, {vhost: @broker.vhost.name, address: @remote_address.to_s}) @log = Logger.new(Log, @metadata) - # @session = @broker.connected(self) + @broker.vhost.add_connection(self) @log.info { "Connection established for user=#{@user.name}" } spawn read_loop end @@ -55,9 +55,11 @@ module LavinMQ @log.warn { "Connect error #{ex.inspect}" } rescue ex : ::IO::Error @log.warn(exception: ex) { "Read Loop error" } - ensure - pp "ensuring" + publish_will if @will + rescue ex publish_will if @will + raise ex + ensure @broker.clear_session(client_id) if @clean_session @socket.close @broker.vhost.rm_connection(self) @@ -110,20 +112,16 @@ module LavinMQ end def recieve_subscribe(packet : MQTT::Subscribe) - name = "mqtt.#{@client_id}" - durable = false - auto_delete = true - tbl = AMQP::Table.new - # TODO: declare Session instead - @broker.start_session(@client_id, @clean_session) - q = @broker.vhost.declare_queue(name, durable, auto_delete, tbl) + @broker.subscribe(self, packet) qos = Array(MQTT::SubAck::ReturnCode).new packet.topic_filters.each do |tf| qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) rk = topicfilter_to_routingkey(tf.topic) - @broker.vhost.bind_queue(name, "amq.topic", rk) + #handle bindings in broker. + @broker.vhost.bind_queue("amq.mqtt-#{client_id}", "amq.topic", rk) end - queue = @broker.vhost.queues[name] + # handle add_consumer in broker. + queue = @broker.vhost.queues["amq.mqtt-#{client_id}"] consumer = MqttConsumer.new(self, queue) queue.add_consumer(consumer) send(MQTT::SubAck.new(qos, packet.packet_id)) @@ -145,19 +143,6 @@ module LavinMQ }.merge(stats_details) end - def start_session(client) : MQTT::Session - if @clean_session - @log.trace { "clear session" } - @broker.clear_session(client) - end - @broker.start_session(client) - end - - def disconnect_session(client) - @log.trace { "disconnect session" } - @broker.clear_session(client) - end - # TODO: actually publish will to session private def publish_will if will = @will diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index e07fb8f144..f8cb01706e 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -5,22 +5,17 @@ module LavinMQ getter clean_session def initialize(@vhost : VHost, @name : String, - @exclusive = true, @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) - super + super(@vhost, @name, false, @auto_delete, arguments) end - def set_clean_session - pp "Setting clean session" - clear_session - @clean_session = true + def clean_session? + @auto_delete end - - #Maybe use something other than purge? - def clear_session - purge + def durable? + !clean_session? end #TODO: implement subscribers array and session_present? and send instead of false diff --git a/src/lavinmq/queue_factory.cr b/src/lavinmq/queue_factory.cr index 6a0513fd9b..f4b9d4e04a 100644 --- a/src/lavinmq/queue_factory.cr +++ b/src/lavinmq/queue_factory.cr @@ -24,7 +24,9 @@ module LavinMQ elsif frame.auto_delete raise Error::PreconditionFailed.new("A stream queue cannot be auto-delete") end - AMQP::StreamQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) + StreamQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) + elsif mqtt_session? frame + MQTT::Session.new(vhost, frame.queue_name, frame.auto_delete, frame.arguments) else warn_if_unsupported_queue_type frame AMQP::DurableQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) @@ -36,6 +38,8 @@ module LavinMQ AMQP::PriorityQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif stream_queue? frame raise Error::PreconditionFailed.new("A stream queue cannot be non-durable") + elsif mqtt_session? frame + MQTT::Session.new(vhost, frame.queue_name, frame.auto_delete, frame.arguments) else warn_if_unsupported_queue_type frame AMQP::Queue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) @@ -60,5 +64,9 @@ module LavinMQ Log.info { "The queue type #{frame.arguments["x-queue-type"]} is not supported by LavinMQ and will be changed to the default queue type" } end end + + private def self.mqtt_session?(frame) : Bool + frame.arguments["x-queue-type"]? == "mqtt" + end end end From a97fd38c3a210cc20185ff18df453b9d64b2a0c8 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 25 Sep 2024 10:44:16 +0200 Subject: [PATCH 028/202] fixup! add sessions to broker, and handle incoming subscribe with session --- src/lavinmq/amqp/channel.cr | 1 - src/lavinmq/http/controller/queues.cr | 32 +++++++++++++-------------- src/lavinmq/mqtt/session_store.cr | 15 ------------- src/lavinmq/vhost.cr | 5 +---- 4 files changed, 17 insertions(+), 36 deletions(-) delete mode 100644 src/lavinmq/mqtt/session_store.cr diff --git a/src/lavinmq/amqp/channel.cr b/src/lavinmq/amqp/channel.cr index 94bc6f6655..192d2d8665 100644 --- a/src/lavinmq/amqp/channel.cr +++ b/src/lavinmq/amqp/channel.cr @@ -247,7 +247,6 @@ module LavinMQ end confirm do - #here ok = @client.vhost.publish msg, @next_publish_immediate, @visited, @found_queues basic_return(msg, @next_publish_mandatory, @next_publish_immediate) unless ok rescue e : LavinMQ::Error::PreconditionFailed diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index c375d96a2c..80bc386d1c 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -42,22 +42,22 @@ module LavinMQ end end - # get "/api/queues/:vhost/:name/unacked" do |context, params| - # with_vhost(context, params) do |vhost| - # refuse_unless_management(context, user(context), vhost) - # q = queue(context, params, vhost) - # # unacked_messages = q.consumers.each.flat_map do |c| - # # c.unacked_messages.each.compact_map do |u| - # # next unless u.queue == q - # # if consumer = u.consumer - # # UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) - # # end - # # end - # # end - # # unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) - # # page(context, unacked_messages) - # end - # end + get "/api/queues/:vhost/:name/unacked" do |context, params| + with_vhost(context, params) do |vhost| + refuse_unless_management(context, user(context), vhost) + q = queue(context, params, vhost) + # unacked_messages = q.consumers.each.flat_map do |c| + # c.unacked_messages.each.compact_map do |u| + # next unless u.queue == q + # if consumer = u.consumer + # UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) + # end + # end + # end + # unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) + # page(context, unacked_messages) + end + end put "/api/queues/:vhost/:name" do |context, params| with_vhost(context, params) do |vhost| diff --git a/src/lavinmq/mqtt/session_store.cr b/src/lavinmq/mqtt/session_store.cr deleted file mode 100644 index b60aff58dd..0000000000 --- a/src/lavinmq/mqtt/session_store.cr +++ /dev/null @@ -1,15 +0,0 @@ -#holds all sessions in a vhost -require "./session" -module LavinMQ - module MQTT - class SessionStore - getter vhost, sessions - def initialize(@vhost : VHost) - @sessions = Hash(String, MQTT::Session).new - end - - forward_missing_to @sessions - - end - end -end diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index ad7d4b6c56..0ee4c5f690 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -15,6 +15,7 @@ require "./event_type" require "./stats" require "./queue_factory" require "./mqtt/session_store" +require "./mqtt/session" module LavinMQ class VHost @@ -37,7 +38,6 @@ module LavinMQ @direct_reply_consumers = Hash(String, Client::Channel).new @shovels : ShovelStore? @upstreams : Federation::UpstreamStore? - @sessions : MQTT::SessionStore? @connections = Array(Client).new(512) @definitions_file : File @definitions_lock = Mutex.new(:reentrant) @@ -60,7 +60,6 @@ module LavinMQ @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator, vhost: @name) @shovels = ShovelStore.new(self) @upstreams = Federation::UpstreamStore.new(self) - @sessions = MQTT::SessionStore.new(self) load! spawn check_consumer_timeouts_loop, name: "Consumer timeouts loop" end @@ -339,8 +338,6 @@ module LavinMQ @connections.delete client end - - SHOVEL = "shovel" FEDERATION_UPSTREAM = "federation-upstream" FEDERATION_UPSTREAM_SET = "federation-upstream-set" From e2ce6486360827d32bcbd10f7162a000b8248195 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 25 Sep 2024 11:01:05 +0200 Subject: [PATCH 029/202] fixup! fixup! add sessions to broker, and handle incoming subscribe with session --- src/lavinmq/vhost.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index 0ee4c5f690..8b102c4c15 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -27,7 +27,7 @@ module LavinMQ "redeliver", "reject", "consumer_added", "consumer_removed"}) getter name, exchanges, queues, data_dir, operator_policies, policies, parameters, shovels, - direct_reply_consumers, connections, dir, users, sessions + direct_reply_consumers, connections, dir, users property? flow = true getter? closed = false property max_connections : Int32? From b1ba2897638961266d1f9997fdb2671319254ff9 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 27 Sep 2024 09:18:50 +0200 Subject: [PATCH 030/202] subscribe and bind a session --- spec/mqtt/integrations/subscribe_spec.cr | 22 ++------------- src/lavinmq/mqtt/broker.cr | 36 +++++++++++++----------- src/lavinmq/mqtt/client.cr | 23 ++++----------- src/lavinmq/mqtt/session.cr | 28 +++++++++++------- 4 files changed, 45 insertions(+), 64 deletions(-) diff --git a/spec/mqtt/integrations/subscribe_spec.cr b/spec/mqtt/integrations/subscribe_spec.cr index ee9b84382d..6081abc59f 100644 --- a/spec/mqtt/integrations/subscribe_spec.cr +++ b/spec/mqtt/integrations/subscribe_spec.cr @@ -93,7 +93,7 @@ module MqttSpecs end end - pending "should replace old subscription with new [MQTT-3.8.4-3]" do + it "should replace old subscription with new [MQTT-3.8.4-3]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -124,7 +124,7 @@ module MqttSpecs publish(io, topic: "a/b", payload: "a".to_slice, qos: 1u8) # ... consume it... packet = read_packet(io).as(MQTT::Protocol::Publish) - # ... and verify it be qos0 (i.e. our subscribe is correct) + # ... and verify it be qos1 (i.e. our second subscribe is correct) packet.qos.should eq(1u8) io.should be_drained @@ -132,22 +132,4 @@ module MqttSpecs end end end - - describe "amqp" do - pending "should create a queue and subscribe queue to amq.topic" do - with_server do |server| - with_client_io(server) do |io| - connect(io) - - topic_filters = mk_topic_filters({"a/b", 0}) - suback = subscribe(io, topic_filters: topic_filters) - suback.should be_a(MQTT::Protocol::SubAck) - - q = server.vhosts["/"].queues["mqtt.client_id"] - binding = q.bindings.find { |a, b| a.is_a?(LavinMQ::TopicExchange) && b[0] == "a.b" } - binding.should_not be_nil - end - end - end - end end diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 680e33a790..d04f7380ad 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -1,7 +1,6 @@ module LavinMQ module MQTT struct Sessions - @queues : Hash(String, Queue) def initialize( @vhost : VHost) @@ -17,11 +16,10 @@ module LavinMQ end def declare(client_id : String, clean_session : Bool) - if session = self[client_id]? - return session + self[client_id]? || begin + @vhost.declare_queue("amq.mqtt-#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) + self[client_id] end - @vhost.declare_queue("amq.mqtt-#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) - return self[client_id] end def delete(client_id : String) @@ -30,7 +28,6 @@ module LavinMQ end class Broker - getter vhost, sessions def initialize(@vhost : VHost) @@ -38,7 +35,12 @@ module LavinMQ @clients = Hash(String, Client).new end - #remember to remove the old client entry form the hash if you replace a client. (maybe it already does?) + def session_present?(client_id : String, clean_session) : Bool + session = @sessions[client_id]? + return false if session.nil? || clean_session + true + end + def connect_client(socket, connection_info, user, vhost, packet) if prev_client = @clients[packet.client_id]? Log.trace { "Found previous client connected with client_id: #{packet.client_id}, closing" } @@ -50,18 +52,18 @@ module LavinMQ end def subscribe(client, packet) - name = "amq.mqtt-#{client.client_id}" - durable = false - auto_delete = false - pp "clean_session: #{client.@clean_session}" - @sessions.declare(client.client_id, client.@clean_session) - # Handle bindings, packet.topics + session = @sessions.declare(client.client_id, client.@clean_session) + qos = Array(MQTT::SubAck::ReturnCode).new(packet.topic_filters.size) + packet.topic_filters.each do |tf| + qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) + rk = topicfilter_to_routingkey(tf.topic) + session.subscribe(rk, tf.qos) + end + qos end - def session_present?(client_id : String, clean_session) : Bool - session = @sessions[client_id]? - return false if session.nil? || clean_session - true + def topicfilter_to_routingkey(tf) : String + tf.gsub("/", ".") end def clear_session(client_id) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 5828bc4383..b3d7b7c7a8 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -42,6 +42,7 @@ module LavinMQ end private def read_loop + loop do @log.trace { "waiting for packet" } packet = read_and_handle_packet @@ -95,7 +96,7 @@ module LavinMQ end def recieve_publish(packet : MQTT::Publish) - rk = topicfilter_to_routingkey(packet.topic) + rk = @broker.topicfilter_to_routingkey(packet.topic) props = AMQ::Protocol::Properties.new( message_id: packet.packet_id.to_s ) @@ -112,25 +113,13 @@ module LavinMQ end def recieve_subscribe(packet : MQTT::Subscribe) - @broker.subscribe(self, packet) - qos = Array(MQTT::SubAck::ReturnCode).new - packet.topic_filters.each do |tf| - qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) - rk = topicfilter_to_routingkey(tf.topic) - #handle bindings in broker. - @broker.vhost.bind_queue("amq.mqtt-#{client_id}", "amq.topic", rk) - end - # handle add_consumer in broker. - queue = @broker.vhost.queues["amq.mqtt-#{client_id}"] - consumer = MqttConsumer.new(self, queue) - queue.add_consumer(consumer) + qos = @broker.subscribe(self, packet) + session = @broker.sessions[@client_id] + consumer = MqttConsumer.new(self, session) + session.add_consumer(consumer) send(MQTT::SubAck.new(qos, packet.packet_id)) end - def topicfilter_to_routingkey(tf) : String - tf.gsub("/", ".") - end - def recieve_unsubscribe(packet) end diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index f8cb01706e..22d8438771 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,8 +1,10 @@ module LavinMQ module MQTT class Session < Queue - @clean_session : Bool = false - getter clean_session + @clean_session : Bool = false + @subscriptions : Int32 = 0 + getter clean_session + def initialize(@vhost : VHost, @name : String, @auto_delete = false, @@ -10,17 +12,23 @@ module LavinMQ super(@vhost, @name, false, @auto_delete, arguments) end - def clean_session? - @auto_delete - end + def clean_session?; @auto_delete; end + def durable?; !clean_session?; end - def durable? - !clean_session? + # TODO: "amq.tocpic" is hardcoded, should be the mqtt-exchange when that is finished + def subscribe(rk, qos) + arguments = AMQP::Table.new({"x-mqtt-qos": qos}) + if binding = bindings.find { |b| b.binding_key.routing_key == rk } + return if binding.binding_key.arguments == arguments + @vhost.unbind_queue(@name, "amq.topic", rk, binding.binding_key.arguments || AMQP::Table.new) + end + @vhost.bind_queue(@name, "amq.topic", rk, arguments) end - #TODO: implement subscribers array and session_present? and send instead of false - def connect(client) - client.send(MQTT::Connack.new(false, MQTT::Connack::ReturnCode::Accepted)) + def unsubscribe(rk) + # unbind session from the exchange + # decrease @subscriptions by 1 + # if subscriptions is empty, delete the session(do that from broker?) end end end From 01313486edfbb63f3ba833ab089437229510ac8c Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 1 Oct 2024 14:03:32 +0200 Subject: [PATCH 031/202] temp --- spec/mqtt/integrations/unsubscribe_spec.cr | 6 +++--- src/lavinmq/mqtt/broker.cr | 8 ++++++++ src/lavinmq/mqtt/client.cr | 10 +++++++++- src/lavinmq/mqtt/session.cr | 19 +++++++++++++------ 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/spec/mqtt/integrations/unsubscribe_spec.cr b/spec/mqtt/integrations/unsubscribe_spec.cr index 94b3d0cf6e..ceb7de992c 100644 --- a/spec/mqtt/integrations/unsubscribe_spec.cr +++ b/spec/mqtt/integrations/unsubscribe_spec.cr @@ -5,7 +5,7 @@ module MqttSpecs extend MqttMatchers describe "unsubscribe" do - pending "bits 3,2,1,0 must be set to 0,0,1,0 [MQTT-3.10.1-1]" do + it "bits 3,2,1,0 must be set to 0,0,1,0 [MQTT-3.10.1-1]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -23,7 +23,7 @@ module MqttSpecs end end - pending "must contain at least one topic filter [MQTT-3.10.3-2]" do + it "must contain at least one topic filter [MQTT-3.10.3-2]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -41,7 +41,7 @@ module MqttSpecs end end - pending "must stop adding any new messages for delivery to the Client, but completes delivery of previous messages [MQTT-3.10.4-2] and [MQTT-3.10.4-3]" do + it "must stop adding any new messages for delivery to the Client, but completes delivery of previous messages [MQTT-3.10.4-2] and [MQTT-3.10.4-3]" do with_server do |server| with_client_io(server) do |pubio| connect(pubio, client_id: "publisher") diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index d04f7380ad..cbf1b2c652 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -62,6 +62,14 @@ module LavinMQ qos end + def unsubscribe(client, packet) + session = @sessions[client.client_id] + packet.topics.each do |tf| + rk = topicfilter_to_routingkey(tf) + session.unsubscribe(rk) + end + end + def topicfilter_to_routingkey(tf) : String tf.gsub("/", ".") end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index b3d7b7c7a8..93ea2658dd 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -75,7 +75,7 @@ module LavinMQ when MQTT::Publish then recieve_publish(packet) when MQTT::PubAck then pp "puback" when MQTT::Subscribe then recieve_subscribe(packet) - when MQTT::Unsubscribe then pp "unsubscribe" + when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) when MQTT::Disconnect then return packet else raise "invalid packet type for client to send" @@ -101,8 +101,10 @@ module LavinMQ message_id: packet.packet_id.to_s ) # TODO: String.new around payload.. should be stored as Bytes + # Send to MQTT-exchange msg = Message.new("amq.topic", rk, String.new(packet.payload), props) @broker.vhost.publish(msg) + # Ok to not send anything if qos = 0 (at most once delivery) if packet.qos > 0 && (packet_id = packet.packet_id) send(MQTT::PubAck.new(packet_id)) @@ -121,6 +123,12 @@ module LavinMQ end def recieve_unsubscribe(packet) + session = @broker.sessions[@client_id] + @broker.unsubscribe(self, packet) + if consumer = session.consumers.find { |c| c.tag == "mqtt" } + session.rm_consumer(consumer) + end + send(MQTT::UnsubAck.new(packet.packet_id)) end def details_tuple diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 22d8438771..a6b9b3c2a0 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -2,7 +2,6 @@ module LavinMQ module MQTT class Session < Queue @clean_session : Bool = false - @subscriptions : Int32 = 0 getter clean_session def initialize(@vhost : VHost, @@ -18,17 +17,25 @@ module LavinMQ # TODO: "amq.tocpic" is hardcoded, should be the mqtt-exchange when that is finished def subscribe(rk, qos) arguments = AMQP::Table.new({"x-mqtt-qos": qos}) - if binding = bindings.find { |b| b.binding_key.routing_key == rk } + if binding = find_binding(rk) return if binding.binding_key.arguments == arguments - @vhost.unbind_queue(@name, "amq.topic", rk, binding.binding_key.arguments || AMQP::Table.new) + unbind(rk, binding.binding_key.arguments) end @vhost.bind_queue(@name, "amq.topic", rk, arguments) end def unsubscribe(rk) - # unbind session from the exchange - # decrease @subscriptions by 1 - # if subscriptions is empty, delete the session(do that from broker?) + if binding = find_binding(rk) + unbind(rk, binding.binding_key.arguments) + end + end + + private def find_binding(rk) + bindings.find { |b| b.binding_key.routing_key == rk } + end + + private def unbind(rk, arguments) + @vhost.unbind_queue(@name, "amq.topic", rk, arguments || AMQP::Table.new) end end end From 39b17e5c6fc5df28354f3b158eb700176078afa6 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Wed, 18 Sep 2024 15:14:46 +0200 Subject: [PATCH 032/202] exchange.publish --- src/lavinmq/http/controller/queues.cr | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index 80bc386d1c..7c7f054b18 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -46,16 +46,16 @@ module LavinMQ with_vhost(context, params) do |vhost| refuse_unless_management(context, user(context), vhost) q = queue(context, params, vhost) - # unacked_messages = q.consumers.each.flat_map do |c| - # c.unacked_messages.each.compact_map do |u| - # next unless u.queue == q - # if consumer = u.consumer - # UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) - # end - # end - # end - # unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) - # page(context, unacked_messages) + unacked_messages = q.consumers.each.flat_map do |c| + c.unacked_messages.each.compact_map do |u| + next unless u.queue == q + if consumer = u.consumer + UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) + end + end + end + unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) + page(context, unacked_messages) end end From ebcaebbe7a0ed80ab63305bdc4191b5cebd04f22 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 1 Oct 2024 15:18:15 +0200 Subject: [PATCH 033/202] mqtt exchange --- src/lavinmq/exchange/exchange.cr | 6 +++ src/lavinmq/exchange/mqtt.cr | 64 ++++++++++++++++++++++++++++++++ src/lavinmq/mqtt/broker.cr | 6 ++- src/lavinmq/mqtt/client.cr | 17 +++++---- src/lavinmq/mqtt/session.cr | 15 +++++--- 5 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 src/lavinmq/exchange/mqtt.cr diff --git a/src/lavinmq/exchange/exchange.cr b/src/lavinmq/exchange/exchange.cr index 0a271b0395..4adc9007e4 100644 --- a/src/lavinmq/exchange/exchange.cr +++ b/src/lavinmq/exchange/exchange.cr @@ -174,6 +174,12 @@ module LavinMQ return 0 if queues.empty? return 0 if immediate && !queues.any? &.immediate_delivery? + # TODO: For each matching binding, get the QoS and write to message header "qos" + # Should be done in the MQTTExchange not in this super class + # if a = arguments[msg.routing_key] + # msg.properties.header["qos"] = q.qos + # end + count = 0 queues.each do |queue| if queue.publish(msg) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr new file mode 100644 index 0000000000..2709b24df3 --- /dev/null +++ b/src/lavinmq/exchange/mqtt.cr @@ -0,0 +1,64 @@ +require "./exchange" + +module LavinMQ + class MQTTExchange < Exchange + # record Binding, topic_filter : String, qos : UInt8 + + @bindings = Hash(BindingKey, Set(Destination)).new do |h, k| + h[k] = Set(Destination).new + end + + def type : String + "mqtt" + end + + def bindings_details : Iterator(BindingDetails) + @bindings.each.flat_map do |binding_key, ds| + ds.each.map do |d| + BindingDetails.new(name, vhost.name, binding_key, d) + end + end + end + + def bind(destination : Destination, topic_filter : String, arguments = nil) : Bool + # binding = Binding.new(topic_filter, arguments["x-mqtt-qos"]) + binding_key = BindingKey.new(topic_filter, arguments) + return false unless @bindings[binding_key].add? destination + data = BindingDetails.new(name, vhost.name, binding_key, destination) + notify_observers(ExchangeEvent::Bind, data) + true + end + + def unbind(destination : Destination, routing_key, headers = nil) : Bool + binding_key = BindingKey.new(routing_key, arguments) + rk_bindings = @bindings[binding_key] + return false unless rk_bindings.delete destination + @bindings.delete binding_key if rk_bindings.empty? + + data = BindingDetails.new(name, vhost.name, binding_key, destination) + notify_observers(ExchangeEvent::Unbind, data) + + delete if @auto_delete && @bindings.each_value.all?(&.empty?) + true + end + + protected def bindings : Iterator(Destination) + @bindings.values.each.flat_map(&.each) + end + + protected def bindings(routing_key, headers) : Iterator(Destination) + binding_key = BindingKey.new(routing_key, headers) + matches(binding_key).each + end + + private def matches(binding_key : BindingKey) : Iterator(Destination) + @bindings.each.select do |binding, destinations| + msg_qos = binding_key.arguments.try { |a| a["qos"]?.try(&.as(UInt8)) } || 0 + binding_qos = binding.arguments.try { |a| a["x-mqtt-pos"]?.try(&.as(UInt8)) } || 0 + + # Use Jons tree finder.. + binding.routing_key == binding_key.routing_key && msg_qos >= binding_qos + end.flat_map { |_, v| v.each } + end + end +end diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index cbf1b2c652..ea78904188 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -3,7 +3,7 @@ module LavinMQ struct Sessions @queues : Hash(String, Queue) - def initialize( @vhost : VHost) + def initialize(@vhost : VHost) @queues = @vhost.queues end @@ -16,7 +16,7 @@ module LavinMQ end def declare(client_id : String, clean_session : Bool) - self[client_id]? || begin + self[client_id]? || begin @vhost.declare_queue("amq.mqtt-#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) self[client_id] end @@ -33,6 +33,8 @@ module LavinMQ def initialize(@vhost : VHost) @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new + exchange = MQTTExchange.new(@vhost, "mqtt.default", true, false, true) + @vhost.exchanges["mqtt.default"] = exchange end def session_present?(client_id : String, clean_session) : Bool diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 93ea2658dd..046dde152f 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -23,8 +23,7 @@ module LavinMQ @broker : MQTT::Broker, @client_id : String, @clean_session = false, - @will : MQTT::Will? = nil - ) + @will : MQTT::Will? = nil) @io = MQTT::IO.new(@socket) @lock = Mutex.new @remote_address = @connection_info.src @@ -42,7 +41,6 @@ module LavinMQ end private def read_loop - loop do @log.trace { "waiting for packet" } packet = read_and_handle_packet @@ -56,7 +54,7 @@ module LavinMQ @log.warn { "Connect error #{ex.inspect}" } rescue ex : ::IO::Error @log.warn(exception: ex) { "Read Loop error" } - publish_will if @will + publish_will if @will rescue ex publish_will if @will raise ex @@ -97,12 +95,16 @@ module LavinMQ def recieve_publish(packet : MQTT::Publish) rk = @broker.topicfilter_to_routingkey(packet.topic) + headers = AMQ::Protocol::Table.new( + qos: packet.qos, + packet_id: packet_id + ) props = AMQ::Protocol::Properties.new( - message_id: packet.packet_id.to_s + headers: headers ) # TODO: String.new around payload.. should be stored as Bytes # Send to MQTT-exchange - msg = Message.new("amq.topic", rk, String.new(packet.payload), props) + msg = Message.new("mqtt.default", rk, String.new(packet.payload), props) @broker.vhost.publish(msg) # Ok to not send anything if qos = 0 (at most once delivery) @@ -207,11 +209,12 @@ module LavinMQ if message_id = msg.properties.message_id packet_id = message_id.to_u16 unless message_id.empty? end + qos = 0u8 # msg.properties.qos pub_args = { packet_id: packet_id, payload: msg.body, dup: false, - qos: 0u8, + qos: qos, retain: false, topic: "test", } diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index a6b9b3c2a0..885af97df8 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,8 +1,8 @@ module LavinMQ module MQTT class Session < Queue - @clean_session : Bool = false - getter clean_session + @clean_session : Bool = false + getter clean_session def initialize(@vhost : VHost, @name : String, @@ -11,8 +11,13 @@ module LavinMQ super(@vhost, @name, false, @auto_delete, arguments) end - def clean_session?; @auto_delete; end - def durable?; !clean_session?; end + def clean_session? + @auto_delete + end + + def durable? + !clean_session? + end # TODO: "amq.tocpic" is hardcoded, should be the mqtt-exchange when that is finished def subscribe(rk, qos) @@ -21,7 +26,7 @@ module LavinMQ return if binding.binding_key.arguments == arguments unbind(rk, binding.binding_key.arguments) end - @vhost.bind_queue(@name, "amq.topic", rk, arguments) + @vhost.bind_queue(@name, "mqtt.default", rk, arguments) end def unsubscribe(rk) From 50f0ab9c5d511dbc579caee6cf73aed6fd1011a2 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 2 Oct 2024 11:51:24 +0200 Subject: [PATCH 034/202] refactor mqtt session --- src/lavinmq/exchange/exchange.cr | 6 ------ src/lavinmq/exchange/mqtt.cr | 9 +++++++++ src/lavinmq/mqtt/broker.cr | 24 +++++++++++++++++++++--- src/lavinmq/mqtt/client.cr | 31 +++++++++++++++++++++---------- src/lavinmq/mqtt/session.cr | 14 +++++++++++++- 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/lavinmq/exchange/exchange.cr b/src/lavinmq/exchange/exchange.cr index 4adc9007e4..0a271b0395 100644 --- a/src/lavinmq/exchange/exchange.cr +++ b/src/lavinmq/exchange/exchange.cr @@ -174,12 +174,6 @@ module LavinMQ return 0 if queues.empty? return 0 if immediate && !queues.any? &.immediate_delivery? - # TODO: For each matching binding, get the QoS and write to message header "qos" - # Should be done in the MQTTExchange not in this super class - # if a = arguments[msg.routing_key] - # msg.properties.header["qos"] = q.qos - # end - count = 0 queues.each do |queue| if queue.publish(msg) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 2709b24df3..ea725cd2a7 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -20,6 +20,15 @@ module LavinMQ end end + + # TODO: For each matching binding, get the QoS and write to message header "qos" + # Should be done in the MQTTExchange not in this super class + # if a = arguments[msg.routing_key] + # msg.properties.header["qos"] = q.qos + # end + # use delivery_mode on properties instead, always set to 1 or 0 + + def bind(destination : Destination, topic_filter : String, arguments = nil) : Bool # binding = Binding.new(topic_filter, arguments["x-mqtt-qos"]) binding_key = BindingKey.new(topic_filter, arguments) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index ea78904188..b219167425 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -48,13 +48,31 @@ module LavinMQ Log.trace { "Found previous client connected with client_id: #{packet.client_id}, closing" } prev_client.close end + client = MQTT::Client.new(socket, connection_info, user, vhost, self, packet.client_id, packet.clean_session?, packet.will) + if session = @sessions[client.client_id]? + session.client = client + end @clients[packet.client_id] = client client end + def disconnect_client(client_id) + if session = @sessions[client_id]? + if session.clean_session? + sessions.delete(client_id) + else + session.client = nil + end + end + @clients.delete client_id + end + def subscribe(client, packet) - session = @sessions.declare(client.client_id, client.@clean_session) + unless session = @sessions[client.client_id]? + session = sessions.declare(client.client_id, client.@clean_session) + session.client = client + end qos = Array(MQTT::SubAck::ReturnCode).new(packet.topic_filters.size) packet.topic_filters.each do |tf| qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) @@ -65,7 +83,7 @@ module LavinMQ end def unsubscribe(client, packet) - session = @sessions[client.client_id] + session = sessions[client.client_id] packet.topics.each do |tf| rk = topicfilter_to_routingkey(tf) session.unsubscribe(rk) @@ -77,7 +95,7 @@ module LavinMQ end def clear_session(client_id) - @sessions.delete client_id + sessions.delete client_id end end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 046dde152f..0fb0383390 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -59,14 +59,15 @@ module LavinMQ publish_will if @will raise ex ensure - @broker.clear_session(client_id) if @clean_session + @broker.disconnect_client(client_id) + @socket.close @broker.vhost.rm_connection(self) end def read_and_handle_packet packet : MQTT::Packet = MQTT::Packet.from_io(@io) - @log.info { "recv #{packet.inspect}" } + @log.info { "RECIEVED PACKET: #{packet.inspect}" } @recv_oct_count += packet.bytesize case packet @@ -76,16 +77,22 @@ module LavinMQ when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) when MQTT::Disconnect then return packet + else raise "invalid packet type for client to send" end packet end def send(packet) + pp "SEND PACKET: #{packet.inspect}" @lock.synchronize do + pp 1 packet.to_io(@io) + pp 2 @socket.flush + pp 3 end + pp 4 @send_oct_count += packet.bytesize end @@ -95,10 +102,10 @@ module LavinMQ def recieve_publish(packet : MQTT::Publish) rk = @broker.topicfilter_to_routingkey(packet.topic) - headers = AMQ::Protocol::Table.new( - qos: packet.qos, - packet_id: packet_id - ) + headers = AMQ::Protocol::Table.new({ + "qos": packet.qos, + "packet_id": packet.packet_id + }) props = AMQ::Protocol::Properties.new( headers: headers ) @@ -114,13 +121,12 @@ module LavinMQ end def recieve_puback(packet) + end def recieve_subscribe(packet : MQTT::Subscribe) qos = @broker.subscribe(self, packet) session = @broker.sessions[@client_id] - consumer = MqttConsumer.new(self, session) - session.add_consumer(consumer) send(MQTT::SubAck.new(qos, packet.packet_id)) end @@ -184,7 +190,7 @@ module LavinMQ end rescue LavinMQ::Queue::ClosedError rescue ex - puts "deliver loop exiting: #{ex.inspect}" + puts "deliver loop exiting: #{ex.inspect_with_backtrace}" end def details_tuple @@ -205,11 +211,16 @@ module LavinMQ end def deliver(msg, sp, redelivered = false, recover = false) + pp "Deliver MSG: #{msg.inspect}" + packet_id = nil if message_id = msg.properties.message_id packet_id = message_id.to_u16 unless message_id.empty? end - qos = 0u8 # msg.properties.qos + + qos = msg.properties.delivery_mode + qos = 0u8 + # qos = 1u8 pub_args = { packet_id: packet_id, payload: msg.body, diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 885af97df8..83e4c04b34 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -15,11 +15,23 @@ module LavinMQ @auto_delete end + def client=(client : MQTT::Client?) + return if @closed + @last_get_time = RoughTime.monotonic + @consumers_lock.synchronize do + consumers.each &.close + @consumers.clear + if c = client + @consumers << MqttConsumer.new(c, self) + end + end + @log.debug { "Setting MQTT client" } + end + def durable? !clean_session? end - # TODO: "amq.tocpic" is hardcoded, should be the mqtt-exchange when that is finished def subscribe(rk, qos) arguments = AMQP::Table.new({"x-mqtt-qos": qos}) if binding = find_binding(rk) From da575267dc07b926f751f05e50422d9da89597b6 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 2 Oct 2024 11:51:51 +0200 Subject: [PATCH 035/202] add get method to prepare for qos 1 --- src/lavinmq/mqtt/session.cr | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 83e4c04b34..140f29e16c 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -54,6 +54,41 @@ module LavinMQ private def unbind(rk, arguments) @vhost.unbind_queue(@name, "amq.topic", rk, arguments || AMQP::Table.new) end + + private def get(no_ack : Bool, & : Envelope -> Nil) : Bool + raise ClosedError.new if @closed + loop do # retry if msg expired or deliver limit hit + env = @msg_store_lock.synchronize { @msg_store.shift? } || break + + sp = env.segment_position + no_ack = env.message.properties.delivery_mode == 0 + if no_ack + begin + yield env # deliver the message + rescue ex # requeue failed delivery + @msg_store_lock.synchronize { @msg_store.requeue(sp) } + raise ex + end + delete_message(sp) + else + mark_unacked(sp) do + yield env # deliver the message + end + end + return true + end + false + rescue ex : MessageStore::Error + @log.error(ex) { "Queue closed due to error" } + close + raise ClosedError.new(cause: ex) + end + + private def message_expire_loop + end + + private def queue_expire_loop + end end end end From 6535f158b96be6231f928e1a3bb5a309ef1c396e Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 3 Oct 2024 13:09:38 +0200 Subject: [PATCH 036/202] feel like this got ugly, but subscribe specs are now passing --- src/lavinmq/exchange/mqtt.cr | 43 ++++++++++++++++++++++++++++-------- src/lavinmq/mqtt/client.cr | 19 ++++++---------- src/lavinmq/mqtt/session.cr | 12 +++++++++- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index ea725cd2a7..cbcb4249f8 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -20,14 +20,40 @@ module LavinMQ end end + def publish(msg : Message, immediate : Bool, + queues : Set(Queue) = Set(Queue).new, + exchanges : Set(Exchange) = Set(Exchange).new) : Int32 + @publish_in_count += 1 + headers = msg.properties.headers + find_queues(msg.routing_key, headers, queues, exchanges) + if queues.empty? + @unroutable_count += 1 + return 0 + end + return 0 if immediate && !queues.any? &.immediate_delivery? - # TODO: For each matching binding, get the QoS and write to message header "qos" - # Should be done in the MQTTExchange not in this super class - # if a = arguments[msg.routing_key] - # msg.properties.header["qos"] = q.qos - # end - # use delivery_mode on properties instead, always set to 1 or 0 + count = 0 + queues.each do |queue| + qos = 0_u8 + @bindings.each do |binding_key, destinations| + if binding_key.routing_key == msg.routing_key + if arg = binding_key.arguments + if qos_value = arg["x-mqtt-qos"]? + qos = qos_value.try &.as(UInt8) + end + end + end + end + msg.properties.delivery_mode = qos + if queue.publish(msg) + @publish_out_count += 1 + count += 1 + msg.body_io.seek(-msg.bodysize.to_i64, IO::Seek::Current) # rewind + end + end + count + end def bind(destination : Destination, topic_filter : String, arguments = nil) : Bool # binding = Binding.new(topic_filter, arguments["x-mqtt-qos"]) @@ -39,6 +65,7 @@ module LavinMQ end def unbind(destination : Destination, routing_key, headers = nil) : Bool + pp "GETS HERE 3.1" binding_key = BindingKey.new(routing_key, arguments) rk_bindings = @bindings[binding_key] return false unless rk_bindings.delete destination @@ -62,11 +89,9 @@ module LavinMQ private def matches(binding_key : BindingKey) : Iterator(Destination) @bindings.each.select do |binding, destinations| - msg_qos = binding_key.arguments.try { |a| a["qos"]?.try(&.as(UInt8)) } || 0 - binding_qos = binding.arguments.try { |a| a["x-mqtt-pos"]?.try(&.as(UInt8)) } || 0 # Use Jons tree finder.. - binding.routing_key == binding_key.routing_key && msg_qos >= binding_qos + binding.routing_key == binding_key.routing_key end.flat_map { |_, v| v.each } end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 0fb0383390..7e2e87a6be 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -84,15 +84,10 @@ module LavinMQ end def send(packet) - pp "SEND PACKET: #{packet.inspect}" @lock.synchronize do - pp 1 packet.to_io(@io) - pp 2 @socket.flush - pp 3 end - pp 4 @send_oct_count += packet.bytesize end @@ -213,14 +208,14 @@ module LavinMQ def deliver(msg, sp, redelivered = false, recover = false) pp "Deliver MSG: #{msg.inspect}" - packet_id = nil - if message_id = msg.properties.message_id - packet_id = message_id.to_u16 unless message_id.empty? - end + # packet_id = nil + # if message_id = msg.properties.message_id + # packet_id = message_id.to_u16 unless message_id.empty? + # end + + packet_id = 3u16 - qos = msg.properties.delivery_mode - qos = 0u8 - # qos = 1u8 + qos = msg.properties.delivery_mode || 0u8 pub_args = { packet_id: packet_id, payload: msg.body, diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 140f29e16c..dbdcc8e4c1 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -52,10 +52,11 @@ module LavinMQ end private def unbind(rk, arguments) - @vhost.unbind_queue(@name, "amq.topic", rk, arguments || AMQP::Table.new) + @vhost.unbind_queue(@name, "mqtt.default", rk, arguments || AMQP::Table.new) end private def get(no_ack : Bool, & : Envelope -> Nil) : Bool + #let packet_id be message counter, look at myra for counter raise ClosedError.new if @closed loop do # retry if msg expired or deliver limit hit env = @msg_store_lock.synchronize { @msg_store.shift? } || break @@ -71,6 +72,15 @@ module LavinMQ end delete_message(sp) else + + # packet_id = generate packet id + # save packet id to hash + # add hash to env + # + # + # + # Generate_next_id = next_id from mqtt + mark_unacked(sp) do yield env # deliver the message end From 6083866e59205855827fa2e7740a5c6adbfaf234 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 3 Oct 2024 13:57:36 +0200 Subject: [PATCH 037/202] cleanup old code and use session instead of queue --- src/lavinmq/exchange/mqtt.cr | 1 - src/lavinmq/mqtt/client.cr | 21 ++++++--------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index cbcb4249f8..5c88da97c3 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -65,7 +65,6 @@ module LavinMQ end def unbind(destination : Destination, routing_key, headers = nil) : Bool - pp "GETS HERE 3.1" binding_key = BindingKey.new(routing_key, arguments) rk_bindings = @bindings[binding_key] return false unless rk_bindings.delete destination diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 7e2e87a6be..e043ddcb55 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -67,7 +67,7 @@ module LavinMQ def read_and_handle_packet packet : MQTT::Packet = MQTT::Packet.from_io(@io) - @log.info { "RECIEVED PACKET: #{packet.inspect}" } + @log.info { "Recieved packet: #{packet.inspect}" } @recv_oct_count += packet.bytesize case packet @@ -97,16 +97,8 @@ module LavinMQ def recieve_publish(packet : MQTT::Publish) rk = @broker.topicfilter_to_routingkey(packet.topic) - headers = AMQ::Protocol::Table.new({ - "qos": packet.qos, - "packet_id": packet.packet_id - }) - props = AMQ::Protocol::Properties.new( - headers: headers - ) # TODO: String.new around payload.. should be stored as Bytes - # Send to MQTT-exchange - msg = Message.new("mqtt.default", rk, String.new(packet.payload), props) + msg = Message.new("mqtt.default", rk, String.new(packet.payload)) @broker.vhost.publish(msg) # Ok to not send anything if qos = 0 (at most once delivery) @@ -169,28 +161,27 @@ module LavinMQ getter tag : String = "mqtt" property prefetch_count = 1 - def initialize(@client : Client, @queue : Queue) + def initialize(@client : Client, @session : MQTT::Session) @has_capacity.try_send? true spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end private def deliver_loop - queue = @queue + session = @session i = 0 loop do - queue.consume_get(self) do |env| + session.consume_get(self) do |env| deliver(env.message, env.segment_position, env.redelivered) end Fiber.yield if (i &+= 1) % 32768 == 0 end - rescue LavinMQ::Queue::ClosedError rescue ex puts "deliver loop exiting: #{ex.inspect_with_backtrace}" end def details_tuple { - queue: { + session: { name: "mqtt.client_id", vhost: "mqtt", }, From afbb44a6dd91dd44f54ba53022c0971bee2dba21 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 10 Oct 2024 09:37:15 +0200 Subject: [PATCH 038/202] refctoring consumer handling, and handle msg acks better --- spec/mqtt/integrations/connect_spec.cr | 16 ++++++ spec/mqtt/integrations/unsubscribe_spec.cr | 5 ++ spec/mqtt_spec.cr | 3 +- src/lavinmq/exchange/mqtt.cr | 25 +++++---- src/lavinmq/mqtt/broker.cr | 8 +-- src/lavinmq/mqtt/client.cr | 18 +++--- src/lavinmq/mqtt/session.cr | 65 +++++++++++++++------- 7 files changed, 92 insertions(+), 48 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 262eff3393..ee2cbfd8f1 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -262,6 +262,22 @@ module MqttSpecs end end end + + it "should not publish after disconnect" do + with_server do |server| + # Create a non-clean session with an active subscription + with_client_io(server) do |io| + connect(io, clean_session: false) + topics = mk_topic_filters({"a/b", 1}) + subscribe(io, topic_filters: topics) + disconnect(io) + pp server.vhosts["/"].queues["amq.mqtt-client_id"].consumers + end + sleep 0.1 + server.vhosts["/"].queues["amq.mqtt-client_id"].consumers.should be_empty + end + end + end end end diff --git a/spec/mqtt/integrations/unsubscribe_spec.cr b/spec/mqtt/integrations/unsubscribe_spec.cr index ceb7de992c..be3e19148b 100644 --- a/spec/mqtt/integrations/unsubscribe_spec.cr +++ b/spec/mqtt/integrations/unsubscribe_spec.cr @@ -53,10 +53,13 @@ module MqttSpecs subscribe(io, topic_filters: topics) disconnect(io) end + pp "first consumers: #{server.vhosts["/"].queues["amq.mqtt-client_id"].consumers}" # Publish messages that will be stored for the subscriber 2.times { |i| publish(pubio, topic: "a/b", payload: i.to_s.to_slice, qos: 0u8) } + pp "first msg count: #{server.vhosts["/"].queues["amq.mqtt-client_id"].message_count}" + # Let the subscriber connect and read the messages, but don't ack. Then unsubscribe. # We must read the Publish packets before unsubscribe, else the "suback" will be stuck. with_client_io(server) do |io| @@ -70,6 +73,8 @@ module MqttSpecs unsubscribe(io, topics: ["a/b"]) disconnect(io) end + pp "second msg count: #{server.vhosts["/"].queues["amq.mqtt-client_id"].message_count}" + pp "unacked msgs: #{server.vhosts["/"].queues["amq.mqtt-client_id"].unacked_count}" # Publish more messages 2.times { |i| publish(pubio, topic: "a/b", payload: (2 + i).to_s.to_slice, qos: 0u8) } diff --git a/spec/mqtt_spec.cr b/spec/mqtt_spec.cr index a7e15c05ba..1c42c4213a 100644 --- a/spec/mqtt_spec.cr +++ b/spec/mqtt_spec.cr @@ -11,7 +11,8 @@ def setup_connection(s, pass) MQTT::Protocol::Connect.new("abc", false, 60u16, "usr", pass.to_slice, nil).to_io(io) connection_factory = LavinMQ::MQTT::ConnectionFactory.new( s.users, - s.vhosts["/"]) + s.vhosts["/"], + LavinMQ::MQTT::Broker.new(s.vhosts["/"])) {connection_factory.start(right, LavinMQ::ConnectionInfo.local), io} end diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 5c88da97c3..38181b10eb 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -20,6 +20,8 @@ module LavinMQ end end + + # TODO: we can probably clean this up a bit def publish(msg : Message, immediate : Bool, queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 @@ -35,14 +37,11 @@ module LavinMQ count = 0 queues.each do |queue| qos = 0_u8 - @bindings.each do |binding_key, destinations| - if binding_key.routing_key == msg.routing_key - if arg = binding_key.arguments - if qos_value = arg["x-mqtt-qos"]? - qos = qos_value.try &.as(UInt8) - end - end - end + bindings_details.each do |binding_detail| + next unless binding_detail.destination == queue + next unless arg = binding_detail.binding_key.arguments + next unless qos_value = arg["x-mqtt-qos"]? + qos = qos_value.try &.as(UInt8) end msg.properties.delivery_mode = qos @@ -55,9 +54,12 @@ module LavinMQ count end - def bind(destination : Destination, topic_filter : String, arguments = nil) : Bool + def bind(destination : Destination, routing_key : String, headers = nil) : Bool # binding = Binding.new(topic_filter, arguments["x-mqtt-qos"]) - binding_key = BindingKey.new(topic_filter, arguments) + + # TODO: build spec for this early return + raise LavinMQ::Exchange::AccessRefused.new(self) unless destination.is_a?(MQTT::Session) + binding_key = BindingKey.new(routing_key, headers) return false unless @bindings[binding_key].add? destination data = BindingDetails.new(name, vhost.name, binding_key, destination) notify_observers(ExchangeEvent::Bind, data) @@ -65,11 +67,10 @@ module LavinMQ end def unbind(destination : Destination, routing_key, headers = nil) : Bool - binding_key = BindingKey.new(routing_key, arguments) + binding_key = BindingKey.new(routing_key, headers) rk_bindings = @bindings[binding_key] return false unless rk_bindings.delete destination @bindings.delete binding_key if rk_bindings.empty? - data = BindingDetails.new(name, vhost.name, binding_key, destination) notify_observers(ExchangeEvent::Unbind, data) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index b219167425..52f20f8fea 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -59,12 +59,10 @@ module LavinMQ def disconnect_client(client_id) if session = @sessions[client_id]? - if session.clean_session? - sessions.delete(client_id) - else - session.client = nil - end + session.client = nil + sessions.delete(client_id) if session.clean_session? end + @clients.delete client_id end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index e043ddcb55..17db0fc750 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -62,6 +62,7 @@ module LavinMQ @broker.disconnect_client(client_id) @socket.close + #move to disconnect client @broker.vhost.rm_connection(self) end @@ -120,9 +121,6 @@ module LavinMQ def recieve_unsubscribe(packet) session = @broker.sessions[@client_id] @broker.unsubscribe(self, packet) - if consumer = session.consumers.find { |c| c.tag == "mqtt" } - session.rm_consumer(consumer) - end send(MQTT::UnsubAck.new(packet.packet_id)) end @@ -169,6 +167,7 @@ module LavinMQ private def deliver_loop session = @session i = 0 + # move deliver loop to session in order to control flow based on the consumer loop do session.consume_get(self) do |env| deliver(env.message, env.segment_position, env.redelivered) @@ -197,14 +196,11 @@ module LavinMQ end def deliver(msg, sp, redelivered = false, recover = false) - pp "Deliver MSG: #{msg.inspect}" - - # packet_id = nil - # if message_id = msg.properties.message_id - # packet_id = message_id.to_u16 unless message_id.empty? - # end - - packet_id = 3u16 + pp "Delivering message: #{msg.inspect}" + packet_id = nil + if message_id = msg.properties.message_id + packet_id = message_id.to_u16 unless message_id.empty? + end qos = msg.properties.delivery_mode || 0u8 pub_args = { diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index dbdcc8e4c1..255ca9932d 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -8,6 +8,8 @@ module LavinMQ @name : String, @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) + @count = 0u16 + @unacked = Deque(SegmentPosition).new super(@vhost, @name, false, @auto_delete, arguments) end @@ -17,13 +19,21 @@ module LavinMQ def client=(client : MQTT::Client?) return if @closed - @last_get_time = RoughTime.monotonic - @consumers_lock.synchronize do - consumers.each &.close - @consumers.clear - if c = client - @consumers << MqttConsumer.new(c, self) - end + @last_get_time = RoughTime.monotonic + consumers.each do |c| + c.close + rm_consumer c + end + + @msg_store_lock.synchronize do + @unacked.each do |sp| + @msg_store.requeue(sp) + end + end + @unacked.clear + + if c = client + @consumers << MqttConsumer.new(c, self) end @log.debug { "Setting MQTT client" } end @@ -63,7 +73,7 @@ module LavinMQ sp = env.segment_position no_ack = env.message.properties.delivery_mode == 0 - if no_ack + if false begin yield env # deliver the message rescue ex # requeue failed delivery @@ -72,17 +82,10 @@ module LavinMQ end delete_message(sp) else - - # packet_id = generate packet id - # save packet id to hash - # add hash to env - # - # - # - # Generate_next_id = next_id from mqtt - + env.message.properties.message_id = next_id.to_s mark_unacked(sp) do yield env # deliver the message + @unacked << sp end end return true @@ -94,10 +97,34 @@ module LavinMQ raise ClosedError.new(cause: ex) end - private def message_expire_loop + def ack(sp : SegmentPosition) : Nil + # TDO: maybe risky to not have locka round this + @unacked.delete sp + super sp end - private def queue_expire_loop + private def message_expire_loop; end + + private def queue_expire_loop; end + + private def next_id : UInt16? + @count += 1u16 + + # return nil if @unacked.size == @max_inflight + # start_id = @packet_id + # next_id : UInt16 = start_id + 1 + # while @unacked.has_key?(next_id) + # if next_id == 65_535 + # next_id = 1 + # else + # next_id += 1 + # end + # if next_id == start_id + # return nil + # end + # end + # @packet_id = next_id + # next_id end end end From b484d5db4115fc2bb4750736dfa494552e24740a Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 10 Oct 2024 09:52:59 +0200 Subject: [PATCH 039/202] move consumers deliver_loop to session, specs do not pass --- src/lavinmq/mqtt/client.cr | 15 --------------- src/lavinmq/mqtt/session.cr | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 17db0fc750..38fe5fa124 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -161,21 +161,6 @@ module LavinMQ def initialize(@client : Client, @session : MQTT::Session) @has_capacity.try_send? true - spawn deliver_loop, name: "Consumer deliver loop", same_thread: true - end - - private def deliver_loop - session = @session - i = 0 - # move deliver loop to session in order to control flow based on the consumer - loop do - session.consume_get(self) do |env| - deliver(env.message, env.segment_position, env.redelivered) - end - Fiber.yield if (i &+= 1) % 32768 == 0 - end - rescue ex - puts "deliver loop exiting: #{ex.inspect_with_backtrace}" end def details_tuple diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 255ca9932d..8a56c63cba 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -11,12 +11,26 @@ module LavinMQ @count = 0u16 @unacked = Deque(SegmentPosition).new super(@vhost, @name, false, @auto_delete, arguments) + spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end def clean_session? @auto_delete end + private def deliver_loop + i = 0 + loop do + break if consumers.empty? + consume_get(consumers.first) do |env| + consumers.first.deliver(env.message, env.segment_position, env.redelivered) + end + Fiber.yield if (i &+= 1) % 32768 == 0 + end + rescue ex + puts "deliver loop exiting: #{ex.inspect_with_backtrace}" + end + def client=(client : MQTT::Client?) return if @closed @last_get_time = RoughTime.monotonic @@ -34,6 +48,7 @@ module LavinMQ if c = client @consumers << MqttConsumer.new(c, self) + spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end @log.debug { "Setting MQTT client" } end From 20e606151ac18e748db1cc2262837c3edd05fc22 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 10 Oct 2024 11:41:45 +0200 Subject: [PATCH 040/202] cleanup --- spec/mqtt/integrations/unsubscribe_spec.cr | 7 ++----- src/lavinmq/mqtt/client.cr | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/mqtt/integrations/unsubscribe_spec.cr b/spec/mqtt/integrations/unsubscribe_spec.cr index be3e19148b..13dd813fca 100644 --- a/spec/mqtt/integrations/unsubscribe_spec.cr +++ b/spec/mqtt/integrations/unsubscribe_spec.cr @@ -53,13 +53,11 @@ module MqttSpecs subscribe(io, topic_filters: topics) disconnect(io) end - pp "first consumers: #{server.vhosts["/"].queues["amq.mqtt-client_id"].consumers}" + sleep 1 # Publish messages that will be stored for the subscriber 2.times { |i| publish(pubio, topic: "a/b", payload: i.to_s.to_slice, qos: 0u8) } - pp "first msg count: #{server.vhosts["/"].queues["amq.mqtt-client_id"].message_count}" - # Let the subscriber connect and read the messages, but don't ack. Then unsubscribe. # We must read the Publish packets before unsubscribe, else the "suback" will be stuck. with_client_io(server) do |io| @@ -73,8 +71,7 @@ module MqttSpecs unsubscribe(io, topics: ["a/b"]) disconnect(io) end - pp "second msg count: #{server.vhosts["/"].queues["amq.mqtt-client_id"].message_count}" - pp "unacked msgs: #{server.vhosts["/"].queues["amq.mqtt-client_id"].unacked_count}" + sleep 1 # Publish more messages 2.times { |i| publish(pubio, topic: "a/b", payload: (2 + i).to_s.to_slice, qos: 0u8) } diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 38fe5fa124..1ad0a37731 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -181,7 +181,6 @@ module LavinMQ end def deliver(msg, sp, redelivered = false, recover = false) - pp "Delivering message: #{msg.inspect}" packet_id = nil if message_id = msg.properties.message_id packet_id = message_id.to_u16 unless message_id.empty? From 3f775f3cb69af95002caa22e4d1ab4a372b0dec8 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 10 Oct 2024 12:50:40 +0200 Subject: [PATCH 041/202] SubscriptionTree --- spec/message_routing_spec.cr | 14 ++ spec/mqtt/string_token_iterator.cr | 33 ++++ spec/mqtt/subscription_tree_spec.cr | 195 ++++++++++++++++++++++ src/lavinmq/exchange/mqtt.cr | 26 +-- src/lavinmq/http/controller/queues.cr | 22 +-- src/lavinmq/mqtt/string_token_iterator.cr | 50 ++++++ src/lavinmq/mqtt/subscription_tree.cr | 144 ++++++++++++++++ 7 files changed, 463 insertions(+), 21 deletions(-) create mode 100644 spec/mqtt/string_token_iterator.cr create mode 100644 spec/mqtt/subscription_tree_spec.cr create mode 100644 src/lavinmq/mqtt/string_token_iterator.cr create mode 100644 src/lavinmq/mqtt/subscription_tree.cr diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index da3ab9bdbd..ad0c999cde 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -421,3 +421,17 @@ describe LavinMQ::Exchange do end end end +describe LavinMQ::MQTTExchange do + it "should only allow Session to bind" do + with_amqp_server do |s| + vhost = s.vhosts.create("x") + q1 = LavinMQ::Queue.new(vhost, "q1") + s1 = LavinMQ::MQTT::Session.new(vhost, "q1") + x = LavinMQ::MQTTExchange.new(vhost, "") + x.bind(s1, "s1", LavinMQ::AMQP::Table.new) + expect_raises(LavinMQ::Exchange::AccessRefused) do + x.bind(q1, "q1", LavinMQ::AMQP::Table.new) + end + end + end +end diff --git a/spec/mqtt/string_token_iterator.cr b/spec/mqtt/string_token_iterator.cr new file mode 100644 index 0000000000..29152555da --- /dev/null +++ b/spec/mqtt/string_token_iterator.cr @@ -0,0 +1,33 @@ +require "./spec_helper" +require "../src/myramq/string_token_iterator" + +def strings + [ + # { input, expected } + {"a", ["a"]}, + {"/", ["", ""]}, + {"a/", ["a", ""]}, + {"/a", ["", "a"]}, + {"a/b/c", ["a", "b", "c"]}, + {"a//c", ["a", "", "c"]}, + {"a//b/c/aa", ["a", "", "b", "c", "aa"]}, + {"long name here/and another long here", + ["long name here", "and another long here"]}, + ] +end + +Spectator.describe MyraMQ::SubscriptionTree do + sample strings do |testdata| + it "is iterated correct" do + itr = MyraMQ::StringTokenIterator.new(testdata[0], '/') + res = Array(String).new + while itr.next? + val = itr.next + expect(val).to_not be_nil + res << val.not_nil! + end + expect(itr.next?).to be_false + expect(res).to eq testdata[1] + end + end +end diff --git a/spec/mqtt/subscription_tree_spec.cr b/spec/mqtt/subscription_tree_spec.cr new file mode 100644 index 0000000000..256da472fd --- /dev/null +++ b/spec/mqtt/subscription_tree_spec.cr @@ -0,0 +1,195 @@ +require "./spec_helper" +require "../../src/lavinmq/mqtt/subscription_tree" +# require "../src/myramq/broker" +# require "../src/myramq/session" + +describe LavinMQ::MQTT::SubscriptionTree do + tree = LavinMQ::MQTT::SubscriptionTree.new + + describe "#any?" do + it "returns false for empty tree" do + tree.any?("a").should be_false + end + + describe "with subs" do + before_each do + test_data = [ + "a/b", + "a/+/b", + "a/b/c/d/#", + "a/+/c/d/#", + ] + session = LavinMQ::MQTT::Session.new + + test_data.each do |topic| + tree.subscribe(topic, session, 0u8) + end + end + + it "returns false for no matching subscriptions" do + tree.any?("a").should be_false + end + + it "returns true for matching non-wildcard subs" do + tree.any?("a/b").should be_true + end + + it "returns true for matching '+'-wildcard subs" do + tree.any?("a/r/b").should be_true + end + + it "returns true for matching '#'-wildcard subs" do + tree.any?("a/b/c/d/e/f").should be_true + end + end + end + + describe "#empty?" do + it "returns true before any subscribe" do + tree.empty?.should be_true + end + + it "returns false after a non-wildcard subscribe" do + session = mock(MQTT::Session) + tree.subscribe("topic", session, 0u8) + tree.empty?.should be_false + end + + it "returns false after a +-wildcard subscribe" do + session = mock(MQTT::Session) + tree.subscribe("a/+/topic", session, 0u8) + tree.empty?.should be_false + end + + it "returns false after a #-wildcard subscribe" do + session = mock(MQTT::Session) + tree.subscribe("a/#/topic", session, 0u8) + tree.empty?.should be_false + end + + it "returns true after unsubscribing only existing non-wildcard subscription" do + session = mock(MQTT::Session) + tree.subscribe("topic", session, 0u8) + tree.unsubscribe("topic", session) + tree.empty?.should be_true + end + + it "returns true after unsubscribing only existing +-wildcard subscription" do + session = mock(MQTT::Session) + tree.subscribe("a/+/topic", session, 0u8) + tree.unsubscribe("a/+/topic", session) + tree.empty?.should be_true + end + + it "returns true after unsubscribing only existing #+-wildcard subscription" do + session = mock(MQTT::Session) + tree.subscribe("a/b/#", session, 0u8) + tree.unsubscribe("a/b/#", session) + tree.empty?.should be_true + end + + it "returns true after unsubscribing many different subscriptions" do + test_data = [ + {mock(MQTT::Session), "a/b"}, + {mock(MQTT::Session), "a/+/b"}, + {mock(MQTT::Session), "a/b/c/d#"}, + {mock(MQTT::Session), "a/+/c/d/#"}, + {mock(MQTT::Session), "#"}, + ] + + test_data.each do |session, topic| + tree.subscribe(topic, session, 0u8) + end + + test_data.shuffle.each do |session, topic| + tree.unsubscribe(topic, session) + end + + tree.empty?.should be_true + end + end + + it "subscriptions is found" do + test_data = [ + {mock(MQTT::Session), [{"a/b", 0u8}]}, + {mock(MQTT::Session), [{"a/b", 0u8}]}, + {mock(MQTT::Session), [{"a/c", 0u8}]}, + {mock(MQTT::Session), [{"a/+", 0u8}]}, + {mock(MQTT::Session), [{"#", 0u8}]}, + ] + + test_data.each do |s| + session, subscriptions = s + subscriptions.each do |tq| + t, q = tq + tree.subscribe(t, session, q) + end + end + + calls = 0 + tree.each_entry "a/b" do |_session, qos| + qos.should eq 0u8 + calls += 1 + end + calls.should eq 4 + end + + it "unsubscribe unsubscribes" do + test_data = [ + {mock(MQTT::Session), [{"a/b", 0u8}]}, + {mock(MQTT::Session), [{"a/b", 0u8}]}, + {mock(MQTT::Session), [{"a/c", 0u8}]}, + {mock(MQTT::Session), [{"a/+", 0u8}]}, + {mock(MQTT::Session), [{"#", 0u8}]}, + ] + + test_data.each do |session, subscriptions| + subscriptions.each do |topic, qos| + tree.subscribe(topic, session, qos) + end + end + + test_data[1, 3].each do |session, subscriptions| + subscriptions.each do |topic, _qos| + tree.unsubscribe(topic, session) + end + end + calls = 0 + tree.each_entry "a/b" do |_session, _qos| + calls += 1 + end + calls.should eq 2 + end + + it "changes qos level" do + session = mock(MQTT::Session) + tree.subscribe("a/b", session, 0u8) + tree.each_entry "a/b" { |_sess, qos| qos.should eq 0u8 } + tree.subscribe("a/b", session, 1u8) + tree.each_entry "a/b" { |_sess, qos| qos.should eq 1u8 } + end + + it "can iterate all entries" do + test_data = [ + {mock(MQTT::Session), [{"a/b", 0u8}]}, + {mock(MQTT::Session), [{"a/b/c/d/e", 0u8}]}, + {mock(MQTT::Session), [{"+/c", 0u8}]}, + {mock(MQTT::Session), [{"a/+", 0u8}]}, + {mock(MQTT::Session), [{"#", 0u8}]}, + {mock(MQTT::Session), [{"a/b/#", 0u8}]}, + {mock(MQTT::Session), [{"a/+/c", 0u8}]}, + ] + + test_data.each do |session, subscriptions| + subscriptions.each do |topic, qos| + tree.subscribe(topic, session, qos) + end + end + + calls = 0 + tree.each_entry do |_session, _qos| + calls += 1 + end + calls.should eq 7 + end +end diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 38181b10eb..71e05bc534 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -1,12 +1,12 @@ require "./exchange" +require "../mqtt/subscription_tree" module LavinMQ class MQTTExchange < Exchange - # record Binding, topic_filter : String, qos : UInt8 - @bindings = Hash(BindingKey, Set(Destination)).new do |h, k| h[k] = Set(Destination).new end + @tree = MQTT::SubscriptionTree.new def type : String "mqtt" @@ -20,11 +20,10 @@ module LavinMQ end end - # TODO: we can probably clean this up a bit def publish(msg : Message, immediate : Bool, - queues : Set(Queue) = Set(Queue).new, - exchanges : Set(Exchange) = Set(Exchange).new) : Int32 + queues : Set(Queue) = Set(Queue).new, + exchanges : Set(Exchange) = Set(Exchange).new) : Int32 @publish_in_count += 1 headers = msg.properties.headers find_queues(msg.routing_key, headers, queues, exchanges) @@ -55,22 +54,28 @@ module LavinMQ end def bind(destination : Destination, routing_key : String, headers = nil) : Bool - # binding = Binding.new(topic_filter, arguments["x-mqtt-qos"]) - - # TODO: build spec for this early return raise LavinMQ::Exchange::AccessRefused.new(self) unless destination.is_a?(MQTT::Session) + binding_key = BindingKey.new(routing_key, headers) return false unless @bindings[binding_key].add? destination + + qos = headers.try { |h| h.fetch("x-mqtt-qos", "0").as(UInt8) } + @tree.subscribe(routing_key, destination, qos) + data = BindingDetails.new(name, vhost.name, binding_key, destination) notify_observers(ExchangeEvent::Bind, data) true end def unbind(destination : Destination, routing_key, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) unless destination.is_a?(MQTT::Session) binding_key = BindingKey.new(routing_key, headers) rk_bindings = @bindings[binding_key] return false unless rk_bindings.delete destination @bindings.delete binding_key if rk_bindings.empty? + + @tree.unsubscribe(routing_key, destination) + data = BindingDetails.new(name, vhost.name, binding_key, destination) notify_observers(ExchangeEvent::Unbind, data) @@ -88,9 +93,10 @@ module LavinMQ end private def matches(binding_key : BindingKey) : Iterator(Destination) - @bindings.each.select do |binding, destinations| + @tree.each_entry(binding_key.routing_key) do |session, qos| + end - # Use Jons tree finder.. + @bindings.each.select do |binding, destinations| binding.routing_key == binding_key.routing_key end.flat_map { |_, v| v.each } end diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index 7c7f054b18..51c8351158 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -45,17 +45,17 @@ module LavinMQ get "/api/queues/:vhost/:name/unacked" do |context, params| with_vhost(context, params) do |vhost| refuse_unless_management(context, user(context), vhost) - q = queue(context, params, vhost) - unacked_messages = q.consumers.each.flat_map do |c| - c.unacked_messages.each.compact_map do |u| - next unless u.queue == q - if consumer = u.consumer - UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) - end - end - end - unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) - page(context, unacked_messages) + # q = queue(context, params, vhost) + # unacked_messages = q.consumers.each.flat_map do |c| + # c.unacked_messages.each.compact_map do |u| + # next unless u.queue == q + # if consumer = u.consumer + # UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) + # end + # end + # end + # unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) + # page(context, unacked_messages) end end diff --git a/src/lavinmq/mqtt/string_token_iterator.cr b/src/lavinmq/mqtt/string_token_iterator.cr new file mode 100644 index 0000000000..c62d39eb95 --- /dev/null +++ b/src/lavinmq/mqtt/string_token_iterator.cr @@ -0,0 +1,50 @@ +# +# str = "my/example/string" +# it = StringTokenIterator.new(str, '/') +# while substr = it.next +# puts substr +# end +# outputs: +# my +# example +# string +# +# Note that "empty" parts will also be returned +# str = "/" will result in two "" +# str "a//b" will result in "a", "" and "b" +# +module LavinMQ + module MQTT + struct StringTokenIterator + def initialize(@str : String, @delimiter : Char = '/') + @reader = Char::Reader.new(@str) + @iteration = 0 + end + + def next : String? + return if @reader.pos >= @str.size + # This is to make sure we return an empty string first iteration if @str starts with @delimiter + @reader.next_char unless @iteration.zero? + @iteration += 1 + head = @reader.pos + while @reader.has_next? && @reader.current_char != @delimiter + @reader.next_char + end + tail = @reader.pos + @str[head, tail - head] + end + + def next? + @reader.pos < @str.size + end + + def to_s + @str + end + + def inspect + "#{self.class.name}(@str=#{@str} @reader.pos=#{@reader.pos} @reader.current_char=#{@reader.current_char} @iteration=#{@iteration})" + end + end + end +end diff --git a/src/lavinmq/mqtt/subscription_tree.cr b/src/lavinmq/mqtt/subscription_tree.cr new file mode 100644 index 0000000000..88955aba3b --- /dev/null +++ b/src/lavinmq/mqtt/subscription_tree.cr @@ -0,0 +1,144 @@ +require "./session" +require "./string_token_iterator" + +module LavinMQ + module MQTT + class SubscriptionTree + @wildcard_rest = Hash(Session, UInt8).new + @plus : SubscriptionTree? + @leafs = Hash(Session, UInt8).new + # Non wildcards may be an unnecessary "optimization". We store all subscriptions without + # wildcard in the first level. No need to make a tree out of them. + @non_wildcards = Hash(String, Hash(Session, UInt8)).new do |h, k| + h[k] = Hash(Session, UInt8).new + h[k].compare_by_identity + h[k] + end + @sublevels = Hash(String, SubscriptionTree).new + + def initialize + @wildcard_rest.compare_by_identity + @leafs.compare_by_identity + end + + def subscribe(filter : String, session : Session, qos : UInt8) + if filter.index('#').nil? && filter.index('+').nil? + @non_wildcards[filter][session] = qos + return + end + subscribe(StringTokenIterator.new(filter), session, qos) + end + + protected def subscribe(filter : StringTokenIterator, session : Session, qos : UInt8) + unless current = filter.next + @leafs[session] = qos + return + end + if current == "#" + @wildcard_rest[session] = qos + return + end + if current == "+" + plus = (@plus ||= SubscriptionTree.new) + plus.subscribe filter, session, qos + return + end + if !(sublevels = @sublevels[current]?) + sublevels = @sublevels[current] = SubscriptionTree.new + end + sublevels.subscribe filter, session, qos + return + end + + def unsubscribe(filter : String, session : Session) + if subs = @non_wildcards[filter]? + return unless subs.delete(session).nil? + end + unsubscribe(StringTokenIterator.new(filter), session) + end + + protected def unsubscribe(filter : StringTokenIterator, session : Session) + unless current = filter.next + @leafs.delete session + return + end + if current == "#" + @wildcard_rest.delete session + end + if (plus = @plus) && current == "+" + plus.unsubscribe filter, session + end + if sublevel = @sublevels[current]? + sublevel.unsubscribe filter, session + if sublevel.empty? + @sublevels.delete current + end + end + end + + # Returns wether any subscription matches the given filter + def any?(filter : String) : Bool + if subs = @non_wildcards[filter]? + return !subs.empty? + end + any?(StringTokenIterator.new(filter)) + end + + protected def any?(filter : StringTokenIterator) + return !@leafs.empty? unless current = filter.next + return true if !@wildcard_rest.empty? + return true if @plus.try &.any?(filter) + return true if @sublevels[current]?.try &.any?(filter) + false + end + + def empty? + return false unless @non_wildcards.empty? || @non_wildcards.values.all? &.empty? + return false unless @leafs.empty? + return false unless @wildcard_rest.empty? + if plus = @plus + return false unless plus.empty? + end + if sublevels = @sublevels + return false unless sublevels.empty? + end + true + end + + def each_entry(topic : String, &block : (Session, UInt8) -> _) + if subs = @non_wildcards[topic]? + subs.each &block + end + each_entry(StringTokenIterator.new(topic), &block) + end + + protected def each_entry(topic : StringTokenIterator, &block : (Session, UInt8) -> _) + unless current = topic.next + @leafs.each &block + return + end + @wildcard_rest.each &block + @plus.try &.each_entry topic, &block + if sublevel = @sublevels.fetch(current, nil) + sublevel.each_entry topic, &block + end + end + + def each_entry(&block : (Session, UInt8) -> _) + @non_wildcards.each do |_, entries| + entries.each &block + end + @leafs.each &block + @wildcard_rest.each &block + @plus.try &.each_entry &block + @sublevels.each do |_, sublevel| + sublevel.each_entry &block + end + end + + def inspect + "#{self.class.name}(@wildcard_rest=#{@wildcard_rest.inspect}, @non_wildcards=#{@non_wildcards.inspect}, @plus=#{@plus.inspect}, @sublevels=#{@sublevels.inspect}, @leafs=#{@leafs.inspect})" + end + end + end +end From bc9327f8bf962bc895e83176e62cd3352d630472 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 14 Oct 2024 10:44:07 +0200 Subject: [PATCH 042/202] rebase main --- spec/message_routing_spec.cr | 12 +++ spec/mqtt/integrations/connect_spec.cr | 6 +- spec/mqtt/integrations/message_qos_spec.cr | 12 ++- spec/mqtt/integrations/publish_spec.cr | 26 ------ src/lavinmq/exchange/exchange.cr | 1 + src/lavinmq/exchange/mqtt.cr | 97 +++++++++------------- src/lavinmq/mqtt/session.cr | 7 +- 7 files changed, 68 insertions(+), 93 deletions(-) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index ad0c999cde..5ec71b3157 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -434,4 +434,16 @@ describe LavinMQ::MQTTExchange do end end end + + it "publish messages to queues with it's own publish method" do + with_amqp_server do |s| + vhost = s.vhosts.create("x") + s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") + x = LavinMQ::MQTTExchange.new(vhost, "mqtt.default") + x.bind(s1, "s1", LavinMQ::AMQP::Table.new) + msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") + x.publish(msg, false) + s1.message_count.should eq 1 + end + end end diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index ee2cbfd8f1..eccbac53db 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -31,6 +31,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) + sleep 0.1 end with_client_io(server) do |io| connack = connect(io, clean_session: true) @@ -50,6 +51,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) + sleep 0.1 end with_client_io(server) do |io| connack = connect(io, clean_session: false) @@ -69,6 +71,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) + sleep 0.1 end with_client_io(server) do |io| connack = connect(io, clean_session: true) @@ -88,6 +91,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) + sleep 0.1 end with_client_io(server) do |io| connack = connect(io, clean_session: false) @@ -106,7 +110,6 @@ module MqttSpecs connack = connect(io) connack.should be_a(MQTT::Protocol::Connack) connack = connack.as(MQTT::Protocol::Connack) - pp connack.return_code connack.return_code.should eq(MQTT::Protocol::Connack::ReturnCode::Accepted) end end @@ -271,7 +274,6 @@ module MqttSpecs topics = mk_topic_filters({"a/b", 1}) subscribe(io, topic_filters: topics) disconnect(io) - pp server.vhosts["/"].queues["amq.mqtt-client_id"].consumers end sleep 0.1 server.vhosts["/"].queues["amq.mqtt-client_id"].consumers.should be_empty diff --git a/spec/mqtt/integrations/message_qos_spec.cr b/spec/mqtt/integrations/message_qos_spec.cr index 467da52e2b..96cd3e1ecd 100644 --- a/spec/mqtt/integrations/message_qos_spec.cr +++ b/spec/mqtt/integrations/message_qos_spec.cr @@ -4,7 +4,7 @@ module MqttSpecs extend MqttHelpers extend MqttMatchers describe "message qos" do - pending "both qos bits can't be set [MQTT-3.3.1-4]" do + it "both qos bits can't be set [MQTT-3.3.1-4]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -19,7 +19,7 @@ module MqttSpecs end end - pending "qos is set according to subscription qos [MYRA non-normative]" do + it "qos is set according to subscription qos [LavinMQ non-normative]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -46,13 +46,14 @@ module MqttSpecs end end - pending "qos1 messages are stored for offline sessions [MQTT-3.1.2-5]" do + it "qos1 messages are stored for offline sessions [MQTT-3.1.2-5]" do with_server do |server| with_client_io(server) do |io| connect(io) topic_filters = mk_topic_filters({"a/b", 1u8}) subscribe(io, topic_filters: topic_filters) disconnect(io) + sleep 0.1 end with_client_io(server) do |publisher_io| @@ -62,6 +63,7 @@ module MqttSpecs publish(publisher_io, topic: "a/b", qos: 0u8) end disconnect(publisher_io) + sleep 0.1 end with_client_io(server) do |io| @@ -78,7 +80,7 @@ module MqttSpecs end end - pending "acked qos1 message won't be sent again" do + it "acked qos1 message won't be sent again" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -90,6 +92,7 @@ module MqttSpecs publish(publisher_io, topic: "a/b", payload: "1".to_slice, qos: 0u8) publish(publisher_io, topic: "a/b", payload: "2".to_slice, qos: 0u8) disconnect(publisher_io) + sleep 0.1 end pkt = read_packet(io) @@ -98,6 +101,7 @@ module MqttSpecs puback(io, pub.packet_id) end disconnect(io) + sleep 0.1 end with_client_io(server) do |io| diff --git a/spec/mqtt/integrations/publish_spec.cr b/spec/mqtt/integrations/publish_spec.cr index 68fad531ac..35c2ea1df9 100644 --- a/spec/mqtt/integrations/publish_spec.cr +++ b/spec/mqtt/integrations/publish_spec.cr @@ -27,31 +27,5 @@ module MqttSpecs end end end - - it "should put the message in a queue" do - with_server do |server| - with_channel(server) do |ch| - x = ch.exchange("amq.topic", "topic") - q = ch.queue("test") - q.bind(x.name, q.name) - - with_client_io(server) do |io| - connect(io) - - payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] - ack = publish(io, topic: "test", payload: payload, qos: 1u8) - ack.should_not be_nil - - body = q.get(no_ack: true).try do |v| - s = Slice(UInt8).new(payload.size) - v.body_io.read(s) - s - end - body.should eq(payload) - disconnect(io) - end - end - end - end end end diff --git a/src/lavinmq/exchange/exchange.cr b/src/lavinmq/exchange/exchange.cr index 0a271b0395..333f1c72b6 100644 --- a/src/lavinmq/exchange/exchange.cr +++ b/src/lavinmq/exchange/exchange.cr @@ -152,6 +152,7 @@ module LavinMQ queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 @publish_in_count += 1 + pp "pub" count = do_publish(msg, immediate, queues, exchanges) @unroutable_count += 1 if count.zero? @publish_out_count += count diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 71e05bc534..5c459df0c9 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -3,7 +3,21 @@ require "../mqtt/subscription_tree" module LavinMQ class MQTTExchange < Exchange - @bindings = Hash(BindingKey, Set(Destination)).new do |h, k| + struct MqttBindingKey + def initialize(routing_key : String, arguments : AMQP::Table? = nil) + @binding_key = BindingKey.new(routing_key, arguments) + end + + def inner + @binding_key + end + + def hash + @binding_key.routing_key.hash + end + end + + @bindings = Hash(MqttBindingKey, Set(Destination)).new do |h, k| h[k] = Set(Destination).new end @tree = MQTT::SubscriptionTree.new @@ -12,40 +26,13 @@ module LavinMQ "mqtt" end - def bindings_details : Iterator(BindingDetails) - @bindings.each.flat_map do |binding_key, ds| - ds.each.map do |d| - BindingDetails.new(name, vhost.name, binding_key, d) - end - end - end - - # TODO: we can probably clean this up a bit - def publish(msg : Message, immediate : Bool, - queues : Set(Queue) = Set(Queue).new, - exchanges : Set(Exchange) = Set(Exchange).new) : Int32 - @publish_in_count += 1 - headers = msg.properties.headers - find_queues(msg.routing_key, headers, queues, exchanges) - if queues.empty? - @unroutable_count += 1 - return 0 - end - return 0 if immediate && !queues.any? &.immediate_delivery? - + private def _publish(msg : Message, immediate : Bool, + queues : Set(Queue) = Set(Queue).new, + exchanges : Set(Exchange) = Set(Exchange).new) : Int32 count = 0 - queues.each do |queue| - qos = 0_u8 - bindings_details.each do |binding_detail| - next unless binding_detail.destination == queue - next unless arg = binding_detail.binding_key.arguments - next unless qos_value = arg["x-mqtt-qos"]? - qos = qos_value.try &.as(UInt8) - end + @tree.each_entry(msg.routing_key) do |queue, qos| msg.properties.delivery_mode = qos - if queue.publish(msg) - @publish_out_count += 1 count += 1 msg.body_io.seek(-msg.bodysize.to_i64, IO::Seek::Current) # rewind end @@ -53,52 +40,46 @@ module LavinMQ count end + def bindings_details : Iterator(BindingDetails) + @bindings.each.flat_map do |binding_key, ds| + ds.each.map do |d| + BindingDetails.new(name, vhost.name, binding_key.inner, d) + end + end + end + + # Only here to make superclass happy + protected def bindings(routing_key, headers) : Iterator(Destination) + Iterator(Destination).empty + end + def bind(destination : Destination, routing_key : String, headers = nil) : Bool raise LavinMQ::Exchange::AccessRefused.new(self) unless destination.is_a?(MQTT::Session) - binding_key = BindingKey.new(routing_key, headers) - return false unless @bindings[binding_key].add? destination - - qos = headers.try { |h| h.fetch("x-mqtt-qos", "0").as(UInt8) } + qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 + binding_key = MqttBindingKey.new(routing_key, headers) + @bindings[binding_key].add destination @tree.subscribe(routing_key, destination, qos) - data = BindingDetails.new(name, vhost.name, binding_key, destination) + data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) notify_observers(ExchangeEvent::Bind, data) true end def unbind(destination : Destination, routing_key, headers = nil) : Bool raise LavinMQ::Exchange::AccessRefused.new(self) unless destination.is_a?(MQTT::Session) - binding_key = BindingKey.new(routing_key, headers) + binding_key = MqttBindingKey.new(routing_key, headers) rk_bindings = @bindings[binding_key] - return false unless rk_bindings.delete destination + rk_bindings.delete destination @bindings.delete binding_key if rk_bindings.empty? @tree.unsubscribe(routing_key, destination) - data = BindingDetails.new(name, vhost.name, binding_key, destination) + data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) notify_observers(ExchangeEvent::Unbind, data) delete if @auto_delete && @bindings.each_value.all?(&.empty?) true end - - protected def bindings : Iterator(Destination) - @bindings.values.each.flat_map(&.each) - end - - protected def bindings(routing_key, headers) : Iterator(Destination) - binding_key = BindingKey.new(routing_key, headers) - matches(binding_key).each - end - - private def matches(binding_key : BindingKey) : Iterator(Destination) - @tree.each_entry(binding_key.routing_key) do |session, qos| - end - - @bindings.each.select do |binding, destinations| - binding.routing_key == binding_key.routing_key - end.flat_map { |_, v| v.each } - end end end diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 8a56c63cba..ab6ab7be6e 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -81,14 +81,14 @@ module LavinMQ end private def get(no_ack : Bool, & : Envelope -> Nil) : Bool - #let packet_id be message counter, look at myra for counter raise ClosedError.new if @closed loop do # retry if msg expired or deliver limit hit env = @msg_store_lock.synchronize { @msg_store.shift? } || break sp = env.segment_position no_ack = env.message.properties.delivery_mode == 0 - if false + if no_ack + pp "no ack" begin yield env # deliver the message rescue ex # requeue failed delivery @@ -113,7 +113,8 @@ module LavinMQ end def ack(sp : SegmentPosition) : Nil - # TDO: maybe risky to not have locka round this + # TODO: maybe risky to not have lock around this + pp "Acking?" @unacked.delete sp super sp end From 88d422865c47683be2e803c8ffb46a145a924ba6 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 14 Oct 2024 10:52:49 +0200 Subject: [PATCH 043/202] format --- spec/mqtt/integrations/connect_spec.cr | 3 +-- src/lavinmq/mqtt/client.cr | 4 +--- src/lavinmq/mqtt/connection_factory.cr | 2 +- src/lavinmq/mqtt/session.cr | 4 ++-- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index eccbac53db..faf7f2f2d6 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -268,7 +268,7 @@ module MqttSpecs it "should not publish after disconnect" do with_server do |server| - # Create a non-clean session with an active subscription + # Create a non-clean session with an active subscription with_client_io(server) do |io| connect(io, clean_session: false) topics = mk_topic_filters({"a/b", 1}) @@ -279,7 +279,6 @@ module MqttSpecs server.vhosts["/"].queues["amq.mqtt-client_id"].consumers.should be_empty end end - end end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 1ad0a37731..a933976d33 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -62,7 +62,7 @@ module LavinMQ @broker.disconnect_client(client_id) @socket.close - #move to disconnect client + # move to disconnect client @broker.vhost.rm_connection(self) end @@ -78,7 +78,6 @@ module LavinMQ when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) when MQTT::Disconnect then return packet - else raise "invalid packet type for client to send" end packet @@ -109,7 +108,6 @@ module LavinMQ end def recieve_puback(packet) - end def recieve_subscribe(packet : MQTT::Subscribe) diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 1d8574a090..2e6812c791 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -10,7 +10,7 @@ module LavinMQ module MQTT class ConnectionFactory def initialize(@users : UserStore, - @vhost : VHost, + @vhost : VHost, @broker : MQTT::Broker) end diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index ab6ab7be6e..dd5ffaf421 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -8,8 +8,8 @@ module LavinMQ @name : String, @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) - @count = 0u16 - @unacked = Deque(SegmentPosition).new + @count = 0u16 + @unacked = Deque(SegmentPosition).new super(@vhost, @name, false, @auto_delete, arguments) spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end From ed848306beb3ef46943db9043b4455e4d961e74b Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 14 Oct 2024 12:59:32 +0200 Subject: [PATCH 044/202] override method --- src/lavinmq/exchange/mqtt.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 5c459df0c9..4e8fac8fe8 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -26,9 +26,9 @@ module LavinMQ "mqtt" end - private def _publish(msg : Message, immediate : Bool, - queues : Set(Queue) = Set(Queue).new, - exchanges : Set(Exchange) = Set(Exchange).new) : Int32 + private def do_publish(msg : Message, immediate : Bool, + queues : Set(Queue) = Set(Queue).new, + exchanges : Set(Exchange) = Set(Exchange).new) : Int32 count = 0 @tree.each_entry(msg.routing_key) do |queue, qos| msg.properties.delivery_mode = qos From 2edfd7d9c883c38943b9435b9539e164666ce7d7 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 09:51:12 +0200 Subject: [PATCH 045/202] string token specs --- spec/mqtt/string_token_iterator.cr | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/mqtt/string_token_iterator.cr b/spec/mqtt/string_token_iterator.cr index 29152555da..787fba28c4 100644 --- a/spec/mqtt/string_token_iterator.cr +++ b/spec/mqtt/string_token_iterator.cr @@ -1,5 +1,5 @@ require "./spec_helper" -require "../src/myramq/string_token_iterator" +require "../../src/lavinmq/mqtt/string_token_iterator" def strings [ @@ -16,18 +16,18 @@ def strings ] end -Spectator.describe MyraMQ::SubscriptionTree do - sample strings do |testdata| +describe LavinMQ::MQTT::StringTokenIterator do + strings.each do |testdata| it "is iterated correct" do - itr = MyraMQ::StringTokenIterator.new(testdata[0], '/') + itr = LavinMQ::MQTT::StringTokenIterator.new(testdata[0], '/') res = Array(String).new while itr.next? val = itr.next - expect(val).to_not be_nil + val.should_not be_nil res << val.not_nil! end - expect(itr.next?).to be_false - expect(res).to eq testdata[1] + itr.next?.should be_false + res.should eq testdata[1] end end end From aee1c1364446faba31fead93aabc574e2c9f586a Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 09:52:21 +0200 Subject: [PATCH 046/202] crystal 1.14 fixes --- spec/mqtt/integrations/connect_spec.cr | 10 +++++----- spec/mqtt/integrations/message_qos_spec.cr | 8 ++++---- spec/mqtt/integrations/unsubscribe_spec.cr | 4 ++-- src/lavinmq/exchange/exchange.cr | 1 - src/lavinmq/mqtt/client.cr | 2 +- src/lavinmq/mqtt/session.cr | 2 +- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index faf7f2f2d6..a00410b377 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -31,7 +31,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 0.1 + sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: true) @@ -51,7 +51,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 0.1 + sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: false) @@ -71,7 +71,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 0.1 + sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: true) @@ -91,7 +91,7 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 0.1 + sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: false) @@ -275,7 +275,7 @@ module MqttSpecs subscribe(io, topic_filters: topics) disconnect(io) end - sleep 0.1 + sleep 100.milliseconds server.vhosts["/"].queues["amq.mqtt-client_id"].consumers.should be_empty end end diff --git a/spec/mqtt/integrations/message_qos_spec.cr b/spec/mqtt/integrations/message_qos_spec.cr index 96cd3e1ecd..156b7d9fd6 100644 --- a/spec/mqtt/integrations/message_qos_spec.cr +++ b/spec/mqtt/integrations/message_qos_spec.cr @@ -53,7 +53,7 @@ module MqttSpecs topic_filters = mk_topic_filters({"a/b", 1u8}) subscribe(io, topic_filters: topic_filters) disconnect(io) - sleep 0.1 + sleep 100.milliseconds end with_client_io(server) do |publisher_io| @@ -63,7 +63,7 @@ module MqttSpecs publish(publisher_io, topic: "a/b", qos: 0u8) end disconnect(publisher_io) - sleep 0.1 + sleep 100.milliseconds end with_client_io(server) do |io| @@ -92,7 +92,7 @@ module MqttSpecs publish(publisher_io, topic: "a/b", payload: "1".to_slice, qos: 0u8) publish(publisher_io, topic: "a/b", payload: "2".to_slice, qos: 0u8) disconnect(publisher_io) - sleep 0.1 + sleep 100.milliseconds end pkt = read_packet(io) @@ -101,7 +101,7 @@ module MqttSpecs puback(io, pub.packet_id) end disconnect(io) - sleep 0.1 + sleep 100.milliseconds end with_client_io(server) do |io| diff --git a/spec/mqtt/integrations/unsubscribe_spec.cr b/spec/mqtt/integrations/unsubscribe_spec.cr index 13dd813fca..75670ff2bd 100644 --- a/spec/mqtt/integrations/unsubscribe_spec.cr +++ b/spec/mqtt/integrations/unsubscribe_spec.cr @@ -53,7 +53,7 @@ module MqttSpecs subscribe(io, topic_filters: topics) disconnect(io) end - sleep 1 + sleep 1.second # Publish messages that will be stored for the subscriber 2.times { |i| publish(pubio, topic: "a/b", payload: i.to_s.to_slice, qos: 0u8) } @@ -71,7 +71,7 @@ module MqttSpecs unsubscribe(io, topics: ["a/b"]) disconnect(io) end - sleep 1 + sleep 1.second # Publish more messages 2.times { |i| publish(pubio, topic: "a/b", payload: (2 + i).to_s.to_slice, qos: 0u8) } diff --git a/src/lavinmq/exchange/exchange.cr b/src/lavinmq/exchange/exchange.cr index 333f1c72b6..0a271b0395 100644 --- a/src/lavinmq/exchange/exchange.cr +++ b/src/lavinmq/exchange/exchange.cr @@ -152,7 +152,6 @@ module LavinMQ queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 @publish_in_count += 1 - pp "pub" count = do_publish(msg, immediate, queues, exchanges) @unroutable_count += 1 if count.zero? @publish_out_count += count diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index a933976d33..7e3a5bf949 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -73,7 +73,7 @@ module LavinMQ case packet when MQTT::Publish then recieve_publish(packet) - when MQTT::PubAck then pp "puback" + when MQTT::PubAck then nil when MQTT::Subscribe then recieve_subscribe(packet) when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index dd5ffaf421..90860ff8b3 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -88,7 +88,7 @@ module LavinMQ sp = env.segment_position no_ack = env.message.properties.delivery_mode == 0 if no_ack - pp "no ack" + # pp "no ack" begin yield env # deliver the message rescue ex # requeue failed delivery From ffa3ea20c39c292d1e079d8aca9f90d40572d70a Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 15:33:09 +0200 Subject: [PATCH 047/202] subscription tree specs --- spec/mqtt/subscription_tree_spec.cr | 81 +++++++++++++++------------ src/lavinmq/exchange/mqtt.cr | 2 +- src/lavinmq/mqtt/subscription_tree.cr | 32 +++++------ 3 files changed, 63 insertions(+), 52 deletions(-) diff --git a/spec/mqtt/subscription_tree_spec.cr b/spec/mqtt/subscription_tree_spec.cr index 256da472fd..1af4bd4555 100644 --- a/spec/mqtt/subscription_tree_spec.cr +++ b/spec/mqtt/subscription_tree_spec.cr @@ -1,17 +1,15 @@ require "./spec_helper" require "../../src/lavinmq/mqtt/subscription_tree" -# require "../src/myramq/broker" -# require "../src/myramq/session" describe LavinMQ::MQTT::SubscriptionTree do - tree = LavinMQ::MQTT::SubscriptionTree.new - describe "#any?" do it "returns false for empty tree" do + tree = LavinMQ::MQTT::SubscriptionTree(String).new tree.any?("a").should be_false end describe "with subs" do + tree = LavinMQ::MQTT::SubscriptionTree(String).new before_each do test_data = [ "a/b", @@ -19,10 +17,10 @@ describe LavinMQ::MQTT::SubscriptionTree do "a/b/c/d/#", "a/+/c/d/#", ] - session = LavinMQ::MQTT::Session.new + target = "target" test_data.each do |topic| - tree.subscribe(topic, session, 0u8) + tree.subscribe(topic, target, 0u8) end end @@ -46,55 +44,63 @@ describe LavinMQ::MQTT::SubscriptionTree do describe "#empty?" do it "returns true before any subscribe" do + tree = LavinMQ::MQTT::SubscriptionTree(String).new tree.empty?.should be_true end it "returns false after a non-wildcard subscribe" do - session = mock(MQTT::Session) + tree = LavinMQ::MQTT::SubscriptionTree(String).new + session = "target" tree.subscribe("topic", session, 0u8) tree.empty?.should be_false end it "returns false after a +-wildcard subscribe" do - session = mock(MQTT::Session) + tree = LavinMQ::MQTT::SubscriptionTree(String).new + session = "target" tree.subscribe("a/+/topic", session, 0u8) tree.empty?.should be_false end it "returns false after a #-wildcard subscribe" do - session = mock(MQTT::Session) + tree = LavinMQ::MQTT::SubscriptionTree(String).new + session = "session" tree.subscribe("a/#/topic", session, 0u8) tree.empty?.should be_false end it "returns true after unsubscribing only existing non-wildcard subscription" do - session = mock(MQTT::Session) + tree = LavinMQ::MQTT::SubscriptionTree(String).new + session = "session" tree.subscribe("topic", session, 0u8) tree.unsubscribe("topic", session) tree.empty?.should be_true end it "returns true after unsubscribing only existing +-wildcard subscription" do - session = mock(MQTT::Session) + tree = LavinMQ::MQTT::SubscriptionTree(String).new + session = "session" tree.subscribe("a/+/topic", session, 0u8) tree.unsubscribe("a/+/topic", session) tree.empty?.should be_true end it "returns true after unsubscribing only existing #+-wildcard subscription" do - session = mock(MQTT::Session) + tree = LavinMQ::MQTT::SubscriptionTree(String).new + session = "session" tree.subscribe("a/b/#", session, 0u8) tree.unsubscribe("a/b/#", session) tree.empty?.should be_true end it "returns true after unsubscribing many different subscriptions" do + tree = LavinMQ::MQTT::SubscriptionTree(String).new test_data = [ - {mock(MQTT::Session), "a/b"}, - {mock(MQTT::Session), "a/+/b"}, - {mock(MQTT::Session), "a/b/c/d#"}, - {mock(MQTT::Session), "a/+/c/d/#"}, - {mock(MQTT::Session), "#"}, + {"session", "a/b"}, + {"session", "a/+/b"}, + {"session", "a/b/c/d#"}, + {"session", "a/+/c/d/#"}, + {"session", "#"}, ] test_data.each do |session, topic| @@ -110,12 +116,13 @@ describe LavinMQ::MQTT::SubscriptionTree do end it "subscriptions is found" do + tree = LavinMQ::MQTT::SubscriptionTree(String).new test_data = [ - {mock(MQTT::Session), [{"a/b", 0u8}]}, - {mock(MQTT::Session), [{"a/b", 0u8}]}, - {mock(MQTT::Session), [{"a/c", 0u8}]}, - {mock(MQTT::Session), [{"a/+", 0u8}]}, - {mock(MQTT::Session), [{"#", 0u8}]}, + {"session", [{"a/b", 0u8}]}, + {"session", [{"a/b", 0u8}]}, + {"session", [{"a/c", 0u8}]}, + {"session", [{"a/+", 0u8}]}, + {"session", [{"#", 0u8}]}, ] test_data.each do |s| @@ -128,6 +135,7 @@ describe LavinMQ::MQTT::SubscriptionTree do calls = 0 tree.each_entry "a/b" do |_session, qos| + puts "qos=#{qos}" qos.should eq 0u8 calls += 1 end @@ -135,12 +143,13 @@ describe LavinMQ::MQTT::SubscriptionTree do end it "unsubscribe unsubscribes" do + tree = LavinMQ::MQTT::SubscriptionTree(String).new test_data = [ - {mock(MQTT::Session), [{"a/b", 0u8}]}, - {mock(MQTT::Session), [{"a/b", 0u8}]}, - {mock(MQTT::Session), [{"a/c", 0u8}]}, - {mock(MQTT::Session), [{"a/+", 0u8}]}, - {mock(MQTT::Session), [{"#", 0u8}]}, + {"session", [{"a/b", 0u8}]}, + {"session", [{"a/b", 0u8}]}, + {"session", [{"a/c", 0u8}]}, + {"session", [{"a/+", 0u8}]}, + {"session", [{"#", 0u8}]}, ] test_data.each do |session, subscriptions| @@ -162,7 +171,8 @@ describe LavinMQ::MQTT::SubscriptionTree do end it "changes qos level" do - session = mock(MQTT::Session) + tree = LavinMQ::MQTT::SubscriptionTree(String).new + session = "session" tree.subscribe("a/b", session, 0u8) tree.each_entry "a/b" { |_sess, qos| qos.should eq 0u8 } tree.subscribe("a/b", session, 1u8) @@ -170,14 +180,15 @@ describe LavinMQ::MQTT::SubscriptionTree do end it "can iterate all entries" do + tree = LavinMQ::MQTT::SubscriptionTree(String).new test_data = [ - {mock(MQTT::Session), [{"a/b", 0u8}]}, - {mock(MQTT::Session), [{"a/b/c/d/e", 0u8}]}, - {mock(MQTT::Session), [{"+/c", 0u8}]}, - {mock(MQTT::Session), [{"a/+", 0u8}]}, - {mock(MQTT::Session), [{"#", 0u8}]}, - {mock(MQTT::Session), [{"a/b/#", 0u8}]}, - {mock(MQTT::Session), [{"a/+/c", 0u8}]}, + {"session", [{"a/b", 0u8}]}, + {"session", [{"a/b/c/d/e", 0u8}]}, + {"session", [{"+/c", 0u8}]}, + {"session", [{"a/+", 0u8}]}, + {"session", [{"#", 0u8}]}, + {"session", [{"a/b/#", 0u8}]}, + {"session", [{"a/+/c", 0u8}]}, ] test_data.each do |session, subscriptions| diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 4e8fac8fe8..e8dd0933f8 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -20,7 +20,7 @@ module LavinMQ @bindings = Hash(MqttBindingKey, Set(Destination)).new do |h, k| h[k] = Set(Destination).new end - @tree = MQTT::SubscriptionTree.new + @tree = MQTT::SubscriptionTree(Destination).new def type : String "mqtt" diff --git a/src/lavinmq/mqtt/subscription_tree.cr b/src/lavinmq/mqtt/subscription_tree.cr index 88955aba3b..b0285c539e 100644 --- a/src/lavinmq/mqtt/subscription_tree.cr +++ b/src/lavinmq/mqtt/subscription_tree.cr @@ -3,25 +3,25 @@ require "./string_token_iterator" module LavinMQ module MQTT - class SubscriptionTree - @wildcard_rest = Hash(Session, UInt8).new - @plus : SubscriptionTree? - @leafs = Hash(Session, UInt8).new + class SubscriptionTree(T) + @wildcard_rest = Hash(T, UInt8).new + @plus : SubscriptionTree(T)? + @leafs = Hash(T, UInt8).new # Non wildcards may be an unnecessary "optimization". We store all subscriptions without # wildcard in the first level. No need to make a tree out of them. - @non_wildcards = Hash(String, Hash(Session, UInt8)).new do |h, k| - h[k] = Hash(Session, UInt8).new + @non_wildcards = Hash(String, Hash(T, UInt8)).new do |h, k| + h[k] = Hash(T, UInt8).new h[k].compare_by_identity h[k] end - @sublevels = Hash(String, SubscriptionTree).new + @sublevels = Hash(String, SubscriptionTree(T)).new def initialize @wildcard_rest.compare_by_identity @leafs.compare_by_identity end - def subscribe(filter : String, session : Session, qos : UInt8) + def subscribe(filter : String, session : T, qos : UInt8) if filter.index('#').nil? && filter.index('+').nil? @non_wildcards[filter][session] = qos return @@ -29,7 +29,7 @@ module LavinMQ subscribe(StringTokenIterator.new(filter), session, qos) end - protected def subscribe(filter : StringTokenIterator, session : Session, qos : UInt8) + protected def subscribe(filter : StringTokenIterator, session : T, qos : UInt8) unless current = filter.next @leafs[session] = qos return @@ -39,25 +39,25 @@ module LavinMQ return end if current == "+" - plus = (@plus ||= SubscriptionTree.new) + plus = (@plus ||= SubscriptionTree(T).new) plus.subscribe filter, session, qos return end if !(sublevels = @sublevels[current]?) - sublevels = @sublevels[current] = SubscriptionTree.new + sublevels = @sublevels[current] = SubscriptionTree(T).new end sublevels.subscribe filter, session, qos return end - def unsubscribe(filter : String, session : Session) + def unsubscribe(filter : String, session : T) if subs = @non_wildcards[filter]? return unless subs.delete(session).nil? end unsubscribe(StringTokenIterator.new(filter), session) end - protected def unsubscribe(filter : StringTokenIterator, session : Session) + protected def unsubscribe(filter : StringTokenIterator, session : T) unless current = filter.next @leafs.delete session return @@ -105,14 +105,14 @@ module LavinMQ true end - def each_entry(topic : String, &block : (Session, UInt8) -> _) + def each_entry(topic : String, &block : (T, UInt8) -> _) if subs = @non_wildcards[topic]? subs.each &block end each_entry(StringTokenIterator.new(topic), &block) end - protected def each_entry(topic : StringTokenIterator, &block : (Session, UInt8) -> _) + protected def each_entry(topic : StringTokenIterator, &block : (T, UInt8) -> _) unless current = topic.next @leafs.each &block return @@ -124,7 +124,7 @@ module LavinMQ end end - def each_entry(&block : (Session, UInt8) -> _) + def each_entry(&block : (T, UInt8) -> _) @non_wildcards.each do |_, entries| entries.each &block end From 0bdde3d242c414c77fb77c60a9d0d370dcfc9cc7 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 15 Oct 2024 15:49:23 +0200 Subject: [PATCH 048/202] beginning of puback --- spec/mqtt/integrations/connect_spec.cr | 3 ++- src/lavinmq/mqtt/client.cr | 3 ++- src/lavinmq/mqtt/session.cr | 21 ++++++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index a00410b377..ecd4835eb2 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -192,7 +192,8 @@ module MqttSpecs it "if first packet is not a CONNECT [MQTT-3.1.0-1]" do with_server do |server| with_client_io(server) do |io| - ping(io) + payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + publish(io, topic: "test", payload: payload, qos: 0u8) io.should be_closed end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 7e3a5bf949..88c5e6a91a 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -73,7 +73,7 @@ module LavinMQ case packet when MQTT::Publish then recieve_publish(packet) - when MQTT::PubAck then nil + when MQTT::PubAck then recieve_puback(packet) when MQTT::Subscribe then recieve_subscribe(packet) when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) @@ -108,6 +108,7 @@ module LavinMQ end def recieve_puback(packet) + @broker.sessions[@client_id].ack(packet) end def recieve_subscribe(packet : MQTT::Subscribe) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 90860ff8b3..e5d193537a 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -9,7 +9,8 @@ module LavinMQ @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) @count = 0u16 - @unacked = Deque(SegmentPosition).new + @unacked = Hash(UInt16, SegmentPosition).new + super(@vhost, @name, false, @auto_delete, arguments) spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end @@ -40,7 +41,7 @@ module LavinMQ end @msg_store_lock.synchronize do - @unacked.each do |sp| + @unacked.values.each do |sp| @msg_store.requeue(sp) end end @@ -83,12 +84,12 @@ module LavinMQ private def get(no_ack : Bool, & : Envelope -> Nil) : Bool raise ClosedError.new if @closed loop do # retry if msg expired or deliver limit hit + id = next_id env = @msg_store_lock.synchronize { @msg_store.shift? } || break sp = env.segment_position no_ack = env.message.properties.delivery_mode == 0 if no_ack - # pp "no ack" begin yield env # deliver the message rescue ex # requeue failed delivery @@ -97,10 +98,10 @@ module LavinMQ end delete_message(sp) else - env.message.properties.message_id = next_id.to_s + env.message.properties.message_id = id.to_s mark_unacked(sp) do yield env # deliver the message - @unacked << sp + @unacked[id] = sp end end return true @@ -112,10 +113,11 @@ module LavinMQ raise ClosedError.new(cause: ex) end - def ack(sp : SegmentPosition) : Nil + def ack(packet : MQTT::PubAck) : Nil # TODO: maybe risky to not have lock around this - pp "Acking?" - @unacked.delete sp + id = packet.packet_id + sp = @unacked[id] + @unacked.delete id super sp end @@ -124,7 +126,8 @@ module LavinMQ private def queue_expire_loop; end private def next_id : UInt16? - @count += 1u16 + @count &+= 1u16 + # return nil if @unacked.size == @max_inflight # start_id = @packet_id From cc25b6abaa276b9cba44e516ea50cb59cc120d70 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 15:49:19 +0200 Subject: [PATCH 049/202] subscription tree specs --- src/lavinmq/exchange/mqtt.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index e8dd0933f8..1f1b758fc0 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -1,5 +1,6 @@ require "./exchange" require "../mqtt/subscription_tree" +require "../mqtt/session" module LavinMQ class MQTTExchange < Exchange @@ -17,10 +18,10 @@ module LavinMQ end end - @bindings = Hash(MqttBindingKey, Set(Destination)).new do |h, k| - h[k] = Set(Destination).new + @bindings = Hash(MqttBindingKey, Set(MQTT::Session)).new do |h, k| + h[k] = Set(MQTT::Session).new end - @tree = MQTT::SubscriptionTree(Destination).new + @tree = MQTT::SubscriptionTree(MQTT::Session).new def type : String "mqtt" From e73be606a33f8ca8dfd08c6a950bdaba8c3a6b09 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 15:53:27 +0200 Subject: [PATCH 050/202] remove puts --- spec/mqtt/subscription_tree_spec.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/mqtt/subscription_tree_spec.cr b/spec/mqtt/subscription_tree_spec.cr index 1af4bd4555..237ba41c28 100644 --- a/spec/mqtt/subscription_tree_spec.cr +++ b/spec/mqtt/subscription_tree_spec.cr @@ -135,7 +135,6 @@ describe LavinMQ::MQTT::SubscriptionTree do calls = 0 tree.each_entry "a/b" do |_session, qos| - puts "qos=#{qos}" qos.should eq 0u8 calls += 1 end From 19940352e099b7f4a5ef9defdacf39a77deff4a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 10:56:55 +0200 Subject: [PATCH 051/202] Use monkey patched `IO::Buffered#peek(n)` to ensure data exists --- spec/stdlib/io_buffered_spec.cr | 79 +++++++++++++++++++++++++++++++++ src/stdlib/io_buffered.cr | 25 +++++++++++ 2 files changed, 104 insertions(+) create mode 100644 spec/stdlib/io_buffered_spec.cr create mode 100644 src/stdlib/io_buffered.cr diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr new file mode 100644 index 0000000000..82f88d0059 --- /dev/null +++ b/spec/stdlib/io_buffered_spec.cr @@ -0,0 +1,79 @@ +require "random" +require "spec" +require "socket" +require "wait_group" +require "../../src/stdlib/io_buffered" + +def with_io(initial_content = "foo bar baz".to_slice, &) + read_io, write_io = UNIXSocket.pair + write_io.write initial_content + yield read_io, write_io +ensure + read_io.try &.close + write_io.try &.close +end + +describe IO::Buffered do + describe "#peek" do + it "raises if read_buffering is false" do + with_io do |read_io, _write_io| + read_io.read_buffering = false + expect_raises(RuntimeError) do + read_io.peek(5) + end + end + end + + it "raises if size is greater than buffer_size" do + with_io do |read_io, _write_io| + read_io.read_buffering = true + read_io.buffer_size = 5 + expect_raises(ArgumentError) do + read_io.peek(10) + end + end + end + + it "will read until buffer contains at least size bytes" do + initial_data = Bytes.new(3) + Random::Secure.random_bytes(initial_data) + with_io(initial_data) do |read_io, write_io| + read_io.read_buffering = true + read_io.buffer_size = 100 + wg = WaitGroup.new(1) + spawn do + read_io.peek(20) + wg.done + end + Fiber.yield + read_io.peek.size.should eq initial_data.size + extra_data = Bytes.new(17) + Random::Secure.random_bytes(extra_data) + write_io.write extra_data + wg.wait + read_io.peek.size.should eq(initial_data.size + extra_data.size) + end + end + + it "will read up to buffer size" do + initial_data = Bytes.new(3) + Random::Secure.random_bytes(initial_data) + with_io(initial_data) do |read_io, write_io| + read_io.read_buffering = true + read_io.buffer_size = 200 + wg = WaitGroup.new(1) + spawn do + read_io.peek(20) + wg.done + end + Fiber.yield + read_io.peek.size.should eq initial_data.size + extra_data = Bytes.new(500) + Random::Secure.random_bytes(extra_data) + write_io.write extra_data + wg.wait + read_io.peek.size.should eq(read_io.buffer_size) + end + end + end +end diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr new file mode 100644 index 0000000000..86d5eba26e --- /dev/null +++ b/src/stdlib/io_buffered.cr @@ -0,0 +1,25 @@ +module IO::Buffered + def peek(size : Int) + raise RuntimeError.new("Can't prefill when read_buffering is #{read_buffering?}") unless read_buffering? + + return unless size.positive? + + if size > @buffer_size + raise ArgumentError.new("size (#{size}) can't be greater than buffer_size #{@buffer_size}") + end + + to_read = @buffer_size - @in_buffer_rem.size + return unless to_read.positive? + + in_buffer = in_buffer() + + while @in_buffer_rem.size < size + target = Slice.new(in_buffer + @in_buffer_rem.size, to_read) + read_size = unbuffered_read(target).to_i + @in_buffer_rem = Slice.new(in_buffer, @in_buffer_rem.size + read_size) + to_read -= read_size + end + + @in_buffer_rem[0, size] + end +end From 4521da816a125112faf36eaab24d3ee1e466ff04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 11:09:07 +0200 Subject: [PATCH 052/202] It should return slice --- spec/stdlib/io_buffered_spec.cr | 8 ++++++++ src/stdlib/io_buffered.cr | 7 +++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr index 82f88d0059..2e09e3b97b 100644 --- a/spec/stdlib/io_buffered_spec.cr +++ b/spec/stdlib/io_buffered_spec.cr @@ -34,6 +34,14 @@ describe IO::Buffered do end end + it "returns slice of requested size" do + with_io("foo bar".to_slice) do |read_io, _write_io| + read_io.read_buffering = true + read_io.buffer_size = 5 + read_io.peek(3).should eq "foo".to_slice + end + end + it "will read until buffer contains at least size bytes" do initial_data = Bytes.new(3) Random::Secure.random_bytes(initial_data) diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr index 86d5eba26e..3d71058571 100644 --- a/src/stdlib/io_buffered.cr +++ b/src/stdlib/io_buffered.cr @@ -1,15 +1,14 @@ module IO::Buffered def peek(size : Int) - raise RuntimeError.new("Can't prefill when read_buffering is #{read_buffering?}") unless read_buffering? - - return unless size.positive? + raise RuntimeError.new("Can't fill buffer when read_buffering is #{read_buffering?}") unless read_buffering? + raise ArgumentError.new("size must be positive") unless size.positive? if size > @buffer_size raise ArgumentError.new("size (#{size}) can't be greater than buffer_size #{@buffer_size}") end to_read = @buffer_size - @in_buffer_rem.size - return unless to_read.positive? + return @in_buffer_rem[0, size] unless to_read.positive? in_buffer = in_buffer() From 4149757885834fc9c4e131abab81ec8fc13016e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 11:11:01 +0200 Subject: [PATCH 053/202] Add spec to verify positive size check --- spec/stdlib/io_buffered_spec.cr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr index 2e09e3b97b..a39b3de018 100644 --- a/spec/stdlib/io_buffered_spec.cr +++ b/spec/stdlib/io_buffered_spec.cr @@ -34,6 +34,15 @@ describe IO::Buffered do end end + it "raises unless size is positive" do + with_io do |read_io, _write_io| + read_io.read_buffering = true + expect_raises(ArgumentError) do + read_io.peek(-10) + end + end + end + it "returns slice of requested size" do with_io("foo bar".to_slice) do |read_io, _write_io| read_io.read_buffering = true From a631464539dff42caa5dfd3169a7d7de0ce5751f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 14:13:55 +0200 Subject: [PATCH 054/202] Rename stuff in an attempt to make things more readable --- src/stdlib/io_buffered.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr index 3d71058571..f9266cf1d1 100644 --- a/src/stdlib/io_buffered.cr +++ b/src/stdlib/io_buffered.cr @@ -7,16 +7,16 @@ module IO::Buffered raise ArgumentError.new("size (#{size}) can't be greater than buffer_size #{@buffer_size}") end - to_read = @buffer_size - @in_buffer_rem.size - return @in_buffer_rem[0, size] unless to_read.positive? + remaining_capacity = @buffer_size - @in_buffer_rem.size + return @in_buffer_rem[0, size] unless remaining_capacity.positive? in_buffer = in_buffer() while @in_buffer_rem.size < size - target = Slice.new(in_buffer + @in_buffer_rem.size, to_read) + target = Slice.new(in_buffer + @in_buffer_rem.size, remaining_capacity) read_size = unbuffered_read(target).to_i - @in_buffer_rem = Slice.new(in_buffer, @in_buffer_rem.size + read_size) - to_read -= read_size + remaining_capacity -= read_size + @in_buffer_rem = Slice.new(in_buffer, @buffer_size - remaining_capacity) end @in_buffer_rem[0, size] From 2ad3d7a15b5f0c5a42c3e2f5ebc00aa84233ba71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 14:14:05 +0200 Subject: [PATCH 055/202] Refactor specs --- spec/stdlib/io_buffered_spec.cr | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr index a39b3de018..a1a77b8fdd 100644 --- a/spec/stdlib/io_buffered_spec.cr +++ b/spec/stdlib/io_buffered_spec.cr @@ -52,44 +52,40 @@ describe IO::Buffered do end it "will read until buffer contains at least size bytes" do - initial_data = Bytes.new(3) - Random::Secure.random_bytes(initial_data) + initial_data = "foo".to_slice with_io(initial_data) do |read_io, write_io| read_io.read_buffering = true - read_io.buffer_size = 100 + read_io.buffer_size = 10 wg = WaitGroup.new(1) + peeked = nil spawn do - read_io.peek(20) + peeked = read_io.peek(6) wg.done end - Fiber.yield - read_io.peek.size.should eq initial_data.size - extra_data = Bytes.new(17) - Random::Secure.random_bytes(extra_data) + read_io.peek.should eq "foo".to_slice + extra_data = "barbaz".to_slice write_io.write extra_data wg.wait - read_io.peek.size.should eq(initial_data.size + extra_data.size) + peeked.should eq "foobar".to_slice end end it "will read up to buffer size" do - initial_data = Bytes.new(3) - Random::Secure.random_bytes(initial_data) + initial_data = "foo".to_slice with_io(initial_data) do |read_io, write_io| read_io.read_buffering = true - read_io.buffer_size = 200 + read_io.buffer_size = 9 wg = WaitGroup.new(1) + peeked = nil spawn do - read_io.peek(20) + peeked = read_io.peek(6) wg.done end - Fiber.yield - read_io.peek.size.should eq initial_data.size - extra_data = Bytes.new(500) - Random::Secure.random_bytes(extra_data) + read_io.peek.should eq "foo".to_slice + extra_data = "barbaz".to_slice write_io.write extra_data wg.wait - read_io.peek.size.should eq(read_io.buffer_size) + peeked.should eq "foobar".to_slice end end end From da18332a23ea265cc7b0f9fe4e14a810a76e3c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 14:15:25 +0200 Subject: [PATCH 056/202] Cleaner specs --- spec/stdlib/io_buffered_spec.cr | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr index a1a77b8fdd..2f83609830 100644 --- a/spec/stdlib/io_buffered_spec.cr +++ b/spec/stdlib/io_buffered_spec.cr @@ -56,16 +56,13 @@ describe IO::Buffered do with_io(initial_data) do |read_io, write_io| read_io.read_buffering = true read_io.buffer_size = 10 - wg = WaitGroup.new(1) - peeked = nil - spawn do - peeked = read_io.peek(6) - wg.done - end + read_io.peek.should eq "foo".to_slice + extra_data = "barbaz".to_slice write_io.write extra_data - wg.wait + + peeked = read_io.peek(6) peeked.should eq "foobar".to_slice end end @@ -75,16 +72,13 @@ describe IO::Buffered do with_io(initial_data) do |read_io, write_io| read_io.read_buffering = true read_io.buffer_size = 9 - wg = WaitGroup.new(1) - peeked = nil - spawn do - peeked = read_io.peek(6) - wg.done - end + read_io.peek.should eq "foo".to_slice + extra_data = "barbaz".to_slice write_io.write extra_data - wg.wait + + peeked = read_io.peek(6) peeked.should eq "foobar".to_slice end end From b764101d20fd8b6b725a52f3b7b52e8d4fae4ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 14:16:21 +0200 Subject: [PATCH 057/202] Update spec description --- spec/stdlib/io_buffered_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr index 2f83609830..4ed9c458f0 100644 --- a/spec/stdlib/io_buffered_spec.cr +++ b/spec/stdlib/io_buffered_spec.cr @@ -67,7 +67,7 @@ describe IO::Buffered do end end - it "will read up to buffer size" do + it "will read up to buffer size if possible" do initial_data = "foo".to_slice with_io(initial_data) do |read_io, write_io| read_io.read_buffering = true From 587052b69ac2ee1c583eaa1705349df775e1e465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 14:20:26 +0200 Subject: [PATCH 058/202] Fix condition when checking if enough data exists --- src/stdlib/io_buffered.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr index f9266cf1d1..22e24875dd 100644 --- a/src/stdlib/io_buffered.cr +++ b/src/stdlib/io_buffered.cr @@ -7,9 +7,9 @@ module IO::Buffered raise ArgumentError.new("size (#{size}) can't be greater than buffer_size #{@buffer_size}") end - remaining_capacity = @buffer_size - @in_buffer_rem.size - return @in_buffer_rem[0, size] unless remaining_capacity.positive? + return @in_buffer_rem[0, size] if size < @in_buffer_rem.size + remaining_capacity = @buffer_size - @in_buffer_rem.size in_buffer = in_buffer() while @in_buffer_rem.size < size From d6d94b7d63e568d44fd4dda474f97fafddbc27b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 14:44:00 +0200 Subject: [PATCH 059/202] Move data in existing buffer if needed --- spec/stdlib/io_buffered_spec.cr | 36 +++++++++++++++++++++++++++++++++ src/stdlib/io_buffered.cr | 9 +++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr index 4ed9c458f0..2418818262 100644 --- a/spec/stdlib/io_buffered_spec.cr +++ b/spec/stdlib/io_buffered_spec.cr @@ -82,5 +82,41 @@ describe IO::Buffered do peeked.should eq "foobar".to_slice end end + + it "will move existing data to beginning of internal buffer " do + initial_data = "000foo".to_slice + with_io(initial_data) do |read_io, write_io| + read_io.read_buffering = true + read_io.buffer_size = 9 + + data = Bytes.new(3) + read_io.read data + data.should eq "000".to_slice + + extra_data = "barbaz".to_slice + write_io.write extra_data + + peeked = read_io.peek(6) + peeked.should eq "foobar".to_slice + end + end + + it "raises if io is closed" do + initial_data = "000foo".to_slice + with_io(initial_data) do |read_io, write_io| + read_io.read_buffering = true + read_io.buffer_size = 9 + + data = Bytes.new(3) + read_io.read data + + data.should eq "000".to_slice + write_io.close + + expect_raises(IO::Error) do + read_io.peek(6) + end + end + end end end diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr index 22e24875dd..c995074de8 100644 --- a/src/stdlib/io_buffered.cr +++ b/src/stdlib/io_buffered.cr @@ -12,10 +12,15 @@ module IO::Buffered remaining_capacity = @buffer_size - @in_buffer_rem.size in_buffer = in_buffer() + if @in_buffer_rem.to_unsafe != in_buffer + @in_buffer_rem.copy_to(in_buffer, @in_buffer_rem.size) + end + while @in_buffer_rem.size < size target = Slice.new(in_buffer + @in_buffer_rem.size, remaining_capacity) - read_size = unbuffered_read(target).to_i - remaining_capacity -= read_size + bytes_read = unbuffered_read(target).to_i + raise IO::Error.new("io closed?") if bytes_read.zero? + remaining_capacity -= bytes_read @in_buffer_rem = Slice.new(in_buffer, @buffer_size - remaining_capacity) end From 8240ac20d8c39fde6ccf554f019c6cdfd0b7cba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 11 Oct 2024 16:33:47 +0200 Subject: [PATCH 060/202] Return what we got instead of rasing in read fails --- spec/stdlib/io_buffered_spec.cr | 10 ++++------ src/stdlib/io_buffered.cr | 10 +++++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr index 2418818262..0dbe020584 100644 --- a/spec/stdlib/io_buffered_spec.cr +++ b/spec/stdlib/io_buffered_spec.cr @@ -101,21 +101,19 @@ describe IO::Buffered do end end - it "raises if io is closed" do - initial_data = "000foo".to_slice + it "returns what's in buffer upto size if io is closed" do + initial_data = "foobar".to_slice with_io(initial_data) do |read_io, write_io| read_io.read_buffering = true read_io.buffer_size = 9 data = Bytes.new(3) read_io.read data + data.should eq "foo".to_slice - data.should eq "000".to_slice write_io.close - expect_raises(IO::Error) do - read_io.peek(6) - end + read_io.peek(6).should eq "bar".to_slice end end end diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr index c995074de8..d2d343e768 100644 --- a/src/stdlib/io_buffered.cr +++ b/src/stdlib/io_buffered.cr @@ -17,11 +17,15 @@ module IO::Buffered end while @in_buffer_rem.size < size - target = Slice.new(in_buffer + @in_buffer_rem.size, remaining_capacity) + target = Slice.new(in_buffer + @in_buffer_rem.size, @buffer_size - @in_buffer_rem.size) bytes_read = unbuffered_read(target).to_i - raise IO::Error.new("io closed?") if bytes_read.zero? + break if bytes_read.zero? remaining_capacity -= bytes_read - @in_buffer_rem = Slice.new(in_buffer, @buffer_size - remaining_capacity) + @in_buffer_rem = Slice.new(in_buffer, @in_buffer_rem.size + bytes_read) + end + + if @in_buffer_rem.size < size + return @in_buffer_rem end @in_buffer_rem[0, size] From ad81285b2af3a69d6123aeedadc847f4c99869db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Sat, 12 Oct 2024 08:40:03 +0200 Subject: [PATCH 061/202] Remove unused code --- src/stdlib/io_buffered.cr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr index d2d343e768..45ffdb3e88 100644 --- a/src/stdlib/io_buffered.cr +++ b/src/stdlib/io_buffered.cr @@ -2,16 +2,16 @@ module IO::Buffered def peek(size : Int) raise RuntimeError.new("Can't fill buffer when read_buffering is #{read_buffering?}") unless read_buffering? raise ArgumentError.new("size must be positive") unless size.positive? - if size > @buffer_size raise ArgumentError.new("size (#{size}) can't be greater than buffer_size #{@buffer_size}") end + # Enough data in buffer already return @in_buffer_rem[0, size] if size < @in_buffer_rem.size - remaining_capacity = @buffer_size - @in_buffer_rem.size in_buffer = in_buffer() + # Move data to beginning of in_buffer if needed if @in_buffer_rem.to_unsafe != in_buffer @in_buffer_rem.copy_to(in_buffer, @in_buffer_rem.size) end @@ -20,7 +20,6 @@ module IO::Buffered target = Slice.new(in_buffer + @in_buffer_rem.size, @buffer_size - @in_buffer_rem.size) bytes_read = unbuffered_read(target).to_i break if bytes_read.zero? - remaining_capacity -= bytes_read @in_buffer_rem = Slice.new(in_buffer, @in_buffer_rem.size + bytes_read) end From 49752e6691d1843b574e4025243473183ff382ce Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 15 Oct 2024 16:23:50 +0200 Subject: [PATCH 062/202] merge jons pr and fix sleep problem --- spec/mqtt/integrations/message_qos_spec.cr | 13 +++++-------- spec/mqtt/integrations/unsubscribe_spec.cr | 2 -- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/spec/mqtt/integrations/message_qos_spec.cr b/spec/mqtt/integrations/message_qos_spec.cr index 156b7d9fd6..82a8c4db74 100644 --- a/spec/mqtt/integrations/message_qos_spec.cr +++ b/spec/mqtt/integrations/message_qos_spec.cr @@ -53,7 +53,6 @@ module MqttSpecs topic_filters = mk_topic_filters({"a/b", 1u8}) subscribe(io, topic_filters: topic_filters) disconnect(io) - sleep 100.milliseconds end with_client_io(server) do |publisher_io| @@ -63,7 +62,6 @@ module MqttSpecs publish(publisher_io, topic: "a/b", qos: 0u8) end disconnect(publisher_io) - sleep 100.milliseconds end with_client_io(server) do |io| @@ -92,7 +90,6 @@ module MqttSpecs publish(publisher_io, topic: "a/b", payload: "1".to_slice, qos: 0u8) publish(publisher_io, topic: "a/b", payload: "2".to_slice, qos: 0u8) disconnect(publisher_io) - sleep 100.milliseconds end pkt = read_packet(io) @@ -101,7 +98,6 @@ module MqttSpecs puback(io, pub.packet_id) end disconnect(io) - sleep 100.milliseconds end with_client_io(server) do |io| @@ -116,7 +112,7 @@ module MqttSpecs end end - pending "acks must not be ordered" do + it "acks must not be ordered" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -151,7 +147,8 @@ module MqttSpecs end end - pending "cannot ack invalid packet id" do + # TODO: rescue so we don't get ugly missing hash key errors + it "cannot ack invalid packet id" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -164,7 +161,7 @@ module MqttSpecs end end - pending "cannot ack a message twice" do + it "cannot ack a message twice" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -189,7 +186,7 @@ module MqttSpecs end end - pending "qos1 unacked messages re-sent in the initial order [MQTT-4.6.0-1]" do + it "qos1 unacked messages re-sent in the initial order [MQTT-4.6.0-1]" do max_inflight_messages = 10 # We'll only ACK odd packet ids, and the first id is 1, so if we don't # do -1 the last packet (id=20) won't be sent because we've reached max diff --git a/spec/mqtt/integrations/unsubscribe_spec.cr b/spec/mqtt/integrations/unsubscribe_spec.cr index 75670ff2bd..ceb7de992c 100644 --- a/spec/mqtt/integrations/unsubscribe_spec.cr +++ b/spec/mqtt/integrations/unsubscribe_spec.cr @@ -53,7 +53,6 @@ module MqttSpecs subscribe(io, topic_filters: topics) disconnect(io) end - sleep 1.second # Publish messages that will be stored for the subscriber 2.times { |i| publish(pubio, topic: "a/b", payload: i.to_s.to_slice, qos: 0u8) } @@ -71,7 +70,6 @@ module MqttSpecs unsubscribe(io, topics: ["a/b"]) disconnect(io) end - sleep 1.second # Publish more messages 2.times { |i| publish(pubio, topic: "a/b", payload: (2 + i).to_s.to_slice, qos: 0u8) } From f3df04c1e869bca24476e540001a3e287753091d Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 16:42:42 +0200 Subject: [PATCH 063/202] spec fix for subtree --- spec/mqtt/subscription_tree_spec.cr | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/mqtt/subscription_tree_spec.cr b/spec/mqtt/subscription_tree_spec.cr index 237ba41c28..1918edeb8a 100644 --- a/spec/mqtt/subscription_tree_spec.cr +++ b/spec/mqtt/subscription_tree_spec.cr @@ -118,11 +118,11 @@ describe LavinMQ::MQTT::SubscriptionTree do it "subscriptions is found" do tree = LavinMQ::MQTT::SubscriptionTree(String).new test_data = [ - {"session", [{"a/b", 0u8}]}, - {"session", [{"a/b", 0u8}]}, - {"session", [{"a/c", 0u8}]}, - {"session", [{"a/+", 0u8}]}, - {"session", [{"#", 0u8}]}, + {"session1", [{"a/b", 0u8}]}, + {"session2", [{"a/b", 0u8}]}, + {"session3", [{"a/c", 0u8}]}, + {"session4", [{"a/+", 0u8}]}, + {"session5", [{"#", 0u8}]}, ] test_data.each do |s| @@ -144,11 +144,11 @@ describe LavinMQ::MQTT::SubscriptionTree do it "unsubscribe unsubscribes" do tree = LavinMQ::MQTT::SubscriptionTree(String).new test_data = [ - {"session", [{"a/b", 0u8}]}, - {"session", [{"a/b", 0u8}]}, - {"session", [{"a/c", 0u8}]}, - {"session", [{"a/+", 0u8}]}, - {"session", [{"#", 0u8}]}, + {"session1", [{"a/b", 0u8}]}, + {"session2", [{"a/b", 0u8}]}, + {"session3", [{"a/c", 0u8}]}, + {"session4", [{"a/+", 0u8}]}, + {"session5", [{"#", 0u8}]}, ] test_data.each do |session, subscriptions| From 95ca6e26a6fe589e2188e5cd8c486edb5e240662 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 16 Oct 2024 10:51:51 +0200 Subject: [PATCH 064/202] set dup value when delivering a msg --- src/lavinmq/mqtt/client.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 88c5e6a91a..aef2ec4715 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -96,6 +96,7 @@ module LavinMQ end def recieve_publish(packet : MQTT::Publish) + rk = @broker.topicfilter_to_routingkey(packet.topic) # TODO: String.new around payload.. should be stored as Bytes msg = Message.new("mqtt.default", rk, String.new(packet.payload)) @@ -189,7 +190,7 @@ module LavinMQ pub_args = { packet_id: packet_id, payload: msg.body, - dup: false, + dup: redelivered, qos: qos, retain: false, topic: "test", From 6d47ab146a809d54dee5d6f7394d5c19b587d075 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 16 Oct 2024 11:18:58 +0200 Subject: [PATCH 065/202] cleanup --- src/lavinmq/mqtt/session.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index e5d193537a..6f3e3b1c2c 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -83,16 +83,16 @@ module LavinMQ private def get(no_ack : Bool, & : Envelope -> Nil) : Bool raise ClosedError.new if @closed - loop do # retry if msg expired or deliver limit hit + loop do id = next_id env = @msg_store_lock.synchronize { @msg_store.shift? } || break - sp = env.segment_position no_ack = env.message.properties.delivery_mode == 0 if no_ack + env.message.properties.message_id = id.to_s begin - yield env # deliver the message - rescue ex # requeue failed delivery + yield env + rescue ex @msg_store_lock.synchronize { @msg_store.requeue(sp) } raise ex end @@ -100,7 +100,7 @@ module LavinMQ else env.message.properties.message_id = id.to_s mark_unacked(sp) do - yield env # deliver the message + yield env @unacked[id] = sp end end From 2274f53d48c96d1d65521d128cc620ca8c7820e1 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 16 Oct 2024 16:39:40 +0200 Subject: [PATCH 066/202] retain_store and topic_tree --- .../integrations/retained_messages_spec.cr | 2 +- src/lavinmq/mqtt/broker.cr | 8 + src/lavinmq/mqtt/client.cr | 1 - src/lavinmq/mqtt/retain_store.cr | 200 ++++++++++++++++++ src/lavinmq/mqtt/topic_tree.cr | 124 +++++++++++ 5 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 src/lavinmq/mqtt/retain_store.cr create mode 100644 src/lavinmq/mqtt/topic_tree.cr diff --git a/spec/mqtt/integrations/retained_messages_spec.cr b/spec/mqtt/integrations/retained_messages_spec.cr index fda6d2c356..5a78bd9a1a 100644 --- a/spec/mqtt/integrations/retained_messages_spec.cr +++ b/spec/mqtt/integrations/retained_messages_spec.cr @@ -4,7 +4,7 @@ module MqttSpecs extend MqttHelpers extend MqttMatchers describe "retained messages" do - pending "retained messages are received on subscribe" do + it "retained messages are received on subscribe" do with_server do |server| with_client_io(server) do |io| connect(io, client_id: "publisher") diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 52f20f8fea..85f6a84a11 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -33,6 +33,9 @@ module LavinMQ def initialize(@vhost : VHost) @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new + #retain store init, and give to exhcange + # #one topic per file line, (use read lines etc) + # #TODO: remember to block the mqtt namespace exchange = MQTTExchange.new(@vhost, "mqtt.default", true, false, true) @vhost.exchanges["mqtt.default"] = exchange end @@ -77,9 +80,14 @@ module LavinMQ rk = topicfilter_to_routingkey(tf.topic) session.subscribe(rk, tf.qos) end + publish_retained_messages packet.topic_filters qos end + def publish_retained_messages(topic_filters) + end + + def unsubscribe(client, packet) session = sessions[client.client_id] packet.topics.each do |tf| diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index aef2ec4715..12508a1a79 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -96,7 +96,6 @@ module LavinMQ end def recieve_publish(packet : MQTT::Publish) - rk = @broker.topicfilter_to_routingkey(packet.topic) # TODO: String.new around payload.. should be stored as Bytes msg = Message.new("mqtt.default", rk, String.new(packet.payload)) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr new file mode 100644 index 0000000000..44fe1023f5 --- /dev/null +++ b/src/lavinmq/mqtt/retain_store.cr @@ -0,0 +1,200 @@ +require "./topic_tree" + +module LavinMQ + class RetainStore + Log = MyraMQ::Log.for("retainstore") + + RETAINED_STORE_DIR_NAME = "retained" + MESSAGE_FILE_SUFFIX = ".msg" + INDEX_FILE_NAME = "index" + + # This is a helper strut to read and write retain index entries + struct RetainIndexEntry + getter topic, sp + + def initialize(@topic : String, @sp : SegmentPosition) + end + + def to_io(io : IO) + io.write_bytes @topic.bytesize.to_u16, ::IO::ByteFormat::NetworkEndian + io.write @topic.to_slice + @sp.to_io(io, ::IO::ByteFormat::NetworkEndian) + end + + def self.from_io(io : IO) + topic_length = UInt16.from_io(io, ::IO::ByteFormat::NetworkEndian) + topic = io.read_string(topic_length) + sp = SegmentPosition.from_io(io, ::IO::ByteFormat::NetworkEndian) + self.new(topic, sp) + end + end + + alias IndexTree = TopicTree(RetainIndexEntry) + + # TODO: change frm "data_dir" to "retained_dir", i.e. pass + # equivalent of File.join(data_dir, RETAINED_STORE_DIR_NAME) as + # data_dir. + def initialize(data_dir : String, @index = IndexTree.new, index_file : IO? = nil) + @dir = File.join(data_dir, RETAINED_STORE_DIR_NAME) + Dir.mkdir_p @dir + + @index_file = index_file || File.new(File.join(@dir, INDEX_FILE_NAME), "a+") + if (buffered_index_file = @index_file).is_a?(IO::Buffered) + buffered_index_file.sync = true + end + + @segment = 0u64 + + @lock = Mutex.new + @lock.synchronize do + if @index.empty? + restore_index(@index, @index_file) + write_index + end + end + end + + def close + @lock.synchronize do + write_index + end + end + + private def restore_index(index : IndexTree, index_file : IO) + Log.info { "restoring index" } + # If @segment is greater than zero we've already restored index or have been + # writing messages + raise "restore_index: can't restore, @segment=#{@segment} > 0" unless @segment.zero? + dir = @dir + + # Create a set with all segment positions based on msg files + msg_file_segments = Set(UInt64).new( + Dir[Path[dir, "*#{MESSAGE_FILE_SUFFIX}"]].compact_map do |fname| + File.basename(fname, MESSAGE_FILE_SUFFIX).to_u64? + end + ) + + segment = 0u64 + msg_count = 0 + loop do + entry = RetainIndexEntry.from_io(index_file) + + unless msg_file_segments.delete(entry.sp.to_u64) + Log.warn { "msg file for topic #{entry.topic} missing, dropping from index" } + next + end + + index.insert(entry.topic, entry) + segment = Math.max(segment, entry.sp.to_u64 + 1) + Log.debug { "restored #{entry.topic}" } + msg_count += 1 + rescue IO::EOFError + break + end + + # TODO: Device what's the truth: index file or msgs file. Mybe drop the index file and rebuild + # index from msg files? + unless msg_file_segments.empty? + Log.warn { "unreferenced messages: " \ + "#{msg_file_segments.map { |u| "#{u}#{MESSAGE_FILE_SUFFIX}" }.join(",")}" } + end + + @segment = segment + Log.info { "restoring index done, @segment = #{@segment}, msg_count = #{msg_count}" } + end + + def retain(topic : String, body : Bytes) : Nil + @lock.synchronize do + Log.trace { "retain topic=#{topic} body.bytesize=#{body.bytesize}" } + + # An empty message with retain flag means clear the topic from retained messages + if body.bytesize.zero? + delete_from_index(topic) + return + end + + sp = if entry = @index[topic]? + entry.sp + else + new_sp = SegmentPosition.new(@segment) + @segment += 1 + add_to_index(topic, new_sp) + new_sp + end + + File.open(File.join(@dir, self.class.make_file_name(sp)), "w+") do |f| + f.sync = true + MessageData.new(topic, body).to_io(f) + end + end + end + + private def write_index + tmp_file = File.join(@dir, "#{INDEX_FILE_NAME}.next") + File.open(tmp_file, "w+") do |f| + @index.each do |entry| + entry.to_io(f) + end + end + File.rename tmp_file, File.join(@dir, INDEX_FILE_NAME) + ensure + FileUtils.rm_rf tmp_file unless tmp_file.nil? + end + + private def add_to_index(topic : String, sp : SegmentPosition) : Nil + entry = RetainIndexEntry.new(topic, sp) + @index.insert topic, entry + entry.to_io(@index_file) + end + + private def delete_from_index(topic : String) : Nil + if entry = @index.delete topic + file_name = self.class.make_file_name(entry.sp) + Log.trace { "deleted '#{topic}' from index, deleting file #{file_name}" } + File.delete? File.join(@dir, file_name) + end + end + + def each(subscription : String, &block : String, SegmentPosition -> Nil) : Nil + @lock.synchronize do + @index.each(subscription) do |entry| + block.call(entry.topic, entry.sp) + end + end + nil + end + + def read(sp : SegmentPosition) : MessageData? + unless sp.retain? + Log.error { "can't handle sp with retain=true" } + return + end + @lock.synchronize do + read_message_file(sp) + end + end + + def retained_messages + @lock.synchronize do + @index.size + end + end + + @[AlwaysInline] + private def read_message_file(sp : SegmentPosition) : MessageData? + file_name = self.class.make_file_name(sp) + file = File.join @dir, file_name + File.open(file, "r") do |f| + MessageData.from_io(f) + end + rescue e : File::NotFoundError + Log.error { "message file #{file_name} doesn't exist" } + nil + end + + @[AlwaysInline] + def self.make_file_name(sp : SegmentPosition) : String + "#{sp.to_u64}#{MESSAGE_FILE_SUFFIX}" + end + end +end diff --git a/src/lavinmq/mqtt/topic_tree.cr b/src/lavinmq/mqtt/topic_tree.cr new file mode 100644 index 0000000000..e2932d00ff --- /dev/null +++ b/src/lavinmq/mqtt/topic_tree.cr @@ -0,0 +1,124 @@ + +module LavinMQ + class TopicTree(TEntity) + @sublevels = Hash(String, TopicTree(TEntity)).new do |h, k| + h[k] = TopicTree(TEntity).new + end + + @leafs = Hash(String, TEntity).new + + def initialize + end + + # Returns the replaced value or nil + def insert(topic : String, entity : TEntity) : TEntity? + insert(StringTokenIterator.new(topic, '/'), entity) + end + + # Returns the replaced value or nil + def insert(topic : StringTokenIterator, entity : TEntity) : TEntity? + current = topic.next.not_nil! + if topic.next? + @sublevels[current].insert(topic, entity) + else + old_value = @leafs[current]? + @leafs[current] = entity + old_value + end + end + + def []?(topic : String) : (TEntity | Nil) + self[StringTokenIterator.new(topic, '/')]? + end + + def []?(topic : StringTokenIterator) : (TEntity | Nil) + current = topic.next + if topic.next? + return unless @sublevels.has_key?(current) + @sublevels[current][topic]? + else + @leafs[current]? + end + end + + def [](topic : String) : TEntity + self[StringTokenIterator.new(topic, '/')] + rescue KeyError + raise KeyError.new "#{topic} not found" + end + + def [](topic : StringTokenIterator) : TEntity + current = topic.next + if topic.next? + raise KeyError.new unless @sublevels.has_key?(current) + @sublevels[current][topic] + else + @leafs[current] + end + end + + def delete(topic : String) + delete(StringTokenIterator.new(topic, '/')) + end + + def delete(topic : StringTokenIterator) + current = topic.next + if topic.next? + return unless @sublevels.has_key?(current) + deleted = @sublevels[current].delete(topic) + if @sublevels[current].empty? + @sublevels.delete(current) + end + deleted + else + @leafs.delete(current) + end + end + + def empty? + @leafs.empty? && @sublevels.empty? + end + + def size + @leafs.size + @sublevels.values.sum(0, &.size) + end + + def each(filter : String, &blk : (TEntity) -> _) + each(StringTokenIterator.new(filter, '/'), &blk) + end + + def each(filter : StringTokenIterator, &blk : (TEntity) -> _) + current = filter.next + if current == "#" + each &blk + return + end + if current == "+" + if filter.next? + @sublevels.values.each(&.each(filter, &blk)) + else + @leafs.values.each &blk + end + return + end + if filter.next? + if sublevel = @sublevels.fetch(current, nil) + sublevel.each filter, &blk + end + else + if leaf = @leafs.fetch(current, nil) + yield leaf + end + end + end + + def each(&blk : (TEntity) -> _) + @leafs.values.each &blk + @sublevels.values.each(&.each(&blk)) + end + + def inspect + "#{self.class.name}(@sublevels=#{@sublevels.inspect} @leafs=#{@leafs.inspect})" + end + end +end From ad4520cb9ca767a6ced107360722007fa63f54b1 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 17 Oct 2024 13:31:29 +0200 Subject: [PATCH 067/202] Restructure retain-store and topic_tree for better fit in LavinMQ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jon Börjesson --- src/lavinmq/exchange/mqtt.cr | 15 ++ src/lavinmq/mqtt/broker.cr | 21 +-- src/lavinmq/mqtt/client.cr | 8 +- src/lavinmq/mqtt/retain_store.cr | 279 ++++++++++++------------------- src/lavinmq/mqtt/topic_tree.cr | 189 ++++++++++----------- src/lavinmq/server.cr | 4 +- 6 files changed, 241 insertions(+), 275 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 1f1b758fc0..bf230577ac 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -1,9 +1,11 @@ require "./exchange" require "../mqtt/subscription_tree" require "../mqtt/session" +require "../mqtt/retain_store" module LavinMQ class MQTTExchange < Exchange + struct MqttBindingKey def initialize(routing_key : String, arguments : AMQP::Table? = nil) @binding_key = BindingKey.new(routing_key, arguments) @@ -27,10 +29,19 @@ module LavinMQ "mqtt" end + def initialize(vhost : VHost, name : String, @retain_store : MQTT::RetainStore) + super(vhost, name, true, false, true) + end + private def do_publish(msg : Message, immediate : Bool, queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 count = 0 + + if msg.properties.try &.headers.try &.["x-mqtt-retain"]? + @retain_store.retain(routing_key_to_topic(msg.routing_key), msg.body_io, msg.bodysize) + end + @tree.each_entry(msg.routing_key) do |queue, qos| msg.properties.delivery_mode = qos if queue.publish(msg) @@ -41,6 +52,10 @@ module LavinMQ count end + def routing_key_to_topic(routing_key : String) : String + routing_key.tr(".*", "/+") + end + def bindings_details : Iterator(BindingDetails) @bindings.each.flat_map do |binding_key, ds| ds.each.map do |d| diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 85f6a84a11..1da6f7da63 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -1,3 +1,5 @@ +require "./retain_store" + module LavinMQ module MQTT struct Sessions @@ -31,12 +33,11 @@ module LavinMQ getter vhost, sessions def initialize(@vhost : VHost) + #TODO: remember to block the mqtt namespace @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new - #retain store init, and give to exhcange - # #one topic per file line, (use read lines etc) - # #TODO: remember to block the mqtt namespace - exchange = MQTTExchange.new(@vhost, "mqtt.default", true, false, true) + @retained_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s) + exchange = MQTTExchange.new(@vhost, "mqtt.default", @retained_store) @vhost.exchanges["mqtt.default"] = exchange end @@ -79,15 +80,11 @@ module LavinMQ qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) rk = topicfilter_to_routingkey(tf.topic) session.subscribe(rk, tf.qos) + # deliver retained messages retain store .each (Use TF!!!) end - publish_retained_messages packet.topic_filters qos end - def publish_retained_messages(topic_filters) - end - - def unsubscribe(client, packet) session = sessions[client.client_id] packet.topics.each do |tf| @@ -97,12 +94,16 @@ module LavinMQ end def topicfilter_to_routingkey(tf) : String - tf.gsub("/", ".") + tf.tr("/+", ".*") end def clear_session(client_id) sessions.delete client_id end + + def close() + @retain_store.close + end end end end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 12508a1a79..78e49c4173 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -97,10 +97,14 @@ module LavinMQ def recieve_publish(packet : MQTT::Publish) rk = @broker.topicfilter_to_routingkey(packet.topic) + properties = if packet.retain? + AMQP::Properties.new(headers: AMQP::Table.new({ "x-mqtt-retain": true})) + else + AMQ::Protocol::Properties.new + end # TODO: String.new around payload.. should be stored as Bytes - msg = Message.new("mqtt.default", rk, String.new(packet.payload)) + msg = Message.new("mqtt.default", rk, String.new(packet.payload), properties) @broker.vhost.publish(msg) - # Ok to not send anything if qos = 0 (at most once delivery) if packet.qos > 0 && (packet_id = packet.packet_id) send(MQTT::PubAck.new(packet_id)) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 44fe1023f5..baa1219510 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -1,200 +1,143 @@ require "./topic_tree" +require "digest/sha256" module LavinMQ - class RetainStore - Log = MyraMQ::Log.for("retainstore") - - RETAINED_STORE_DIR_NAME = "retained" - MESSAGE_FILE_SUFFIX = ".msg" - INDEX_FILE_NAME = "index" - - # This is a helper strut to read and write retain index entries - struct RetainIndexEntry - getter topic, sp - - def initialize(@topic : String, @sp : SegmentPosition) - end - - def to_io(io : IO) - io.write_bytes @topic.bytesize.to_u16, ::IO::ByteFormat::NetworkEndian - io.write @topic.to_slice - @sp.to_io(io, ::IO::ByteFormat::NetworkEndian) - end - - def self.from_io(io : IO) - topic_length = UInt16.from_io(io, ::IO::ByteFormat::NetworkEndian) - topic = io.read_string(topic_length) - sp = SegmentPosition.from_io(io, ::IO::ByteFormat::NetworkEndian) - self.new(topic, sp) - end - end - - alias IndexTree = TopicTree(RetainIndexEntry) - - # TODO: change frm "data_dir" to "retained_dir", i.e. pass - # equivalent of File.join(data_dir, RETAINED_STORE_DIR_NAME) as - # data_dir. - def initialize(data_dir : String, @index = IndexTree.new, index_file : IO? = nil) - @dir = File.join(data_dir, RETAINED_STORE_DIR_NAME) - Dir.mkdir_p @dir - - @index_file = index_file || File.new(File.join(@dir, INDEX_FILE_NAME), "a+") - if (buffered_index_file = @index_file).is_a?(IO::Buffered) - buffered_index_file.sync = true + module MQTT + class RetainStore + Log = LavinMQ::Log.for("retainstore") + + MESSAGE_FILE_SUFFIX = ".msg" + INDEX_FILE_NAME = "index" + + alias IndexTree = TopicTree(String) + + def initialize(@dir : String, @index = IndexTree.new) + Dir.mkdir_p @dir + @index_file = File.new(File.join(@dir, INDEX_FILE_NAME), "a+") + @lock = Mutex.new + @lock.synchronize do + if @index.empty? + restore_index(@index, @index_file) + write_index + @index_file = File.new(File.join(@dir, INDEX_FILE_NAME), "a+") + end + end end - @segment = 0u64 - - @lock = Mutex.new - @lock.synchronize do - if @index.empty? - restore_index(@index, @index_file) + def close + @lock.synchronize do write_index end end - end - def close - @lock.synchronize do - write_index - end - end - - private def restore_index(index : IndexTree, index_file : IO) - Log.info { "restoring index" } - # If @segment is greater than zero we've already restored index or have been - # writing messages - raise "restore_index: can't restore, @segment=#{@segment} > 0" unless @segment.zero? - dir = @dir - - # Create a set with all segment positions based on msg files - msg_file_segments = Set(UInt64).new( - Dir[Path[dir, "*#{MESSAGE_FILE_SUFFIX}"]].compact_map do |fname| - File.basename(fname, MESSAGE_FILE_SUFFIX).to_u64? + private def restore_index(index : IndexTree, index_file : ::IO) + Log.info { "restoring index" } + dir = @dir + msg_count = 0 + msg_file_segments = Set(String).new( + Dir[Path[dir, "*#{MESSAGE_FILE_SUFFIX}"]].compact_map do |fname| + File.basename(fname) + end + ) + + while topic = index_file.gets + msg_file_name = make_file_name(topic) + unless msg_file_segments.delete(msg_file_name) + Log.warn { "msg file for topic #{topic} missing, dropping from index" } + next + end + index.insert(topic, msg_file_name) + Log.debug { "restored #{topic}" } + msg_count += 1 end - ) - - segment = 0u64 - msg_count = 0 - loop do - entry = RetainIndexEntry.from_io(index_file) - unless msg_file_segments.delete(entry.sp.to_u64) - Log.warn { "msg file for topic #{entry.topic} missing, dropping from index" } - next + # TODO: Device what's the truth: index file or msgs file. Mybe drop the index file and rebuild + # index from msg files? + unless msg_file_segments.empty? + Log.warn { "unreferenced messages: #{msg_file_segments.join(",")}" } end - - index.insert(entry.topic, entry) - segment = Math.max(segment, entry.sp.to_u64 + 1) - Log.debug { "restored #{entry.topic}" } - msg_count += 1 - rescue IO::EOFError - break - end - - # TODO: Device what's the truth: index file or msgs file. Mybe drop the index file and rebuild - # index from msg files? - unless msg_file_segments.empty? - Log.warn { "unreferenced messages: " \ - "#{msg_file_segments.map { |u| "#{u}#{MESSAGE_FILE_SUFFIX}" }.join(",")}" } - end - - @segment = segment - Log.info { "restoring index done, @segment = #{@segment}, msg_count = #{msg_count}" } - end - - def retain(topic : String, body : Bytes) : Nil - @lock.synchronize do - Log.trace { "retain topic=#{topic} body.bytesize=#{body.bytesize}" } - - # An empty message with retain flag means clear the topic from retained messages - if body.bytesize.zero? - delete_from_index(topic) - return - end - - sp = if entry = @index[topic]? - entry.sp - else - new_sp = SegmentPosition.new(@segment) - @segment += 1 - add_to_index(topic, new_sp) - new_sp - end - - File.open(File.join(@dir, self.class.make_file_name(sp)), "w+") do |f| - f.sync = true - MessageData.new(topic, body).to_io(f) + #TODO: delete unreferenced messages? + Log.info { "restoring index done, msg_count = #{msg_count}" } + end + + def retain(topic : String, body_io : ::IO, size : UInt64) : Nil + @lock.synchronize do + Log.debug { "retain topic=#{topic} body.bytesize=#{size}" } + # An empty message with retain flag means clear the topic from retained messages + if size.zero? + delete_from_index(topic) + return + end + + unless msg_file_name = @index[topic]? + msg_file_name = make_file_name(topic) + add_to_index(topic, msg_file_name) + end + + File.open(File.join(@dir, msg_file_name), "w+") do |f| + f.sync = true + ::IO.copy(body_io, f) + end end end - end - private def write_index - tmp_file = File.join(@dir, "#{INDEX_FILE_NAME}.next") - File.open(tmp_file, "w+") do |f| - @index.each do |entry| - entry.to_io(f) + private def write_index + tmp_file = File.join(@dir, "#{INDEX_FILE_NAME}.next") + File.open(tmp_file, "w+") do |f| + @index.each do |topic, _filename| + f.puts topic + end end + File.rename tmp_file, File.join(@dir, INDEX_FILE_NAME) + ensure + FileUtils.rm_rf tmp_file unless tmp_file.nil? end - File.rename tmp_file, File.join(@dir, INDEX_FILE_NAME) - ensure - FileUtils.rm_rf tmp_file unless tmp_file.nil? - end - - private def add_to_index(topic : String, sp : SegmentPosition) : Nil - entry = RetainIndexEntry.new(topic, sp) - @index.insert topic, entry - entry.to_io(@index_file) - end - private def delete_from_index(topic : String) : Nil - if entry = @index.delete topic - file_name = self.class.make_file_name(entry.sp) - Log.trace { "deleted '#{topic}' from index, deleting file #{file_name}" } - File.delete? File.join(@dir, file_name) + private def add_to_index(topic : String, file_name : String) : Nil + @index.insert topic, file_name + @index_file.puts topic + @index_file.flush end - end - def each(subscription : String, &block : String, SegmentPosition -> Nil) : Nil - @lock.synchronize do - @index.each(subscription) do |entry| - block.call(entry.topic, entry.sp) + private def delete_from_index(topic : String) : Nil + if file_name = @index.delete topic + Log.trace { "deleted '#{topic}' from index, deleting file #{file_name}" } + File.delete? File.join(@dir, file_name) end end - nil - end - def read(sp : SegmentPosition) : MessageData? - unless sp.retain? - Log.error { "can't handle sp with retain=true" } - return - end - @lock.synchronize do - read_message_file(sp) + def each(subscription : String, &block : String, Bytes -> Nil) : Nil + @lock.synchronize do + @index.each(subscription) do |topic, file_name| + yield topic, read(file_name) + end + end + nil end - end - def retained_messages - @lock.synchronize do - @index.size + private def read(file_name : String) : Bytes + @lock.synchronize do + File.open(File.join(@dir, file_name), "r") do |f| + body = slice.new(f.size) + f.read_fully(body) + body + end + end end - end - @[AlwaysInline] - private def read_message_file(sp : SegmentPosition) : MessageData? - file_name = self.class.make_file_name(sp) - file = File.join @dir, file_name - File.open(file, "r") do |f| - MessageData.from_io(f) + def retained_messages + @lock.synchronize do + @index.size + end end - rescue e : File::NotFoundError - Log.error { "message file #{file_name} doesn't exist" } - nil - end - @[AlwaysInline] - def self.make_file_name(sp : SegmentPosition) : String - "#{sp.to_u64}#{MESSAGE_FILE_SUFFIX}" + @hasher = Digest::MD5.new + def make_file_name(topic : String) : String + @hasher.update topic.to_slice + hash = @hasher.hexfinal + @hasher.reset + "#{hash}#{MESSAGE_FILE_SUFFIX}" + end end end end diff --git a/src/lavinmq/mqtt/topic_tree.cr b/src/lavinmq/mqtt/topic_tree.cr index e2932d00ff..b1fa6fd25c 100644 --- a/src/lavinmq/mqtt/topic_tree.cr +++ b/src/lavinmq/mqtt/topic_tree.cr @@ -1,124 +1,125 @@ +require "./string_token_iterator" module LavinMQ - class TopicTree(TEntity) - @sublevels = Hash(String, TopicTree(TEntity)).new do |h, k| - h[k] = TopicTree(TEntity).new - end + module MQTT + class TopicTree(TEntity) + @sublevels = Hash(String, TopicTree(TEntity)).new do |h, k| + h[k] = TopicTree(TEntity).new + end - @leafs = Hash(String, TEntity).new + @leafs = Hash(String, Tuple(String, TEntity)).new - def initialize - end + def initialize + end - # Returns the replaced value or nil - def insert(topic : String, entity : TEntity) : TEntity? - insert(StringTokenIterator.new(topic, '/'), entity) - end + def insert(topic : String, entity : TEntity) : TEntity? + insert(StringTokenIterator.new(topic, '/'), entity) + end - # Returns the replaced value or nil - def insert(topic : StringTokenIterator, entity : TEntity) : TEntity? - current = topic.next.not_nil! - if topic.next? - @sublevels[current].insert(topic, entity) - else - old_value = @leafs[current]? - @leafs[current] = entity - old_value + def insert(topic : StringTokenIterator, entity : TEntity) : TEntity? + current = topic.next.not_nil! + if topic.next? + @sublevels[current].insert(topic, entity) + else + old_value = @leafs[current]? + @leafs[current] = {topic.to_s, entity} + old_value.try &.last + end end - end - def []?(topic : String) : (TEntity | Nil) - self[StringTokenIterator.new(topic, '/')]? - end + def []?(topic : String) : (TEntity | Nil) + self[StringTokenIterator.new(topic, '/')]? + end - def []?(topic : StringTokenIterator) : (TEntity | Nil) - current = topic.next - if topic.next? - return unless @sublevels.has_key?(current) - @sublevels[current][topic]? - else - @leafs[current]? + def []?(topic : StringTokenIterator) : (TEntity | Nil) + current = topic.next + if topic.next? + return unless @sublevels.has_key?(current) + @sublevels[current][topic]? + else + @leafs[current]?.try &.last + end end - end - def [](topic : String) : TEntity - self[StringTokenIterator.new(topic, '/')] - rescue KeyError - raise KeyError.new "#{topic} not found" - end + def [](topic : String) : TEntity + self[StringTokenIterator.new(topic, '/')] + rescue KeyError + raise KeyError.new "#{topic} not found" + end - def [](topic : StringTokenIterator) : TEntity - current = topic.next - if topic.next? - raise KeyError.new unless @sublevels.has_key?(current) - @sublevels[current][topic] - else - @leafs[current] + def [](topic : StringTokenIterator) : TEntity + current = topic.next + if topic.next? + raise KeyError.new unless @sublevels.has_key?(current) + @sublevels[current][topic] + else + @leafs[current].last + end end - end - def delete(topic : String) - delete(StringTokenIterator.new(topic, '/')) - end + def delete(topic : String) + delete(StringTokenIterator.new(topic, '/')) + end - def delete(topic : StringTokenIterator) - current = topic.next - if topic.next? - return unless @sublevels.has_key?(current) - deleted = @sublevels[current].delete(topic) - if @sublevels[current].empty? - @sublevels.delete(current) + def delete(topic : StringTokenIterator) + current = topic.next + if topic.next? + return unless @sublevels.has_key?(current) + deleted = @sublevels[current].delete(topic) + if @sublevels[current].empty? + @sublevels.delete(current) + end + deleted + else + @leafs.delete(current).try &.last end - deleted - else - @leafs.delete(current) end - end - - def empty? - @leafs.empty? && @sublevels.empty? - end - def size - @leafs.size + @sublevels.values.sum(0, &.size) - end + def empty? + @leafs.empty? && @sublevels.empty? + end - def each(filter : String, &blk : (TEntity) -> _) - each(StringTokenIterator.new(filter, '/'), &blk) - end + def size + @leafs.size + @sublevels.values.sum(0, &.size) + end - def each(filter : StringTokenIterator, &blk : (TEntity) -> _) - current = filter.next - if current == "#" - each &blk - return + def each(filter : String, &blk : (Stirng, TEntity) -> _) + each(StringTokenIterator.new(filter, '/'), &blk) end - if current == "+" - if filter.next? - @sublevels.values.each(&.each(filter, &blk)) - else - @leafs.values.each &blk + + def each(filter : StringTokenIterator, &blk : (String, TEntity) -> _) + current = filter.next + if current == "#" + each &blk + return end - return - end - if filter.next? - if sublevel = @sublevels.fetch(current, nil) - sublevel.each filter, &blk + if current == "+" + if filter.next? + @sublevels.values.each(&.each(filter, &blk)) + else + @leafs.values.each &blk + end + return end - else - if leaf = @leafs.fetch(current, nil) - yield leaf + if filter.next? + if sublevel = @sublevels.fetch(current, nil) + sublevel.each filter, &blk + end + else + if leaf = @leafs.fetch(current, nil) + yield leaf.first, leaf.last + end end end - end - def each(&blk : (TEntity) -> _) - @leafs.values.each &blk - @sublevels.values.each(&.each(&blk)) - end + def each(&blk : (String, TEntity) -> _) + @leafs.values.each &blk + @sublevels.values.each(&.each(&blk)) + end - def inspect - "#{self.class.name}(@sublevels=#{@sublevels.inspect} @leafs=#{@leafs.inspect})" + def inspect + "#{self.class.name}(@sublevels=#{@sublevels.inspect} @leafs=#{@leafs.inspect})" + end end end end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 57e39f2976..918e63b270 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -39,7 +39,8 @@ module LavinMQ @vhosts = VHostStore.new(@data_dir, @users, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) @amqp_connection_factory = LavinMQ::AMQP::ConnectionFactory.new - @mqtt_connection_factory = MQTT::ConnectionFactory.new(@users, @vhosts["/"], MQTT::Broker.new(@vhosts["/"])) + @broker = LavinMQ::MQTT::Broker.new(@vhosts["/"]) + @mqtt_connection_factory = MQTT::ConnectionFactory.new(@users, @vhosts["/"], @broker) apply_parameter spawn stats_loop, name: "Server#stats_loop" end @@ -63,6 +64,7 @@ module LavinMQ @closed = true @vhosts.close @replicator.clear + @broker.close Fiber.yield end From fc2245e368341b8558e84dfffbb844447d77024a Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 17 Oct 2024 16:44:11 +0200 Subject: [PATCH 068/202] publish retained message --- src/lavinmq/exchange/mqtt.cr | 1 - src/lavinmq/mqtt/broker.cr | 9 ++++++--- src/lavinmq/mqtt/retain_store.cr | 8 ++++---- src/lavinmq/mqtt/topic_tree.cr | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index bf230577ac..2fd88ce103 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -37,7 +37,6 @@ module LavinMQ queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 count = 0 - if msg.properties.try &.headers.try &.["x-mqtt-retain"]? @retain_store.retain(routing_key_to_topic(msg.routing_key), msg.body_io, msg.bodysize) end diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 1da6f7da63..f866807aa1 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -36,8 +36,8 @@ module LavinMQ #TODO: remember to block the mqtt namespace @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new - @retained_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s) - exchange = MQTTExchange.new(@vhost, "mqtt.default", @retained_store) + @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s) + exchange = MQTTExchange.new(@vhost, "mqtt.default", @retain_store) @vhost.exchanges["mqtt.default"] = exchange end @@ -80,7 +80,10 @@ module LavinMQ qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) rk = topicfilter_to_routingkey(tf.topic) session.subscribe(rk, tf.qos) - # deliver retained messages retain store .each (Use TF!!!) + @retain_store.each(tf.topic) do |topic, body| + msg = Message.new("mqtt.default", topic, String.new(body)) + @vhost.publish(msg) + end end qos end diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index baa1219510..3487fab7cc 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -109,20 +109,20 @@ module LavinMQ def each(subscription : String, &block : String, Bytes -> Nil) : Nil @lock.synchronize do @index.each(subscription) do |topic, file_name| - yield topic, read(file_name) + block.call(topic, read(file_name)) end end nil end private def read(file_name : String) : Bytes - @lock.synchronize do + # @lock.synchronize do File.open(File.join(@dir, file_name), "r") do |f| - body = slice.new(f.size) + body = Slice(UInt8).new(f.size) f.read_fully(body) body end - end + # end end def retained_messages diff --git a/src/lavinmq/mqtt/topic_tree.cr b/src/lavinmq/mqtt/topic_tree.cr index b1fa6fd25c..e39e030a98 100644 --- a/src/lavinmq/mqtt/topic_tree.cr +++ b/src/lavinmq/mqtt/topic_tree.cr @@ -83,7 +83,7 @@ module LavinMQ @leafs.size + @sublevels.values.sum(0, &.size) end - def each(filter : String, &blk : (Stirng, TEntity) -> _) + def each(filter : String, &blk : (String, TEntity) -> _) each(StringTokenIterator.new(filter, '/'), &blk) end From df43824dc156dccb1f08cc0865295fa94099f6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20Dahl=C3=A9n?= <85930202+kickster97@users.noreply.github.com> Date: Fri, 18 Oct 2024 08:59:53 +0200 Subject: [PATCH 069/202] Update src/lavinmq/mqtt/retain_store.cr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jon Börjesson --- src/lavinmq/mqtt/retain_store.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 3487fab7cc..b2e4d9c02b 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -118,7 +118,7 @@ module LavinMQ private def read(file_name : String) : Bytes # @lock.synchronize do File.open(File.join(@dir, file_name), "r") do |f| - body = Slice(UInt8).new(f.size) + body = Bytes.new(f.size) f.read_fully(body) body end From afd66b80eceaf55e4afcc177468c4de1a983f87a Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 18 Oct 2024 11:47:09 +0200 Subject: [PATCH 070/202] retained messages spec pass --- .../integrations/retained_messages_spec.cr | 6 +-- src/lavinmq/exchange/mqtt.cr | 5 ++- src/lavinmq/mqtt/broker.cr | 37 +++++++++++++------ src/lavinmq/mqtt/client.cr | 17 +++------ src/lavinmq/mqtt/session.cr | 10 ++++- 5 files changed, 44 insertions(+), 31 deletions(-) diff --git a/spec/mqtt/integrations/retained_messages_spec.cr b/spec/mqtt/integrations/retained_messages_spec.cr index 5a78bd9a1a..d260be6447 100644 --- a/spec/mqtt/integrations/retained_messages_spec.cr +++ b/spec/mqtt/integrations/retained_messages_spec.cr @@ -23,7 +23,7 @@ module MqttSpecs end end - pending "retained messages are redelivered for subscriptions with qos1" do + it "retained messages are redelivered for subscriptions with qos1" do with_server do |server| with_client_io(server) do |io| connect(io, client_id: "publisher") @@ -54,7 +54,7 @@ module MqttSpecs end end - pending "retain is set in PUBLISH for retained messages" do + it "retain is set in PUBLISH for retained messages" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -69,7 +69,7 @@ module MqttSpecs subscribe(io, topic_filters: topic_filters) pub = read_packet(io).as(MQTT::Protocol::Publish) - pub.retain?.should be(true) + pub.retain?.should eq(true) disconnect(io) end diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 2fd88ce103..06a38a7043 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -37,11 +37,12 @@ module LavinMQ queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 count = 0 + topic = routing_key_to_topic(msg.routing_key) if msg.properties.try &.headers.try &.["x-mqtt-retain"]? - @retain_store.retain(routing_key_to_topic(msg.routing_key), msg.body_io, msg.bodysize) + @retain_store.retain(topic, msg.body_io, msg.bodysize) end - @tree.each_entry(msg.routing_key) do |queue, qos| + @tree.each_entry(topic) do |queue, qos| msg.properties.delivery_mode = qos if queue.publish(msg) count += 1 diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index f866807aa1..02c9b6bff2 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -37,8 +37,8 @@ module LavinMQ @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s) - exchange = MQTTExchange.new(@vhost, "mqtt.default", @retain_store) - @vhost.exchanges["mqtt.default"] = exchange + @exchange = MQTTExchange.new(@vhost, "mqtt.default", @retain_store) + @vhost.exchanges["mqtt.default"] = @exchange end def session_present?(client_id : String, clean_session) : Bool @@ -70,6 +70,22 @@ module LavinMQ @clients.delete client_id end + def publish(packet : MQTT::Publish) + rk = topicfilter_to_routingkey(packet.topic) + properties = if packet.retain? + AMQP::Properties.new(headers: AMQP::Table.new({ "x-mqtt-retain": true})) + else + AMQ::Protocol::Properties.new + end + # TODO: String.new around payload.. should be stored as Bytes + msg = Message.new("mqtt.default", rk, String.new(packet.payload), properties) + @exchange.publish(msg, false) + end + + def topicfilter_to_routingkey(tf) : String + tf.tr("/+", ".*") + end + def subscribe(client, packet) unless session = @sessions[client.client_id]? session = sessions.declare(client.client_id, client.@clean_session) @@ -78,11 +94,13 @@ module LavinMQ qos = Array(MQTT::SubAck::ReturnCode).new(packet.topic_filters.size) packet.topic_filters.each do |tf| qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) - rk = topicfilter_to_routingkey(tf.topic) - session.subscribe(rk, tf.qos) + session.subscribe(tf.topic, tf.qos) @retain_store.each(tf.topic) do |topic, body| - msg = Message.new("mqtt.default", topic, String.new(body)) - @vhost.publish(msg) + rk = topicfilter_to_routingkey(topic) + msg = Message.new("mqtt.default", rk, String.new(body), + AMQP::Properties.new(headers: AMQP::Table.new({ "x-mqtt-retain": true}), + delivery_mode: tf.qos )) + session.publish(msg) end end qos @@ -91,15 +109,10 @@ module LavinMQ def unsubscribe(client, packet) session = sessions[client.client_id] packet.topics.each do |tf| - rk = topicfilter_to_routingkey(tf) - session.unsubscribe(rk) + session.unsubscribe(tf) end end - def topicfilter_to_routingkey(tf) : String - tf.tr("/+", ".*") - end - def clear_session(client_id) sessions.delete client_id end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 78e49c4173..8daaff6a2b 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -68,7 +68,7 @@ module LavinMQ def read_and_handle_packet packet : MQTT::Packet = MQTT::Packet.from_io(@io) - @log.info { "Recieved packet: #{packet.inspect}" } + @log.trace { "Recieved packet: #{packet.inspect}" } @recv_oct_count += packet.bytesize case packet @@ -96,15 +96,7 @@ module LavinMQ end def recieve_publish(packet : MQTT::Publish) - rk = @broker.topicfilter_to_routingkey(packet.topic) - properties = if packet.retain? - AMQP::Properties.new(headers: AMQP::Table.new({ "x-mqtt-retain": true})) - else - AMQ::Protocol::Properties.new - end - # TODO: String.new around payload.. should be stored as Bytes - msg = Message.new("mqtt.default", rk, String.new(packet.payload), properties) - @broker.vhost.publish(msg) + @broker.publish(packet) # Ok to not send anything if qos = 0 (at most once delivery) if packet.qos > 0 && (packet_id = packet.packet_id) send(MQTT::PubAck.new(packet_id)) @@ -188,6 +180,7 @@ module LavinMQ if message_id = msg.properties.message_id packet_id = message_id.to_u16 unless message_id.empty? end + retained = msg.properties.try &.headers.try &.["x-mqtt-retain"]? == true qos = msg.properties.delivery_mode || 0u8 pub_args = { @@ -195,8 +188,8 @@ module LavinMQ payload: msg.body, dup: redelivered, qos: qos, - retain: false, - topic: "test", + retain: retained, + topic: msg.routing_key.tr(".", "/"), } @client.send(::MQTT::Protocol::Publish.new(**pub_args)) # MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 6f3e3b1c2c..b1e6fe24b4 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -58,7 +58,8 @@ module LavinMQ !clean_session? end - def subscribe(rk, qos) + def subscribe(tf, qos) + rk = topicfilter_to_routingkey(tf) arguments = AMQP::Table.new({"x-mqtt-qos": qos}) if binding = find_binding(rk) return if binding.binding_key.arguments == arguments @@ -67,12 +68,17 @@ module LavinMQ @vhost.bind_queue(@name, "mqtt.default", rk, arguments) end - def unsubscribe(rk) + def unsubscribe(tf) + rk = topicfilter_to_routingkey(tf) if binding = find_binding(rk) unbind(rk, binding.binding_key.arguments) end end + def topicfilter_to_routingkey(tf) : String + tf.tr("/+", ".*") + end + private def find_binding(rk) bindings.find { |b| b.binding_key.routing_key == rk } end From c14ba7f4be341e5881f149ab1a6ade2f9310bec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 18 Oct 2024 14:54:38 +0200 Subject: [PATCH 071/202] Improve handling of non-clean sesssions Delete existing session on connect if it's a clean session. Probably a potential bug in this too. Removes the need for sleeps in specs anymore. --- spec/mqtt/integrations/connect_spec.cr | 4 ---- src/lavinmq/mqtt/broker.cr | 30 ++++++++++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index ecd4835eb2..426cc76833 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -31,7 +31,6 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: true) @@ -51,7 +50,6 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: false) @@ -71,7 +69,6 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: true) @@ -91,7 +88,6 @@ module MqttSpecs packet_id: 1u16 ) disconnect(io) - sleep 100.milliseconds end with_client_io(server) do |io| connack = connect(io, clean_session: false) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 02c9b6bff2..8bbc2fcd31 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -27,13 +27,17 @@ module LavinMQ def delete(client_id : String) @vhost.delete_queue("amq.mqtt-#{client_id}") end + + def delete(session : Session) + session.delete + end end class Broker getter vhost, sessions def initialize(@vhost : VHost) - #TODO: remember to block the mqtt namespace + # TODO: remember to block the mqtt namespace @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s) @@ -42,8 +46,9 @@ module LavinMQ end def session_present?(client_id : String, clean_session) : Bool + return false if clean_session session = @sessions[client_id]? - return false if session.nil? || clean_session + return false if session.nil? || session.clean_session? true end @@ -52,10 +57,13 @@ module LavinMQ Log.trace { "Found previous client connected with client_id: #{packet.client_id}, closing" } prev_client.close end - client = MQTT::Client.new(socket, connection_info, user, vhost, self, packet.client_id, packet.clean_session?, packet.will) if session = @sessions[client.client_id]? - session.client = client + if session.clean_session? + sessions.delete session + else + session.client = client + end end @clients[packet.client_id] = client client @@ -73,10 +81,10 @@ module LavinMQ def publish(packet : MQTT::Publish) rk = topicfilter_to_routingkey(packet.topic) properties = if packet.retain? - AMQP::Properties.new(headers: AMQP::Table.new({ "x-mqtt-retain": true})) - else - AMQ::Protocol::Properties.new - end + AMQP::Properties.new(headers: AMQP::Table.new({"x-mqtt-retain": true})) + else + AMQ::Protocol::Properties.new + end # TODO: String.new around payload.. should be stored as Bytes msg = Message.new("mqtt.default", rk, String.new(packet.payload), properties) @exchange.publish(msg, false) @@ -98,8 +106,8 @@ module LavinMQ @retain_store.each(tf.topic) do |topic, body| rk = topicfilter_to_routingkey(topic) msg = Message.new("mqtt.default", rk, String.new(body), - AMQP::Properties.new(headers: AMQP::Table.new({ "x-mqtt-retain": true}), - delivery_mode: tf.qos )) + AMQP::Properties.new(headers: AMQP::Table.new({"x-mqtt-retain": true}), + delivery_mode: tf.qos)) session.publish(msg) end end @@ -117,7 +125,7 @@ module LavinMQ sessions.delete client_id end - def close() + def close @retain_store.close end end From 90aae91cf0dcdfd2f7fadad4df162ce4e576ac05 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:09:42 +0200 Subject: [PATCH 072/202] pass will specs --- spec/mqtt/integrations/will_spec.cr | 14 +++++++------- src/lavinmq/exchange/mqtt.cr | 2 +- src/lavinmq/mqtt/broker.cr | 17 +++++++++++------ src/lavinmq/mqtt/client.cr | 3 +-- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/spec/mqtt/integrations/will_spec.cr b/spec/mqtt/integrations/will_spec.cr index e4eec3337b..48178a48d5 100644 --- a/spec/mqtt/integrations/will_spec.cr +++ b/spec/mqtt/integrations/will_spec.cr @@ -5,7 +5,7 @@ module MqttSpecs extend MqttMatchers describe "client will" do - pending "will is not delivered on graceful disconnect [MQTT-3.14.4-3]" do + it "will is not delivered on graceful disconnect [MQTT-3.14.4-3]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -34,7 +34,7 @@ module MqttSpecs end end - pending "will is delivered on ungraceful disconnect" do + it "will is delivered on ungraceful disconnect" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -59,7 +59,7 @@ module MqttSpecs end end - pending "will can be retained [MQTT-3.1.2-17]" do + it "will can be retained [MQTT-3.1.2-17]" do with_server do |server| with_client_io(server) do |io2| will = MQTT::Protocol::Will.new( @@ -85,7 +85,7 @@ module MqttSpecs end end - pending "will won't be published if missing permission" do + it "will won't be published if missing permission" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -110,7 +110,7 @@ module MqttSpecs end end - pending "will qos can't be set of will flag is unset [MQTT-3.1.2-13]" do + it "will qos can't be set of will flag is unset [MQTT-3.1.2-13]" do with_server do |server| with_client_io(server) do |io| temp_io = IO::Memory.new @@ -127,7 +127,7 @@ module MqttSpecs end end - pending "will qos must not be 3 [MQTT-3.1.2-14]" do + it "will qos must not be 3 [MQTT-3.1.2-14]" do with_server do |server| with_client_io(server) do |io| temp_io = IO::Memory.new @@ -146,7 +146,7 @@ module MqttSpecs end end - pending "will retain can't be set of will flag is unset [MQTT-3.1.2-15]" do + it "will retain can't be set of will flag is unset [MQTT-3.1.2-15]" do with_server do |server| with_client_io(server) do |io| temp_io = IO::Memory.new diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 06a38a7043..98da513710 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -42,7 +42,7 @@ module LavinMQ @retain_store.retain(topic, msg.body_io, msg.bodysize) end - @tree.each_entry(topic) do |queue, qos| + @tree.each_entry(msg.routing_key) do |queue, qos| msg.properties.delivery_mode = qos if queue.publish(msg) count += 1 diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 8bbc2fcd31..693b6936dc 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -78,13 +78,16 @@ module LavinMQ @clients.delete client_id end - def publish(packet : MQTT::Publish) + def create_properties(packet : MQTT::Publish | MQTT::Will) : AMQP::Properties + headers = AMQP::Table.new + headers["x-mqtt-retain"] = true if packet.retain? + headers["x-mqtt-will"] = true if packet.is_a?(MQTT::Will) + AMQP::Properties.new(headers: headers).tap { |props| props.delivery_mode = packet.qos if packet.responds_to?(:qos) } + end + + def publish(packet : MQTT::Publish | MQTT::Will) rk = topicfilter_to_routingkey(packet.topic) - properties = if packet.retain? - AMQP::Properties.new(headers: AMQP::Table.new({"x-mqtt-retain": true})) - else - AMQ::Protocol::Properties.new - end + properties = create_properties(packet) # TODO: String.new around payload.. should be stored as Bytes msg = Message.new("mqtt.default", rk, String.new(packet.payload), properties) @exchange.publish(msg, false) @@ -103,6 +106,8 @@ module LavinMQ packet.topic_filters.each do |tf| qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) session.subscribe(tf.topic, tf.qos) + + #Publish retained messages @retain_store.each(tf.topic) do |topic, body| rk = topicfilter_to_routingkey(topic) msg = Message.new("mqtt.default", rk, String.new(body), diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 8daaff6a2b..539042fe95 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -60,7 +60,6 @@ module LavinMQ raise ex ensure @broker.disconnect_client(client_id) - @socket.close # move to disconnect client @broker.vhost.rm_connection(self) @@ -128,9 +127,9 @@ module LavinMQ }.merge(stats_details) end - # TODO: actually publish will to session private def publish_will if will = @will + @broker.publish(will) end rescue ex @log.warn { "Failed to publish will: #{ex.message}" } From 0a3a8da9277b6b7c50485c14140bfd6023415834 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:12:04 +0200 Subject: [PATCH 073/202] cleanup --- src/lavinmq/mqtt/broker.cr | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 693b6936dc..5417bf98f5 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -106,8 +106,6 @@ module LavinMQ packet.topic_filters.each do |tf| qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) session.subscribe(tf.topic, tf.qos) - - #Publish retained messages @retain_store.each(tf.topic) do |topic, body| rk = topicfilter_to_routingkey(topic) msg = Message.new("mqtt.default", rk, String.new(body), From 7c880765b99eacec54ff0f2703fa99488a64127b Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:22:42 +0200 Subject: [PATCH 074/202] pass duplicate message specs --- spec/mqtt/integrations/duplicate_message_spec.cr | 6 +++--- src/lavinmq/mqtt/session.cr | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/mqtt/integrations/duplicate_message_spec.cr b/spec/mqtt/integrations/duplicate_message_spec.cr index 70ccf10349..da7ac20c30 100644 --- a/spec/mqtt/integrations/duplicate_message_spec.cr +++ b/spec/mqtt/integrations/duplicate_message_spec.cr @@ -5,7 +5,7 @@ module MqttSpecs extend MqttMatchers describe "duplicate messages" do - pending "dup must not be set if qos is 0 [MQTT-3.3.1-2]" do + it "dup must not be set if qos is 0 [MQTT-3.3.1-2]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -32,7 +32,7 @@ module MqttSpecs end end - pending "dup is set when a message is being redelivered [MQTT-3.3.1.-1]" do + it "dup is set when a message is being redelivered [MQTT-3.3.1.-1]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -59,7 +59,7 @@ module MqttSpecs end end - pending "dup on incoming messages is not propagated to other clients [MQTT-3.3.1-3]" do + it "dup on incoming messages is not propagated to other clients [MQTT-3.3.1-3]" do with_server do |server| with_client_io(server) do |io| connect(io) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index b1e6fe24b4..d39fc32032 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -134,7 +134,7 @@ module LavinMQ private def next_id : UInt16? @count &+= 1u16 - + #TODO: implement this? # return nil if @unacked.size == @max_inflight # start_id = @packet_id # next_id : UInt16 = start_id + 1 From 7081c8bca97cf75e1c6af17efdb21529f5b391c9 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:42:24 +0200 Subject: [PATCH 075/202] format --- spec/message_routing_spec.cr | 4 ++-- src/lavinmq/exchange/mqtt.cr | 3 +-- src/lavinmq/mqtt/client.cr | 3 ++- src/lavinmq/mqtt/retain_store.cr | 17 +++++++++-------- src/lavinmq/mqtt/session.cr | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index 5ec71b3157..fb4ade2d83 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -427,7 +427,7 @@ describe LavinMQ::MQTTExchange do vhost = s.vhosts.create("x") q1 = LavinMQ::Queue.new(vhost, "q1") s1 = LavinMQ::MQTT::Session.new(vhost, "q1") - x = LavinMQ::MQTTExchange.new(vhost, "") + x = LavinMQ::MQTTExchange.new(vhost, "", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) expect_raises(LavinMQ::Exchange::AccessRefused) do x.bind(q1, "q1", LavinMQ::AMQP::Table.new) @@ -439,7 +439,7 @@ describe LavinMQ::MQTTExchange do with_amqp_server do |s| vhost = s.vhosts.create("x") s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") - x = LavinMQ::MQTTExchange.new(vhost, "mqtt.default") + x = LavinMQ::MQTTExchange.new(vhost, "mqtt.default", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") x.publish(msg, false) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 98da513710..88954117c5 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -5,7 +5,6 @@ require "../mqtt/retain_store" module LavinMQ class MQTTExchange < Exchange - struct MqttBindingKey def initialize(routing_key : String, arguments : AMQP::Table? = nil) @binding_key = BindingKey.new(routing_key, arguments) @@ -53,7 +52,7 @@ module LavinMQ end def routing_key_to_topic(routing_key : String) : String - routing_key.tr(".*", "/+") + routing_key.tr(".*", "/+") end def bindings_details : Iterator(BindingDetails) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 539042fe95..0eab181f74 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -77,7 +77,8 @@ module LavinMQ when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) when MQTT::Disconnect then return packet - else raise "invalid packet type for client to send" + # TODO: do we raise here? or just disconnect if we get an invalid frame + else raise "invalid packet type for client to send" end packet end diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index b2e4d9c02b..0d553d22b7 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -6,8 +6,8 @@ module LavinMQ class RetainStore Log = LavinMQ::Log.for("retainstore") - MESSAGE_FILE_SUFFIX = ".msg" - INDEX_FILE_NAME = "index" + MESSAGE_FILE_SUFFIX = ".msg" + INDEX_FILE_NAME = "index" alias IndexTree = TopicTree(String) @@ -56,7 +56,7 @@ module LavinMQ unless msg_file_segments.empty? Log.warn { "unreferenced messages: #{msg_file_segments.join(",")}" } end - #TODO: delete unreferenced messages? + # TODO: delete unreferenced messages? Log.info { "restoring index done, msg_count = #{msg_count}" } end @@ -117,11 +117,11 @@ module LavinMQ private def read(file_name : String) : Bytes # @lock.synchronize do - File.open(File.join(@dir, file_name), "r") do |f| - body = Bytes.new(f.size) - f.read_fully(body) - body - end + File.open(File.join(@dir, file_name), "r") do |f| + body = Bytes.new(f.size) + f.read_fully(body) + body + end # end end @@ -132,6 +132,7 @@ module LavinMQ end @hasher = Digest::MD5.new + def make_file_name(topic : String) : String @hasher.update topic.to_slice hash = @hasher.hexfinal diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index d39fc32032..efb2f8199a 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -134,7 +134,7 @@ module LavinMQ private def next_id : UInt16? @count &+= 1u16 - #TODO: implement this? + # TODO: implement this? # return nil if @unacked.size == @max_inflight # start_id = @packet_id # next_id : UInt16 = start_id + 1 From a92ab4ce2020d9db122a7131d8e475857b9b1c72 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:55:50 +0200 Subject: [PATCH 076/202] fix publish --- src/lavinmq/mqtt/broker.cr | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 5417bf98f5..7d57567b6e 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -78,16 +78,13 @@ module LavinMQ @clients.delete client_id end - def create_properties(packet : MQTT::Publish | MQTT::Will) : AMQP::Properties + def publish(packet : MQTT::Publish | MQTT::Will) headers = AMQP::Table.new headers["x-mqtt-retain"] = true if packet.retain? headers["x-mqtt-will"] = true if packet.is_a?(MQTT::Will) - AMQP::Properties.new(headers: headers).tap { |props| props.delivery_mode = packet.qos if packet.responds_to?(:qos) } - end + properties = AMQP::Properties.new(headers: headers).tap { |props| props.delivery_mode = packet.qos if packet.responds_to?(:qos) } - def publish(packet : MQTT::Publish | MQTT::Will) rk = topicfilter_to_routingkey(packet.topic) - properties = create_properties(packet) # TODO: String.new around payload.. should be stored as Bytes msg = Message.new("mqtt.default", rk, String.new(packet.payload), properties) @exchange.publish(msg, false) From 50d950be94ca285b4afbfd6209de2efcc720138d Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 14:40:05 +0200 Subject: [PATCH 077/202] remove obsolete header --- spec/mqtt/integrations/message_qos_spec.cr | 3 ++- src/lavinmq/mqtt/broker.cr | 15 +++++++-------- src/lavinmq/mqtt/client.cr | 3 +++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/spec/mqtt/integrations/message_qos_spec.cr b/spec/mqtt/integrations/message_qos_spec.cr index 82a8c4db74..d1a8d36504 100644 --- a/spec/mqtt/integrations/message_qos_spec.cr +++ b/spec/mqtt/integrations/message_qos_spec.cr @@ -147,11 +147,12 @@ module MqttSpecs end end - # TODO: rescue so we don't get ugly missing hash key errors it "cannot ack invalid packet id" do with_server do |server| with_client_io(server) do |io| connect(io) + topic_filters = mk_topic_filters({"a/b", 1u8}) + subscribe(io, topic_filters: topic_filters) puback(io, 123u16) expect_raises(IO::Error) do diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 7d57567b6e..5bf674cfbb 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -79,14 +79,13 @@ module LavinMQ end def publish(packet : MQTT::Publish | MQTT::Will) - headers = AMQP::Table.new - headers["x-mqtt-retain"] = true if packet.retain? - headers["x-mqtt-will"] = true if packet.is_a?(MQTT::Will) - properties = AMQP::Properties.new(headers: headers).tap { |props| props.delivery_mode = packet.qos if packet.responds_to?(:qos) } - - rk = topicfilter_to_routingkey(packet.topic) - # TODO: String.new around payload.. should be stored as Bytes - msg = Message.new("mqtt.default", rk, String.new(packet.payload), properties) + headers = AMQP::Table.new.tap do |h| + h["x-mqtt-retain"] = true if packet.retain? + end + properties = AMQP::Properties.new(headers: headers).tap do |p| + p.delivery_mode = packet.qos if packet.responds_to?(:qos) + end + msg = Message.new("mqtt.default", topicfilter_to_routingkey(packet.topic), String.new(packet.payload), properties) @exchange.publish(msg, false) end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 0eab181f74..329a4e3d84 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -104,6 +104,9 @@ module LavinMQ end def recieve_puback(packet) + pp packet.inspect + pp @client_id + pp @broker.sessions @broker.sessions[@client_id].ack(packet) end From 2ac47f7424d3c0ea77a0b49018d31254c4c1a357 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 14:42:04 +0200 Subject: [PATCH 078/202] cleanup --- src/lavinmq/mqtt/client.cr | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 329a4e3d84..0eab181f74 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -104,9 +104,6 @@ module LavinMQ end def recieve_puback(packet) - pp packet.inspect - pp @client_id - pp @broker.sessions @broker.sessions[@client_id].ack(packet) end From e7da53a56b2b95d6efbb1610842a6561ccfc45a2 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 15:07:26 +0200 Subject: [PATCH 079/202] raise io error for invalid package_id --- spec/mqtt/integrations/message_qos_spec.cr | 1 + src/lavinmq/mqtt/session.cr | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/spec/mqtt/integrations/message_qos_spec.cr b/spec/mqtt/integrations/message_qos_spec.cr index d1a8d36504..6109f76376 100644 --- a/spec/mqtt/integrations/message_qos_spec.cr +++ b/spec/mqtt/integrations/message_qos_spec.cr @@ -151,6 +151,7 @@ module MqttSpecs with_server do |server| with_client_io(server) do |io| connect(io) + # we need to subscribe in order to have a session topic_filters = mk_topic_filters({"a/b", 1u8}) subscribe(io, topic_filters: topic_filters) puback(io, 123u16) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index efb2f8199a..b7b4e3eae2 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,3 +1,5 @@ +require "../error" + module LavinMQ module MQTT class Session < Queue @@ -125,6 +127,8 @@ module LavinMQ sp = @unacked[id] @unacked.delete id super sp + rescue + raise ::IO::Error.new("Could not acknowledge package with id: #{id}") end private def message_expire_loop; end From 5f40c82be5307d1b468fcb0c2e0fa50eff7200e8 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 15:31:05 +0200 Subject: [PATCH 080/202] handle raise for double connect --- src/lavinmq/mqtt/client.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 0eab181f74..f8f85e3b96 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -76,9 +76,9 @@ module LavinMQ when MQTT::Subscribe then recieve_subscribe(packet) when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) + when MQTT::Connect then raise ::IO::Error.new("Can not connect already connected client") when MQTT::Disconnect then return packet - # TODO: do we raise here? or just disconnect if we get an invalid frame - else raise "invalid packet type for client to send" + else raise "invalid packet type for client to send: #{packet.inspect}" end packet end From 76fbe985207e80551bfa734b81004adc7608a57b Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 09:49:35 +0200 Subject: [PATCH 081/202] revert double connect rescue --- src/lavinmq/mqtt/client.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index f8f85e3b96..0eab181f74 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -76,9 +76,9 @@ module LavinMQ when MQTT::Subscribe then recieve_subscribe(packet) when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) - when MQTT::Connect then raise ::IO::Error.new("Can not connect already connected client") when MQTT::Disconnect then return packet - else raise "invalid packet type for client to send: #{packet.inspect}" + # TODO: do we raise here? or just disconnect if we get an invalid frame + else raise "invalid packet type for client to send" end packet end From 5d09957eb2e84e5f3cf7c892869680c827d5541f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 11:04:17 +0200 Subject: [PATCH 082/202] Remove unused variable --- spec/mqtt/integrations/subscribe_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mqtt/integrations/subscribe_spec.cr b/spec/mqtt/integrations/subscribe_spec.cr index 6081abc59f..94b5ffda91 100644 --- a/spec/mqtt/integrations/subscribe_spec.cr +++ b/spec/mqtt/integrations/subscribe_spec.cr @@ -15,7 +15,7 @@ module MqttSpecs with_client_io(server) do |pub_io| connect(pub_io, client_id: "pub") - payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + payload = Bytes[1, 254, 200, 197, 123, 4, 87] packet_id = next_packet_id ack = publish(pub_io, topic: "test", From 6f0e8555730398e944bd41a819817733f5432a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 11:05:09 +0200 Subject: [PATCH 083/202] Use method overloading instead of type check --- src/lavinmq/exchange/mqtt.cr | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 88954117c5..4c0bf087a8 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -68,9 +68,7 @@ module LavinMQ Iterator(Destination).empty end - def bind(destination : Destination, routing_key : String, headers = nil) : Bool - raise LavinMQ::Exchange::AccessRefused.new(self) unless destination.is_a?(MQTT::Session) - + def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 binding_key = MqttBindingKey.new(routing_key, headers) @bindings[binding_key].add destination @@ -81,8 +79,7 @@ module LavinMQ true end - def unbind(destination : Destination, routing_key, headers = nil) : Bool - raise LavinMQ::Exchange::AccessRefused.new(self) unless destination.is_a?(MQTT::Session) + def unbind(destination : MQTT::Session, routing_key, headers = nil) : Bool binding_key = MqttBindingKey.new(routing_key, headers) rk_bindings = @bindings[binding_key] rk_bindings.delete destination @@ -96,5 +93,13 @@ module LavinMQ delete if @auto_delete && @bindings.each_value.all?(&.empty?) true end + + def bind(destination : Destination, routing_key : String, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end + + def unbind(destination : Destination, routing_key, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end end end From 9b2912fe106927861582d12d2c2f8aa958fffce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 11:11:52 +0200 Subject: [PATCH 084/202] Use lowercase log source --- src/lavinmq/mqtt/client.cr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 0eab181f74..293e491ce2 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -14,7 +14,7 @@ module LavinMQ @channels = Hash(UInt16, Client::Channel).new @session : MQTT::Session? rate_stats({"send_oct", "recv_oct"}) - Log = ::Log.for "MQTT.client" + Log = ::Log.for "mqtt.client" def initialize(@socket : ::IO, @connection_info : ConnectionInfo, @@ -77,8 +77,7 @@ module LavinMQ when MQTT::Unsubscribe then recieve_unsubscribe(packet) when MQTT::PingReq then receive_pingreq(packet) when MQTT::Disconnect then return packet - # TODO: do we raise here? or just disconnect if we get an invalid frame - else raise "invalid packet type for client to send" + else raise "received unexpected packet: #{packet}" end packet end From 00cc73a6ef1d0deb93439628af16a38a2d9b4a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 11:13:06 +0200 Subject: [PATCH 085/202] Use mqtt.session as log source for Session --- src/lavinmq/mqtt/session.cr | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index b7b4e3eae2..b9002b2501 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,8 +1,11 @@ +require "../queue" require "../error" module LavinMQ module MQTT class Session < Queue + Log = ::LavinMQ::Log.for "mqtt.session" + @clean_session : Bool = false getter clean_session @@ -14,6 +17,8 @@ module LavinMQ @unacked = Hash(UInt16, SegmentPosition).new super(@vhost, @name, false, @auto_delete, arguments) + + @log = Logger.new(Log, @metadata) spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end @@ -53,7 +58,7 @@ module LavinMQ @consumers << MqttConsumer.new(c, self) spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end - @log.debug { "Setting MQTT client" } + @log.debug { "client set to '#{client.try &.name}'" } end def durable? @@ -128,7 +133,7 @@ module LavinMQ @unacked.delete id super sp rescue - raise ::IO::Error.new("Could not acknowledge package with id: #{id}") + raise ::IO::Error.new("Could not acknowledge package with id: #{id}") end private def message_expire_loop; end From 4a966bd9c61d77ff9c071c7657ed17edab23b0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 12:58:59 +0200 Subject: [PATCH 086/202] Add spec to test publisher->subscriber flow --- spec/mqtt/integrations/various_spec.cr | 31 ++++++++++++++++++++++++++ spec/mqtt/spec_helper/mqtt_helpers.cr | 17 ++++++++++---- 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 spec/mqtt/integrations/various_spec.cr diff --git a/spec/mqtt/integrations/various_spec.cr b/spec/mqtt/integrations/various_spec.cr new file mode 100644 index 0000000000..22e5288243 --- /dev/null +++ b/spec/mqtt/integrations/various_spec.cr @@ -0,0 +1,31 @@ +require "../spec_helper" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + describe "publish and subscribe flow" do + topic = "a/b/c" + {"a/b/c", "a/#", "a/+/c", "a/b/#", "a/b/+", "#"}.each do |topic_filter| + it "should route #{topic} to #{topic_filter}" do + with_server do |server| + with_client_io(server) do |sub| + connect(sub, client_id: "sub") + subscribe(sub, topic_filters: mk_topic_filters({topic_filter, 0})) + + with_client_io(server) do |pub_io| + connect(pub_io, client_id: "pub") + publish(pub_io, topic: "a/b/c", qos: 0u8) + end + + begin + packet = read_packet(sub).should be_a(MQTT::Protocol::Publish) + packet.topic.should eq "a/b/c" + rescue + fail "timeout; message not routed" + end + end + end + end + end + end +end diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr index dd428479a2..f5b8564980 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -9,7 +9,7 @@ module MqttHelpers GENERATOR.next.as(UInt16) end - def with_client_socket(server, &) + def with_client_socket(server) listener = server.listeners.find { |l| l[:protocol] == :mqtt } tcp_listener = listener.as(NamedTuple(ip_address: String, protocol: Symbol, port: Int32)) @@ -26,6 +26,11 @@ module MqttHelpers socket.read_buffering = true socket.buffer_size = 16384 socket.read_timeout = 1.seconds + socket + end + + def with_client_socket(server, &) + socket = with_client_socket(server) yield socket ensure socket.try &.close @@ -46,10 +51,14 @@ module MqttHelpers end end + def with_client_io(server) + socket = with_client_socket(server) + MQTT::Protocol::IO.new(socket) + end + def with_client_io(server, &) - with_client_socket(server) do |socket| - io = MQTT::Protocol::IO.new(socket) - with MqttHelpers yield io + with_client_socket(server) do |io| + with MqttHelpers yield MQTT::Protocol::IO.new(io) end end From 45b7ec2fb2764f98285cd6c06bf01c8f6af809da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 13:11:03 +0200 Subject: [PATCH 087/202] Add spec to verify that session is restored properly --- spec/mqtt/integrations/various_spec.cr | 27 +++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/spec/mqtt/integrations/various_spec.cr b/spec/mqtt/integrations/various_spec.cr index 22e5288243..07666bd298 100644 --- a/spec/mqtt/integrations/various_spec.cr +++ b/spec/mqtt/integrations/various_spec.cr @@ -10,7 +10,7 @@ module MqttSpecs with_server do |server| with_client_io(server) do |sub| connect(sub, client_id: "sub") - subscribe(sub, topic_filters: mk_topic_filters({topic_filter, 0})) + subscribe(sub, topic_filters: [subtopic(topic_filter, 1u8)]) with_client_io(server) do |pub_io| connect(pub_io, client_id: "pub") @@ -28,4 +28,29 @@ module MqttSpecs end end end + + describe "session handling" do + it "messages are delivered to client that connects to a existing session" do + with_server do |server| + with_client_io(server) do |io| + connect(io, clean_session: false) + subscribe(io, topic_filters: [subtopic("a/b/c", 1u8)]) + disconnect(io) + end + + with_client_io(server) do |io| + connect(io, clean_session: true, client_id: "pub") + publish(io, topic: "a/b/c", qos: 0u8) + end + + with_client_io(server) do |io| + connect(io, clean_session: false) + packet = read_packet(io).should be_a(MQTT::Protocol::Publish) + packet.topic.should eq "a/b/c" + rescue + fail "timeout; message not routed" + end + end + end + end end From 82cda00853a12e45eb2cbd6469dec14b3f795d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 13:27:17 +0200 Subject: [PATCH 088/202] Use getter instead of instance variable --- src/lavinmq/mqtt/broker.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 5bf674cfbb..a8fab17587 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -47,7 +47,7 @@ module LavinMQ def session_present?(client_id : String, clean_session) : Bool return false if clean_session - session = @sessions[client_id]? + session = sessions[client_id]? return false if session.nil? || session.clean_session? true end @@ -58,7 +58,7 @@ module LavinMQ prev_client.close end client = MQTT::Client.new(socket, connection_info, user, vhost, self, packet.client_id, packet.clean_session?, packet.will) - if session = @sessions[client.client_id]? + if session = sessions[client.client_id]? if session.clean_session? sessions.delete session else @@ -70,7 +70,7 @@ module LavinMQ end def disconnect_client(client_id) - if session = @sessions[client_id]? + if session = sessions[client_id]? session.client = nil sessions.delete(client_id) if session.clean_session? end @@ -94,7 +94,7 @@ module LavinMQ end def subscribe(client, packet) - unless session = @sessions[client.client_id]? + unless session = sessions[client.client_id]? session = sessions.declare(client.client_id, client.@clean_session) session.client = client end From ba0e46e352380470875ff2ba9968cbe9c8aa24e9 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 15:38:55 +0200 Subject: [PATCH 089/202] beginning of max_inflight --- src/lavinmq/mqtt/session.cr | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index b9002b2501..68146125fa 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -14,6 +14,7 @@ module LavinMQ @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) @count = 0u16 + @max_inflight = 65_535u16 # TODO: get this from config @unacked = Hash(UInt16, SegmentPosition).new super(@vhost, @name, false, @auto_delete, arguments) @@ -97,12 +98,10 @@ module LavinMQ private def get(no_ack : Bool, & : Envelope -> Nil) : Bool raise ClosedError.new if @closed loop do - id = next_id env = @msg_store_lock.synchronize { @msg_store.shift? } || break sp = env.segment_position no_ack = env.message.properties.delivery_mode == 0 if no_ack - env.message.properties.message_id = id.to_s begin yield env rescue ex @@ -111,6 +110,9 @@ module LavinMQ end delete_message(sp) else + id = next_id + pp id + return false unless id env.message.properties.message_id = id.to_s mark_unacked(sp) do yield env @@ -141,24 +143,16 @@ module LavinMQ private def queue_expire_loop; end private def next_id : UInt16? - @count &+= 1u16 - - # TODO: implement this? - # return nil if @unacked.size == @max_inflight - # start_id = @packet_id - # next_id : UInt16 = start_id + 1 - # while @unacked.has_key?(next_id) - # if next_id == 65_535 - # next_id = 1 - # else - # next_id += 1 - # end - # if next_id == start_id - # return nil - # end - # end - # @packet_id = next_id - # next_id + return nil if @unacked.size == @max_inflight + start_id = @count + next_id : UInt16 = start_id &+ 1_u16 + while @unacked.has_key?(next_id) + next_id &+= 1u16 + next_id = 1u16 if next_id == 0 + return nil if next_id == start_id + end + @count = next_id + next_id end end end From f07093e24527f14265b7557ea2a8b6a738eb3f76 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 16:07:39 +0200 Subject: [PATCH 090/202] send in topic to subscription tree, pass specs --- src/lavinmq/exchange/mqtt.cr | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 4c0bf087a8..2097fd26af 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -41,7 +41,7 @@ module LavinMQ @retain_store.retain(topic, msg.body_io, msg.bodysize) end - @tree.each_entry(msg.routing_key) do |queue, qos| + @tree.each_entry(topic) do |queue, qos| msg.properties.delivery_mode = qos if queue.publish(msg) count += 1 @@ -69,10 +69,11 @@ module LavinMQ end def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool + topic = routing_key_to_topic(routing_key) qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 binding_key = MqttBindingKey.new(routing_key, headers) @bindings[binding_key].add destination - @tree.subscribe(routing_key, destination, qos) + @tree.subscribe(topic, destination, qos) data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) notify_observers(ExchangeEvent::Bind, data) @@ -80,12 +81,13 @@ module LavinMQ end def unbind(destination : MQTT::Session, routing_key, headers = nil) : Bool + topic = routing_key_to_topic(routing_key) binding_key = MqttBindingKey.new(routing_key, headers) rk_bindings = @bindings[binding_key] rk_bindings.delete destination @bindings.delete binding_key if rk_bindings.empty? - @tree.unsubscribe(routing_key, destination) + @tree.unsubscribe(topic, destination) data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) notify_observers(ExchangeEvent::Unbind, data) From 0bfd4ab39d32bec957ce8d2617d6162176304627 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 16:16:23 +0200 Subject: [PATCH 091/202] clean up --- src/lavinmq/mqtt/session.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 68146125fa..8c8a7e61f0 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -111,7 +111,6 @@ module LavinMQ delete_message(sp) else id = next_id - pp id return false unless id env.message.properties.message_id = id.to_s mark_unacked(sp) do From ce2d7fc4014ef1191ba3dea3f452915860544c2d Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 16:42:13 +0200 Subject: [PATCH 092/202] create publish packet in client instead of accepting will in #broker:publish --- src/lavinmq/mqtt/broker.cr | 2 +- src/lavinmq/mqtt/client.cr | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index a8fab17587..84187a515e 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -78,7 +78,7 @@ module LavinMQ @clients.delete client_id end - def publish(packet : MQTT::Publish | MQTT::Will) + def publish(packet : MQTT::Publish) headers = AMQP::Table.new.tap do |h| h["x-mqtt-retain"] = true if packet.retain? end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 293e491ce2..a454435308 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -128,9 +128,16 @@ module LavinMQ end private def publish_will - if will = @will - @broker.publish(will) - end + return unless will = @will + packet = MQTT::Publish.new( + topic: will.topic, + payload: will.payload, + packet_id: nil, + qos: will.qos, + retain: will.retain?, + dup: false, + ) + @broker.publish(packet) rescue ex @log.warn { "Failed to publish will: #{ex.message}" } end From 8ed744e4b224b6f9da24437e9964a498eee602e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 13:54:54 +0200 Subject: [PATCH 093/202] Be consistent with typing --- src/lavinmq/mqtt/client.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index a454435308..bed45c6d9e 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -102,7 +102,7 @@ module LavinMQ end end - def recieve_puback(packet) + def recieve_puback(packet : MQTT::PubAck) @broker.sessions[@client_id].ack(packet) end @@ -112,7 +112,7 @@ module LavinMQ send(MQTT::SubAck.new(qos, packet.packet_id)) end - def recieve_unsubscribe(packet) + def recieve_unsubscribe(packet : MQTT::Unsubscribe) session = @broker.sessions[@client_id] @broker.unsubscribe(self, packet) send(MQTT::UnsubAck.new(packet.packet_id)) From b3680ea956efb6c4b1d91661ceead17d7a530bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 16:46:08 +0200 Subject: [PATCH 094/202] Add routing specs --- spec/mqtt/integrations/various_spec.cr | 28 +--------- spec/mqtt/routing_spec.cr | 72 ++++++++++++++++++++++++++ spec/mqtt/spec_helper/mqtt_client.cr | 2 +- 3 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 spec/mqtt/routing_spec.cr diff --git a/spec/mqtt/integrations/various_spec.cr b/spec/mqtt/integrations/various_spec.cr index 07666bd298..23656080a7 100644 --- a/spec/mqtt/integrations/various_spec.cr +++ b/spec/mqtt/integrations/various_spec.cr @@ -3,31 +3,6 @@ require "../spec_helper" module MqttSpecs extend MqttHelpers extend MqttMatchers - describe "publish and subscribe flow" do - topic = "a/b/c" - {"a/b/c", "a/#", "a/+/c", "a/b/#", "a/b/+", "#"}.each do |topic_filter| - it "should route #{topic} to #{topic_filter}" do - with_server do |server| - with_client_io(server) do |sub| - connect(sub, client_id: "sub") - subscribe(sub, topic_filters: [subtopic(topic_filter, 1u8)]) - - with_client_io(server) do |pub_io| - connect(pub_io, client_id: "pub") - publish(pub_io, topic: "a/b/c", qos: 0u8) - end - - begin - packet = read_packet(sub).should be_a(MQTT::Protocol::Publish) - packet.topic.should eq "a/b/c" - rescue - fail "timeout; message not routed" - end - end - end - end - end - end describe "session handling" do it "messages are delivered to client that connects to a existing session" do @@ -39,7 +14,7 @@ module MqttSpecs end with_client_io(server) do |io| - connect(io, clean_session: true, client_id: "pub") + connect(io, clean_session: false, client_id: "pub") publish(io, topic: "a/b/c", qos: 0u8) end @@ -47,6 +22,7 @@ module MqttSpecs connect(io, clean_session: false) packet = read_packet(io).should be_a(MQTT::Protocol::Publish) packet.topic.should eq "a/b/c" + pp packet rescue fail "timeout; message not routed" end diff --git a/spec/mqtt/routing_spec.cr b/spec/mqtt/routing_spec.cr new file mode 100644 index 0000000000..dbb059b52e --- /dev/null +++ b/spec/mqtt/routing_spec.cr @@ -0,0 +1,72 @@ +require "./spec_helper" + +module MqttSpecs + extend MqttHelpers + extend MqttMatchers + + describe "message routing" do + topic = "a/b/c" + positive_topic_filters = { + "a/b/c", + "#", + "a/#", + "a/b/#", + "a/b/+", + "a/+/+", + "+/+/+", + "+/+/c", + "+/b/c", + "+/#", + "+/+/#", + "a/+/#", + "a/+/c", + } + negative_topic_filters = { + "c/a/b", + "c/#", + "+/a/+", + "c/+/#", + "+/+/d", + } + positive_topic_filters.each do |topic_filter| + it "should route #{topic} to #{topic_filter}" do + with_server do |server| + with_client_io(server) do |sub| + connect(sub, client_id: "sub") + subscribe(sub, topic_filters: [subtopic(topic_filter, 1u8)]) + + with_client_io(server) do |pub_io| + connect(pub_io, client_id: "pub") + publish(pub_io, topic: "a/b/c", qos: 0u8) + end + + begin + packet = read_packet(sub).should be_a(MQTT::Protocol::Publish) + packet.topic.should eq "a/b/c" + rescue + fail "timeout; message not routed" + end + end + end + end + end + + negative_topic_filters.each do |topic_filter| + it "should not route #{topic} to #{topic_filter}" do + with_server do |server| + with_client_io(server) do |sub| + connect(sub, client_id: "sub") + subscribe(sub, topic_filters: [subtopic(topic_filter, 1u8)]) + + with_client_io(server) do |pub_io| + connect(pub_io, client_id: "pub") + publish(pub_io, topic: "a/b/c", qos: 0u8) + end + + expect_raises(::IO::TimeoutError) { MQTT::Protocol::Packet.from_io(sub) } + end + end + end + end + end +end diff --git a/spec/mqtt/spec_helper/mqtt_client.cr b/spec/mqtt/spec_helper/mqtt_client.cr index 20d0702c3e..e028ac6d32 100644 --- a/spec/mqtt/spec_helper/mqtt_client.cr +++ b/spec/mqtt/spec_helper/mqtt_client.cr @@ -1,6 +1,6 @@ require "mqtt-protocol" -module Specs +module MqttHelpers class MqttClient def next_packet_id @packet_id_generator.next.as(UInt16) From 035ee4d31a0cc450d684eafdb4531ddf10c1397d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 22 Oct 2024 21:55:13 +0200 Subject: [PATCH 095/202] Lint --- spec/mqtt/integrations/connect_spec.cr | 2 +- spec/mqtt/integrations/publish_spec.cr | 4 ++-- src/lavinmq/mqtt/client.cr | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 426cc76833..80bbac859e 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -188,7 +188,7 @@ module MqttSpecs it "if first packet is not a CONNECT [MQTT-3.1.0-1]" do with_server do |server| with_client_io(server) do |io| - payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + payload = Bytes[1, 254, 200, 197, 123, 4, 87] publish(io, topic: "test", payload: payload, qos: 0u8) io.should be_closed end diff --git a/spec/mqtt/integrations/publish_spec.cr b/spec/mqtt/integrations/publish_spec.cr index 35c2ea1df9..6bde29afab 100644 --- a/spec/mqtt/integrations/publish_spec.cr +++ b/spec/mqtt/integrations/publish_spec.cr @@ -9,7 +9,7 @@ module MqttSpecs with_server do |server| with_client_io(server) do |io| connect(io) - payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + payload = Bytes[1, 254, 200, 197, 123, 4, 87] ack = publish(io, topic: "test", payload: payload, qos: 1u8) ack.should be_a(MQTT::Protocol::PubAck) end @@ -21,7 +21,7 @@ module MqttSpecs with_client_io(server) do |io| connect(io) - payload = slice = Bytes[1, 254, 200, 197, 123, 4, 87] + payload = Bytes[1, 254, 200, 197, 123, 4, 87] ack = publish(io, topic: "test", payload: payload, qos: 0u8) ack.should be_nil end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index bed45c6d9e..93e556fe89 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -108,12 +108,10 @@ module LavinMQ def recieve_subscribe(packet : MQTT::Subscribe) qos = @broker.subscribe(self, packet) - session = @broker.sessions[@client_id] send(MQTT::SubAck.new(qos, packet.packet_id)) end def recieve_unsubscribe(packet : MQTT::Unsubscribe) - session = @broker.sessions[@client_id] @broker.unsubscribe(self, packet) send(MQTT::UnsubAck.new(packet.packet_id)) end From 391e237d868dfaa1a6605de367b949183f67546a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Wed, 23 Oct 2024 09:04:36 +0200 Subject: [PATCH 096/202] Return nil and let spec assert --- spec/mqtt/spec_helper/mqtt_helpers.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers.cr index f5b8564980..b569d21438 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers.cr @@ -122,6 +122,6 @@ module MqttHelpers def read_packet(io) MQTT::Protocol::Packet.from_io(io) rescue IO::TimeoutError - fail "Did not get packet on time" + nil end end From f80e0a0758828a45f88d791c89cbcb2ca6b10fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Wed, 23 Oct 2024 09:12:21 +0200 Subject: [PATCH 097/202] Add a will spec (and some clean up) --- spec/mqtt/integrations/will_spec.cr | 87 ++++++++++++++++++----------- src/lavinmq/mqtt/client.cr | 2 +- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/spec/mqtt/integrations/will_spec.cr b/spec/mqtt/integrations/will_spec.cr index 48178a48d5..b43e10ae39 100644 --- a/spec/mqtt/integrations/will_spec.cr +++ b/spec/mqtt/integrations/will_spec.cr @@ -5,7 +5,7 @@ module MqttSpecs extend MqttMatchers describe "client will" do - it "will is not delivered on graceful disconnect [MQTT-3.14.4-3]" do + it "is not delivered on graceful disconnect [MQTT-3.14.4-3]" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -22,10 +22,7 @@ module MqttSpecs # If the will has been published it should be received before this publish(io, topic: "a/b", payload: "alive".to_slice) - pub = read_packet(io) - - pub.should be_a(MQTT::Protocol::Publish) - pub = pub.as(MQTT::Protocol::Publish) + pub = read_packet(io).should be_a(MQTT::Protocol::Publish) pub.payload.should eq("alive".to_slice) pub.topic.should eq("a/b") @@ -34,32 +31,59 @@ module MqttSpecs end end - it "will is delivered on ungraceful disconnect" do - with_server do |server| - with_client_io(server) do |io| - connect(io) - topic_filters = mk_topic_filters({"will/t", 0}) - subscribe(io, topic_filters: topic_filters) - - with_client_io(server) do |io2| - will = MQTT::Protocol::Will.new( - topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: false) - connect(io2, client_id: "will_client", will: will, keepalive: 1u16) + describe "is delivered on ungraceful disconnect" do + it "when client unexpected closes tcp connection" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"will/t", 0}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |io2| + will = MQTT::Protocol::Will.new( + topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: false) + connect(io2, client_id: "will_client", will: will, keepalive: 1u16) + end + + pub = read_packet(io).should be_a(MQTT::Protocol::Publish) + pub.payload.should eq("dead".to_slice) + pub.topic.should eq("will/t") + + disconnect(io) end + end + end - pub = read_packet(io) - - pub.should be_a(MQTT::Protocol::Publish) - pub = pub.as(MQTT::Protocol::Publish) - pub.payload.should eq("dead".to_slice) - pub.topic.should eq("will/t") - - disconnect(io) + it "when server closes connection because protocol error" do + with_server do |server| + with_client_io(server) do |io| + connect(io) + topic_filters = mk_topic_filters({"will/t", 0}) + subscribe(io, topic_filters: topic_filters) + + with_client_io(server) do |io2| + will = MQTT::Protocol::Will.new( + topic: "will/t", payload: "dead".to_slice, qos: 0u8, retain: false) + connect(io2, client_id: "will_client", will: will, keepalive: 20u16) + + broken_packet_io = IO::Memory.new + publish(MQTT::Protocol::IO.new(broken_packet_io), topic: "foo", qos: 1u8, expect_response: false) + broken_packet = broken_packet_io.to_slice + broken_packet[0] |= 0b0000_0110u8 # set both qos bits to 1 + io2.write broken_packet + end + + pub = read_packet(io).should be_a(MQTT::Protocol::Publish) + pub.payload.should eq("dead".to_slice) + pub.topic.should eq("will/t") + + disconnect(io) + end end end end - it "will can be retained [MQTT-3.1.2-17]" do + it "can be retained [MQTT-3.1.2-17]" do with_server do |server| with_client_io(server) do |io2| will = MQTT::Protocol::Will.new( @@ -72,10 +96,7 @@ module MqttSpecs topic_filters = mk_topic_filters({"will/t", 0}) subscribe(io, topic_filters: topic_filters) - pub = read_packet(io) - - pub.should be_a(MQTT::Protocol::Publish) - pub = pub.as(MQTT::Protocol::Publish) + pub = read_packet(io).should be_a(MQTT::Protocol::Publish) pub.payload.should eq("dead".to_slice) pub.topic.should eq("will/t") pub.retain?.should eq(true) @@ -85,7 +106,7 @@ module MqttSpecs end end - it "will won't be published if missing permission" do + it "won't be published if missing permission" do with_server do |server| with_client_io(server) do |io| connect(io) @@ -110,7 +131,7 @@ module MqttSpecs end end - it "will qos can't be set of will flag is unset [MQTT-3.1.2-13]" do + it "qos can't be set of will flag is unset [MQTT-3.1.2-13]" do with_server do |server| with_client_io(server) do |io| temp_io = IO::Memory.new @@ -127,7 +148,7 @@ module MqttSpecs end end - it "will qos must not be 3 [MQTT-3.1.2-14]" do + it "qos must not be 3 [MQTT-3.1.2-14]" do with_server do |server| with_client_io(server) do |io| temp_io = IO::Memory.new @@ -146,7 +167,7 @@ module MqttSpecs end end - it "will retain can't be set of will flag is unset [MQTT-3.1.2-15]" do + it "retain can't be set of will flag is unset [MQTT-3.1.2-15]" do with_server do |server| with_client_io(server) do |io| temp_io = IO::Memory.new diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 93e556fe89..44c1911ff9 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -29,7 +29,7 @@ module LavinMQ @remote_address = @connection_info.src @local_address = @connection_info.dst @name = "#{@remote_address} -> #{@local_address}" - @metadata = ::Log::Metadata.new(nil, {vhost: @broker.vhost.name, address: @remote_address.to_s}) + @metadata = ::Log::Metadata.new(nil, {vhost: @broker.vhost.name, address: @remote_address.to_s, client_id: client_id}) @log = Logger.new(Log, @metadata) @broker.vhost.add_connection(self) @log.info { "Connection established for user=#{@user.name}" } From 89579e4ed7eeb6b43f2379e2844072285d75cfa5 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Oct 2024 09:25:59 +0200 Subject: [PATCH 098/202] publish will if PacketDecode exception --- spec/mqtt/integrations/various_spec.cr | 1 - src/lavinmq/mqtt/client.cr | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/mqtt/integrations/various_spec.cr b/spec/mqtt/integrations/various_spec.cr index 23656080a7..8c9a0d4c3a 100644 --- a/spec/mqtt/integrations/various_spec.cr +++ b/spec/mqtt/integrations/various_spec.cr @@ -22,7 +22,6 @@ module MqttSpecs connect(io, clean_session: false) packet = read_packet(io).should be_a(MQTT::Protocol::Publish) packet.topic.should eq "a/b/c" - pp packet rescue fail "timeout; message not routed" end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 44c1911ff9..8a174150ab 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -48,7 +48,9 @@ module LavinMQ # If we dont breakt the loop here we'll get a IO/Error on next read. break if packet.is_a?(MQTT::Disconnect) end + #do we even need this as a "case" if we don't log anything special? rescue ex : ::MQTT::Protocol::Error::PacketDecode + publish_will if @will @socket.close rescue ex : MQTT::Error::Connect @log.warn { "Connect error #{ex.inspect}" } From 9529a788190bcbdf3754d19f1de7fd101daf0927 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Oct 2024 10:12:26 +0200 Subject: [PATCH 099/202] move vhost logic from client into broker --- src/lavinmq/mqtt/broker.cr | 5 +++-- src/lavinmq/mqtt/client.cr | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 84187a515e..703550915c 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -69,13 +69,14 @@ module LavinMQ client end - def disconnect_client(client_id) + def disconnect_client(client) + client_id = client.client_id if session = sessions[client_id]? session.client = nil sessions.delete(client_id) if session.clean_session? end - @clients.delete client_id + vhost.rm_connection(client) end def publish(packet : MQTT::Publish) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 8a174150ab..4d8e022832 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -48,7 +48,6 @@ module LavinMQ # If we dont breakt the loop here we'll get a IO/Error on next read. break if packet.is_a?(MQTT::Disconnect) end - #do we even need this as a "case" if we don't log anything special? rescue ex : ::MQTT::Protocol::Error::PacketDecode publish_will if @will @socket.close @@ -61,10 +60,8 @@ module LavinMQ publish_will if @will raise ex ensure - @broker.disconnect_client(client_id) + @broker.disconnect_client(self) @socket.close - # move to disconnect client - @broker.vhost.rm_connection(self) end def read_and_handle_packet From 6ab4c636d45676694a80cc8b1e47b24918ec2912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Wed, 23 Oct 2024 11:16:44 +0200 Subject: [PATCH 100/202] Improve logging --- src/lavinmq/mqtt/client.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 4d8e022832..17970eeb90 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -49,10 +49,11 @@ module LavinMQ break if packet.is_a?(MQTT::Disconnect) end rescue ex : ::MQTT::Protocol::Error::PacketDecode + @log.warn(exception: ex) { "Packet decode error" } publish_will if @will @socket.close rescue ex : MQTT::Error::Connect - @log.warn { "Connect error #{ex.inspect}" } + @log.warn { "Connect error: #{ex.message}" } rescue ex : ::IO::Error @log.warn(exception: ex) { "Read Loop error" } publish_will if @will From 306d131bfb289ef17ba774821dc6bed1d17da7a5 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Oct 2024 15:23:48 +0200 Subject: [PATCH 101/202] add retain_store specs for .retain and .each --- spec/mqtt/integrations/retain_store_spec.cr | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 spec/mqtt/integrations/retain_store_spec.cr diff --git a/spec/mqtt/integrations/retain_store_spec.cr b/spec/mqtt/integrations/retain_store_spec.cr new file mode 100644 index 0000000000..c9c1043ead --- /dev/null +++ b/spec/mqtt/integrations/retain_store_spec.cr @@ -0,0 +1,90 @@ +require "../spec_helper" + + +module MqttSpecs + extend MqttHelpers + alias IndexTree = LavinMQ::MQTT::TopicTree(String) + + context "retain_store" do + after_each do + # Clear out the retain_store directory + FileUtils.rm_rf("tmp/retain_store") + end + + describe "retain" do + it "adds to index and writes msg file" do + index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + props = LavinMQ::AMQP::Properties.new + msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) + store.retain("a", msg.body_io, msg.bodysize) + + index.size.should eq(1) + index.@leafs.has_key?("a").should be_true + + entry = index["a"]?.not_nil! + File.exists?(File.join("tmp/retain_store", entry)).should be_true + end + + it "empty body deletes" do + index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + props = LavinMQ::AMQP::Properties.new + msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) + msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) + + store.retain("a", msg.body_io, msg.bodysize) + index.size.should eq(1) + entry = index["a"]?.not_nil! + + store.retain("a", msg.body_io, 0) + index.size.should eq(0) + File.exists?(File.join("tmp/retain_store", entry)).should be_false + end + end + + describe "each" do + it "calls block with correct arguments" do + index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + props = LavinMQ::AMQP::Properties.new + msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) + store.retain("a", msg.body_io, msg.bodysize) + store.retain("b", msg.body_io, msg.bodysize) + + called = [] of Tuple(String, Bytes) + store.each("a") do |topic, bytes| + called << {topic, bytes} + end + + called.size.should eq(1) + called[0][0].should eq("a") + String.new(called[0][1]).should eq("body") + end + + it "handles multiple subscriptions" do + index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + props = LavinMQ::AMQP::Properties.new + msg1 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) + msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) + store.retain("a", msg1.body_io, msg1.bodysize) + store.retain("b", msg2.body_io, msg2.bodysize) + + called = [] of Tuple(String, Bytes) + store.each("a") do |topic, bytes| + called << {topic, bytes} + end + store.each("b") do |topic, bytes| + called << {topic, bytes} + end + + called.size.should eq(2) + called[0][0].should eq("a") + String.new(called[0][1]).should eq("body") + called[1][0].should eq("b") + String.new(called[1][1]).should eq("body") + end + end + end +end From a2cf40bd16fa940ec7fd58897c12073a7abd58a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Thu, 24 Oct 2024 08:44:44 +0200 Subject: [PATCH 102/202] No need to prefix class, use namespace --- src/lavinmq/mqtt/client.cr | 2 +- src/lavinmq/mqtt/session.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 17970eeb90..b55beebc0e 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -153,7 +153,7 @@ module LavinMQ end end - class MqttConsumer < LavinMQ::Client::Channel::Consumer + class Consumer < LavinMQ::Client::Channel::Consumer getter unacked = 0_u32 getter tag : String = "mqtt" property prefetch_count = 1 diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 8c8a7e61f0..6b022a0f77 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -56,7 +56,7 @@ module LavinMQ @unacked.clear if c = client - @consumers << MqttConsumer.new(c, self) + @consumers << MQTT::Consumer.new(c, self) spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end @log.debug { "client set to '#{client.try &.name}'" } From 9a1f4dd1b53e22e3393979ec9978f2ccef83d8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20Dahl=C3=A9n?= <85930202+kickster97@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:13:14 +0200 Subject: [PATCH 103/202] Update src/lavinmq/mqtt/client.cr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jon Börjesson --- src/lavinmq/mqtt/client.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index b55beebc0e..fbd4bdcbc5 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -96,7 +96,7 @@ module LavinMQ def recieve_publish(packet : MQTT::Publish) @broker.publish(packet) - # Ok to not send anything if qos = 0 (at most once delivery) + # Ok to not send anything if qos = 0 (fire and forget) if packet.qos > 0 && (packet_id = packet.packet_id) send(MQTT::PubAck.new(packet_id)) end From 713408d7b88d9aa6026c897175f584783fad896d Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 24 Oct 2024 09:18:28 +0200 Subject: [PATCH 104/202] remove unnessecary socket.close --- spec/mqtt/integrations/retain_store_spec.cr | 1 - src/lavinmq/mqtt/client.cr | 1 - 2 files changed, 2 deletions(-) diff --git a/spec/mqtt/integrations/retain_store_spec.cr b/spec/mqtt/integrations/retain_store_spec.cr index c9c1043ead..f26aac12ae 100644 --- a/spec/mqtt/integrations/retain_store_spec.cr +++ b/spec/mqtt/integrations/retain_store_spec.cr @@ -1,6 +1,5 @@ require "../spec_helper" - module MqttSpecs extend MqttHelpers alias IndexTree = LavinMQ::MQTT::TopicTree(String) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index fbd4bdcbc5..afd83a990c 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -51,7 +51,6 @@ module LavinMQ rescue ex : ::MQTT::Protocol::Error::PacketDecode @log.warn(exception: ex) { "Packet decode error" } publish_will if @will - @socket.close rescue ex : MQTT::Error::Connect @log.warn { "Connect error: #{ex.message}" } rescue ex : ::IO::Error From c2761fd5d075798e2dfc479de9df9cae4757783a Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 24 Oct 2024 09:35:49 +0200 Subject: [PATCH 105/202] log warning instead of raise --- src/lavinmq/mqtt/client.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index afd83a990c..21e3e2e252 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -57,8 +57,8 @@ module LavinMQ @log.warn(exception: ex) { "Read Loop error" } publish_will if @will rescue ex + @log.warn(exception: ex) { "Read Loop error" } publish_will if @will - raise ex ensure @broker.disconnect_client(self) @socket.close From 2ff71feff001454a7135f1fb315b3df8235dd0df Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 24 Oct 2024 09:58:51 +0200 Subject: [PATCH 106/202] fetch max_inflight_messages form config --- src/lavinmq/config.cr | 17 +++++++++++++++++ src/lavinmq/mqtt/session.cr | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index ae8fe4e526..9de53fb05d 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -40,6 +40,7 @@ module LavinMQ property? set_timestamp = false # in message headers when receive property socket_buffer_size = 16384 # bytes property? tcp_nodelay = false # bool + property max_inflight_messages : UInt16 = 65_535 property segment_size : Int32 = 8 * 1024**2 # bytes property? raise_gc_warn : Bool = false property? data_dir_lock : Bool = true @@ -170,6 +171,7 @@ module LavinMQ case section when "main" then parse_main(settings) when "amqp" then parse_amqp(settings) + when "mqtt" then parse_mqtt(settings) when "mgmt", "http" then parse_mgmt(settings) when "clustering" then parse_clustering(settings) when "experimental" then parse_experimental(settings) @@ -281,6 +283,21 @@ module LavinMQ end end + private def parse_mqtt(settings) + settings.each do |config, v| + case config + when "bind" then @mqtt_bind = v + when "port" then @mqtt_port = v.to_i32 + when "tls_cert" then @tls_cert_path = v # backward compatibility + when "tls_key" then @tls_key_path = v # backward compatibility + when "max_inflight_messages" then @max_inflight_messages = v.to_u16 + else + STDERR.puts "WARNING: Unrecognized configuration 'mqtt/#{config}'" + end + end + end + + private def parse_mgmt(settings) settings.each do |config, v| case config diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 6b022a0f77..17186b31f6 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -8,13 +8,13 @@ module LavinMQ @clean_session : Bool = false getter clean_session + getter max_inflight_messages : UInt16? = Config.instance.max_inflight_messages def initialize(@vhost : VHost, @name : String, @auto_delete = false, arguments : ::AMQ::Protocol::Table = AMQP::Table.new) @count = 0u16 - @max_inflight = 65_535u16 # TODO: get this from config @unacked = Hash(UInt16, SegmentPosition).new super(@vhost, @name, false, @auto_delete, arguments) @@ -142,7 +142,7 @@ module LavinMQ private def queue_expire_loop; end private def next_id : UInt16? - return nil if @unacked.size == @max_inflight + return nil if @unacked.size == max_inflight_messages start_id = @count next_id : UInt16 = start_id &+ 1_u16 while @unacked.has_key?(next_id) From 2f892a0f9c69fa29126229233d9733b3f6f73d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Thu, 24 Oct 2024 09:02:56 +0200 Subject: [PATCH 107/202] Convert Publish to Message in exchange --- src/lavinmq/exchange/mqtt.cr | 14 ++++++++++++++ src/lavinmq/mqtt/broker.cr | 9 +-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 2097fd26af..c29fe370bf 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -32,6 +32,16 @@ module LavinMQ super(vhost, name, true, false, true) end + def publish(packet : MQTT::Publish) : Int32 + headers = AMQP::Table.new.tap do |h| + h["x-mqtt-retain"] = true if packet.retain? + end + properties = AMQP::Properties.new(headers: headers).tap do |p| + p.delivery_mode = packet.qos if packet.responds_to?(:qos) + end + publish Message.new("mqtt.default", topicfilter_to_routingkey(packet.topic), String.new(packet.payload), properties), false + end + private def do_publish(msg : Message, immediate : Bool, queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 @@ -51,6 +61,10 @@ module LavinMQ count end + def topicfilter_to_routingkey(tf) : String + tf.tr("/+", ".*") + end + def routing_key_to_topic(routing_key : String) : String routing_key.tr(".*", "/+") end diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 703550915c..cf58cacef5 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -80,14 +80,7 @@ module LavinMQ end def publish(packet : MQTT::Publish) - headers = AMQP::Table.new.tap do |h| - h["x-mqtt-retain"] = true if packet.retain? - end - properties = AMQP::Properties.new(headers: headers).tap do |p| - p.delivery_mode = packet.qos if packet.responds_to?(:qos) - end - msg = Message.new("mqtt.default", topicfilter_to_routingkey(packet.topic), String.new(packet.payload), properties) - @exchange.publish(msg, false) + @exchange.publish(packet) end def topicfilter_to_routingkey(tf) : String From d6b584add4d351047131275b59cd2508d6b29a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Thu, 24 Oct 2024 10:16:53 +0200 Subject: [PATCH 108/202] Less aggressive logging --- src/lavinmq/mqtt/client.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 21e3e2e252..630ae32001 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -54,7 +54,7 @@ module LavinMQ rescue ex : MQTT::Error::Connect @log.warn { "Connect error: #{ex.message}" } rescue ex : ::IO::Error - @log.warn(exception: ex) { "Read Loop error" } + @log.warn { "Client unexpectedly closed connection" } unless @closed publish_will if @will rescue ex @log.warn(exception: ex) { "Read Loop error" } From a2123cbd9135d5a0019948ba0ade5f1410d179b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Thu, 24 Oct 2024 10:18:01 +0200 Subject: [PATCH 109/202] Suspend fiber while waiting for msg or consumer This will also keep the deliver loop fiber alive as long as the session exists, but it may change back again in future refactoring. --- src/lavinmq/mqtt/session.cr | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 17186b31f6..3edaa9b1a3 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -20,7 +20,7 @@ module LavinMQ super(@vhost, @name, false, @auto_delete, arguments) @log = Logger.new(Log, @metadata) - spawn deliver_loop, name: "Consumer deliver loop", same_thread: true + spawn deliver_loop, name: "Session#deliver_loop", same_thread: true end def clean_session? @@ -30,14 +30,20 @@ module LavinMQ private def deliver_loop i = 0 loop do - break if consumers.empty? - consume_get(consumers.first) do |env| + break if @closed + if @msg_store.empty? || @consumers.empty? + Channel.receive_first(@msg_store.empty_change, @consumers_empty_change) + next + end + get(false) do |env| consumers.first.deliver(env.message, env.segment_position, env.redelivered) end Fiber.yield if (i &+= 1) % 32768 == 0 end + rescue ::Channel::ClosedError + return rescue ex - puts "deliver loop exiting: #{ex.inspect_with_backtrace}" + @log.trace(exception: ex) { "deliver loop exiting" } end def client=(client : MQTT::Client?) @@ -57,7 +63,6 @@ module LavinMQ if c = client @consumers << MQTT::Consumer.new(c, self) - spawn deliver_loop, name: "Consumer deliver loop", same_thread: true end @log.debug { "client set to '#{client.try &.name}'" } end From 8376b26be789bb4559bb2322725eb04effec0a81 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 25 Oct 2024 14:44:29 +0200 Subject: [PATCH 110/202] add specs for handling connect packets with empty client_ids --- spec/mqtt/integrations/connect_spec.cr | 40 +++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 80bbac859e..aab1ea2375 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -149,6 +149,45 @@ module MqttSpecs end end + it "client_id must be the first field of the connect packet [MQTT-3.1.3-3]" do + with_server do |server| + with_client_io(server) do |io| + connect = MQTT::Protocol::Connect.new( + client_id: "client_id", + clean_session: true, + keepalive: 30u16, + username: "valid_user", + password: "valid_password".to_slice, + will: nil + ).to_slice + connect[0] = 'x'.ord.to_u8 + io.write_bytes_raw connect + io.should be_closed + end + end + end + + it "accepts zero byte client_id but is assigned a unique client_id [MQTT-3.1.3-6]" do + with_server do |server| + with_client_io(server) do |io| + connack = connect(io, client_id: "", clean_session: true) + server.broker.@clients.first[1].@client_id.should_not eq("") + end + end + end + + it "accepts zero-byte ClientId with CleanSession set to 1 [MQTT-3.1.3-7]" do + with_server do |server| + with_client_io(server) do |io| + connack = connect(io, client_id: "", clean_session: true) + connack.should be_a(MQTT::Protocol::Connack) + connack = connack.as(MQTT::Protocol::Connack) + connack.return_code.should eq(MQTT::Protocol::Connack::ReturnCode::Accepted) + io.should_not be_closed + end + end + end + it "for empty client id with non-clean session [MQTT-3.1.3-8]" do with_server do |server| with_client_io(server) do |io| @@ -156,7 +195,6 @@ module MqttSpecs connack.should be_a(MQTT::Protocol::Connack) connack = connack.as(MQTT::Protocol::Connack) connack.return_code.should eq(MQTT::Protocol::Connack::ReturnCode::IdentifierRejected) - # Verify that connection is closed [MQTT-3.1.4-1] io.should be_closed end end From 9bdc56e3a6204389575e7556cf099340205e5b8e Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 25 Oct 2024 14:44:50 +0200 Subject: [PATCH 111/202] handle connect packets with empty client_id strings --- src/lavinmq/mqtt/connection_factory.cr | 13 ++++++++++++- src/lavinmq/server.cr | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 2e6812c791..20a159e3f0 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -19,6 +19,7 @@ module LavinMQ if packet = MQTT::Packet.from_io(socket).as?(MQTT::Connect) Log.trace { "recv #{packet.inspect}" } if user = authenticate(io, packet) + packet = assign_client_id_to_packet(packet) if packet.client_id.empty? session_present = @broker.session_present?(packet.client_id, packet.clean_session?) MQTT::Connack.new(session_present, MQTT::Connack::ReturnCode::Accepted).to_io(io) io.flush @@ -32,7 +33,7 @@ module LavinMQ end socket.close rescue ex - Log.warn { "Recieved the wrong packet" } + Log.warn { "Recieved invalid Connect packet" } socket.close end @@ -49,6 +50,16 @@ module LavinMQ MQTT::Connack.new(false, MQTT::Connack::ReturnCode::NotAuthorized).to_io(io) nil end + + def assign_client_id_to_packet(packet) + client_id = "#{Random::Secure.base64(32)}" + MQTT::Connect.new(client_id, + packet.clean_session?, + packet.keepalive, + packet.username, + packet.password, + packet.will) + end end end end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 918e63b270..6ba02a0060 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -21,7 +21,7 @@ require "./stats" module LavinMQ class Server - getter vhosts, users, data_dir, parameters + getter vhosts, users, data_dir, parameters, broker getter? closed, flow include ParameterTarget From 6ddfcf62715051a6e418f94353f2f3d78735c362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 29 Oct 2024 16:00:42 +0100 Subject: [PATCH 112/202] fixup! Suspend fiber while waiting for msg or consumer --- src/lavinmq/mqtt/session.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 3edaa9b1a3..e2566d8401 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -62,7 +62,7 @@ module LavinMQ @unacked.clear if c = client - @consumers << MQTT::Consumer.new(c, self) + add_consumer MQTT::Consumer.new(c, self) end @log.debug { "client set to '#{client.try &.name}'" } end From ff95570224e6aed1c69ae36d45ba3ef7c247b04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Wed, 30 Oct 2024 08:26:16 +0100 Subject: [PATCH 113/202] Move Sessions to separate file --- src/lavinmq/mqtt/broker.cr | 36 +++++------------------------------ src/lavinmq/mqtt/sessions.cr | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 src/lavinmq/mqtt/sessions.cr diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index cf58cacef5..a6652924f7 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -1,38 +1,12 @@ +require "./client" +require "./protocol" +require "./session" +require "./sessions" require "./retain_store" +require "../vhost" module LavinMQ module MQTT - struct Sessions - @queues : Hash(String, Queue) - - def initialize(@vhost : VHost) - @queues = @vhost.queues - end - - def []?(client_id : String) : Session? - @queues["amq.mqtt-#{client_id}"]?.try &.as(Session) - end - - def [](client_id : String) : Session - @queues["amq.mqtt-#{client_id}"].as(Session) - end - - def declare(client_id : String, clean_session : Bool) - self[client_id]? || begin - @vhost.declare_queue("amq.mqtt-#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) - self[client_id] - end - end - - def delete(client_id : String) - @vhost.delete_queue("amq.mqtt-#{client_id}") - end - - def delete(session : Session) - session.delete - end - end - class Broker getter vhost, sessions diff --git a/src/lavinmq/mqtt/sessions.cr b/src/lavinmq/mqtt/sessions.cr new file mode 100644 index 0000000000..526f8ee0d0 --- /dev/null +++ b/src/lavinmq/mqtt/sessions.cr @@ -0,0 +1,37 @@ +require "./session" +require "../vhost" + +module LavinMQ + module MQTT + struct Sessions + @queues : Hash(String, Queue) + + def initialize(@vhost : VHost) + @queues = @vhost.queues + end + + def []?(client_id : String) : Session? + @queues["amq.mqtt-#{client_id}"]?.try &.as(Session) + end + + def [](client_id : String) : Session + @queues["amq.mqtt-#{client_id}"].as(Session) + end + + def declare(client_id : String, clean_session : Bool) + self[client_id]? || begin + @vhost.declare_queue("amq.mqtt-#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) + self[client_id] + end + end + + def delete(client_id : String) + @vhost.delete_queue("amq.mqtt-#{client_id}") + end + + def delete(session : Session) + session.delete + end + end + end +end From d5411a4c781eba77d2a76f21c64ca56fc459c99a Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 30 Oct 2024 11:23:23 +0100 Subject: [PATCH 114/202] Dont convert topic to routing key, and use topic all the way through --- src/lavinmq/exchange/mqtt.cr | 13 +++++-------- src/lavinmq/mqtt/broker.cr | 7 +------ src/lavinmq/mqtt/session.cr | 16 +++++----------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index c29fe370bf..34f235aa63 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -39,19 +39,18 @@ module LavinMQ properties = AMQP::Properties.new(headers: headers).tap do |p| p.delivery_mode = packet.qos if packet.responds_to?(:qos) end - publish Message.new("mqtt.default", topicfilter_to_routingkey(packet.topic), String.new(packet.payload), properties), false + publish Message.new("mqtt.default", packet.topic, String.new(packet.payload), properties), false end private def do_publish(msg : Message, immediate : Bool, queues : Set(Queue) = Set(Queue).new, exchanges : Set(Exchange) = Set(Exchange).new) : Int32 count = 0 - topic = routing_key_to_topic(msg.routing_key) if msg.properties.try &.headers.try &.["x-mqtt-retain"]? - @retain_store.retain(topic, msg.body_io, msg.bodysize) + @retain_store.retain(msg.routing_key, msg.body_io, msg.bodysize) end - @tree.each_entry(topic) do |queue, qos| + @tree.each_entry(msg.routing_key) do |queue, qos| msg.properties.delivery_mode = qos if queue.publish(msg) count += 1 @@ -83,11 +82,10 @@ module LavinMQ end def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool - topic = routing_key_to_topic(routing_key) qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 binding_key = MqttBindingKey.new(routing_key, headers) @bindings[binding_key].add destination - @tree.subscribe(topic, destination, qos) + @tree.subscribe(routing_key, destination, qos) data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) notify_observers(ExchangeEvent::Bind, data) @@ -95,13 +93,12 @@ module LavinMQ end def unbind(destination : MQTT::Session, routing_key, headers = nil) : Bool - topic = routing_key_to_topic(routing_key) binding_key = MqttBindingKey.new(routing_key, headers) rk_bindings = @bindings[binding_key] rk_bindings.delete destination @bindings.delete binding_key if rk_bindings.empty? - @tree.unsubscribe(topic, destination) + @tree.unsubscribe(routing_key, destination) data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) notify_observers(ExchangeEvent::Unbind, data) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index a6652924f7..7c1ed3eb45 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -57,10 +57,6 @@ module LavinMQ @exchange.publish(packet) end - def topicfilter_to_routingkey(tf) : String - tf.tr("/+", ".*") - end - def subscribe(client, packet) unless session = sessions[client.client_id]? session = sessions.declare(client.client_id, client.@clean_session) @@ -71,8 +67,7 @@ module LavinMQ qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) session.subscribe(tf.topic, tf.qos) @retain_store.each(tf.topic) do |topic, body| - rk = topicfilter_to_routingkey(topic) - msg = Message.new("mqtt.default", rk, String.new(body), + msg = Message.new("mqtt.default", topic, String.new(body), AMQP::Properties.new(headers: AMQP::Table.new({"x-mqtt-retain": true}), delivery_mode: tf.qos)) session.publish(msg) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index e2566d8401..d15ba33190 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -72,26 +72,20 @@ module LavinMQ end def subscribe(tf, qos) - rk = topicfilter_to_routingkey(tf) arguments = AMQP::Table.new({"x-mqtt-qos": qos}) - if binding = find_binding(rk) + if binding = find_binding(tf) return if binding.binding_key.arguments == arguments - unbind(rk, binding.binding_key.arguments) + unbind(tf, binding.binding_key.arguments) end - @vhost.bind_queue(@name, "mqtt.default", rk, arguments) + @vhost.bind_queue(@name, "mqtt.default", tf, arguments) end def unsubscribe(tf) - rk = topicfilter_to_routingkey(tf) - if binding = find_binding(rk) - unbind(rk, binding.binding_key.arguments) + if binding = find_binding(tf) + unbind(tf, binding.binding_key.arguments) end end - def topicfilter_to_routingkey(tf) : String - tf.tr("/+", ".*") - end - private def find_binding(rk) bindings.find { |b| b.binding_key.routing_key == rk } end From e5381e2bcf3a7b3a5c3d94dfbf2c4cbd06300559 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 30 Oct 2024 13:16:46 +0100 Subject: [PATCH 115/202] prefix sessions with mqtt. and do not let amqp queues create queues that start with .mqtt --- spec/mqtt/integrations/connect_spec.cr | 2 +- src/lavinmq/mqtt/sessions.cr | 8 ++++---- src/lavinmq/vhost.cr | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index aab1ea2375..b080f56026 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -311,7 +311,7 @@ module MqttSpecs disconnect(io) end sleep 100.milliseconds - server.vhosts["/"].queues["amq.mqtt-client_id"].consumers.should be_empty + server.vhosts["/"].queues["mqtt.client_id"].consumers.should be_empty end end end diff --git a/src/lavinmq/mqtt/sessions.cr b/src/lavinmq/mqtt/sessions.cr index 526f8ee0d0..3127226699 100644 --- a/src/lavinmq/mqtt/sessions.cr +++ b/src/lavinmq/mqtt/sessions.cr @@ -11,22 +11,22 @@ module LavinMQ end def []?(client_id : String) : Session? - @queues["amq.mqtt-#{client_id}"]?.try &.as(Session) + @queues["mqtt.#{client_id}"]?.try &.as(Session) end def [](client_id : String) : Session - @queues["amq.mqtt-#{client_id}"].as(Session) + @queues["mqtt.#{client_id}"].as(Session) end def declare(client_id : String, clean_session : Bool) self[client_id]? || begin - @vhost.declare_queue("amq.mqtt-#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) + @vhost.declare_queue("mqtt.#{client_id}", !clean_session, clean_session, AMQP::Table.new({"x-queue-type": "mqtt"})) self[client_id] end end def delete(client_id : String) - @vhost.delete_queue("amq.mqtt-#{client_id}") + @vhost.delete_queue("mqtt.#{client_id}") end def delete(session : Session) diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index 8b102c4c15..04132bcb3f 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -253,6 +253,7 @@ module LavinMQ return false unless src.unbind(dst, f.routing_key, f.arguments) store_definition(f, dirty: true) if !loading && src.durable? && dst.durable? when AMQP::Frame::Queue::Declare + return false if f.queue_name.starts_with?("mqtt.") && f.arguments["x-queue-type"]? != "mqtt" return false if @queues.has_key? f.queue_name q = @queues[f.queue_name] = QueueFactory.make(self, f) apply_policies([q] of Queue) unless loading From f03af4f952b86ad06cabaaaedbed0e2723e29854 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 30 Oct 2024 13:58:27 +0100 Subject: [PATCH 116/202] move validation to queue_factory and return preconditioned fail for amqp queues with .mqtt prefix --- src/lavinmq/queue_factory.cr | 8 ++++++++ src/lavinmq/vhost.cr | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lavinmq/queue_factory.cr b/src/lavinmq/queue_factory.cr index f4b9d4e04a..e02e6553e8 100644 --- a/src/lavinmq/queue_factory.cr +++ b/src/lavinmq/queue_factory.cr @@ -16,6 +16,7 @@ module LavinMQ end private def self.make_durable(vhost, frame) + validate_mqtt_queue_name frame if prio_queue? frame AMQP::DurablePriorityQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif stream_queue? frame @@ -34,6 +35,7 @@ module LavinMQ end private def self.make_queue(vhost, frame) + validate_mqtt_queue_name frame if prio_queue? frame AMQP::PriorityQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif stream_queue? frame @@ -68,5 +70,11 @@ module LavinMQ private def self.mqtt_session?(frame) : Bool frame.arguments["x-queue-type"]? == "mqtt" end + + private def self.validate_mqtt_queue_name(frame) + if frame.queue_name.starts_with?("mqtt.") && !mqtt_session?(frame) + raise Error::PreconditionFailed.new("Only MQTT sessions can create queues with the prefix 'mqtt.'") + end + end end end diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index 04132bcb3f..8b102c4c15 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -253,7 +253,6 @@ module LavinMQ return false unless src.unbind(dst, f.routing_key, f.arguments) store_definition(f, dirty: true) if !loading && src.durable? && dst.durable? when AMQP::Frame::Queue::Declare - return false if f.queue_name.starts_with?("mqtt.") && f.arguments["x-queue-type"]? != "mqtt" return false if @queues.has_key? f.queue_name q = @queues[f.queue_name] = QueueFactory.make(self, f) apply_policies([q] of Queue) unless loading From 14607d23961a47df089f774f50133717e2538969 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 31 Oct 2024 11:42:23 +0100 Subject: [PATCH 117/202] prefix_validation wip --- src/lavinmq/amqp/client.cr | 13 +++++++------ src/lavinmq/http/controller/exchanges.cr | 4 ++-- src/lavinmq/http/controller/queues.cr | 5 +++-- src/lavinmq/prefix_validation.cr | 10 ++++++++++ src/lavinmq/queue_factory.cr | 12 ++++-------- 5 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 src/lavinmq/prefix_validation.cr diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index 8982aa20e8..8273ca68b3 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -4,6 +4,7 @@ require "./channel" require "../client" require "../error" require "../logger" +require "../prefix_validation.cr" module LavinMQ module AMQP @@ -516,8 +517,8 @@ module LavinMQ redeclare_exchange(e, frame) elsif frame.passive send_not_found(frame, "Exchange '#{frame.exchange_name}' doesn't exists") - elsif frame.exchange_name.starts_with? "amq." - send_access_refused(frame, "Not allowed to use the amq. prefix") + elsif PrefixValidation.invalid?(frame.exchange_name) + send_access_refused(frame, "Not allowed to use that prefix") else ae = frame.arguments["x-alternate-exchange"]?.try &.as?(String) ae_ok = ae.nil? || (@user.can_write?(@vhost.name, ae) && @user.can_read?(@vhost.name, frame.exchange_name)) @@ -549,8 +550,8 @@ module LavinMQ send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? send_access_refused(frame, "Not allowed to delete the default exchange") - elsif frame.exchange_name.starts_with? "amq." - send_access_refused(frame, "Not allowed to use the amq. prefix") + elsif PrefixValidation.invalid?(frame.exchange_name) + send_access_refused(frame, "Not allowed to use that prefix") elsif !@vhost.exchanges.has_key? frame.exchange_name # should return not_found according to spec but we make it idempotent send AMQP::Frame::Exchange::DeleteOk.new(frame.channel) unless frame.no_wait @@ -619,8 +620,8 @@ module LavinMQ end elsif frame.passive send_not_found(frame, "Queue '#{frame.queue_name}' doesn't exists") - elsif frame.queue_name.starts_with? "amq." - send_access_refused(frame, "Not allowed to use the amq. prefix") + elsif PrefixValidation.invalid?(frame.queue_name) + send_access_refused(frame, "Not allowed to use that prefix") elsif @vhost.max_queues.try { |max| @vhost.queues.size >= max } send_access_refused(frame, "queue limit in vhost '#{@vhost.name}' (#{@vhost.max_queues}) is reached") else diff --git a/src/lavinmq/http/controller/exchanges.cr b/src/lavinmq/http/controller/exchanges.cr index 8a6dbc0c5e..61dd629ee4 100644 --- a/src/lavinmq/http/controller/exchanges.cr +++ b/src/lavinmq/http/controller/exchanges.cr @@ -69,8 +69,8 @@ module LavinMQ bad_request(context, "Not allowed to publish to internal exchange") end context.response.status_code = 204 - elsif name.starts_with? "amq." - bad_request(context, "Not allowed to use the amq. prefix") + elsif PrefixValidation.invalid?(name) + bad_request(context, "Not allowed to use that prefix") elsif name.bytesize > UInt8::MAX bad_request(context, "Exchange name too long, can't exceed 255 characters") else diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index 51c8351158..dca9dddff1 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -2,6 +2,7 @@ require "uri" require "../controller" require "../binding_helpers" require "../../unacked_message" +require "../../prefix_validation" module LavinMQ module HTTP @@ -80,8 +81,8 @@ module LavinMQ bad_request(context, "Existing queue declared with other arguments arg") end context.response.status_code = 204 - elsif name.starts_with? "amq." - bad_request(context, "Not allowed to use the amq. prefix") + elsif PrefixValidation.invalid?(name) + bad_request(context, "Not allowed to use that prefix") elsif name.bytesize > UInt8::MAX bad_request(context, "Queue name too long, can't exceed 255 characters") else diff --git a/src/lavinmq/prefix_validation.cr b/src/lavinmq/prefix_validation.cr new file mode 100644 index 0000000000..a9196afc58 --- /dev/null +++ b/src/lavinmq/prefix_validation.cr @@ -0,0 +1,10 @@ +require "./error" + +class PrefixValidation + PREFIX_LIST = ["mqtt.", "amq."] + def self.invalid?(name) + prefix = name[0..name.index(".") || name.size - 1] + return true if PREFIX_LIST.includes?(prefix) + return false + end +end diff --git a/src/lavinmq/queue_factory.cr b/src/lavinmq/queue_factory.cr index e02e6553e8..95f6b4a5d3 100644 --- a/src/lavinmq/queue_factory.cr +++ b/src/lavinmq/queue_factory.cr @@ -2,6 +2,8 @@ require "./amqp/queue" require "./amqp/queue/priority_queue" require "./amqp/queue/durable_queue" require "./amqp/queue/stream_queue" +require "../mqtt/session" +require "../prefix_validation" module LavinMQ class QueueFactory @@ -16,7 +18,7 @@ module LavinMQ end private def self.make_durable(vhost, frame) - validate_mqtt_queue_name frame + raise Error::PreconditionFailed.new("Not allowed to use that prefix") if PrefixValidation.invalid?(frame.queue_name) && !mqtt_session?(frame) if prio_queue? frame AMQP::DurablePriorityQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif stream_queue? frame @@ -35,7 +37,7 @@ module LavinMQ end private def self.make_queue(vhost, frame) - validate_mqtt_queue_name frame + raise Error::PreconditionFailed.new("Not allowed to use that prefix") if PrefixValidation.invalid?(frame.queue_name) && !mqtt_session?(frame) if prio_queue? frame AMQP::PriorityQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif stream_queue? frame @@ -70,11 +72,5 @@ module LavinMQ private def self.mqtt_session?(frame) : Bool frame.arguments["x-queue-type"]? == "mqtt" end - - private def self.validate_mqtt_queue_name(frame) - if frame.queue_name.starts_with?("mqtt.") && !mqtt_session?(frame) - raise Error::PreconditionFailed.new("Only MQTT sessions can create queues with the prefix 'mqtt.'") - end - end end end From faa111cf0b8ef23bc76182fc20768ad4d5c79625 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 31 Oct 2024 13:10:54 +0100 Subject: [PATCH 118/202] delete old cherry-picked code, replaced with #818 --- spec/stdlib/io_buffered_spec.cr | 120 -------------------------------- src/stdlib/io_buffered.cr | 32 --------- 2 files changed, 152 deletions(-) delete mode 100644 spec/stdlib/io_buffered_spec.cr delete mode 100644 src/stdlib/io_buffered.cr diff --git a/spec/stdlib/io_buffered_spec.cr b/spec/stdlib/io_buffered_spec.cr deleted file mode 100644 index 0dbe020584..0000000000 --- a/spec/stdlib/io_buffered_spec.cr +++ /dev/null @@ -1,120 +0,0 @@ -require "random" -require "spec" -require "socket" -require "wait_group" -require "../../src/stdlib/io_buffered" - -def with_io(initial_content = "foo bar baz".to_slice, &) - read_io, write_io = UNIXSocket.pair - write_io.write initial_content - yield read_io, write_io -ensure - read_io.try &.close - write_io.try &.close -end - -describe IO::Buffered do - describe "#peek" do - it "raises if read_buffering is false" do - with_io do |read_io, _write_io| - read_io.read_buffering = false - expect_raises(RuntimeError) do - read_io.peek(5) - end - end - end - - it "raises if size is greater than buffer_size" do - with_io do |read_io, _write_io| - read_io.read_buffering = true - read_io.buffer_size = 5 - expect_raises(ArgumentError) do - read_io.peek(10) - end - end - end - - it "raises unless size is positive" do - with_io do |read_io, _write_io| - read_io.read_buffering = true - expect_raises(ArgumentError) do - read_io.peek(-10) - end - end - end - - it "returns slice of requested size" do - with_io("foo bar".to_slice) do |read_io, _write_io| - read_io.read_buffering = true - read_io.buffer_size = 5 - read_io.peek(3).should eq "foo".to_slice - end - end - - it "will read until buffer contains at least size bytes" do - initial_data = "foo".to_slice - with_io(initial_data) do |read_io, write_io| - read_io.read_buffering = true - read_io.buffer_size = 10 - - read_io.peek.should eq "foo".to_slice - - extra_data = "barbaz".to_slice - write_io.write extra_data - - peeked = read_io.peek(6) - peeked.should eq "foobar".to_slice - end - end - - it "will read up to buffer size if possible" do - initial_data = "foo".to_slice - with_io(initial_data) do |read_io, write_io| - read_io.read_buffering = true - read_io.buffer_size = 9 - - read_io.peek.should eq "foo".to_slice - - extra_data = "barbaz".to_slice - write_io.write extra_data - - peeked = read_io.peek(6) - peeked.should eq "foobar".to_slice - end - end - - it "will move existing data to beginning of internal buffer " do - initial_data = "000foo".to_slice - with_io(initial_data) do |read_io, write_io| - read_io.read_buffering = true - read_io.buffer_size = 9 - - data = Bytes.new(3) - read_io.read data - data.should eq "000".to_slice - - extra_data = "barbaz".to_slice - write_io.write extra_data - - peeked = read_io.peek(6) - peeked.should eq "foobar".to_slice - end - end - - it "returns what's in buffer upto size if io is closed" do - initial_data = "foobar".to_slice - with_io(initial_data) do |read_io, write_io| - read_io.read_buffering = true - read_io.buffer_size = 9 - - data = Bytes.new(3) - read_io.read data - data.should eq "foo".to_slice - - write_io.close - - read_io.peek(6).should eq "bar".to_slice - end - end - end -end diff --git a/src/stdlib/io_buffered.cr b/src/stdlib/io_buffered.cr deleted file mode 100644 index 45ffdb3e88..0000000000 --- a/src/stdlib/io_buffered.cr +++ /dev/null @@ -1,32 +0,0 @@ -module IO::Buffered - def peek(size : Int) - raise RuntimeError.new("Can't fill buffer when read_buffering is #{read_buffering?}") unless read_buffering? - raise ArgumentError.new("size must be positive") unless size.positive? - if size > @buffer_size - raise ArgumentError.new("size (#{size}) can't be greater than buffer_size #{@buffer_size}") - end - - # Enough data in buffer already - return @in_buffer_rem[0, size] if size < @in_buffer_rem.size - - in_buffer = in_buffer() - - # Move data to beginning of in_buffer if needed - if @in_buffer_rem.to_unsafe != in_buffer - @in_buffer_rem.copy_to(in_buffer, @in_buffer_rem.size) - end - - while @in_buffer_rem.size < size - target = Slice.new(in_buffer + @in_buffer_rem.size, @buffer_size - @in_buffer_rem.size) - bytes_read = unbuffered_read(target).to_i - break if bytes_read.zero? - @in_buffer_rem = Slice.new(in_buffer, @in_buffer_rem.size + bytes_read) - end - - if @in_buffer_rem.size < size - return @in_buffer_rem - end - - @in_buffer_rem[0, size] - end -end From 115f82c926e283e5f6d8bff18966a5cccbfc5e7d Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 31 Oct 2024 15:01:50 +0100 Subject: [PATCH 119/202] mqtt exchange receives MQTT::Publish but publish AMQP::Message to queue(session) --- src/lavinmq/exchange/mqtt.cr | 40 ++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr index 34f235aa63..2270bf1bd2 100644 --- a/src/lavinmq/exchange/mqtt.cr +++ b/src/lavinmq/exchange/mqtt.cr @@ -32,42 +32,46 @@ module LavinMQ super(vhost, name, true, false, true) end + def publish(msg : Message, immediate : Bool, + queues : Set(Queue) = Set(Queue).new, + exchanges : Set(Exchange) = Set(Exchange).new) : Int32 + raise LavinMQ::Exchange::AccessRefused.new(self) + end + def publish(packet : MQTT::Publish) : Int32 + @publish_in_count += 1 + headers = AMQP::Table.new.tap do |h| h["x-mqtt-retain"] = true if packet.retain? end properties = AMQP::Properties.new(headers: headers).tap do |p| p.delivery_mode = packet.qos if packet.responds_to?(:qos) end - publish Message.new("mqtt.default", packet.topic, String.new(packet.payload), properties), false - end - private def do_publish(msg : Message, immediate : Bool, - queues : Set(Queue) = Set(Queue).new, - exchanges : Set(Exchange) = Set(Exchange).new) : Int32 - count = 0 - if msg.properties.try &.headers.try &.["x-mqtt-retain"]? - @retain_store.retain(msg.routing_key, msg.body_io, msg.bodysize) - end + timestamp = RoughTime.unix_ms + bodysize = packet.payload.size.to_u64 + body = IO::Memory.new(bodysize) + body.write(packet.payload) + body.rewind - @tree.each_entry(msg.routing_key) do |queue, qos| + @retain_store.retain(packet.topic, body, bodysize) if packet.retain? + + body.rewind + msg = Message.new(timestamp, "mqtt.default", packet.topic, properties, bodysize, body) + + count = 0 + @tree.each_entry(packet.topic) do |queue, qos| msg.properties.delivery_mode = qos if queue.publish(msg) count += 1 msg.body_io.seek(-msg.bodysize.to_i64, IO::Seek::Current) # rewind end end + @unroutable_count += 1 if count.zero? + @publish_out_count += count count end - def topicfilter_to_routingkey(tf) : String - tf.tr("/+", ".*") - end - - def routing_key_to_topic(routing_key : String) : String - routing_key.tr(".*", "/+") - end - def bindings_details : Iterator(BindingDetails) @bindings.each.flat_map do |binding_key, ds| ds.each.map do |d| From 0adf3f6977b8436184795220a9e44dbf092dff52 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 31 Oct 2024 15:07:26 +0100 Subject: [PATCH 120/202] remove obsolete spec --- spec/mqtt_spec.cr | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 spec/mqtt_spec.cr diff --git a/spec/mqtt_spec.cr b/spec/mqtt_spec.cr deleted file mode 100644 index 1c42c4213a..0000000000 --- a/spec/mqtt_spec.cr +++ /dev/null @@ -1,47 +0,0 @@ -require "spec" -require "socket" -require "./spec_helper" -require "mqtt-protocol" -require "../src/lavinmq/mqtt/connection_factory" - -def setup_connection(s, pass) - left, right = UNIXSocket.pair - io = MQTT::Protocol::IO.new(left) - s.users.create("usr", "pass", [LavinMQ::Tag::Administrator]) - MQTT::Protocol::Connect.new("abc", false, 60u16, "usr", pass.to_slice, nil).to_io(io) - connection_factory = LavinMQ::MQTT::ConnectionFactory.new( - s.users, - s.vhosts["/"], - LavinMQ::MQTT::Broker.new(s.vhosts["/"])) - {connection_factory.start(right, LavinMQ::ConnectionInfo.local), io} -end - -describe LavinMQ do - it "MQTT connection should pass authentication" do - with_amqp_server do |s| - client, io = setup_connection(s, "pass") - client.should be_a(LavinMQ::MQTT::Client) - # client.close - MQTT::Protocol::Disconnect.new.to_io(io) - end - end - - it "unauthorized MQTT connection should not pass authentication" do - with_amqp_server do |s| - client, io = setup_connection(s, "pa&ss") - client.should_not be_a(LavinMQ::MQTT::Client) - # client.close - MQTT::Protocol::Disconnect.new.to_io(io) - end - end - - it "should handle a Ping" do - with_amqp_server do |s| - client, io = setup_connection(s, "pass") - client.should be_a(LavinMQ::MQTT::Client) - MQTT::Protocol::PingReq.new.to_io(io) - MQTT::Protocol::Packet.from_io(io).should be_a(MQTT::Protocol::Connack) - MQTT::Protocol::Packet.from_io(io).should be_a(MQTT::Protocol::PingResp) - end - end -end From 76b24c6a68d168b96765c6b59070cb23c63f47de Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 1 Nov 2024 08:51:28 +0100 Subject: [PATCH 121/202] format --- spec/mqtt/integrations/retain_store_spec.cr | 6 +++--- src/lavinmq/amqp/client.cr | 2 +- src/lavinmq/config.cr | 17 ++++++++--------- src/lavinmq/mqtt/connection_factory.cr | 16 ++++++++-------- src/lavinmq/prefix_validation.cr | 1 + 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/spec/mqtt/integrations/retain_store_spec.cr b/spec/mqtt/integrations/retain_store_spec.cr index f26aac12ae..240def3cb0 100644 --- a/spec/mqtt/integrations/retain_store_spec.cr +++ b/spec/mqtt/integrations/retain_store_spec.cr @@ -2,12 +2,12 @@ require "../spec_helper" module MqttSpecs extend MqttHelpers - alias IndexTree = LavinMQ::MQTT::TopicTree(String) + alias IndexTree = LavinMQ::MQTT::TopicTree(String) context "retain_store" do after_each do - # Clear out the retain_store directory - FileUtils.rm_rf("tmp/retain_store") + # Clear out the retain_store directory + FileUtils.rm_rf("tmp/retain_store") end describe "retain" do diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index 8273ca68b3..1bccd4406a 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -551,7 +551,7 @@ module LavinMQ elsif frame.exchange_name.empty? send_access_refused(frame, "Not allowed to delete the default exchange") elsif PrefixValidation.invalid?(frame.exchange_name) - send_access_refused(frame, "Not allowed to use that prefix") + send_access_refused(frame, "Not allowed to use that prefix") elsif !@vhost.exchanges.has_key? frame.exchange_name # should return not_found according to spec but we make it idempotent send AMQP::Frame::Exchange::DeleteOk.new(frame.channel) unless frame.no_wait diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index 9de53fb05d..24034b549c 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -32,14 +32,14 @@ module LavinMQ property http_unix_path = "" property http_systemd_socket_name = "lavinmq-http.socket" property amqp_systemd_socket_name = "lavinmq-amqp.socket" - property heartbeat = 300_u16 # second - property frame_max = 131_072_u32 # bytes - property channel_max = 2048_u16 # number - property stats_interval = 5000 # millisecond - property stats_log_size = 120 # 10 mins at 5s interval - property? set_timestamp = false # in message headers when receive - property socket_buffer_size = 16384 # bytes - property? tcp_nodelay = false # bool + property heartbeat = 300_u16 # second + property frame_max = 131_072_u32 # bytes + property channel_max = 2048_u16 # number + property stats_interval = 5000 # millisecond + property stats_log_size = 120 # 10 mins at 5s interval + property? set_timestamp = false # in message headers when receive + property socket_buffer_size = 16384 # bytes + property? tcp_nodelay = false # bool property max_inflight_messages : UInt16 = 65_535 property segment_size : Int32 = 8 * 1024**2 # bytes property? raise_gc_warn : Bool = false @@ -297,7 +297,6 @@ module LavinMQ end end - private def parse_mgmt(settings) settings.each do |config, v| case config diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 20a159e3f0..a1eb9cf3b4 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -51,14 +51,14 @@ module LavinMQ nil end - def assign_client_id_to_packet(packet) - client_id = "#{Random::Secure.base64(32)}" - MQTT::Connect.new(client_id, - packet.clean_session?, - packet.keepalive, - packet.username, - packet.password, - packet.will) + def assign_client_id_to_packet(packet) + client_id = "#{Random::Secure.base64(32)}" + MQTT::Connect.new(client_id, + packet.clean_session?, + packet.keepalive, + packet.username, + packet.password, + packet.will) end end end diff --git a/src/lavinmq/prefix_validation.cr b/src/lavinmq/prefix_validation.cr index a9196afc58..45f7522174 100644 --- a/src/lavinmq/prefix_validation.cr +++ b/src/lavinmq/prefix_validation.cr @@ -2,6 +2,7 @@ require "./error" class PrefixValidation PREFIX_LIST = ["mqtt.", "amq."] + def self.invalid?(name) prefix = name[0..name.index(".") || name.size - 1] return true if PREFIX_LIST.includes?(prefix) From 7e81e9a92f08e9bd6d3ca53e63c47f7a4daa58ad Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 1 Nov 2024 10:15:33 +0100 Subject: [PATCH 122/202] rebase in abstrace queue --- src/lavinmq/mqtt/session.cr | 5 +++-- src/lavinmq/queue_factory.cr | 6 +++--- src/lavinmq/vhost.cr | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index d15ba33190..adc4f1ba67 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,9 +1,10 @@ -require "../queue" +require "../amqp/queue/queue" require "../error" module LavinMQ module MQTT - class Session < Queue + class Session < LavinMQ::AMQP::Queue + include SortableJSON Log = ::LavinMQ::Log.for "mqtt.session" @clean_session : Bool = false diff --git a/src/lavinmq/queue_factory.cr b/src/lavinmq/queue_factory.cr index 95f6b4a5d3..7dcb7b030b 100644 --- a/src/lavinmq/queue_factory.cr +++ b/src/lavinmq/queue_factory.cr @@ -2,8 +2,8 @@ require "./amqp/queue" require "./amqp/queue/priority_queue" require "./amqp/queue/durable_queue" require "./amqp/queue/stream_queue" -require "../mqtt/session" -require "../prefix_validation" +require "./mqtt/session" +require "./prefix_validation" module LavinMQ class QueueFactory @@ -27,7 +27,7 @@ module LavinMQ elsif frame.auto_delete raise Error::PreconditionFailed.new("A stream queue cannot be auto-delete") end - StreamQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) + AMQP::StreamQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif mqtt_session? frame MQTT::Session.new(vhost, frame.queue_name, frame.auto_delete, frame.arguments) else diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index 8b102c4c15..4431555278 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -14,7 +14,6 @@ require "./schema" require "./event_type" require "./stats" require "./queue_factory" -require "./mqtt/session_store" require "./mqtt/session" module LavinMQ From 954fac4e348c641b8dbe6b2a8eca58a33fe0a031 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 1 Nov 2024 14:51:08 +0100 Subject: [PATCH 123/202] adapt for queue abstraction --- spec/message_routing_spec.cr | 2 +- src/lavinmq/launcher.cr | 2 +- src/lavinmq/reporter.cr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index fb4ade2d83..cdf53fafca 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -425,7 +425,7 @@ describe LavinMQ::MQTTExchange do it "should only allow Session to bind" do with_amqp_server do |s| vhost = s.vhosts.create("x") - q1 = LavinMQ::Queue.new(vhost, "q1") + q1 = LavinMQ::AMQP::Queue.new(vhost, "q1") s1 = LavinMQ::MQTT::Session.new(vhost, "q1") x = LavinMQ::MQTTExchange.new(vhost, "", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index f2e433f814..bc8f8fda9b 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -182,7 +182,7 @@ module LavinMQ STDOUT.flush @amqp_server.vhosts.each_value do |vhost| vhost.queues.each_value do |q| - if q = q.as(LavinMQ::AMQP::Queue) + if q = (q.as(LavinMQ::AMQP::Queue) || q.as(LavinMQ::MQTT::Session)) msg_store = q.@msg_store msg_store.@segments.each_value &.unmap msg_store.@acks.each_value &.unmap diff --git a/src/lavinmq/reporter.cr b/src/lavinmq/reporter.cr index c979216b02..3a1d10b3cd 100644 --- a/src/lavinmq/reporter.cr +++ b/src/lavinmq/reporter.cr @@ -17,7 +17,7 @@ module LavinMQ puts_size_capacity vh.@queues, 4 vh.queues.each do |_, q| puts " #{q.name} #{q.durable? ? "durable" : ""} args=#{q.arguments}" - if q = q.as(LavinMQ::AMQP::Queue) + if q = (q.as(LavinMQ::AMQP::Queue) || q.as(LavinMQ::MQTT::Session)) puts_size_capacity q.@consumers, 6 puts_size_capacity q.@deliveries, 6 puts_size_capacity q.@msg_store.@segments, 6 From 039214028f3f0e2f88860a133786552c0e15c958 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 10:47:10 +0100 Subject: [PATCH 124/202] repain broken amqp specs --- src/lavinmq/queue_factory.cr | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lavinmq/queue_factory.cr b/src/lavinmq/queue_factory.cr index 7dcb7b030b..d20ba37c19 100644 --- a/src/lavinmq/queue_factory.cr +++ b/src/lavinmq/queue_factory.cr @@ -18,7 +18,6 @@ module LavinMQ end private def self.make_durable(vhost, frame) - raise Error::PreconditionFailed.new("Not allowed to use that prefix") if PrefixValidation.invalid?(frame.queue_name) && !mqtt_session?(frame) if prio_queue? frame AMQP::DurablePriorityQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif stream_queue? frame @@ -37,7 +36,6 @@ module LavinMQ end private def self.make_queue(vhost, frame) - raise Error::PreconditionFailed.new("Not allowed to use that prefix") if PrefixValidation.invalid?(frame.queue_name) && !mqtt_session?(frame) if prio_queue? frame AMQP::PriorityQueue.new(vhost, frame.queue_name, frame.exclusive, frame.auto_delete, frame.arguments) elsif stream_queue? frame From 918ec5c4b4999348190c1151b36fb65175ec2c83 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:04:57 +0100 Subject: [PATCH 125/202] rename prefix validator to namevalidator and move valid_entity_name into it --- src/lavinmq/amqp/client.cr | 31 ++++++++----------- src/lavinmq/http/controller/exchanges.cr | 2 +- src/lavinmq/http/controller/queues.cr | 4 +-- ...prefix_validation.cr => name_validator.cr} | 9 ++++-- src/lavinmq/queue_factory.cr | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) rename src/lavinmq/{prefix_validation.cr => name_validator.cr} (53%) diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index 1bccd4406a..37da3eac3c 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -4,7 +4,7 @@ require "./channel" require "../client" require "../error" require "../logger" -require "../prefix_validation.cr" +require "../name_validator.cr" module LavinMQ module AMQP @@ -509,7 +509,7 @@ module LavinMQ end private def declare_exchange(frame) - if !valid_entity_name(frame.exchange_name) + if !NameValidator.valid_entity_name(frame.exchange_name) send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? send_access_refused(frame, "Not allowed to declare the default exchange") @@ -517,7 +517,7 @@ module LavinMQ redeclare_exchange(e, frame) elsif frame.passive send_not_found(frame, "Exchange '#{frame.exchange_name}' doesn't exists") - elsif PrefixValidation.invalid?(frame.exchange_name) + elsif NameValidator.valid_prefix?(frame.exchange_name) send_access_refused(frame, "Not allowed to use that prefix") else ae = frame.arguments["x-alternate-exchange"]?.try &.as?(String) @@ -546,11 +546,11 @@ module LavinMQ end private def delete_exchange(frame) - if !valid_entity_name(frame.exchange_name) + if !NameValidator.valid_entity_name(frame.exchange_name) send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? send_access_refused(frame, "Not allowed to delete the default exchange") - elsif PrefixValidation.invalid?(frame.exchange_name) + elsif NameValidator.valid_prefix?(frame.exchange_name) send_access_refused(frame, "Not allowed to use that prefix") elsif !@vhost.exchanges.has_key? frame.exchange_name # should return not_found according to spec but we make it idempotent @@ -570,7 +570,7 @@ module LavinMQ if frame.queue_name.empty? && @last_queue_name frame.queue_name = @last_queue_name.not_nil! end - if !valid_entity_name(frame.queue_name) + if !NameValidator.valid_entity_name(frame.queue_name) send_precondition_failed(frame, "Queue name isn't valid") return end @@ -593,17 +593,12 @@ module LavinMQ end end - private def valid_entity_name(name) : Bool - return true if name.empty? - name.matches?(/\A[ -~]*\z/) - end - def queue_exclusive_to_other_client?(q) q.exclusive? && !@exclusive_queues.includes?(q) end private def declare_queue(frame) - if !frame.queue_name.empty? && !valid_entity_name(frame.queue_name) + if !frame.queue_name.empty? && !NameValidator.valid_entity_name(frame.queue_name) send_precondition_failed(frame, "Queue name isn't valid") elsif q = @vhost.queues.fetch(frame.queue_name, nil) redeclare_queue(frame, q) @@ -620,7 +615,7 @@ module LavinMQ end elsif frame.passive send_not_found(frame, "Queue '#{frame.queue_name}' doesn't exists") - elsif PrefixValidation.invalid?(frame.queue_name) + elsif NameValidator.valid_prefix?(frame.queue_name) send_access_refused(frame, "Not allowed to use that prefix") elsif @vhost.max_queues.try { |max| @vhost.queues.size >= max } send_access_refused(frame, "queue limit in vhost '#{@vhost.name}' (#{@vhost.max_queues}) is reached") @@ -737,10 +732,10 @@ module LavinMQ end private def valid_q_bind_unbind?(frame) : Bool - if !valid_entity_name(frame.queue_name) + if !NameValidator.valid_entity_name(frame.queue_name) send_precondition_failed(frame, "Queue name isn't valid") return false - elsif !valid_entity_name(frame.exchange_name) + elsif !NameValidator.valid_entity_name(frame.exchange_name) send_precondition_failed(frame, "Exchange name isn't valid") return false end @@ -795,7 +790,7 @@ module LavinMQ send_access_refused(frame, "User doesn't have write permissions to queue '#{frame.queue_name}'") return end - if !valid_entity_name(frame.queue_name) + if !NameValidator.valid_entity_name(frame.queue_name) send_precondition_failed(frame, "Queue name isn't valid") elsif q = @vhost.queues.fetch(frame.queue_name, nil) if queue_exclusive_to_other_client?(q) @@ -821,7 +816,7 @@ module LavinMQ if frame.queue.empty? && @last_queue_name frame.queue = @last_queue_name.not_nil! end - if !valid_entity_name(frame.queue) + if !NameValidator.valid_entity_name(frame.queue) send_precondition_failed(frame, "Queue name isn't valid") return end @@ -836,7 +831,7 @@ module LavinMQ if frame.queue.empty? && @last_queue_name frame.queue = @last_queue_name.not_nil! end - if !valid_entity_name(frame.queue) + if !NameValidator.valid_entity_name(frame.queue) send_precondition_failed(frame, "Queue name isn't valid") return end diff --git a/src/lavinmq/http/controller/exchanges.cr b/src/lavinmq/http/controller/exchanges.cr index 61dd629ee4..a736653e0a 100644 --- a/src/lavinmq/http/controller/exchanges.cr +++ b/src/lavinmq/http/controller/exchanges.cr @@ -69,7 +69,7 @@ module LavinMQ bad_request(context, "Not allowed to publish to internal exchange") end context.response.status_code = 204 - elsif PrefixValidation.invalid?(name) + elsif NameValidator.valid_prefix?(name) bad_request(context, "Not allowed to use that prefix") elsif name.bytesize > UInt8::MAX bad_request(context, "Exchange name too long, can't exceed 255 characters") diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index dca9dddff1..6c2615dcf2 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -2,7 +2,7 @@ require "uri" require "../controller" require "../binding_helpers" require "../../unacked_message" -require "../../prefix_validation" +require "../../name_validator" module LavinMQ module HTTP @@ -81,7 +81,7 @@ module LavinMQ bad_request(context, "Existing queue declared with other arguments arg") end context.response.status_code = 204 - elsif PrefixValidation.invalid?(name) + elsif NameValidator.valid_prefix?(name) bad_request(context, "Not allowed to use that prefix") elsif name.bytesize > UInt8::MAX bad_request(context, "Queue name too long, can't exceed 255 characters") diff --git a/src/lavinmq/prefix_validation.cr b/src/lavinmq/name_validator.cr similarity index 53% rename from src/lavinmq/prefix_validation.cr rename to src/lavinmq/name_validator.cr index 45f7522174..1a89fe4f2e 100644 --- a/src/lavinmq/prefix_validation.cr +++ b/src/lavinmq/name_validator.cr @@ -1,11 +1,16 @@ require "./error" -class PrefixValidation +class NameValidator PREFIX_LIST = ["mqtt.", "amq."] - def self.invalid?(name) + def self.valid_prefix?(name) prefix = name[0..name.index(".") || name.size - 1] return true if PREFIX_LIST.includes?(prefix) return false end + + def self.valid_entity_name(name) : Bool + return true if name.empty? + name.matches?(/\A[ -~]*\z/) + end end diff --git a/src/lavinmq/queue_factory.cr b/src/lavinmq/queue_factory.cr index d20ba37c19..20d45cebc3 100644 --- a/src/lavinmq/queue_factory.cr +++ b/src/lavinmq/queue_factory.cr @@ -3,7 +3,7 @@ require "./amqp/queue/priority_queue" require "./amqp/queue/durable_queue" require "./amqp/queue/stream_queue" require "./mqtt/session" -require "./prefix_validation" +require "./name_validator" module LavinMQ class QueueFactory From 09a91378f067b43872a4a358deeb7078ce50461c Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:20:08 +0100 Subject: [PATCH 126/202] remove unnessecary allocation in #NameValidator.valid_prefix --- src/lavinmq/name_validator.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lavinmq/name_validator.cr b/src/lavinmq/name_validator.cr index 1a89fe4f2e..9cb6e4277d 100644 --- a/src/lavinmq/name_validator.cr +++ b/src/lavinmq/name_validator.cr @@ -4,8 +4,7 @@ class NameValidator PREFIX_LIST = ["mqtt.", "amq."] def self.valid_prefix?(name) - prefix = name[0..name.index(".") || name.size - 1] - return true if PREFIX_LIST.includes?(prefix) + return true if PREFIX_LIST.any? { |prefix| name.starts_with? prefix } return false end From 93ecf6d2dc7c3195b889740a0026d6e52ded7ca1 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:31:34 +0100 Subject: [PATCH 127/202] cleanup ordering in connections js --- static/js/connections.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/connections.js b/static/js/connections.js index 949f0c05d1..44ca8d6667 100644 --- a/static/js/connections.js +++ b/static/js/connections.js @@ -33,9 +33,9 @@ Table.renderTable('table', tableOptions, function (tr, item, all) { Table.renderCell(tr, 5, item.tls_version, 'center') Table.renderCell(tr, 6, item.cipher, 'center') Table.renderCell(tr, 7, item.protocol, 'center') + Table.renderCell(tr, 8, item.auth_mechanism) Table.renderCell(tr, 9, item.channel_max, 'right') Table.renderCell(tr, 10, item.timeout, 'right') - Table.renderCell(tr, 8, item.auth_mechanism) const clientDiv = document.createElement('span') if (item?.client_properties) { clientDiv.textContent = `${item.client_properties.product} / ${item.client_properties.platform || ''}` From a5e1e9fb89cae7714cb31e826ef20ebec55182ab Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:45:41 +0100 Subject: [PATCH 128/202] remove sessions from vhost, redundant --- src/lavinmq/vhost.cr | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lavinmq/vhost.cr b/src/lavinmq/vhost.cr index 4431555278..d1428bd454 100644 --- a/src/lavinmq/vhost.cr +++ b/src/lavinmq/vhost.cr @@ -654,10 +654,6 @@ module LavinMQ @shovels.not_nil! end - def sessions - @sessions.not_nil! - end - def event_tick(event_type) case event_type in EventType::ChannelClosed then @channel_closed_count += 1 From 313243157bf209dd3f0f15b5c8d94c2a33c1d5a3 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:49:24 +0100 Subject: [PATCH 129/202] use default random instead of secure for client_id --- src/lavinmq/mqtt/connection_factory.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index a1eb9cf3b4..f356576b0e 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -52,7 +52,7 @@ module LavinMQ end def assign_client_id_to_packet(packet) - client_id = "#{Random::Secure.base64(32)}" + client_id = Random::DEFAULT.base64(32) MQTT::Connect.new(client_id, packet.clean_session?, packet.keepalive, From 3e1f35fdaf75a06ecb90fa30bf49c7137c361044 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 13:43:35 +0100 Subject: [PATCH 130/202] delete unreferences messages in retain store when building index --- src/lavinmq/mqtt/retain_store.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 0d553d22b7..0a9693b8db 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -54,7 +54,10 @@ module LavinMQ # TODO: Device what's the truth: index file or msgs file. Mybe drop the index file and rebuild # index from msg files? unless msg_file_segments.empty? - Log.warn { "unreferenced messages: #{msg_file_segments.join(",")}" } + Log.warn { "unreferenced messages will be deleted: #{msg_file_segments.join(",")}" } + msg_file_segments.each do |msg_file_name| + File.delete? File.join(dir, msg_file_name) + end end # TODO: delete unreferenced messages? Log.info { "restoring index done, msg_count = #{msg_count}" } @@ -116,13 +119,11 @@ module LavinMQ end private def read(file_name : String) : Bytes - # @lock.synchronize do File.open(File.join(@dir, file_name), "r") do |f| body = Bytes.new(f.size) f.read_fully(body) body end - # end end def retained_messages From a30b051515e0144a9a530ee21677de2f3d3bdc91 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 14:20:17 +0100 Subject: [PATCH 131/202] move exchange into the mqtt namespace --- spec/message_routing_spec.cr | 6 +- src/lavinmq/exchange/mqtt.cr | 122 ---------------------------------- src/lavinmq/mqtt/broker.cr | 3 +- src/lavinmq/mqtt/exchange.cr | 124 +++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+), 126 deletions(-) delete mode 100644 src/lavinmq/exchange/mqtt.cr create mode 100644 src/lavinmq/mqtt/exchange.cr diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index cdf53fafca..02eaab0f09 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -421,13 +421,13 @@ describe LavinMQ::Exchange do end end end -describe LavinMQ::MQTTExchange do +describe LavinMQ::MQTT::Exchange do it "should only allow Session to bind" do with_amqp_server do |s| vhost = s.vhosts.create("x") q1 = LavinMQ::AMQP::Queue.new(vhost, "q1") s1 = LavinMQ::MQTT::Session.new(vhost, "q1") - x = LavinMQ::MQTTExchange.new(vhost, "", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) + x = LavinMQ::MQTT::Exchange.new(vhost, "", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) expect_raises(LavinMQ::Exchange::AccessRefused) do x.bind(q1, "q1", LavinMQ::AMQP::Table.new) @@ -439,7 +439,7 @@ describe LavinMQ::MQTTExchange do with_amqp_server do |s| vhost = s.vhosts.create("x") s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") - x = LavinMQ::MQTTExchange.new(vhost, "mqtt.default", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) + x = LavinMQ::MQTT::Exchange.new(vhost, "mqtt.default", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") x.publish(msg, false) diff --git a/src/lavinmq/exchange/mqtt.cr b/src/lavinmq/exchange/mqtt.cr deleted file mode 100644 index 2270bf1bd2..0000000000 --- a/src/lavinmq/exchange/mqtt.cr +++ /dev/null @@ -1,122 +0,0 @@ -require "./exchange" -require "../mqtt/subscription_tree" -require "../mqtt/session" -require "../mqtt/retain_store" - -module LavinMQ - class MQTTExchange < Exchange - struct MqttBindingKey - def initialize(routing_key : String, arguments : AMQP::Table? = nil) - @binding_key = BindingKey.new(routing_key, arguments) - end - - def inner - @binding_key - end - - def hash - @binding_key.routing_key.hash - end - end - - @bindings = Hash(MqttBindingKey, Set(MQTT::Session)).new do |h, k| - h[k] = Set(MQTT::Session).new - end - @tree = MQTT::SubscriptionTree(MQTT::Session).new - - def type : String - "mqtt" - end - - def initialize(vhost : VHost, name : String, @retain_store : MQTT::RetainStore) - super(vhost, name, true, false, true) - end - - def publish(msg : Message, immediate : Bool, - queues : Set(Queue) = Set(Queue).new, - exchanges : Set(Exchange) = Set(Exchange).new) : Int32 - raise LavinMQ::Exchange::AccessRefused.new(self) - end - - def publish(packet : MQTT::Publish) : Int32 - @publish_in_count += 1 - - headers = AMQP::Table.new.tap do |h| - h["x-mqtt-retain"] = true if packet.retain? - end - properties = AMQP::Properties.new(headers: headers).tap do |p| - p.delivery_mode = packet.qos if packet.responds_to?(:qos) - end - - timestamp = RoughTime.unix_ms - bodysize = packet.payload.size.to_u64 - body = IO::Memory.new(bodysize) - body.write(packet.payload) - body.rewind - - @retain_store.retain(packet.topic, body, bodysize) if packet.retain? - - body.rewind - msg = Message.new(timestamp, "mqtt.default", packet.topic, properties, bodysize, body) - - count = 0 - @tree.each_entry(packet.topic) do |queue, qos| - msg.properties.delivery_mode = qos - if queue.publish(msg) - count += 1 - msg.body_io.seek(-msg.bodysize.to_i64, IO::Seek::Current) # rewind - end - end - @unroutable_count += 1 if count.zero? - @publish_out_count += count - count - end - - def bindings_details : Iterator(BindingDetails) - @bindings.each.flat_map do |binding_key, ds| - ds.each.map do |d| - BindingDetails.new(name, vhost.name, binding_key.inner, d) - end - end - end - - # Only here to make superclass happy - protected def bindings(routing_key, headers) : Iterator(Destination) - Iterator(Destination).empty - end - - def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool - qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 - binding_key = MqttBindingKey.new(routing_key, headers) - @bindings[binding_key].add destination - @tree.subscribe(routing_key, destination, qos) - - data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) - notify_observers(ExchangeEvent::Bind, data) - true - end - - def unbind(destination : MQTT::Session, routing_key, headers = nil) : Bool - binding_key = MqttBindingKey.new(routing_key, headers) - rk_bindings = @bindings[binding_key] - rk_bindings.delete destination - @bindings.delete binding_key if rk_bindings.empty? - - @tree.unsubscribe(routing_key, destination) - - data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) - notify_observers(ExchangeEvent::Unbind, data) - - delete if @auto_delete && @bindings.each_value.all?(&.empty?) - true - end - - def bind(destination : Destination, routing_key : String, headers = nil) : Bool - raise LavinMQ::Exchange::AccessRefused.new(self) - end - - def unbind(destination : Destination, routing_key, headers = nil) : Bool - raise LavinMQ::Exchange::AccessRefused.new(self) - end - end -end diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 7c1ed3eb45..d5ab94ec89 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -4,6 +4,7 @@ require "./session" require "./sessions" require "./retain_store" require "../vhost" +require "./exchange" module LavinMQ module MQTT @@ -15,7 +16,7 @@ module LavinMQ @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s) - @exchange = MQTTExchange.new(@vhost, "mqtt.default", @retain_store) + @exchange = MQTT::Exchange.new(@vhost, "mqtt.default", @retain_store) @vhost.exchanges["mqtt.default"] = @exchange end diff --git a/src/lavinmq/mqtt/exchange.cr b/src/lavinmq/mqtt/exchange.cr new file mode 100644 index 0000000000..44932c6c8d --- /dev/null +++ b/src/lavinmq/mqtt/exchange.cr @@ -0,0 +1,124 @@ +require "../exchange" +require "./subscription_tree" +require "./session" +require "./retain_store" + +module LavinMQ + module MQTT + class Exchange < Exchange + struct MqttBindingKey + def initialize(routing_key : String, arguments : AMQP::Table? = nil) + @binding_key = BindingKey.new(routing_key, arguments) + end + + def inner + @binding_key + end + + def hash + @binding_key.routing_key.hash + end + end + + @bindings = Hash(MqttBindingKey, Set(MQTT::Session)).new do |h, k| + h[k] = Set(MQTT::Session).new + end + @tree = MQTT::SubscriptionTree(MQTT::Session).new + + def type : String + "mqtt" + end + + def initialize(vhost : VHost, name : String, @retain_store : MQTT::RetainStore) + super(vhost, name, true, false, true) + end + + def publish(msg : Message, immediate : Bool, + queues : Set(Queue) = Set(Queue).new, + exchanges : Set(Exchange) = Set(Exchange).new) : Int32 + raise LavinMQ::Exchange::AccessRefused.new(self) + end + + def publish(packet : MQTT::Publish) : Int32 + @publish_in_count += 1 + + headers = AMQP::Table.new.tap do |h| + h["x-mqtt-retain"] = true if packet.retain? + end + properties = AMQP::Properties.new(headers: headers).tap do |p| + p.delivery_mode = packet.qos if packet.responds_to?(:qos) + end + + timestamp = RoughTime.unix_ms + bodysize = packet.payload.size.to_u64 + body = ::IO::Memory.new(bodysize) + body.write(packet.payload) + body.rewind + + @retain_store.retain(packet.topic, body, bodysize) if packet.retain? + + body.rewind + msg = Message.new(timestamp, "mqtt.default", packet.topic, properties, bodysize, body) + + count = 0 + @tree.each_entry(packet.topic) do |queue, qos| + msg.properties.delivery_mode = qos + if queue.publish(msg) + count += 1 + msg.body_io.seek(-msg.bodysize.to_i64, ::IO::Seek::Current) # rewind + end + end + @unroutable_count += 1 if count.zero? + @publish_out_count += count + count + end + + def bindings_details : Iterator(BindingDetails) + @bindings.each.flat_map do |binding_key, ds| + ds.each.map do |d| + BindingDetails.new(name, vhost.name, binding_key.inner, d) + end + end + end + + # Only here to make superclass happy + protected def bindings(routing_key, headers) : Iterator(Destination) + Iterator(Destination).empty + end + + def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool + qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 + binding_key = MqttBindingKey.new(routing_key, headers) + @bindings[binding_key].add destination + @tree.subscribe(routing_key, destination, qos) + + data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) + notify_observers(ExchangeEvent::Bind, data) + true + end + + def unbind(destination : MQTT::Session, routing_key, headers = nil) : Bool + binding_key = MqttBindingKey.new(routing_key, headers) + rk_bindings = @bindings[binding_key] + rk_bindings.delete destination + @bindings.delete binding_key if rk_bindings.empty? + + @tree.unsubscribe(routing_key, destination) + + data = BindingDetails.new(name, vhost.name, binding_key.inner, destination) + notify_observers(ExchangeEvent::Unbind, data) + + delete if @auto_delete && @bindings.each_value.all?(&.empty?) + true + end + + def bind(destination : Destination, routing_key : String, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end + + def unbind(destination : Destination, routing_key, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end + end + end +end From e4b8894ed68bd80dfe86720b53d687b9b352f0b1 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 14:27:47 +0100 Subject: [PATCH 132/202] use mqtt namespace MqttBindingKey->BindingKey --- src/lavinmq/mqtt/exchange.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lavinmq/mqtt/exchange.cr b/src/lavinmq/mqtt/exchange.cr index 44932c6c8d..9fa07ec5f9 100644 --- a/src/lavinmq/mqtt/exchange.cr +++ b/src/lavinmq/mqtt/exchange.cr @@ -6,9 +6,9 @@ require "./retain_store" module LavinMQ module MQTT class Exchange < Exchange - struct MqttBindingKey + struct BindingKey def initialize(routing_key : String, arguments : AMQP::Table? = nil) - @binding_key = BindingKey.new(routing_key, arguments) + @binding_key = LavinMQ::BindingKey.new(routing_key, arguments) end def inner @@ -20,7 +20,7 @@ module LavinMQ end end - @bindings = Hash(MqttBindingKey, Set(MQTT::Session)).new do |h, k| + @bindings = Hash(BindingKey, Set(MQTT::Session)).new do |h, k| h[k] = Set(MQTT::Session).new end @tree = MQTT::SubscriptionTree(MQTT::Session).new @@ -88,7 +88,7 @@ module LavinMQ def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 - binding_key = MqttBindingKey.new(routing_key, headers) + binding_key = BindingKey.new(routing_key, headers) @bindings[binding_key].add destination @tree.subscribe(routing_key, destination, qos) @@ -98,7 +98,7 @@ module LavinMQ end def unbind(destination : MQTT::Session, routing_key, headers = nil) : Bool - binding_key = MqttBindingKey.new(routing_key, headers) + binding_key = BindingKey.new(routing_key, headers) rk_bindings = @bindings[binding_key] rk_bindings.delete destination @bindings.delete binding_key if rk_bindings.empty? From 75c66666d6ed57bbb7fac7a46d6abd5e898d023f Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 14:53:32 +0100 Subject: [PATCH 133/202] remove redundant return value --- src/lavinmq/name_validator.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lavinmq/name_validator.cr b/src/lavinmq/name_validator.cr index 9cb6e4277d..7ad8c8d2c7 100644 --- a/src/lavinmq/name_validator.cr +++ b/src/lavinmq/name_validator.cr @@ -5,7 +5,6 @@ class NameValidator def self.valid_prefix?(name) return true if PREFIX_LIST.any? { |prefix| name.starts_with? prefix } - return false end def self.valid_entity_name(name) : Bool From 9d148e22831f6b13ef550aff961a36a994a484a2 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 09:24:57 +0100 Subject: [PATCH 134/202] update name validator --- spec/message_routing_spec.cr | 1 + src/lavinmq/amqp/client.cr | 6 +++--- src/lavinmq/http/controller/exchanges.cr | 2 +- src/lavinmq/http/controller/queues.cr | 2 +- src/lavinmq/name_validator.cr | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index 02eaab0f09..4a13bf9b2c 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -421,6 +421,7 @@ describe LavinMQ::Exchange do end end end + describe LavinMQ::MQTT::Exchange do it "should only allow Session to bind" do with_amqp_server do |s| diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index 37da3eac3c..c76671cea3 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -517,7 +517,7 @@ module LavinMQ redeclare_exchange(e, frame) elsif frame.passive send_not_found(frame, "Exchange '#{frame.exchange_name}' doesn't exists") - elsif NameValidator.valid_prefix?(frame.exchange_name) + elsif NameValidator.reserved_prefix?(frame.exchange_name) send_access_refused(frame, "Not allowed to use that prefix") else ae = frame.arguments["x-alternate-exchange"]?.try &.as?(String) @@ -550,7 +550,7 @@ module LavinMQ send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? send_access_refused(frame, "Not allowed to delete the default exchange") - elsif NameValidator.valid_prefix?(frame.exchange_name) + elsif NameValidator.reserved_prefix?(frame.exchange_name) send_access_refused(frame, "Not allowed to use that prefix") elsif !@vhost.exchanges.has_key? frame.exchange_name # should return not_found according to spec but we make it idempotent @@ -615,7 +615,7 @@ module LavinMQ end elsif frame.passive send_not_found(frame, "Queue '#{frame.queue_name}' doesn't exists") - elsif NameValidator.valid_prefix?(frame.queue_name) + elsif NameValidator.reserved_prefix?(frame.queue_name) send_access_refused(frame, "Not allowed to use that prefix") elsif @vhost.max_queues.try { |max| @vhost.queues.size >= max } send_access_refused(frame, "queue limit in vhost '#{@vhost.name}' (#{@vhost.max_queues}) is reached") diff --git a/src/lavinmq/http/controller/exchanges.cr b/src/lavinmq/http/controller/exchanges.cr index a736653e0a..a7bc14004b 100644 --- a/src/lavinmq/http/controller/exchanges.cr +++ b/src/lavinmq/http/controller/exchanges.cr @@ -69,7 +69,7 @@ module LavinMQ bad_request(context, "Not allowed to publish to internal exchange") end context.response.status_code = 204 - elsif NameValidator.valid_prefix?(name) + elsif NameValidator.reserved_prefix?(name) bad_request(context, "Not allowed to use that prefix") elsif name.bytesize > UInt8::MAX bad_request(context, "Exchange name too long, can't exceed 255 characters") diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index 6c2615dcf2..ebdfc82aa4 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -81,7 +81,7 @@ module LavinMQ bad_request(context, "Existing queue declared with other arguments arg") end context.response.status_code = 204 - elsif NameValidator.valid_prefix?(name) + elsif NameValidator.reserved_prefix?(name) bad_request(context, "Not allowed to use that prefix") elsif name.bytesize > UInt8::MAX bad_request(context, "Queue name too long, can't exceed 255 characters") diff --git a/src/lavinmq/name_validator.cr b/src/lavinmq/name_validator.cr index 7ad8c8d2c7..5e98129860 100644 --- a/src/lavinmq/name_validator.cr +++ b/src/lavinmq/name_validator.cr @@ -3,8 +3,8 @@ require "./error" class NameValidator PREFIX_LIST = ["mqtt.", "amq."] - def self.valid_prefix?(name) - return true if PREFIX_LIST.any? { |prefix| name.starts_with? prefix } + def self.reserved_prefix?(name) + PREFIX_LIST.any? { |prefix| name.starts_with? prefix } end def self.valid_entity_name(name) : Bool From 18214987d04a27511f5bb9f2b8e10e3fa97db036 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 14:46:24 +0100 Subject: [PATCH 135/202] move unacked_messagesapi logic to queue and overload method in session --- src/lavinmq/amqp/queue/queue.cr | 12 ++++++++++++ src/lavinmq/config.cr | 2 +- src/lavinmq/http/controller/queues.cr | 14 +++----------- src/lavinmq/mqtt/session.cr | 4 ++++ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/lavinmq/amqp/queue/queue.cr b/src/lavinmq/amqp/queue/queue.cr index 06e8bae7c7..b17ec1e50c 100644 --- a/src/lavinmq/amqp/queue/queue.cr +++ b/src/lavinmq/amqp/queue/queue.cr @@ -747,6 +747,18 @@ module LavinMQ::AMQP end end + def unacked_messages + unacked_messages = consumers.each.select(AMQP::Consumer).flat_map do |c| + c.unacked_messages.each.compact_map do |u| + next unless u.queue == self + if consumer = u.consumer + UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) + end + end + end + unacked_messages = unacked_messages.chain(self.basic_get_unacked.each) + end + private def with_delivery_count_header(env) : Envelope? if limit = @delivery_limit sp = env.segment_position diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index 24034b549c..7d6885c055 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -8,7 +8,7 @@ require "./in_memory_backend" module LavinMQ class Config - DEFAULT_LOG_LEVEL = ::Log::Severity::Trace + DEFAULT_LOG_LEVEL = ::Log::Severity::Info property data_dir : String = ENV.fetch("STATE_DIRECTORY", "/var/lib/lavinmq") property config_file = File.exists?(File.join(ENV.fetch("CONFIGURATION_DIRECTORY", "/etc/lavinmq"), "lavinmq.ini")) ? File.join(ENV.fetch("CONFIGURATION_DIRECTORY", "/etc/lavinmq"), "lavinmq.ini") : "" diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index ebdfc82aa4..fded43d68e 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -46,17 +46,9 @@ module LavinMQ get "/api/queues/:vhost/:name/unacked" do |context, params| with_vhost(context, params) do |vhost| refuse_unless_management(context, user(context), vhost) - # q = queue(context, params, vhost) - # unacked_messages = q.consumers.each.flat_map do |c| - # c.unacked_messages.each.compact_map do |u| - # next unless u.queue == q - # if consumer = u.consumer - # UnackedMessage.new(c.channel, u.tag, u.delivered_at, consumer.tag) - # end - # end - # end - # unacked_messages = unacked_messages.chain(q.basic_get_unacked.each) - # page(context, unacked_messages) + q = queue(context, params, vhost) + unacked_messages = q.unacked_messages + page(context, unacked_messages) end end diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index adc4f1ba67..0ba262b9b8 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -72,6 +72,10 @@ module LavinMQ !clean_session? end + def unacked_messages + Iterator(UnackedMessage).empty + end + def subscribe(tf, qos) arguments = AMQP::Table.new({"x-mqtt-qos": qos}) if binding = find_binding(tf) From ecc4e563af5d2698530a9b03f06cbe772d4de0c9 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 14:47:33 +0100 Subject: [PATCH 136/202] format --- src/lavinmq/amqp/queue/queue.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/amqp/queue/queue.cr b/src/lavinmq/amqp/queue/queue.cr index b17ec1e50c..3fd7f6c4ba 100644 --- a/src/lavinmq/amqp/queue/queue.cr +++ b/src/lavinmq/amqp/queue/queue.cr @@ -748,7 +748,7 @@ module LavinMQ::AMQP end def unacked_messages - unacked_messages = consumers.each.select(AMQP::Consumer).flat_map do |c| + unacked_messages = consumers.each.select(AMQP::Consumer).flat_map do |c| c.unacked_messages.each.compact_map do |u| next unless u.queue == self if consumer = u.consumer From 457e079889733518f260e718065efeead8a10e48 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 15:00:05 +0100 Subject: [PATCH 137/202] update logs for name validation failures --- src/lavinmq/amqp/client.cr | 10 +++++----- src/lavinmq/http/controller/exchanges.cr | 2 +- src/lavinmq/http/controller/queues.cr | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index c76671cea3..5b373ba84f 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -512,13 +512,13 @@ module LavinMQ if !NameValidator.valid_entity_name(frame.exchange_name) send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? - send_access_refused(frame, "Not allowed to declare the default exchange") + send_access_refused(frame, "Prefix forbidden") elsif e = @vhost.exchanges.fetch(frame.exchange_name, nil) redeclare_exchange(e, frame) elsif frame.passive send_not_found(frame, "Exchange '#{frame.exchange_name}' doesn't exists") elsif NameValidator.reserved_prefix?(frame.exchange_name) - send_access_refused(frame, "Not allowed to use that prefix") + send_access_refused(frame, "Prefix forbidden") else ae = frame.arguments["x-alternate-exchange"]?.try &.as?(String) ae_ok = ae.nil? || (@user.can_write?(@vhost.name, ae) && @user.can_read?(@vhost.name, frame.exchange_name)) @@ -549,9 +549,9 @@ module LavinMQ if !NameValidator.valid_entity_name(frame.exchange_name) send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? - send_access_refused(frame, "Not allowed to delete the default exchange") + send_access_refused(frame, "Prefix forbidden") elsif NameValidator.reserved_prefix?(frame.exchange_name) - send_access_refused(frame, "Not allowed to use that prefix") + send_access_refused(frame, "Prefix forbidden") elsif !@vhost.exchanges.has_key? frame.exchange_name # should return not_found according to spec but we make it idempotent send AMQP::Frame::Exchange::DeleteOk.new(frame.channel) unless frame.no_wait @@ -616,7 +616,7 @@ module LavinMQ elsif frame.passive send_not_found(frame, "Queue '#{frame.queue_name}' doesn't exists") elsif NameValidator.reserved_prefix?(frame.queue_name) - send_access_refused(frame, "Not allowed to use that prefix") + send_access_refused(frame, "Prefix forbidden") elsif @vhost.max_queues.try { |max| @vhost.queues.size >= max } send_access_refused(frame, "queue limit in vhost '#{@vhost.name}' (#{@vhost.max_queues}) is reached") else diff --git a/src/lavinmq/http/controller/exchanges.cr b/src/lavinmq/http/controller/exchanges.cr index a7bc14004b..0da1f06501 100644 --- a/src/lavinmq/http/controller/exchanges.cr +++ b/src/lavinmq/http/controller/exchanges.cr @@ -70,7 +70,7 @@ module LavinMQ end context.response.status_code = 204 elsif NameValidator.reserved_prefix?(name) - bad_request(context, "Not allowed to use that prefix") + bad_request(context, "Prefix forbidden") elsif name.bytesize > UInt8::MAX bad_request(context, "Exchange name too long, can't exceed 255 characters") else diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index fded43d68e..4dcd364542 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -74,7 +74,7 @@ module LavinMQ end context.response.status_code = 204 elsif NameValidator.reserved_prefix?(name) - bad_request(context, "Not allowed to use that prefix") + bad_request(context, "Prefix forbidden") elsif name.bytesize > UInt8::MAX bad_request(context, "Queue name too long, can't exceed 255 characters") else From 3fdf2d4b6c99da972dfc8c3633042d93396896f0 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 7 Nov 2024 13:37:47 +0100 Subject: [PATCH 138/202] don't have risk of overwriting retain store msg files --- src/lavinmq/mqtt/retain_store.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 0a9693b8db..0a51896cd3 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -77,10 +77,12 @@ module LavinMQ add_to_index(topic, msg_file_name) end - File.open(File.join(@dir, msg_file_name), "w+") do |f| + tmp_file = File.join(@dir, "#{msg_file_name}.tmp") + File.open(tmp_file, "w+") do |f| f.sync = true ::IO.copy(body_io, f) end + File.rename tmp_file, File.join(@dir, msg_file_name) end end From 884ec89403b7ff90356bfeeea359aca5a5117b93 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 7 Nov 2024 13:45:45 +0100 Subject: [PATCH 139/202] ensure to remove file --- src/lavinmq/mqtt/retain_store.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 0a51896cd3..aa363141f5 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -83,6 +83,8 @@ module LavinMQ ::IO.copy(body_io, f) end File.rename tmp_file, File.join(@dir, msg_file_name) + ensure + FileUtils.rm_rf tmp_file unless tmp_file.nil? end end From cd0313a21f7c187363bb6d7510cea7a00229d6d2 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 7 Nov 2024 13:49:43 +0100 Subject: [PATCH 140/202] format --- src/lavinmq/mqtt/retain_store.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index aa363141f5..9b5a0948df 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -84,7 +84,7 @@ module LavinMQ end File.rename tmp_file, File.join(@dir, msg_file_name) ensure - FileUtils.rm_rf tmp_file unless tmp_file.nil? + FileUtils.rm_rf tmp_file unless tmp_file.nil? end end From 2b9714579d808b8c82d153b2e84710604900bdef Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 8 Nov 2024 15:21:50 +0100 Subject: [PATCH 141/202] replicate retain store, wip --- spec/clustering_spec.cr | 58 +++++++++++++++++++++ spec/mqtt/integrations/retain_store_spec.cr | 30 ++++++++--- src/lavinmq/mqtt/broker.cr | 5 +- src/lavinmq/mqtt/retain_store.cr | 42 +++++++++------ src/lavinmq/server.cr | 2 +- 5 files changed, 112 insertions(+), 25 deletions(-) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index d1859ee6bb..c26c5fedab 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -2,6 +2,8 @@ require "./spec_helper" require "../src/lavinmq/clustering/client" require "../src/lavinmq/clustering/controller" +alias IndexTree = LavinMQ::MQTT::TopicTree(String) + describe LavinMQ::Clustering::Client do follower_data_dir = "/tmp/lavinmq-follower" @@ -72,6 +74,62 @@ describe LavinMQ::Clustering::Client do end end + # just some playing around with copilot, does not work + # it "replicates and streams retained messages to followers" do + # replicator = LavinMQ::Clustering::Server.new(LavinMQ::Config.instance, LavinMQ::Etcd.new, 0) + # tcp_server = TCPServer.new("localhost", 0) + # spawn(replicator.listen(tcp_server), name: "repli server spec") + # config = LavinMQ::Config.new.tap &.data_dir = follower_data_dir + # repli = LavinMQ::Clustering::Client.new(config, 1, replicator.password, proxy: false) + # done = Channel(Nil).new + # spawn(name: "follow spec") do + # repli.follow("localhost", tcp_server.local_address.port) + # done.send nil + # end + # wait_for { replicator.followers.size == 1 } + + # # Set up the retain store and retain some messages + # index = IndexTree.new + # retain_store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", replicator, index) + # props = LavinMQ::AMQP::Properties.new + # msg1 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body1")) + # msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body2")) + # retain_store.retain("topic1", msg1.body_io, msg1.bodysize) + # retain_store.retain("topic2", msg2.body_io, msg2.bodysize) + + # with_amqp_server(replicator: replicator) do |s| + # with_channel(s) do |ch| + # q = ch.queue("repli") + # q.publish_confirm "hello world" + # end + # repli.close + # done.receive + # end + + # server = LavinMQ::Server.new(follower_data_dir) + # begin + # q = server.vhosts["/"].queues["repli"].as(LavinMQ::AMQP::DurableQueue) + # q.message_count.should eq 1 + # q.basic_get(true) do |env| + # String.new(env.message.body).to_s.should eq "hello world" + # end.should be_true + + # # Verify that the retained messages are streamed to the follower + # follower_retain_store = LavinMQ::MQTT::RetainStore.new(follower_data_dir, replicator, IndexTree.new) + # follower_retain_store.each("topic1") do |topic, bytes| + # topic.should eq("topic1") + # String.new(bytes).should eq("body1") + # end + # follower_retain_store.each("topic2") do |topic, bytes| + # topic.should eq("topic2") + # String.new(bytes).should eq("body2") + # end + # follower_retain_store.retained_messages.should eq(2) + # ensure + # server.close + # end + # end + it "can stream full file" do replicator = LavinMQ::Clustering::Server.new(LavinMQ::Config.instance, LavinMQ::Etcd.new, 0) tcp_server = TCPServer.new("localhost", 0) diff --git a/spec/mqtt/integrations/retain_store_spec.cr b/spec/mqtt/integrations/retain_store_spec.cr index 240def3cb0..e7ada0cd94 100644 --- a/spec/mqtt/integrations/retain_store_spec.cr +++ b/spec/mqtt/integrations/retain_store_spec.cr @@ -13,7 +13,7 @@ module MqttSpecs describe "retain" do it "adds to index and writes msg file" do index = IndexTree.new - store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) props = LavinMQ::AMQP::Properties.new msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) store.retain("a", msg.body_io, msg.bodysize) @@ -21,20 +21,20 @@ module MqttSpecs index.size.should eq(1) index.@leafs.has_key?("a").should be_true - entry = index["a"]?.not_nil! + entry = index["a"]?.should be_a String File.exists?(File.join("tmp/retain_store", entry)).should be_true end it "empty body deletes" do index = IndexTree.new - store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) props = LavinMQ::AMQP::Properties.new msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) store.retain("a", msg.body_io, msg.bodysize) index.size.should eq(1) - entry = index["a"]?.not_nil! + entry = index["a"]?.should be_a String store.retain("a", msg.body_io, 0) index.size.should eq(0) @@ -45,7 +45,7 @@ module MqttSpecs describe "each" do it "calls block with correct arguments" do index = IndexTree.new - store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) props = LavinMQ::AMQP::Properties.new msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) store.retain("a", msg.body_io, msg.bodysize) @@ -63,7 +63,7 @@ module MqttSpecs it "handles multiple subscriptions" do index = IndexTree.new - store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", index) + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) props = LavinMQ::AMQP::Properties.new msg1 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) @@ -85,5 +85,23 @@ module MqttSpecs String.new(called[1][1]).should eq("body") end end + + describe "restore_index" do + it "restores the index from a file" do + index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) + props = LavinMQ::AMQP::Properties.new + msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) + + store.retain("a", msg.body_io, msg.bodysize) + store.close + + new_index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, new_index) + + new_index.size.should eq(1) + new_index.@leafs.has_key?("a").should be_true + end + end end end diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index d5ab94ec89..d6093f92c6 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -11,11 +11,10 @@ module LavinMQ class Broker getter vhost, sessions - def initialize(@vhost : VHost) - # TODO: remember to block the mqtt namespace + def initialize(@vhost : VHost, @replicator : Clustering::Replicator) @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new - @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s) + @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s, @replicator) @exchange = MQTT::Exchange.new(@vhost, "mqtt.default", @retain_store) @vhost.exchanges["mqtt.default"] = @exchange end diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 9b5a0948df..c85518420d 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -11,9 +11,15 @@ module LavinMQ alias IndexTree = TopicTree(String) - def initialize(@dir : String, @index = IndexTree.new) + def initialize(@dir : String, @replicator : Clustering::Replicator, @index = IndexTree.new) Dir.mkdir_p @dir + @files = Hash(String, File).new do |files, file_name| + f = files[file_name] = File.new(File.join(@dir, file_name), "w+") + f.sync = true + f + end @index_file = File.new(File.join(@dir, INDEX_FILE_NAME), "a+") + @replicator.register_file(@index_file) @lock = Mutex.new @lock.synchronize do if @index.empty? @@ -59,7 +65,6 @@ module LavinMQ File.delete? File.join(dir, msg_file_name) end end - # TODO: delete unreferenced messages? Log.info { "restoring index done, msg_count = #{msg_count}" } end @@ -78,13 +83,10 @@ module LavinMQ end tmp_file = File.join(@dir, "#{msg_file_name}.tmp") - File.open(tmp_file, "w+") do |f| - f.sync = true - ::IO.copy(body_io, f) - end - File.rename tmp_file, File.join(@dir, msg_file_name) - ensure - FileUtils.rm_rf tmp_file unless tmp_file.nil? + f = @files[msg_file_name] + f.pos = 0 + ::IO.copy(body_io, f) + @replicator.replace_file(File.join(@dir, msg_file_name)) end end @@ -96,6 +98,8 @@ module LavinMQ end end File.rename tmp_file, File.join(@dir, INDEX_FILE_NAME) + @replicator.replace_file(File.join(@dir, INDEX_FILE_NAME)) + ensure FileUtils.rm_rf tmp_file unless tmp_file.nil? end @@ -104,12 +108,20 @@ module LavinMQ @index.insert topic, file_name @index_file.puts topic @index_file.flush + bytes = Bytes.new(topic.bytesize+1) + bytes.copy_from(topic.to_slice) + bytes[-1] = 10u8 + @replicator.append(file_name, bytes) end private def delete_from_index(topic : String) : Nil if file_name = @index.delete topic Log.trace { "deleted '#{topic}' from index, deleting file #{file_name}" } - File.delete? File.join(@dir, file_name) + if file = @files.delete(file_name) + file.close + file.delete + end + @replicator.delete_file(File.join(@dir, file_name)) end end @@ -123,11 +135,11 @@ module LavinMQ end private def read(file_name : String) : Bytes - File.open(File.join(@dir, file_name), "r") do |f| - body = Bytes.new(f.size) - f.read_fully(body) - body - end + f = @files[file_name] + f.pos = 0 + body = Bytes.new(f.size) + f.read_fully(body) + body end def retained_messages diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 6ba02a0060..a328274563 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -39,7 +39,7 @@ module LavinMQ @vhosts = VHostStore.new(@data_dir, @users, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) @amqp_connection_factory = LavinMQ::AMQP::ConnectionFactory.new - @broker = LavinMQ::MQTT::Broker.new(@vhosts["/"]) + @broker = LavinMQ::MQTT::Broker.new(@vhosts["/"], @replicator) @mqtt_connection_factory = MQTT::ConnectionFactory.new(@users, @vhosts["/"], @broker) apply_parameter spawn stats_loop, name: "Server#stats_loop" From 6990cd01c0c17556e363ebd8aaac996431f5192b Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 11 Nov 2024 21:20:44 +0100 Subject: [PATCH 142/202] add mqtts config + listener --- src/lavinmq/config.cr | 8 ++++++++ src/lavinmq/launcher.cr | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index 7d6885c055..2d139f5fc2 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -19,6 +19,7 @@ module LavinMQ property amqps_port = -1 property mqtt_bind = "127.0.0.1" property mqtt_port = 1883 + property mqtts_port = 8883 property unix_path = "" property unix_proxy_protocol = 1_u8 # PROXY protocol version on unix domain socket connections property tcp_proxy_protocol = 0_u8 # PROXY protocol version on amqp tcp connections @@ -92,6 +93,12 @@ module LavinMQ p.on("--amqp-bind=BIND", "IP address that the AMQP server will listen on (default: 127.0.0.1)") do |v| @amqp_bind = v end + p.on("-m PORT", "--mqtt-port=PORT", "MQTT port to listen on (default: 1883)") do |v| + @mqtt_port = v.to_i + end + p.on("--mqtts-port=PORT", "MQTTS port to listen on (default: 8883)") do |v| + @mqtts_port = v.to_i + end p.on("--http-port=PORT", "HTTP port to listen on (default: 15672)") do |v| @http_port = v.to_i end @@ -288,6 +295,7 @@ module LavinMQ case config when "bind" then @mqtt_bind = v when "port" then @mqtt_port = v.to_i32 + when "tls_port" then @mqtts_port = v.to_i32 when "tls_cert" then @tls_cert_path = v # backward compatibility when "tls_key" then @tls_key_path = v # backward compatibility when "max_inflight_messages" then @max_inflight_messages = v.to_u16 diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index bc8f8fda9b..0dac23681b 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -156,6 +156,13 @@ module LavinMQ spawn @amqp_server.listen(@config.mqtt_bind, @config.mqtt_port, :mqtt), name: "MQTT listening on #{@config.mqtt_port}" end + + if @config.mqtts_port > 0 + if ctx = @tls_context + spawn @amqp_server.listen_tls(@config.mqtt_bind, @config.mqtts_port, ctx, :mqtt), + name: "MQTTS listening on #{@config.mqtts_port}" + end + end end private def dump_debug_info From 2105965636f540d435a38cc45227daea7808c526 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 11 Nov 2024 21:38:38 +0100 Subject: [PATCH 143/202] format --- src/lavinmq/mqtt/retain_store.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index c85518420d..4bbe77e6a6 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -99,7 +99,6 @@ module LavinMQ end File.rename tmp_file, File.join(@dir, INDEX_FILE_NAME) @replicator.replace_file(File.join(@dir, INDEX_FILE_NAME)) - ensure FileUtils.rm_rf tmp_file unless tmp_file.nil? end @@ -108,7 +107,7 @@ module LavinMQ @index.insert topic, file_name @index_file.puts topic @index_file.flush - bytes = Bytes.new(topic.bytesize+1) + bytes = Bytes.new(topic.bytesize + 1) bytes.copy_from(topic.to_slice) bytes[-1] = 10u8 @replicator.append(file_name, bytes) From 86908ad7ce62ff40cb6d2521691e3ab2ca6b4c4f Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 12 Nov 2024 15:55:35 +0100 Subject: [PATCH 144/202] replication spec works correctly --- spec/clustering_spec.cr | 92 +++++++++++++------------------- src/lavinmq/mqtt/retain_store.cr | 2 +- 2 files changed, 38 insertions(+), 56 deletions(-) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index c26c5fedab..7adfaa7472 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -74,61 +74,43 @@ describe LavinMQ::Clustering::Client do end end - # just some playing around with copilot, does not work - # it "replicates and streams retained messages to followers" do - # replicator = LavinMQ::Clustering::Server.new(LavinMQ::Config.instance, LavinMQ::Etcd.new, 0) - # tcp_server = TCPServer.new("localhost", 0) - # spawn(replicator.listen(tcp_server), name: "repli server spec") - # config = LavinMQ::Config.new.tap &.data_dir = follower_data_dir - # repli = LavinMQ::Clustering::Client.new(config, 1, replicator.password, proxy: false) - # done = Channel(Nil).new - # spawn(name: "follow spec") do - # repli.follow("localhost", tcp_server.local_address.port) - # done.send nil - # end - # wait_for { replicator.followers.size == 1 } - - # # Set up the retain store and retain some messages - # index = IndexTree.new - # retain_store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", replicator, index) - # props = LavinMQ::AMQP::Properties.new - # msg1 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body1")) - # msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body2")) - # retain_store.retain("topic1", msg1.body_io, msg1.bodysize) - # retain_store.retain("topic2", msg2.body_io, msg2.bodysize) - - # with_amqp_server(replicator: replicator) do |s| - # with_channel(s) do |ch| - # q = ch.queue("repli") - # q.publish_confirm "hello world" - # end - # repli.close - # done.receive - # end - - # server = LavinMQ::Server.new(follower_data_dir) - # begin - # q = server.vhosts["/"].queues["repli"].as(LavinMQ::AMQP::DurableQueue) - # q.message_count.should eq 1 - # q.basic_get(true) do |env| - # String.new(env.message.body).to_s.should eq "hello world" - # end.should be_true - - # # Verify that the retained messages are streamed to the follower - # follower_retain_store = LavinMQ::MQTT::RetainStore.new(follower_data_dir, replicator, IndexTree.new) - # follower_retain_store.each("topic1") do |topic, bytes| - # topic.should eq("topic1") - # String.new(bytes).should eq("body1") - # end - # follower_retain_store.each("topic2") do |topic, bytes| - # topic.should eq("topic2") - # String.new(bytes).should eq("body2") - # end - # follower_retain_store.retained_messages.should eq(2) - # ensure - # server.close - # end - # end + it "replicates and streams retained messages to followers" do + replicator = LavinMQ::Clustering::Server.new(LavinMQ::Config.instance, LavinMQ::Etcd.new, 0) + tcp_server = TCPServer.new("localhost", 0) + + spawn(replicator.listen(tcp_server), name: "repli server spec") + config = LavinMQ::Config.new.tap &.data_dir = follower_data_dir + repli = LavinMQ::Clustering::Client.new(config, 1, replicator.password, proxy: false) + done = Channel(Nil).new + spawn(name: "follow spec") do + repli.follow("localhost", tcp_server.local_address.port) + done.send nil + end + wait_for { replicator.followers.size == 1 } + + retain_store = LavinMQ::MQTT::RetainStore.new("#{LavinMQ::Config.instance.data_dir}/retain_store", replicator) + props = LavinMQ::AMQP::Properties.new + msg1 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body1")) + msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body2")) + retain_store.retain("topic1", msg1.body_io, msg1.bodysize) + retain_store.retain("topic2", msg2.body_io, msg2.bodysize) + + wait_for { replicator.@followers.first.lag_in_bytes == 0 } + repli.close + done.receive + + follower_retain_store = LavinMQ::MQTT::RetainStore.new("#{follower_data_dir}/retain_store", LavinMQ::Clustering::NoopServer.new) + a = Array(String).new(2) + b = Array(String).new(2) + follower_retain_store.each("#") do |topic, bytes| + a << topic + b << String.new(bytes) + end + + a.sort!.should eq(["topic1", "topic2"]) + b.sort!.should eq(["body1", "body2"]) + follower_retain_store.retained_messages.should eq(2) + end it "can stream full file" do replicator = LavinMQ::Clustering::Server.new(LavinMQ::Config.instance, LavinMQ::Etcd.new, 0) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 4bbe77e6a6..bd3f59f393 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -14,7 +14,7 @@ module LavinMQ def initialize(@dir : String, @replicator : Clustering::Replicator, @index = IndexTree.new) Dir.mkdir_p @dir @files = Hash(String, File).new do |files, file_name| - f = files[file_name] = File.new(File.join(@dir, file_name), "w+") + f = files[file_name] = File.new(File.join(@dir, file_name), "r+") f.sync = true f end From b8f9b7210a6679ca949aad601ceeb20f946929ce Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 09:29:33 +0100 Subject: [PATCH 145/202] add mqtt_proxy for clustering client --- src/lavinmq/clustering/client.cr | 12 ++++++++++++ src/lavinmq/config.cr | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/src/lavinmq/clustering/client.cr b/src/lavinmq/clustering/client.cr index 7cce19a60e..7f6a75de80 100644 --- a/src/lavinmq/clustering/client.cr +++ b/src/lavinmq/clustering/client.cr @@ -11,8 +11,10 @@ module LavinMQ @closed = false @amqp_proxy : Proxy? @http_proxy : Proxy? + @mqtt_proxy : Proxy? @unix_amqp_proxy : Proxy? @unix_http_proxy : Proxy? + @unix_mqtt_proxy : Proxy? @socket : TCPSocket? def initialize(@config : Config, @id : Int32, @password : String, proxy = true) @@ -30,8 +32,10 @@ module LavinMQ if proxy @amqp_proxy = Proxy.new(@config.amqp_bind, @config.amqp_port) @http_proxy = Proxy.new(@config.http_bind, @config.http_port) + @mqtt_proxy = Proxy.new(@config.mqtt_bind, @config.mqtt_port) @unix_amqp_proxy = Proxy.new(@config.unix_path) unless @config.unix_path.empty? @unix_http_proxy = Proxy.new(@config.http_unix_path) unless @config.http_unix_path.empty? + @unix_mqtt_proxy = Proxy.new(@config.mqtt_unix_path) unless @config.mqtt_unix_path.empty? end HTTP::Server.follower_internal_socket_http_server @@ -64,12 +68,18 @@ module LavinMQ if http_proxy = @http_proxy spawn http_proxy.forward_to(host, @config.http_port), name: "HTTP proxy" end + if mqtt_proxy = @mqtt_proxy + spawn mqtt_proxy.forward_to(host, @config.mqtt_port), name: "MQTT proxy" + end if unix_amqp_proxy = @unix_amqp_proxy spawn unix_amqp_proxy.forward_to(host, @config.amqp_port), name: "AMQP proxy" end if unix_http_proxy = @unix_http_proxy spawn unix_http_proxy.forward_to(host, @config.http_port), name: "HTTP proxy" end + if unix_mqtt_proxy = @unix_mqtt_proxy + spawn unix_mqtt_proxy.forward_to(host, @config.mqtt_port), name: "MQTT proxy" + end loop do @socket = socket = TCPSocket.new(host, port) socket.sync = true @@ -274,8 +284,10 @@ module LavinMQ @closed = true @amqp_proxy.try &.close @http_proxy.try &.close + @mqtt_proxy.try &.close @unix_amqp_proxy.try &.close @unix_http_proxy.try &.close + @unix_mqtt_proxy.try &.close @files.each_value &.close @data_dir_lock.release @socket.try &.close diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index 2d139f5fc2..814c8e508a 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -20,6 +20,7 @@ module LavinMQ property mqtt_bind = "127.0.0.1" property mqtt_port = 1883 property mqtts_port = 8883 + property mqtt_unix_path = "" property unix_path = "" property unix_proxy_protocol = 1_u8 # PROXY protocol version on unix domain socket connections property tcp_proxy_protocol = 0_u8 # PROXY protocol version on amqp tcp connections @@ -114,6 +115,9 @@ module LavinMQ p.on("--http-unix-path=PATH", "HTTP UNIX path to listen to") do |v| @http_unix_path = v end + p.on("--mqtt-unix-path=PATH", "MQTT UNIX path to listen to") do |v| + @mqtt_unix_path = v + end p.on("--cert FILE", "TLS certificate (including chain)") { |v| @tls_cert_path = v } p.on("--key FILE", "Private key for the TLS certificate") { |v| @tls_key_path = v } p.on("--ciphers CIPHERS", "List of TLS ciphers to allow") { |v| @tls_ciphers = v } @@ -298,6 +302,7 @@ module LavinMQ when "tls_port" then @mqtts_port = v.to_i32 when "tls_cert" then @tls_cert_path = v # backward compatibility when "tls_key" then @tls_key_path = v # backward compatibility + when "mqtt_unix_path" then @mqtt_unix_path = v when "max_inflight_messages" then @max_inflight_messages = v.to_u16 else STDERR.puts "WARNING: Unrecognized configuration 'mqtt/#{config}'" From 8bda86f06ad9ea65f511cabf20b9700aa785bbdf Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 10:00:56 +0100 Subject: [PATCH 146/202] r+ needs file to exist before open --- src/lavinmq/mqtt/retain_store.cr | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index bd3f59f393..2088a3be9c 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -14,7 +14,11 @@ module LavinMQ def initialize(@dir : String, @replicator : Clustering::Replicator, @index = IndexTree.new) Dir.mkdir_p @dir @files = Hash(String, File).new do |files, file_name| - f = files[file_name] = File.new(File.join(@dir, file_name), "r+") + file_path = File.join(@dir, file_name) + unless File.exists?(file_path) + File.open(file_path, "w").close + end + f = files[file_name] = File.new(file_path, "r+") f.sync = true f end @@ -81,7 +85,6 @@ module LavinMQ msg_file_name = make_file_name(topic) add_to_index(topic, msg_file_name) end - tmp_file = File.join(@dir, "#{msg_file_name}.tmp") f = @files[msg_file_name] f.pos = 0 From 192208a33f06dd952a79fb670c4cf9a091018456 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 10:05:06 +0100 Subject: [PATCH 147/202] format --- src/lavinmq/mqtt/retain_store.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 2088a3be9c..2aaaa196bc 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -14,7 +14,7 @@ module LavinMQ def initialize(@dir : String, @replicator : Clustering::Replicator, @index = IndexTree.new) Dir.mkdir_p @dir @files = Hash(String, File).new do |files, file_name| - file_path = File.join(@dir, file_name) + file_path = File.join(@dir, file_name) unless File.exists?(file_path) File.open(file_path, "w").close end From bcb64c51c51080531909ebff1c0756d919b2f8da Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 11:42:47 +0100 Subject: [PATCH 148/202] fix ameba failures --- spec/mqtt/integrations/connect_spec.cr | 2 +- spec/mqtt/integrations/retain_store_spec.cr | 3 +-- ...g_token_iterator.cr => string_token_iterator_spec.cr} | 8 ++++---- spec/spec_helper.cr | 2 +- src/lavinmq/amqp/queue/queue.cr | 2 +- src/lavinmq/launcher.cr | 5 ++++- src/lavinmq/mqtt/connection_factory.cr | 9 ++------- src/lavinmq/mqtt/retain_store.cr | 5 ++--- src/lavinmq/mqtt/topic_tree.cr | 3 ++- 9 files changed, 18 insertions(+), 21 deletions(-) rename spec/mqtt/{string_token_iterator.cr => string_token_iterator_spec.cr} (86%) diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index b080f56026..831d988dea 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -170,7 +170,7 @@ module MqttSpecs it "accepts zero byte client_id but is assigned a unique client_id [MQTT-3.1.3-6]" do with_server do |server| with_client_io(server) do |io| - connack = connect(io, client_id: "", clean_session: true) + connect(io, client_id: "", clean_session: true) server.broker.@clients.first[1].@client_id.should_not eq("") end end diff --git a/spec/mqtt/integrations/retain_store_spec.cr b/spec/mqtt/integrations/retain_store_spec.cr index e7ada0cd94..5edb0483bc 100644 --- a/spec/mqtt/integrations/retain_store_spec.cr +++ b/spec/mqtt/integrations/retain_store_spec.cr @@ -30,7 +30,6 @@ module MqttSpecs store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) props = LavinMQ::AMQP::Properties.new msg = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) - msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body")) store.retain("a", msg.body_io, msg.bodysize) index.size.should eq(1) @@ -97,7 +96,7 @@ module MqttSpecs store.close new_index = IndexTree.new - store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, new_index) + LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, new_index) new_index.size.should eq(1) new_index.@leafs.has_key?("a").should be_true diff --git a/spec/mqtt/string_token_iterator.cr b/spec/mqtt/string_token_iterator_spec.cr similarity index 86% rename from spec/mqtt/string_token_iterator.cr rename to spec/mqtt/string_token_iterator_spec.cr index 787fba28c4..c4c0249f2b 100644 --- a/spec/mqtt/string_token_iterator.cr +++ b/spec/mqtt/string_token_iterator_spec.cr @@ -18,13 +18,13 @@ end describe LavinMQ::MQTT::StringTokenIterator do strings.each do |testdata| - it "is iterated correct" do + it "is iterated correctly" do itr = LavinMQ::MQTT::StringTokenIterator.new(testdata[0], '/') res = Array(String).new while itr.next? - val = itr.next - val.should_not be_nil - res << val.not_nil! + if val = itr.next + res << val + end end itr.next?.should be_false res.should eq testdata[1] diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index d78553b34f..214c40931c 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -29,7 +29,7 @@ end def with_channel(s : LavinMQ::Server, file = __FILE__, line = __LINE__, **args, &) name = "lavinmq-spec-#{file}:#{line}" - port = s.@listeners + s.@listeners .select { |k, v| k.is_a?(TCPServer) && v == :amqp } .keys .select(TCPServer) diff --git a/src/lavinmq/amqp/queue/queue.cr b/src/lavinmq/amqp/queue/queue.cr index 3fd7f6c4ba..8e00486198 100644 --- a/src/lavinmq/amqp/queue/queue.cr +++ b/src/lavinmq/amqp/queue/queue.cr @@ -756,7 +756,7 @@ module LavinMQ::AMQP end end end - unacked_messages = unacked_messages.chain(self.basic_get_unacked.each) + unacked_messages.chain(self.basic_get_unacked.each) end private def with_delivery_count_header(env) : Envelope? diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index 0dac23681b..03b1ebf172 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -114,7 +114,7 @@ module LavinMQ end end - private def listen + private def listen # ameba:disable Metrics/CyclomaticComplexity if @config.amqp_port > 0 spawn @amqp_server.listen(@config.amqp_bind, @config.amqp_port, :amqp), name: "AMQP listening on #{@config.amqp_port}" @@ -163,6 +163,9 @@ module LavinMQ name: "MQTTS listening on #{@config.mqtts_port}" end end + unless @config.unix_path.empty? + spawn @amqp_server.listen_unix(@config.mqtt_unix_path, :mqtt), name: "MQTT listening at #{@config.unix_path}" + end end private def dump_debug_info diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index f356576b0e..508db5c718 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -40,13 +40,8 @@ module LavinMQ def authenticate(io, packet) return nil unless (username = packet.username) && (password = packet.password) user = @users[username]? - return user if user && user.password && user.password.not_nil!.verify(String.new(password)) - # probably not good to differentiate between user not found and wrong password - if user.nil? - Log.warn { "User \"#{username}\" not found" } - else - Log.warn { "Authentication failure for user \"#{username}\"" } - end + return user if user && user.password && user.password.try(&.verify(String.new(password))) + Log.warn { "Authentication failure for user \"#{username}\"" } MQTT::Connack.new(false, MQTT::Connack::ReturnCode::NotAuthorized).to_io(io) nil end diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 2aaaa196bc..1a2eb75fce 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -65,8 +65,8 @@ module LavinMQ # index from msg files? unless msg_file_segments.empty? Log.warn { "unreferenced messages will be deleted: #{msg_file_segments.join(",")}" } - msg_file_segments.each do |msg_file_name| - File.delete? File.join(dir, msg_file_name) + msg_file_segments.each do |file_name| + File.delete? File.join(dir, file_name) end end Log.info { "restoring index done, msg_count = #{msg_count}" } @@ -85,7 +85,6 @@ module LavinMQ msg_file_name = make_file_name(topic) add_to_index(topic, msg_file_name) end - tmp_file = File.join(@dir, "#{msg_file_name}.tmp") f = @files[msg_file_name] f.pos = 0 ::IO.copy(body_io, f) diff --git a/src/lavinmq/mqtt/topic_tree.cr b/src/lavinmq/mqtt/topic_tree.cr index e39e030a98..85611a9c73 100644 --- a/src/lavinmq/mqtt/topic_tree.cr +++ b/src/lavinmq/mqtt/topic_tree.cr @@ -17,7 +17,8 @@ module LavinMQ end def insert(topic : StringTokenIterator, entity : TEntity) : TEntity? - current = topic.next.not_nil! + current = topic.next + raise ArgumentError.new "topic cannot be empty" unless current if topic.next? @sublevels[current].insert(topic, entity) else From 1e91cf265521179d2550820b22acd8984c387b14 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 11:49:16 +0100 Subject: [PATCH 149/202] satisfy ameba for spec files --- spec/message_routing_spec.cr | 52 +++++++++---------- spec/mqtt/spec_helper.cr | 6 +-- .../{mqtt_client.cr => mqtt_client_spec.cr} | 0 .../{mqtt_helpers.cr => mqtt_helpers_spec.cr} | 2 +- ...mqtt_matchers.cr => mqtt_matchers_spec.cr} | 0 ...mqtt_protocol.cr => mqtt_protocol_spec.cr} | 0 ...rator_spec.cr => string_token_iterator.cr} | 0 7 files changed, 30 insertions(+), 30 deletions(-) rename spec/mqtt/spec_helper/{mqtt_client.cr => mqtt_client_spec.cr} (100%) rename spec/mqtt/spec_helper/{mqtt_helpers.cr => mqtt_helpers_spec.cr} (99%) rename spec/mqtt/spec_helper/{mqtt_matchers.cr => mqtt_matchers_spec.cr} (100%) rename spec/mqtt/spec_helper/{mqtt_protocol.cr => mqtt_protocol_spec.cr} (100%) rename spec/mqtt/{string_token_iterator_spec.cr => string_token_iterator.cr} (100%) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index 4a13bf9b2c..7e6c5c5ae7 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -422,29 +422,29 @@ describe LavinMQ::Exchange do end end -describe LavinMQ::MQTT::Exchange do - it "should only allow Session to bind" do - with_amqp_server do |s| - vhost = s.vhosts.create("x") - q1 = LavinMQ::AMQP::Queue.new(vhost, "q1") - s1 = LavinMQ::MQTT::Session.new(vhost, "q1") - x = LavinMQ::MQTT::Exchange.new(vhost, "", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) - x.bind(s1, "s1", LavinMQ::AMQP::Table.new) - expect_raises(LavinMQ::Exchange::AccessRefused) do - x.bind(q1, "q1", LavinMQ::AMQP::Table.new) - end - end - end - - it "publish messages to queues with it's own publish method" do - with_amqp_server do |s| - vhost = s.vhosts.create("x") - s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") - x = LavinMQ::MQTT::Exchange.new(vhost, "mqtt.default", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) - x.bind(s1, "s1", LavinMQ::AMQP::Table.new) - msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") - x.publish(msg, false) - s1.message_count.should eq 1 - end - end -end +# describe LavinMQ::MQTT::Exchange do +# it "should only allow Session to bind" do +# with_amqp_server do |s| +# vhost = s.vhosts.create("x") +# q1 = LavinMQ::AMQP::Queue.new(vhost, "q1") +# s1 = LavinMQ::MQTT::Session.new(vhost, "q1") +# x = LavinMQ::MQTT::Exchange.new(vhost, "", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) +# x.bind(s1, "s1", LavinMQ::AMQP::Table.new) +# expect_raises(LavinMQ::Exchange::AccessRefused) do +# x.bind(q1, "q1", LavinMQ::AMQP::Table.new) +# end +# end +# end + +# it "publish messages to queues with it's own publish method" do +# with_amqp_server do |s| +# vhost = s.vhosts.create("x") +# s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") +# x = LavinMQ::MQTT::Exchange.new(vhost, "mqtt.default", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) +# x.bind(s1, "s1", LavinMQ::AMQP::Table.new) +# msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") +# x.publish(msg, false) +# s1.message_count.should eq 1 +# end +# end +# end diff --git a/spec/mqtt/spec_helper.cr b/spec/mqtt/spec_helper.cr index ae011c16b6..4a3b767606 100644 --- a/spec/mqtt/spec_helper.cr +++ b/spec/mqtt/spec_helper.cr @@ -1,3 +1,3 @@ -require "./spec_helper/mqtt_helpers" -require "./spec_helper/mqtt_matchers" -require "./spec_helper/mqtt_protocol" +require "./spec_helper/mqtt_helpers_spec" +require "./spec_helper/mqtt_matchers_spec" +require "./spec_helper/mqtt_protocol_spec" diff --git a/spec/mqtt/spec_helper/mqtt_client.cr b/spec/mqtt/spec_helper/mqtt_client_spec.cr similarity index 100% rename from spec/mqtt/spec_helper/mqtt_client.cr rename to spec/mqtt/spec_helper/mqtt_client_spec.cr diff --git a/spec/mqtt/spec_helper/mqtt_helpers.cr b/spec/mqtt/spec_helper/mqtt_helpers_spec.cr similarity index 99% rename from spec/mqtt/spec_helper/mqtt_helpers.cr rename to spec/mqtt/spec_helper/mqtt_helpers_spec.cr index b569d21438..8d72c66dfb 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers_spec.cr @@ -1,5 +1,5 @@ require "mqtt-protocol" -require "./mqtt_client" +require "./mqtt_client_spec" require "../../spec_helper" module MqttHelpers diff --git a/spec/mqtt/spec_helper/mqtt_matchers.cr b/spec/mqtt/spec_helper/mqtt_matchers_spec.cr similarity index 100% rename from spec/mqtt/spec_helper/mqtt_matchers.cr rename to spec/mqtt/spec_helper/mqtt_matchers_spec.cr diff --git a/spec/mqtt/spec_helper/mqtt_protocol.cr b/spec/mqtt/spec_helper/mqtt_protocol_spec.cr similarity index 100% rename from spec/mqtt/spec_helper/mqtt_protocol.cr rename to spec/mqtt/spec_helper/mqtt_protocol_spec.cr diff --git a/spec/mqtt/string_token_iterator_spec.cr b/spec/mqtt/string_token_iterator.cr similarity index 100% rename from spec/mqtt/string_token_iterator_spec.cr rename to spec/mqtt/string_token_iterator.cr From 799d5213d87285938db3aaa8a1f31dab7ef8297b Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 11:53:48 +0100 Subject: [PATCH 150/202] rename specfile with suffix --- .../{string_token_iterator.cr => string_token_iterator_spec.cr} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/mqtt/{string_token_iterator.cr => string_token_iterator_spec.cr} (100%) diff --git a/spec/mqtt/string_token_iterator.cr b/spec/mqtt/string_token_iterator_spec.cr similarity index 100% rename from spec/mqtt/string_token_iterator.cr rename to spec/mqtt/string_token_iterator_spec.cr From c6df728a8f653271e2c3bba0b56ef1aea8c50170 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 12:02:21 +0100 Subject: [PATCH 151/202] fix flaky wait_for --- spec/clustering_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index 7adfaa7472..6b8cc118c1 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -95,7 +95,7 @@ describe LavinMQ::Clustering::Client do retain_store.retain("topic1", msg1.body_io, msg1.bodysize) retain_store.retain("topic2", msg2.body_io, msg2.bodysize) - wait_for { replicator.@followers.first.lag_in_bytes == 0 } + wait_for { replicator.followers.first?.try &.lag_in_bytes == 0 } repli.close done.receive From e8613d57d5dae491eb9832b63504ea91b5cebdeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Wed, 13 Nov 2024 13:29:33 +0100 Subject: [PATCH 152/202] Use enum for protocol to get compile time validation --- src/lavinmq/http/handler/websocket.cr | 2 +- src/lavinmq/launcher.cr | 10 +++++----- src/lavinmq/server.cr | 28 ++++++++++++++------------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/lavinmq/http/handler/websocket.cr b/src/lavinmq/http/handler/websocket.cr index b749807cb1..6f58784d5f 100644 --- a/src/lavinmq/http/handler/websocket.cr +++ b/src/lavinmq/http/handler/websocket.cr @@ -11,7 +11,7 @@ module LavinMQ Socket::IPAddress.new("127.0.0.1", 0) # Fake when UNIXAddress connection_info = ConnectionInfo.new(remote_address, local_address) io = WebSocketIO.new(ws) - spawn amqp_server.handle_connection(io, connection_info, "amqp"), name: "HandleWSconnection #{remote_address}" + spawn amqp_server.handle_connection(io, connection_info, Server::Protocol::AMQP), name: "HandleWSconnection #{remote_address}" end end end diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index 03b1ebf172..102072cfe0 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -116,13 +116,13 @@ module LavinMQ private def listen # ameba:disable Metrics/CyclomaticComplexity if @config.amqp_port > 0 - spawn @amqp_server.listen(@config.amqp_bind, @config.amqp_port, :amqp), + spawn @amqp_server.listen(@config.amqp_bind, @config.amqp_port, Server::Protocol::AMQP), name: "AMQP listening on #{@config.amqp_port}" end if @config.amqps_port > 0 if ctx = @tls_context - spawn @amqp_server.listen_tls(@config.amqp_bind, @config.amqps_port, ctx, :amqp), + spawn @amqp_server.listen_tls(@config.amqp_bind, @config.amqps_port, ctx, Server::Protocol::AMQP), name: "AMQPS listening on #{@config.amqps_port}" end end @@ -132,7 +132,7 @@ module LavinMQ end unless @config.unix_path.empty? - spawn @amqp_server.listen_unix(@config.unix_path, :amqp), name: "AMQP listening at #{@config.unix_path}" + spawn @amqp_server.listen_unix(@config.unix_path, Server::Protocol::AMQP), name: "AMQP listening at #{@config.unix_path}" end if @config.http_port > 0 @@ -153,13 +153,13 @@ module LavinMQ end if @config.mqtt_port > 0 - spawn @amqp_server.listen(@config.mqtt_bind, @config.mqtt_port, :mqtt), + spawn @amqp_server.listen(@config.mqtt_bind, @config.mqtt_port, Server::Protocol::MQTT), name: "MQTT listening on #{@config.mqtt_port}" end if @config.mqtts_port > 0 if ctx = @tls_context - spawn @amqp_server.listen_tls(@config.mqtt_bind, @config.mqtts_port, ctx, :mqtt), + spawn @amqp_server.listen_tls(@config.mqtt_bind, @config.mqtts_port, ctx, Server::Protocol::MQTT), name: "MQTTS listening on #{@config.mqtts_port}" end end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index a328274563..8fe4d17aae 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -21,6 +21,11 @@ require "./stats" module LavinMQ class Server + enum Protocol + AMQP + MQTT + end + getter vhosts, users, data_dir, parameters, broker getter? closed, flow include ParameterTarget @@ -28,7 +33,7 @@ module LavinMQ @start = Time.monotonic @closed = false @flow = true - @listeners = Hash(Socket::Server, Symbol).new # Socket => protocol + @listeners = Hash(Socket::Server, Protocol).new # Socket => protocol @replicator : Clustering::Replicator Log = LavinMQ::Log.for "server" @@ -84,7 +89,7 @@ module LavinMQ Iterator(Client).chain(@vhosts.each_value.map(&.connections.each)) end - def listen(s : TCPServer, protocol) + def listen(s : TCPServer, protocol : Protocol) @listeners[s] = protocol Log.info { "Listening on #{s.local_address}" } loop do @@ -130,7 +135,7 @@ module LavinMQ end end - def listen(s : UNIXServer, protocol) + def listen(s : UNIXServer, protocol : Protocol) @listeners[s] = protocol Log.info { "Listening on #{s.local_address}" } loop do # do not try to use while @@ -157,12 +162,12 @@ module LavinMQ @listeners.delete(s) end - def listen(bind = "::", port = 5672, protocol = :amqp) + def listen(bind = "::", port = 5672, protocol : Protocol = :amqp) s = TCPServer.new(bind, port) listen(s, protocol) end - def listen_tls(s : TCPServer, context, protocol) + def listen_tls(s : TCPServer, context, protocol : Protocol) @listeners[s] = protocol Log.info { "Listening on #{s.local_address} (TLS)" } loop do # do not try to use while @@ -190,11 +195,11 @@ module LavinMQ @listeners.delete(s) end - def listen_tls(bind, port, context, protocol) + def listen_tls(bind, port, context, protocol : Protocol = :amqp) listen_tls(TCPServer.new(bind, port), context, protocol) end - def listen_unix(path : String, protocol) + def listen_unix(path : String, protocol : Protocol) File.delete?(path) s = UNIXServer.new(path) File.chmod(path, 0o666) @@ -254,15 +259,12 @@ module LavinMQ end end - def handle_connection(socket, connection_info, protocol) + def handle_connection(socket, connection_info, protocol : Protocol) case protocol - when :amqp + in .amqp? client = @amqp_connection_factory.start(socket, connection_info, @vhosts, @users) - when :mqtt + in .mqtt? client = @mqtt_connection_factory.start(socket, connection_info) - else - Log.warn { "Unknown protocol '#{protocol}'" } - socket.close end ensure socket.close if client.nil? From cb8f238b6e574a5fbd242de52129f682defbdb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Wed, 13 Nov 2024 13:35:33 +0100 Subject: [PATCH 153/202] Fix specs to use protocol enum --- spec/mqtt/spec_helper/mqtt_helpers_spec.cr | 8 ++++---- spec/spec_helper.cr | 6 +++--- src/lavinmq/launcher.cr | 2 +- src/lavinmq/server.cr | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/mqtt/spec_helper/mqtt_helpers_spec.cr b/spec/mqtt/spec_helper/mqtt_helpers_spec.cr index 8d72c66dfb..5efe4662c2 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers_spec.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers_spec.cr @@ -10,8 +10,8 @@ module MqttHelpers end def with_client_socket(server) - listener = server.listeners.find { |l| l[:protocol] == :mqtt } - tcp_listener = listener.as(NamedTuple(ip_address: String, protocol: Symbol, port: Int32)) + listener = server.listeners.find { |l| l[:protocol].mqtt? } + tcp_listener = listener.as(NamedTuple(ip_address: String, protocol: LavinMQ::Server::Protocol, port: Int32)) socket = TCPSocket.new( tcp_listener[:ip_address], @@ -41,8 +41,8 @@ module MqttHelpers amqp_server = TCPServer.new("localhost", 0) s = LavinMQ::Server.new(LavinMQ::Config.instance.data_dir, LavinMQ::Clustering::NoopServer.new) begin - spawn(name: "amqp tcp listen") { s.listen(amqp_server, :amqp) } - spawn(name: "mqtt tcp listen") { s.listen(mqtt_server, :mqtt) } + spawn(name: "amqp tcp listen") { s.listen(amqp_server, LavinMQ::Server::Protocol::AMQP) } + spawn(name: "mqtt tcp listen") { s.listen(mqtt_server, LavinMQ::Server::Protocol::MQTT) } Fiber.yield yield s ensure diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 214c40931c..be6b512239 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -30,7 +30,7 @@ end def with_channel(s : LavinMQ::Server, file = __FILE__, line = __LINE__, **args, &) name = "lavinmq-spec-#{file}:#{line}" s.@listeners - .select { |k, v| k.is_a?(TCPServer) && v == :amqp } + .select { |k, v| k.is_a?(TCPServer) && v.amqp? } .keys .select(TCPServer) .first @@ -87,9 +87,9 @@ def with_amqp_server(tls = false, replicator = LavinMQ::Clustering::NoopServer.n ctx = OpenSSL::SSL::Context::Server.new ctx.certificate_chain = "spec/resources/server_certificate.pem" ctx.private_key = "spec/resources/server_key.pem" - spawn(name: "amqp tls listen") { s.listen_tls(tcp_server, ctx, :amqp) } + spawn(name: "amqp tls listen") { s.listen_tls(tcp_server, ctx, LavinMQ::Server::Protocol::AMQP) } else - spawn(name: "amqp tcp listen") { s.listen(tcp_server, :amqp) } + spawn(name: "amqp tcp listen") { s.listen(tcp_server, LavinMQ::Server::Protocol::AMQP) } end Fiber.yield yield s diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index 102072cfe0..b179152d25 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -164,7 +164,7 @@ module LavinMQ end end unless @config.unix_path.empty? - spawn @amqp_server.listen_unix(@config.mqtt_unix_path, :mqtt), name: "MQTT listening at #{@config.unix_path}" + spawn @amqp_server.listen_unix(@config.mqtt_unix_path, Server::Protocol::MQTT), name: "MQTT listening at #{@config.unix_path}" end end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 8fe4d17aae..741b2a0dcc 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -56,7 +56,7 @@ module LavinMQ def amqp_url addr = @listeners - .select { |k, v| k.is_a?(TCPServer) && v == :amqp } + .select { |k, v| k.is_a?(TCPServer) && v.amqp? } .keys .select(TCPServer) .first From 954f98751c9fdca30162c31151cb4f1e0e401df1 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 13:48:27 +0100 Subject: [PATCH 154/202] use short block notation --- spec/mqtt/spec_helper/mqtt_helpers_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mqtt/spec_helper/mqtt_helpers_spec.cr b/spec/mqtt/spec_helper/mqtt_helpers_spec.cr index 5efe4662c2..795c5548c7 100644 --- a/spec/mqtt/spec_helper/mqtt_helpers_spec.cr +++ b/spec/mqtt/spec_helper/mqtt_helpers_spec.cr @@ -10,7 +10,7 @@ module MqttHelpers end def with_client_socket(server) - listener = server.listeners.find { |l| l[:protocol].mqtt? } + listener = server.listeners.find(&.[:protocol].mqtt?) tcp_listener = listener.as(NamedTuple(ip_address: String, protocol: LavinMQ::Server::Protocol, port: Int32)) socket = TCPSocket.new( From 437e3a4269d86d43b0c3a9ab259a38bde875b4b4 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 08:50:59 +0100 Subject: [PATCH 155/202] fix mqtt exchange routing spec --- spec/message_routing_spec.cr | 66 ++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index 7e6c5c5ae7..7c8fd78c71 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -422,29 +422,43 @@ describe LavinMQ::Exchange do end end -# describe LavinMQ::MQTT::Exchange do -# it "should only allow Session to bind" do -# with_amqp_server do |s| -# vhost = s.vhosts.create("x") -# q1 = LavinMQ::AMQP::Queue.new(vhost, "q1") -# s1 = LavinMQ::MQTT::Session.new(vhost, "q1") -# x = LavinMQ::MQTT::Exchange.new(vhost, "", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) -# x.bind(s1, "s1", LavinMQ::AMQP::Table.new) -# expect_raises(LavinMQ::Exchange::AccessRefused) do -# x.bind(q1, "q1", LavinMQ::AMQP::Table.new) -# end -# end -# end - -# it "publish messages to queues with it's own publish method" do -# with_amqp_server do |s| -# vhost = s.vhosts.create("x") -# s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") -# x = LavinMQ::MQTT::Exchange.new(vhost, "mqtt.default", LavinMQ::MQTT::RetainStore.new(vhost.data_dir)) -# x.bind(s1, "s1", LavinMQ::AMQP::Table.new) -# msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") -# x.publish(msg, false) -# s1.message_count.should eq 1 -# end -# end -# end +alias IndexTree = LavinMQ::MQTT::TopicTree(String) +describe LavinMQ::MQTT::Exchange do + it "should only allow Session to bind" do + with_amqp_server do |s| + vhost = s.vhosts.create("x") + q1 = LavinMQ::AMQP::Queue.new(vhost, "q1") + s1 = LavinMQ::MQTT::Session.new(vhost, "q1") + index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) + x = LavinMQ::MQTT::Exchange.new(vhost, "", store) + x.bind(s1, "s1", LavinMQ::AMQP::Table.new) + expect_raises(LavinMQ::Exchange::AccessRefused) do + x.bind(q1, "q1", LavinMQ::AMQP::Table.new) + end + end + end + + it "publish messages to queues with it's own publish method" do + with_amqp_server do |s| + vhost = s.vhosts.create("x") + s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") + index = IndexTree.new + store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) + x = LavinMQ::MQTT::Exchange.new(vhost, "mqtt.default", store) + x.bind(s1, "s1", LavinMQ::AMQP::Table.new) + # msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") + pub_args = { + packet_id: 1u16, + payload: Bytes.new(0), + dup: false, + qos: 0u8, + retain: false, + topic: "s1", + } + msg = MQTT::Protocol::Publish.new(**pub_args) + x.publish(msg) + s1.message_count.should eq 1 + end + end +end From 85e1cce98ecf1d92bab5de33fa411bc04ef4a114 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 08:51:14 +0100 Subject: [PATCH 156/202] remove comment --- spec/message_routing_spec.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index 7c8fd78c71..f2f650cf45 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -447,7 +447,6 @@ describe LavinMQ::MQTT::Exchange do store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) x = LavinMQ::MQTT::Exchange.new(vhost, "mqtt.default", store) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) - # msg = LavinMQ::Message.new("mqtt.default", "s1", "hej") pub_args = { packet_id: 1u16, payload: Bytes.new(0), From 85a632f6fecd10e0d5d6903a656fffbab10b1248 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 09:00:20 +0100 Subject: [PATCH 157/202] scope fix --- spec/message_routing_spec.cr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/message_routing_spec.cr b/spec/message_routing_spec.cr index f2f650cf45..de32981ef6 100644 --- a/spec/message_routing_spec.cr +++ b/spec/message_routing_spec.cr @@ -422,14 +422,13 @@ describe LavinMQ::Exchange do end end -alias IndexTree = LavinMQ::MQTT::TopicTree(String) describe LavinMQ::MQTT::Exchange do it "should only allow Session to bind" do with_amqp_server do |s| vhost = s.vhosts.create("x") q1 = LavinMQ::AMQP::Queue.new(vhost, "q1") s1 = LavinMQ::MQTT::Session.new(vhost, "q1") - index = IndexTree.new + index = LavinMQ::MQTT::TopicTree(String).new store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) x = LavinMQ::MQTT::Exchange.new(vhost, "", store) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) @@ -443,7 +442,7 @@ describe LavinMQ::MQTT::Exchange do with_amqp_server do |s| vhost = s.vhosts.create("x") s1 = LavinMQ::MQTT::Session.new(vhost, "session 1") - index = IndexTree.new + index = LavinMQ::MQTT::TopicTree(String).new store = LavinMQ::MQTT::RetainStore.new("tmp/retain_store", LavinMQ::Clustering::NoopServer.new, index) x = LavinMQ::MQTT::Exchange.new(vhost, "mqtt.default", store) x.bind(s1, "s1", LavinMQ::AMQP::Table.new) From 25e9d482ec4f7919b636b2be6d9d42c55c808a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Thu, 14 Nov 2024 10:02:35 +0100 Subject: [PATCH 158/202] Multi-vhost support --- spec/mqtt/integrations/connect_spec.cr | 2 +- spec/mqtt/multi_vhost_spec.cr | 40 ++++++++++++++++ src/lavinmq/amqp/connection_factory.cr | 24 ++++++---- src/lavinmq/mqtt/broker.cr | 13 +++--- src/lavinmq/mqtt/brokers.cr | 37 +++++++++++++++ src/lavinmq/mqtt/client.cr | 1 + src/lavinmq/mqtt/connection_factory.cr | 64 +++++++++++++++++--------- src/lavinmq/mqtt/consts.cr | 6 +++ src/lavinmq/mqtt/exchange.cr | 5 +- src/lavinmq/mqtt/retain_store.cr | 2 +- src/lavinmq/mqtt/session.cr | 7 +-- src/lavinmq/server.cr | 16 ++----- src/lavinmq/vhost_store.cr | 20 +++++++- 13 files changed, 181 insertions(+), 56 deletions(-) create mode 100644 spec/mqtt/multi_vhost_spec.cr create mode 100644 src/lavinmq/mqtt/brokers.cr create mode 100644 src/lavinmq/mqtt/consts.cr diff --git a/spec/mqtt/integrations/connect_spec.cr b/spec/mqtt/integrations/connect_spec.cr index 831d988dea..d5f3215d1c 100644 --- a/spec/mqtt/integrations/connect_spec.cr +++ b/spec/mqtt/integrations/connect_spec.cr @@ -171,7 +171,7 @@ module MqttSpecs with_server do |server| with_client_io(server) do |io| connect(io, client_id: "", clean_session: true) - server.broker.@clients.first[1].@client_id.should_not eq("") + server.vhosts["/"].connections.select(LavinMQ::MQTT::Client).first.client_id.should_not eq("") end end end diff --git a/spec/mqtt/multi_vhost_spec.cr b/spec/mqtt/multi_vhost_spec.cr new file mode 100644 index 0000000000..d960712f20 --- /dev/null +++ b/spec/mqtt/multi_vhost_spec.cr @@ -0,0 +1,40 @@ +require "./spec_helper" + +module MqttSpecs + extend MqttHelpers + describe LavinMQ::MQTT do + describe "multi-vhost" do + it "should create mqtt exchange when vhost is created" do + with_amqp_server do |server| + server.vhosts.create("new") + server.vhosts["new"].exchanges[LavinMQ::MQTT::EXCHANGE]?.should_not be_nil + end + end + + describe "authentication" do + it "should deny mqtt access for user lacking vhost permissions" do + with_server do |server| + server.users.create("foo", "bar") + with_client_io(server) do |io| + resp = connect io, username: "foo", password: "bar".to_slice + resp = resp.should be_a(MQTT::Protocol::Connack) + resp.return_code.should eq MQTT::Protocol::Connack::ReturnCode::NotAuthorized + end + end + end + + it "should allow mqtt access for user with vhost permissions" do + with_server do |server| + server.users.create("foo", "bar") + server.users.add_permission "foo", "/", /.*/, /.*/, /.*/ + with_client_io(server) do |io| + resp = connect io, username: "foo", password: "bar".to_slice + resp = resp.should be_a(MQTT::Protocol::Connack) + resp.return_code.should eq MQTT::Protocol::Connack::ReturnCode::Accepted + end + end + end + end + end + end +end diff --git a/src/lavinmq/amqp/connection_factory.cr b/src/lavinmq/amqp/connection_factory.cr index a427ba5eee..208cd53f1e 100644 --- a/src/lavinmq/amqp/connection_factory.cr +++ b/src/lavinmq/amqp/connection_factory.cr @@ -1,6 +1,8 @@ require "../version" require "../logger" require "./client" +require "../user_store" +require "../vhost_store" require "../client/connection_factory" module LavinMQ @@ -8,16 +10,19 @@ module LavinMQ class ConnectionFactory < LavinMQ::ConnectionFactory Log = LavinMQ::Log.for "amqp.connection_factory" - def start(socket, connection_info, vhosts, users) : Client? + def initialize(@users : UserStore, @vhosts : VHostStore) + end + + def start(socket, connection_info : ConnectionInfo) : Client? remote_address = connection_info.src socket.read_timeout = 15.seconds metadata = ::Log::Metadata.build({address: remote_address.to_s}) logger = Logger.new(Log, metadata) if confirm_header(socket, logger) if start_ok = start(socket, logger) - if user = authenticate(socket, remote_address, users, start_ok, logger) + if user = authenticate(socket, remote_address, start_ok, logger) if tune_ok = tune(socket, logger) - if vhost = open(socket, vhosts, user, logger) + if vhost = open(socket, user, logger) socket.read_timeout = heartbeat_timeout(tune_ok) return LavinMQ::AMQP::Client.new(socket, connection_info, vhost, user, tune_ok, start_ok) end @@ -71,7 +76,7 @@ module LavinMQ }, }) - def start(socket, log) + def start(socket, log : Logger) start = AMQP::Frame::Connection::Start.new(server_properties: SERVER_PROPERTIES) socket.write_bytes start, ::IO::ByteFormat::NetworkEndian socket.flush @@ -100,9 +105,9 @@ module LavinMQ end end - def authenticate(socket, remote_address, users, start_ok, log) + def authenticate(socket, remote_address, start_ok, log) username, password = credentials(start_ok) - user = users[username]? + user = @users[username]? return user if user && user.password && user.password.not_nil!.verify(password) && guest_only_loopback?(remote_address, user) @@ -150,10 +155,10 @@ module LavinMQ tune_ok end - def open(socket, vhosts, user, log) + def open(socket, user, log) open = AMQP::Frame.from_io(socket) { |f| f.as(AMQP::Frame::Connection::Open) } vhost_name = open.vhost.empty? ? "/" : open.vhost - if vhost = vhosts[vhost_name]? + if vhost = @vhosts[vhost_name]? if user.permissions[vhost_name]? if vhost.max_connections.try { |max| vhost.connections.size >= max } log.warn { "Max connections (#{vhost.max_connections}) reached for vhost #{vhost_name}" } @@ -187,6 +192,9 @@ module LavinMQ return true unless Config.instance.guest_only_loopback? remote_address.loopback? end + + def close + end end end end diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index d6093f92c6..6f7eaa350a 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -1,10 +1,11 @@ require "./client" +require "./consts" +require "./exchange" require "./protocol" require "./session" require "./sessions" require "./retain_store" require "../vhost" -require "./exchange" module LavinMQ module MQTT @@ -15,8 +16,8 @@ module LavinMQ @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s, @replicator) - @exchange = MQTT::Exchange.new(@vhost, "mqtt.default", @retain_store) - @vhost.exchanges["mqtt.default"] = @exchange + @exchange = MQTT::Exchange.new(@vhost, EXCHANGE, @retain_store) + @vhost.exchanges[EXCHANGE] = @exchange end def session_present?(client_id : String, clean_session) : Bool @@ -26,12 +27,12 @@ module LavinMQ true end - def connect_client(socket, connection_info, user, vhost, packet) + def connect_client(socket, connection_info, user, packet) if prev_client = @clients[packet.client_id]? Log.trace { "Found previous client connected with client_id: #{packet.client_id}, closing" } prev_client.close end - client = MQTT::Client.new(socket, connection_info, user, vhost, self, packet.client_id, packet.clean_session?, packet.will) + client = MQTT::Client.new(socket, connection_info, user, @vhost, self, packet.client_id, packet.clean_session?, packet.will) if session = sessions[client.client_id]? if session.clean_session? sessions.delete session @@ -67,7 +68,7 @@ module LavinMQ qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) session.subscribe(tf.topic, tf.qos) @retain_store.each(tf.topic) do |topic, body| - msg = Message.new("mqtt.default", topic, String.new(body), + msg = Message.new(EXCHANGE, topic, String.new(body), AMQP::Properties.new(headers: AMQP::Table.new({"x-mqtt-retain": true}), delivery_mode: tf.qos)) session.publish(msg) diff --git a/src/lavinmq/mqtt/brokers.cr b/src/lavinmq/mqtt/brokers.cr new file mode 100644 index 0000000000..4853e8bb6c --- /dev/null +++ b/src/lavinmq/mqtt/brokers.cr @@ -0,0 +1,37 @@ +require "./broker" +require "../clustering/replicator" +require "../observable" +require "../vhost_store" + +module LavinMQ + module MQTT + class Brokers + include Observer(VHostStore::Event) + + def initialize(@vhosts : VHostStore, @replicator : Clustering::Replicator) + @brokers = Hash(String, Broker).new(initial_capacity: @vhosts.size) + @vhosts.each do |(name, vhost)| + @brokers[name] = Broker.new(vhost, @replicator) + end + @vhosts.register_observer(self) + end + + def []?(vhost : String) : Broker? + @brokers[vhost]? + end + + def on(event : VHostStore::Event, data : Object?) + return if data.nil? + vhost = data.to_s + case event + in VHostStore::Event::Added + @brokers[vhost] = Broker.new(@vhosts[vhost], @replicator) + in VHostStore::Event::Deleted + @brokers.delete(vhost) + in VHostStore::Event::Closed + @brokers[vhost].close + end + end + end + end +end diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 630ae32001..0261be4725 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -3,6 +3,7 @@ require "socket" require "../client" require "../error" require "./session" +require "./protocol" module LavinMQ module MQTT diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 508db5c718..12cc0d4662 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -1,54 +1,74 @@ +require "log" require "socket" require "./protocol" -require "log" require "./client" -require "../vhost" +require "./brokers" require "../user" -require "./broker" +require "../client/connection_factory" module LavinMQ module MQTT - class ConnectionFactory + class ConnectionFactory < LavinMQ::ConnectionFactory def initialize(@users : UserStore, - @vhost : VHost, - @broker : MQTT::Broker) + @vhosts : VHostStore, + replicator : Clustering::Replicator) + @brokers = Brokers.new(@vhosts, replicator) end def start(socket : ::IO, connection_info : ConnectionInfo) io = MQTT::IO.new(socket) - if packet = MQTT::Packet.from_io(socket).as?(MQTT::Connect) + if packet = Packet.from_io(socket).as?(Connect) Log.trace { "recv #{packet.inspect}" } - if user = authenticate(io, packet) - packet = assign_client_id_to_packet(packet) if packet.client_id.empty? - session_present = @broker.session_present?(packet.client_id, packet.clean_session?) - MQTT::Connack.new(session_present, MQTT::Connack::ReturnCode::Accepted).to_io(io) - io.flush - return @broker.connect_client(socket, connection_info, user, @vhost, packet) + if user_and_broker = authenticate(io, packet) + user, broker = user_and_broker + packet = assign_client_id(packet) if packet.client_id.empty? + session_present = broker.session_present?(packet.client_id, packet.clean_session?) + connack io, session_present, Connack::ReturnCode::Accepted + return broker.connect_client(socket, connection_info, user, packet) + else + Log.warn { "Authentication failure for user \"#{packet.username}\"" } + connack io, false, Connack::ReturnCode::NotAuthorized end end rescue ex : MQTT::Error::Connect Log.warn { "Connect error #{ex.inspect}" } if io - MQTT::Connack.new(false, MQTT::Connack::ReturnCode.new(ex.return_code)).to_io(io) + connack io, false, Connack::ReturnCode.new(ex.return_code) end socket.close rescue ex - Log.warn { "Recieved invalid Connect packet" } + Log.warn { "Recieved invalid Connect packet: #{ex.inspect}" } socket.close end + private def connack(io : MQTT::IO, session_present : Bool, return_code : Connack::ReturnCode) + Connack.new(session_present, return_code).to_io(io) + io.flush + end + def authenticate(io, packet) - return nil unless (username = packet.username) && (password = packet.password) + return unless (username = packet.username) && (password = packet.password) + + vhost = "/" + if split_pos = username.index(':') + vhost = username[0, split_pos] + username = username[split_pos + 1..] + end + user = @users[username]? - return user if user && user.password && user.password.try(&.verify(String.new(password))) - Log.warn { "Authentication failure for user \"#{username}\"" } - MQTT::Connack.new(false, MQTT::Connack::ReturnCode::NotAuthorized).to_io(io) - nil + return unless user + return unless user.password && user.password.try(&.verify(String.new(password))) + has_vhost_permissions = user.try &.permissions.has_key?(vhost) + return unless has_vhost_permissions + broker = @brokers[vhost]? + return unless broker + + {user, broker} end - def assign_client_id_to_packet(packet) + def assign_client_id(packet) client_id = Random::DEFAULT.base64(32) - MQTT::Connect.new(client_id, + Connect.new(client_id, packet.clean_session?, packet.keepalive, packet.username, diff --git a/src/lavinmq/mqtt/consts.cr b/src/lavinmq/mqtt/consts.cr new file mode 100644 index 0000000000..ef75104698 --- /dev/null +++ b/src/lavinmq/mqtt/consts.cr @@ -0,0 +1,6 @@ +module LavinMQ + module MQTT + EXCHANGE = "mqtt.default" + QOS_HEADER = "x-mqtt-qos" + end +end diff --git a/src/lavinmq/mqtt/exchange.cr b/src/lavinmq/mqtt/exchange.cr index 9fa07ec5f9..3479a9e4f1 100644 --- a/src/lavinmq/mqtt/exchange.cr +++ b/src/lavinmq/mqtt/exchange.cr @@ -1,4 +1,5 @@ require "../exchange" +require "./consts" require "./subscription_tree" require "./session" require "./retain_store" @@ -58,7 +59,7 @@ module LavinMQ @retain_store.retain(packet.topic, body, bodysize) if packet.retain? body.rewind - msg = Message.new(timestamp, "mqtt.default", packet.topic, properties, bodysize, body) + msg = Message.new(timestamp, EXCHANGE, packet.topic, properties, bodysize, body) count = 0 @tree.each_entry(packet.topic) do |queue, qos| @@ -87,7 +88,7 @@ module LavinMQ end def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool - qos = headers.try { |h| h["x-mqtt-qos"]?.try(&.as(UInt8)) } || 0u8 + qos = headers.try { |h| h[QOS_HEADER]?.try(&.as(UInt8)) } || 0u8 binding_key = BindingKey.new(routing_key, headers) @bindings[binding_key].add destination @tree.subscribe(routing_key, destination, qos) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 1a2eb75fce..b07e1cfd3b 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -1,5 +1,5 @@ require "./topic_tree" -require "digest/sha256" +require "digest/md5" module LavinMQ module MQTT diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 0ba262b9b8..e81f6423b9 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -1,5 +1,6 @@ require "../amqp/queue/queue" require "../error" +require "./consts" module LavinMQ module MQTT @@ -77,12 +78,12 @@ module LavinMQ end def subscribe(tf, qos) - arguments = AMQP::Table.new({"x-mqtt-qos": qos}) + arguments = AMQP::Table.new({QOS_HEADER: qos}) if binding = find_binding(tf) return if binding.binding_key.arguments == arguments unbind(tf, binding.binding_key.arguments) end - @vhost.bind_queue(@name, "mqtt.default", tf, arguments) + @vhost.bind_queue(@name, EXCHANGE, tf, arguments) end def unsubscribe(tf) @@ -96,7 +97,7 @@ module LavinMQ end private def unbind(rk, arguments) - @vhost.unbind_queue(@name, "mqtt.default", rk, arguments || AMQP::Table.new) + @vhost.unbind_queue(@name, EXCHANGE, rk, arguments || AMQP::Table.new) end private def get(no_ack : Bool, & : Envelope -> Nil) : Bool diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 741b2a0dcc..b70dc97d1c 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -26,7 +26,7 @@ module LavinMQ MQTT end - getter vhosts, users, data_dir, parameters, broker + getter vhosts, users, data_dir, parameters getter? closed, flow include ParameterTarget @@ -34,6 +34,7 @@ module LavinMQ @closed = false @flow = true @listeners = Hash(Socket::Server, Protocol).new # Socket => protocol + @connection_factories = Hash(Protocol, ConnectionFactory).new @replicator : Clustering::Replicator Log = LavinMQ::Log.for "server" @@ -43,9 +44,8 @@ module LavinMQ @users = UserStore.new(@data_dir, @replicator) @vhosts = VHostStore.new(@data_dir, @users, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) - @amqp_connection_factory = LavinMQ::AMQP::ConnectionFactory.new - @broker = LavinMQ::MQTT::Broker.new(@vhosts["/"], @replicator) - @mqtt_connection_factory = MQTT::ConnectionFactory.new(@users, @vhosts["/"], @broker) + @connection_factories[Protocol::AMQP] = AMQP::ConnectionFactory.new(@users, @vhosts) + @connection_factories[Protocol::MQTT] = MQTT::ConnectionFactory.new(@users, @vhosts, @replicator) apply_parameter spawn stats_loop, name: "Server#stats_loop" end @@ -69,7 +69,6 @@ module LavinMQ @closed = true @vhosts.close @replicator.clear - @broker.close Fiber.yield end @@ -260,12 +259,7 @@ module LavinMQ end def handle_connection(socket, connection_info, protocol : Protocol) - case protocol - in .amqp? - client = @amqp_connection_factory.start(socket, connection_info, @vhosts, @users) - in .mqtt? - client = @mqtt_connection_factory.start(socket, connection_info) - end + client = @connection_factories[protocol].start(socket, connection_info) ensure socket.close if client.nil? end diff --git a/src/lavinmq/vhost_store.cr b/src/lavinmq/vhost_store.cr index fb52bab78f..32fab952a6 100644 --- a/src/lavinmq/vhost_store.cr +++ b/src/lavinmq/vhost_store.cr @@ -1,10 +1,21 @@ require "json" require "./vhost" require "./user" +require "./observable" module LavinMQ + class VHostStore + enum Event + Added + Deleted + Closed + end + end + class VHostStore include Enumerable({String, VHost}) + include Observable(Event) + Log = LavinMQ::Log.for "vhost_store" def initialize(@data_dir : String, @users : UserStore, @replicator : Clustering::Replicator) @@ -30,14 +41,16 @@ module LavinMQ @users.add_permission(UserStore::DIRECT_USER, name, /.*/, /.*/, /.*/) @vhosts[name] = vhost save! if save + notify_observers(Event::Added, name) vhost end def delete(name) : Nil if vhost = @vhosts.delete name - Log.info { "Deleted vhost #{name}" } @users.rm_vhost_permissions_for_all(name) vhost.delete + notify_observers(Event::Deleted, name) + Log.info { "Deleted vhost #{name}" } save! end end @@ -45,7 +58,10 @@ module LavinMQ def close WaitGroup.wait do |wg| @vhosts.each_value do |vhost| - wg.spawn &->vhost.close + wg.spawn do + vhost.close + notify_observers(Event::Closed, vhost.name) + end end end end From 4134084cb05fab25395667a3357ad293172511bc Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 11:47:43 +0100 Subject: [PATCH 159/202] expand details tuple for consumer UI --- src/lavinmq/mqtt/client.cr | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 0261be4725..590462dc29 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -11,7 +11,7 @@ module LavinMQ include Stats include SortableJSON - getter vhost, channels, log, name, user, client_id, socket + getter vhost, channels, log, name, user, client_id, socket, remote_address, connection_info @channels = Hash(UInt16, Client::Channel).new @session : MQTT::Session? rate_stats({"send_oct", "recv_oct"}) @@ -165,9 +165,19 @@ module LavinMQ def details_tuple { session: { - name: "mqtt.client_id", + name: "mqtt.#{@client.client_id}", vhost: "mqtt", }, + channel_details: { + peer_host: "#{@client.remote_address}", + peer_port: "#{@client.connection_info.src}", + connection_name: "mqtt.#{@client.client_id}", + user: "#{@client.user}", + number: "", + name: "mqtt.#{@client.client_id}", + }, + prefetch_count: prefetch_count, + consumer_tag: "-", } end From 09273050f1729efcab33369fa5672cb3a8b45daa Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 12:00:03 +0100 Subject: [PATCH 160/202] no need to convert routing key --- src/lavinmq/mqtt/client.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 590462dc29..3539641f74 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -203,7 +203,7 @@ module LavinMQ dup: redelivered, qos: qos, retain: retained, - topic: msg.routing_key.tr(".", "/"), + topic: msg.routing_key, } @client.send(::MQTT::Protocol::Publish.new(**pub_args)) # MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response From ec3901279712910c43e9fdeb8fc3814b2a88a7f0 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 12:00:55 +0100 Subject: [PATCH 161/202] format --- src/lavinmq/mqtt/client.cr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 3539641f74..342914c463 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -169,15 +169,15 @@ module LavinMQ vhost: "mqtt", }, channel_details: { - peer_host: "#{@client.remote_address}", - peer_port: "#{@client.connection_info.src}", + peer_host: "#{@client.remote_address}", + peer_port: "#{@client.connection_info.src}", connection_name: "mqtt.#{@client.client_id}", - user: "#{@client.user}", - number: "", - name: "mqtt.#{@client.client_id}", + user: "#{@client.user}", + number: "", + name: "mqtt.#{@client.client_id}", }, prefetch_count: prefetch_count, - consumer_tag: "-", + consumer_tag: "-", } end From a443e5c6e1f919359d391fc2194b80aee70d2c9c Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 15:26:33 +0100 Subject: [PATCH 162/202] handle unexpected close from client --- src/lavinmq/mqtt/client.cr | 21 ++++++++++++++------- src/lavinmq/mqtt/session.cr | 5 ++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 342914c463..3f4cfdc7aa 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -62,7 +62,7 @@ module LavinMQ publish_will if @will ensure @broker.disconnect_client(self) - @socket.close + close_socket end def read_and_handle_packet @@ -151,6 +151,15 @@ module LavinMQ def force_close end + + private def close_socket + socket = @socket + if socket.responds_to?(:"write_timeout=") + socket.write_timeout = 1.seconds + end + socket.close + rescue ::IO::Error + end end class Consumer < LavinMQ::Client::Channel::Consumer @@ -164,9 +173,9 @@ module LavinMQ def details_tuple { - session: { + queue: { name: "mqtt.#{@client.client_id}", - vhost: "mqtt", + vhost: @client.vhost.name, }, channel_details: { peer_host: "#{@client.remote_address}", @@ -177,7 +186,7 @@ module LavinMQ name: "mqtt.#{@client.client_id}", }, prefetch_count: prefetch_count, - consumer_tag: "-", + consumer_tag: @client.client_id, } end @@ -195,18 +204,16 @@ module LavinMQ packet_id = message_id.to_u16 unless message_id.empty? end retained = msg.properties.try &.headers.try &.["x-mqtt-retain"]? == true - qos = msg.properties.delivery_mode || 0u8 pub_args = { packet_id: packet_id, payload: msg.body, - dup: redelivered, + dup: qos.zero? ? false : redelivered, qos: qos, retain: retained, topic: msg.routing_key, } @client.send(::MQTT::Protocol::Publish.new(**pub_args)) - # MQTT::Protocol::PubAck.from_io(io) if pub_args[:qos].positive? && expect_response end def exclusive? diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index e81f6423b9..4e151753d6 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -41,11 +41,14 @@ module LavinMQ consumers.first.deliver(env.message, env.segment_position, env.redelivered) end Fiber.yield if (i &+= 1) % 32768 == 0 + rescue ::IO::Error + rescue ex + @log.error(exception: ex) { "Unexpected error in deliver loop" } end rescue ::Channel::ClosedError return rescue ex - @log.trace(exception: ex) { "deliver loop exiting" } + @log.error(exception: ex) { "deliver loop exited unexpectedly" } end def client=(client : MQTT::Client?) From 9da13606664dc6c00e059665effc9fbf93ca00fd Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 22:11:53 +0100 Subject: [PATCH 163/202] connection_at for mqtt connections --- src/lavinmq/mqtt/client.cr | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 3f4cfdc7aa..2b6ef7a9dd 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -12,6 +12,7 @@ module LavinMQ include SortableJSON getter vhost, channels, log, name, user, client_id, socket, remote_address, connection_info + @connected_at = RoughTime.unix_ms @channels = Hash(UInt16, Client::Channel).new @session : MQTT::Session? rate_stats({"send_oct", "recv_oct"}) @@ -118,10 +119,11 @@ module LavinMQ def details_tuple { - vhost: @broker.vhost.name, - user: @user.name, - protocol: "MQTT", - client_id: @client_id, + vhost: @broker.vhost.name, + user: @user.name, + protocol: "MQTT", + client_id: @client_id, + connected_at: @connected_at, }.merge(stats_details) end From d9cdc82fe8615805574dfd5336e4cf889e63d087 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 22:34:23 +0100 Subject: [PATCH 164/202] deliver packet not msg from session (#843) * deliver packet not msg from session * it's already a topic, no convert needed * fixes --- src/lavinmq/mqtt/client.cr | 19 ++++--------------- src/lavinmq/mqtt/session.cr | 28 ++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 2b6ef7a9dd..fd51753cf0 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -200,22 +200,11 @@ module LavinMQ true end + def deliver(msg : MQTT::Publish) + @client.send(msg) + end + def deliver(msg, sp, redelivered = false, recover = false) - packet_id = nil - if message_id = msg.properties.message_id - packet_id = message_id.to_u16 unless message_id.empty? - end - retained = msg.properties.try &.headers.try &.["x-mqtt-retain"]? == true - qos = msg.properties.delivery_mode || 0u8 - pub_args = { - packet_id: packet_id, - payload: msg.body, - dup: qos.zero? ? false : redelivered, - qos: qos, - retain: retained, - topic: msg.routing_key, - } - @client.send(::MQTT::Protocol::Publish.new(**pub_args)) end def exclusive? diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 4e151753d6..f9949e9187 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -37,8 +37,9 @@ module LavinMQ Channel.receive_first(@msg_store.empty_change, @consumers_empty_change) next end - get(false) do |env| - consumers.first.deliver(env.message, env.segment_position, env.redelivered) + consumer = consumers.first.as(MQTT::Consumer) + get_packet(false) do |pub_packet| + consumer.deliver(pub_packet) end Fiber.yield if (i &+= 1) % 32768 == 0 rescue ::IO::Error @@ -103,15 +104,16 @@ module LavinMQ @vhost.unbind_queue(@name, EXCHANGE, rk, arguments || AMQP::Table.new) end - private def get(no_ack : Bool, & : Envelope -> Nil) : Bool + private def get_packet(no_ack : Bool, & : MQTT::Publish -> Nil) : Bool raise ClosedError.new if @closed loop do env = @msg_store_lock.synchronize { @msg_store.shift? } || break sp = env.segment_position no_ack = env.message.properties.delivery_mode == 0 if no_ack + packet = build_packet(env, nil) begin - yield env + yield packet rescue ex @msg_store_lock.synchronize { @msg_store.requeue(sp) } raise ex @@ -120,9 +122,9 @@ module LavinMQ else id = next_id return false unless id - env.message.properties.message_id = id.to_s + packet = build_packet(env, id) mark_unacked(sp) do - yield env + yield packet @unacked[id] = sp end end @@ -135,6 +137,20 @@ module LavinMQ raise ClosedError.new(cause: ex) end + def build_packet(env, packet_id) : MQTT::Publish + msg = env.message + retained = msg.properties.try &.headers.try &.["x-mqtt-retain"]? == true + qos = msg.properties.delivery_mode || 0u8 + MQTT::Publish.new( + packet_id: packet_id, + payload: msg.body, + dup: env.redelivered, + qos: qos, + retain: retained, + topic: msg.routing_key + ) + end + def ack(packet : MQTT::PubAck) : Nil # TODO: maybe risky to not have lock around this id = packet.packet_id From 188a64497f47afd0a286efe1cc1df228bd85263a Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 15 Nov 2024 10:17:12 +0100 Subject: [PATCH 165/202] truncate the previous content before you retain a message --- src/lavinmq/mqtt/retain_store.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index b07e1cfd3b..b50dc6e16b 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -86,6 +86,7 @@ module LavinMQ add_to_index(topic, msg_file_name) end f = @files[msg_file_name] + f.truncate(0) f.pos = 0 ::IO.copy(body_io, f) @replicator.replace_file(File.join(@dir, msg_file_name)) From c48d5ae1a82af25f63444b09bbf742686239572a Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 15 Nov 2024 11:25:37 +0100 Subject: [PATCH 166/202] safely overwrite retained messages --- src/lavinmq/mqtt/retain_store.cr | 37 ++++++++++++-------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index b50dc6e16b..9ae9c1b3a9 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -13,15 +13,6 @@ module LavinMQ def initialize(@dir : String, @replicator : Clustering::Replicator, @index = IndexTree.new) Dir.mkdir_p @dir - @files = Hash(String, File).new do |files, file_name| - file_path = File.join(@dir, file_name) - unless File.exists?(file_path) - File.open(file_path, "w").close - end - f = files[file_name] = File.new(file_path, "r+") - f.sync = true - f - end @index_file = File.new(File.join(@dir, INDEX_FILE_NAME), "a+") @replicator.register_file(@index_file) @lock = Mutex.new @@ -85,11 +76,14 @@ module LavinMQ msg_file_name = make_file_name(topic) add_to_index(topic, msg_file_name) end - f = @files[msg_file_name] - f.truncate(0) - f.pos = 0 - ::IO.copy(body_io, f) - @replicator.replace_file(File.join(@dir, msg_file_name)) + tmp_file = File.join(@dir, "#{msg_file_name}.tmp") + File.open(tmp_file, "w+") do |f| + f.sync = true + ::IO.copy(body_io, f) + end + File.rename tmp_file, File.join(@dir, msg_file_name) + ensure + FileUtils.rm_rf tmp_file unless tmp_file.nil? end end @@ -119,10 +113,7 @@ module LavinMQ private def delete_from_index(topic : String) : Nil if file_name = @index.delete topic Log.trace { "deleted '#{topic}' from index, deleting file #{file_name}" } - if file = @files.delete(file_name) - file.close - file.delete - end + File.delete? File.join(@dir, file_name) @replicator.delete_file(File.join(@dir, file_name)) end end @@ -137,11 +128,11 @@ module LavinMQ end private def read(file_name : String) : Bytes - f = @files[file_name] - f.pos = 0 - body = Bytes.new(f.size) - f.read_fully(body) - body + File.open(File.join(@dir, file_name), "r") do |f| + body = Bytes.new(f.size) + f.read_fully(body) + body + end end def retained_messages From 9349685a47db1a0a4b70df43a40a7c1f6085cdf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Fri, 15 Nov 2024 11:25:08 +0100 Subject: [PATCH 167/202] Cant use constant as key in NamedTuple --- src/lavinmq/mqtt/session.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index f9949e9187..981de07fa6 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -82,7 +82,8 @@ module LavinMQ end def subscribe(tf, qos) - arguments = AMQP::Table.new({QOS_HEADER: qos}) + arguments = AMQP::Table.new + arguments[QOS_HEADER] = qos if binding = find_binding(tf) return if binding.binding_key.arguments == arguments unbind(tf, binding.binding_key.arguments) From 8d8d6853e12475cd9142664cf397a7c02563e713 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 15 Nov 2024 12:09:08 +0100 Subject: [PATCH 168/202] merge solutions for retain store --- src/lavinmq/mqtt/retain_store.cr | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 9ae9c1b3a9..1db60c3cb2 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -13,6 +13,15 @@ module LavinMQ def initialize(@dir : String, @replicator : Clustering::Replicator, @index = IndexTree.new) Dir.mkdir_p @dir + @files = Hash(String, File).new do |files, file_name| + file_path = File.join(@dir, file_name) + unless File.exists?(file_path) + File.open(file_path, "w").close + end + f = files[file_name] = File.new(file_path, "r+") + f.sync = true + f + end @index_file = File.new(File.join(@dir, INDEX_FILE_NAME), "a+") @replicator.register_file(@index_file) @lock = Mutex.new @@ -76,12 +85,17 @@ module LavinMQ msg_file_name = make_file_name(topic) add_to_index(topic, msg_file_name) end + tmp_file = File.join(@dir, "#{msg_file_name}.tmp") File.open(tmp_file, "w+") do |f| f.sync = true ::IO.copy(body_io, f) end - File.rename tmp_file, File.join(@dir, msg_file_name) + final_file_path = File.join(@dir, msg_file_name) + File.rename(tmp_file, final_file_path) + @files.delete(final_file_path) + @files[final_file_path] = File.new(final_file_path, "r+") + @replicator.replace_file(final_file_path) ensure FileUtils.rm_rf tmp_file unless tmp_file.nil? end @@ -113,7 +127,11 @@ module LavinMQ private def delete_from_index(topic : String) : Nil if file_name = @index.delete topic Log.trace { "deleted '#{topic}' from index, deleting file #{file_name}" } - File.delete? File.join(@dir, file_name) + if file = @files[file_name] + @files.delete(file) + file.close + file.delete + end @replicator.delete_file(File.join(@dir, file_name)) end end From 5b0ea0634b0d240ca3f0c6f0e15485850fc186ee Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 17:45:10 +0100 Subject: [PATCH 169/202] general fixup after comments --- src/lavinmq/amqp/client.cr | 10 +++++----- src/lavinmq/amqp/connection_factory.cr | 3 --- src/lavinmq/config.cr | 4 ++-- src/lavinmq/http/controller/exchanges.cr | 2 +- src/lavinmq/http/controller/queues.cr | 2 +- src/lavinmq/launcher.cr | 2 +- src/lavinmq/mqtt/broker.cr | 16 ++++++++++++++-- src/lavinmq/mqtt/consts.cr | 3 ++- src/lavinmq/mqtt/exchange.cr | 2 +- src/lavinmq/mqtt/session.cr | 2 +- src/lavinmq/server.cr | 2 ++ 11 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index 5b373ba84f..e40e1c9e2d 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -512,13 +512,13 @@ module LavinMQ if !NameValidator.valid_entity_name(frame.exchange_name) send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? - send_access_refused(frame, "Prefix forbidden") + send_access_refused(frame, "Not allowed to declare the default exchange") elsif e = @vhost.exchanges.fetch(frame.exchange_name, nil) redeclare_exchange(e, frame) elsif frame.passive send_not_found(frame, "Exchange '#{frame.exchange_name}' doesn't exists") elsif NameValidator.reserved_prefix?(frame.exchange_name) - send_access_refused(frame, "Prefix forbidden") + send_access_refused(frame, "Prefix #{NameValidator::PREFIX_LIST} forbidden, please choose another name") else ae = frame.arguments["x-alternate-exchange"]?.try &.as?(String) ae_ok = ae.nil? || (@user.can_write?(@vhost.name, ae) && @user.can_read?(@vhost.name, frame.exchange_name)) @@ -549,9 +549,9 @@ module LavinMQ if !NameValidator.valid_entity_name(frame.exchange_name) send_precondition_failed(frame, "Exchange name isn't valid") elsif frame.exchange_name.empty? - send_access_refused(frame, "Prefix forbidden") + send_access_refused(frame, "Prefix #{NameValidator::PREFIX_LIST} forbidden, please choose another name") elsif NameValidator.reserved_prefix?(frame.exchange_name) - send_access_refused(frame, "Prefix forbidden") + send_access_refused(frame, "Prefix #{NameValidator::PREFIX_LIST} forbidden, please choose another name") elsif !@vhost.exchanges.has_key? frame.exchange_name # should return not_found according to spec but we make it idempotent send AMQP::Frame::Exchange::DeleteOk.new(frame.channel) unless frame.no_wait @@ -616,7 +616,7 @@ module LavinMQ elsif frame.passive send_not_found(frame, "Queue '#{frame.queue_name}' doesn't exists") elsif NameValidator.reserved_prefix?(frame.queue_name) - send_access_refused(frame, "Prefix forbidden") + send_access_refused(frame, "Prefix #{NameValidator::PREFIX_LIST} forbidden, please choose another name") elsif @vhost.max_queues.try { |max| @vhost.queues.size >= max } send_access_refused(frame, "queue limit in vhost '#{@vhost.name}' (#{@vhost.max_queues}) is reached") else diff --git a/src/lavinmq/amqp/connection_factory.cr b/src/lavinmq/amqp/connection_factory.cr index 208cd53f1e..b182550db1 100644 --- a/src/lavinmq/amqp/connection_factory.cr +++ b/src/lavinmq/amqp/connection_factory.cr @@ -192,9 +192,6 @@ module LavinMQ return true unless Config.instance.guest_only_loopback? remote_address.loopback? end - - def close - end end end end diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index 814c8e508a..c99388d2a1 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -300,8 +300,8 @@ module LavinMQ when "bind" then @mqtt_bind = v when "port" then @mqtt_port = v.to_i32 when "tls_port" then @mqtts_port = v.to_i32 - when "tls_cert" then @tls_cert_path = v # backward compatibility - when "tls_key" then @tls_key_path = v # backward compatibility + when "tls_cert" then @tls_cert_path = v + when "tls_key" then @tls_key_path = v when "mqtt_unix_path" then @mqtt_unix_path = v when "max_inflight_messages" then @max_inflight_messages = v.to_u16 else diff --git a/src/lavinmq/http/controller/exchanges.cr b/src/lavinmq/http/controller/exchanges.cr index 0da1f06501..1edf4a49f3 100644 --- a/src/lavinmq/http/controller/exchanges.cr +++ b/src/lavinmq/http/controller/exchanges.cr @@ -70,7 +70,7 @@ module LavinMQ end context.response.status_code = 204 elsif NameValidator.reserved_prefix?(name) - bad_request(context, "Prefix forbidden") + bad_request(context, "Prefix #{NameValidator::PREFIX_LIST} forbidden, please choose another name") elsif name.bytesize > UInt8::MAX bad_request(context, "Exchange name too long, can't exceed 255 characters") else diff --git a/src/lavinmq/http/controller/queues.cr b/src/lavinmq/http/controller/queues.cr index 4dcd364542..6880598d87 100644 --- a/src/lavinmq/http/controller/queues.cr +++ b/src/lavinmq/http/controller/queues.cr @@ -74,7 +74,7 @@ module LavinMQ end context.response.status_code = 204 elsif NameValidator.reserved_prefix?(name) - bad_request(context, "Prefix forbidden") + bad_request(context, "Prefix #{NameValidator::PREFIX_LIST} forbidden, please choose another name") elsif name.bytesize > UInt8::MAX bad_request(context, "Queue name too long, can't exceed 255 characters") else diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index b179152d25..2af5579f14 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -192,7 +192,7 @@ module LavinMQ STDOUT.flush @amqp_server.vhosts.each_value do |vhost| vhost.queues.each_value do |q| - if q = (q.as(LavinMQ::AMQP::Queue) || q.as(LavinMQ::MQTT::Session)) + if q = (q.as(LavinMQ::AMQP::Queue) || q.as?(LavinMQ::MQTT::Session)) msg_store = q.@msg_store msg_store.@segments.each_value &.unmap msg_store.@acks.each_value &.unmap diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 6f7eaa350a..0d2ae52a3c 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -11,7 +11,17 @@ module LavinMQ module MQTT class Broker getter vhost, sessions - + # The Broker class acts as an intermediary between the MQTT client and the Vhost & Server, + # It is initialized when starting a connection and it manages a clients connections, + # sessions, and message exchange. + # The broker is responsible for: + # - Handling client connections and disconnections + # - Managing client sessions, including clean and persistent sessions + # - Publishing messages to the exchange + # - Subscribing and unsubscribing clients to/from topics + # - Handling the retain_store + # - Interfacing with the virtual host (vhost) and the exchange to route messages. + # The Broker class helps keep the MQTT Client concise and focused on the protocol. def initialize(@vhost : VHost, @replicator : Clustering::Replicator) @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new @@ -68,8 +78,10 @@ module LavinMQ qos << MQTT::SubAck::ReturnCode.from_int(tf.qos) session.subscribe(tf.topic, tf.qos) @retain_store.each(tf.topic) do |topic, body| + headers = AMQP::Table.new + headers[RETAIN_HEADER] = true msg = Message.new(EXCHANGE, topic, String.new(body), - AMQP::Properties.new(headers: AMQP::Table.new({"x-mqtt-retain": true}), + AMQP::Properties.new(headers: headers, delivery_mode: tf.qos)) session.publish(msg) end diff --git a/src/lavinmq/mqtt/consts.cr b/src/lavinmq/mqtt/consts.cr index ef75104698..fcf7e8f29c 100644 --- a/src/lavinmq/mqtt/consts.cr +++ b/src/lavinmq/mqtt/consts.cr @@ -1,6 +1,7 @@ module LavinMQ module MQTT EXCHANGE = "mqtt.default" - QOS_HEADER = "x-mqtt-qos" + QOS_HEADER = "mqtt.qos" + RETAIN_HEADER = "mqtt.retain" end end diff --git a/src/lavinmq/mqtt/exchange.cr b/src/lavinmq/mqtt/exchange.cr index 3479a9e4f1..29964560a9 100644 --- a/src/lavinmq/mqtt/exchange.cr +++ b/src/lavinmq/mqtt/exchange.cr @@ -44,7 +44,7 @@ module LavinMQ @publish_in_count += 1 headers = AMQP::Table.new.tap do |h| - h["x-mqtt-retain"] = true if packet.retain? + h[RETAIN_HEADER] = true if packet.retain? end properties = AMQP::Properties.new(headers: headers).tap do |p| p.delivery_mode = packet.qos if packet.responds_to?(:qos) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 981de07fa6..6ce48e00ca 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -140,7 +140,7 @@ module LavinMQ def build_packet(env, packet_id) : MQTT::Publish msg = env.message - retained = msg.properties.try &.headers.try &.["x-mqtt-retain"]? == true + retained = msg.properties.try &.headers.try &.["mqtt.retain"]? == true qos = msg.properties.delivery_mode || 0u8 MQTT::Publish.new( packet_id: packet_id, diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index b70dc97d1c..0825de06d9 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -78,6 +78,8 @@ module LavinMQ Schema.migrate(@data_dir, @replicator) @users = UserStore.new(@data_dir, @replicator) @vhosts = VHostStore.new(@data_dir, @users, @replicator) + @connection_factories[Protocol::AMQP] = AMQP::ConnectionFactory.new(@users, @vhosts) + @connection_factories[Protocol::MQTT] = MQTT::ConnectionFactory.new(@users, @vhosts, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) apply_parameter @closed = false From a5cf929863baf6a8b23e65fc152f67ccddb5d1db Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 17:46:41 +0100 Subject: [PATCH 170/202] format --- src/lavinmq/mqtt/broker.cr | 1 + src/lavinmq/mqtt/consts.cr | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 0d2ae52a3c..6644dfa5e8 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -11,6 +11,7 @@ module LavinMQ module MQTT class Broker getter vhost, sessions + # The Broker class acts as an intermediary between the MQTT client and the Vhost & Server, # It is initialized when starting a connection and it manages a clients connections, # sessions, and message exchange. diff --git a/src/lavinmq/mqtt/consts.cr b/src/lavinmq/mqtt/consts.cr index fcf7e8f29c..c51ddfb0c8 100644 --- a/src/lavinmq/mqtt/consts.cr +++ b/src/lavinmq/mqtt/consts.cr @@ -1,7 +1,7 @@ module LavinMQ module MQTT - EXCHANGE = "mqtt.default" - QOS_HEADER = "mqtt.qos" + EXCHANGE = "mqtt.default" + QOS_HEADER = "mqtt.qos" RETAIN_HEADER = "mqtt.retain" end end From ed2ffa69645670a55ca67c4cacf7e18da6f141d0 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 20:11:57 +0100 Subject: [PATCH 171/202] set flaky spec to pending --- spec/clustering_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index 6b8cc118c1..4482730179 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -74,7 +74,7 @@ describe LavinMQ::Clustering::Client do end end - it "replicates and streams retained messages to followers" do + pending "replicates and streams retained messages to followers" do replicator = LavinMQ::Clustering::Server.new(LavinMQ::Config.instance, LavinMQ::Etcd.new, 0) tcp_server = TCPServer.new("localhost", 0) From 406a6e1138f15703960ea60588efe5e46f2e4729 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 20:55:51 +0100 Subject: [PATCH 172/202] just a test --- spec/clustering_spec.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index 4482730179..84c91a8f7e 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -74,7 +74,7 @@ describe LavinMQ::Clustering::Client do end end - pending "replicates and streams retained messages to followers" do + it "replicates and streams retained messages to followers" do replicator = LavinMQ::Clustering::Server.new(LavinMQ::Config.instance, LavinMQ::Etcd.new, 0) tcp_server = TCPServer.new("localhost", 0) @@ -95,7 +95,7 @@ describe LavinMQ::Clustering::Client do retain_store.retain("topic1", msg1.body_io, msg1.bodysize) retain_store.retain("topic2", msg2.body_io, msg2.bodysize) - wait_for { replicator.followers.first?.try &.lag_in_bytes == 0 } + wait_for(10) { replicator.followers.first?.try &.lag_in_bytes == 0 } repli.close done.receive From 922a6164bc265f950a445a2ae4f079dcd77ec901 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 20:59:28 +0100 Subject: [PATCH 173/202] just a test --- spec/clustering_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index 84c91a8f7e..72a1aa6539 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -95,7 +95,7 @@ describe LavinMQ::Clustering::Client do retain_store.retain("topic1", msg1.body_io, msg1.bodysize) retain_store.retain("topic2", msg2.body_io, msg2.bodysize) - wait_for(10) { replicator.followers.first?.try &.lag_in_bytes == 0 } + wait_for(10.seconds) { replicator.followers.first?.try &.lag_in_bytes == 0 } repli.close done.receive From a88b1cb2335e3e803fee8dc9130b8ee7933973ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20B=C3=A4lter?= Date: Tue, 19 Nov 2024 13:33:31 +0100 Subject: [PATCH 174/202] tmp: debug clustering_spec --- spec/clustering_spec.cr | 2 ++ src/lavinmq/clustering/follower.cr | 3 +++ 2 files changed, 5 insertions(+) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index 72a1aa6539..5d5b0257da 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -110,6 +110,8 @@ describe LavinMQ::Clustering::Client do a.sort!.should eq(["topic1", "topic2"]) b.sort!.should eq(["body1", "body2"]) follower_retain_store.retained_messages.should eq(2) + ensure + replicator.try &.close end it "can stream full file" do diff --git a/src/lavinmq/clustering/follower.cr b/src/lavinmq/clustering/follower.cr index 7e72ea6722..a9e3d24680 100644 --- a/src/lavinmq/clustering/follower.cr +++ b/src/lavinmq/clustering/follower.cr @@ -73,8 +73,10 @@ module LavinMQ end private def read_ack(socket = @socket) : Int64 + # Sometimes when running clustering_spec len is greater than sent_bytes. Causing lag_in_bytes to be negative. len = socket.read_bytes(Int64, IO::ByteFormat::LittleEndian) @acked_bytes += len + STDOUT.write "Follower #{@remote_address} read ack of #{len} acked_bytes=#{@acked_bytes} sent_bytes=#{@sent_bytes}\n".to_slice if @closed && lag_in_bytes.zero? @closed_and_in_sync.close end @@ -168,6 +170,7 @@ module LavinMQ private def send_action(action : Action) : Int64 lag_size = action.lag_size @sent_bytes += lag_size + STDOUT.write "Follower #{@remote_address} sent bytes: #{lag_size} acked_bytes=#{@acked_bytes} sent_bytes=#{@sent_bytes}\n".to_slice @actions.send action lag_size end From 796147596bb704c1d320b219b48b1f27f242c402 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 20 Nov 2024 16:41:11 +0100 Subject: [PATCH 175/202] finalize clustering spec --- spec/clustering_spec.cr | 4 +++- src/lavinmq/mqtt/retain_store.cr | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/clustering_spec.cr b/spec/clustering_spec.cr index 5d5b0257da..1c82b80992 100644 --- a/spec/clustering_spec.cr +++ b/spec/clustering_spec.cr @@ -89,13 +89,15 @@ describe LavinMQ::Clustering::Client do wait_for { replicator.followers.size == 1 } retain_store = LavinMQ::MQTT::RetainStore.new("#{LavinMQ::Config.instance.data_dir}/retain_store", replicator) + wait_for { replicator.followers.first?.try &.lag_in_bytes == 0 } + props = LavinMQ::AMQP::Properties.new msg1 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body1")) msg2 = LavinMQ::Message.new(100, "test", "rk", props, 10, IO::Memory.new("body2")) retain_store.retain("topic1", msg1.body_io, msg1.bodysize) retain_store.retain("topic2", msg2.body_io, msg2.bodysize) - wait_for(10.seconds) { replicator.followers.first?.try &.lag_in_bytes == 0 } + wait_for { replicator.followers.first?.try &.lag_in_bytes == 0 } repli.close done.receive diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 1db60c3cb2..e0963368fb 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -121,7 +121,7 @@ module LavinMQ bytes = Bytes.new(topic.bytesize + 1) bytes.copy_from(topic.to_slice) bytes[-1] = 10u8 - @replicator.append(file_name, bytes) + @replicator.append(File.join(@dir, INDEX_FILE_NAME), bytes) end private def delete_from_index(topic : String) : Nil From 94f821bee254bc0d9463d17e6fcd291514a5a169 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 21 Nov 2024 10:25:39 +0100 Subject: [PATCH 176/202] use instance var for index file name --- src/lavinmq/mqtt/retain_store.cr | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index e0963368fb..1642fe68ad 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -22,14 +22,15 @@ module LavinMQ f.sync = true f end - @index_file = File.new(File.join(@dir, INDEX_FILE_NAME), "a+") + @index_file_name = File.join(@dir, INDEX_FILE_NAME) + @index_file = File.new(@index_file_name, "a+") @replicator.register_file(@index_file) @lock = Mutex.new @lock.synchronize do if @index.empty? restore_index(@index, @index_file) write_index - @index_file = File.new(File.join(@dir, INDEX_FILE_NAME), "a+") + @index_file = File.new(@index_file_name, "a+") end end end @@ -108,8 +109,8 @@ module LavinMQ f.puts topic end end - File.rename tmp_file, File.join(@dir, INDEX_FILE_NAME) - @replicator.replace_file(File.join(@dir, INDEX_FILE_NAME)) + File.rename tmp_file, @index_file_name + @replicator.replace_file(@index_file_name) ensure FileUtils.rm_rf tmp_file unless tmp_file.nil? end @@ -121,7 +122,7 @@ module LavinMQ bytes = Bytes.new(topic.bytesize + 1) bytes.copy_from(topic.to_slice) bytes[-1] = 10u8 - @replicator.append(File.join(@dir, INDEX_FILE_NAME), bytes) + @replicator.append(@index_file_name, bytes) end private def delete_from_index(topic : String) : Nil From 45b595096fd90242a62faae4b847cb3810c74549 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 2 Dec 2024 11:46:41 +0100 Subject: [PATCH 177/202] rescue argumenterror in deliver loop --- src/lavinmq/mqtt/broker.cr | 1 - src/lavinmq/mqtt/session.cr | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 6644dfa5e8..0d2ae52a3c 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -11,7 +11,6 @@ module LavinMQ module MQTT class Broker getter vhost, sessions - # The Broker class acts as an intermediary between the MQTT client and the Vhost & Server, # It is initialized when starting a connection and it manages a clients connections, # sessions, and message exchange. diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 6ce48e00ca..d73f2f28b9 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -43,6 +43,7 @@ module LavinMQ end Fiber.yield if (i &+= 1) % 32768 == 0 rescue ::IO::Error + rescue ArgumentError rescue ex @log.error(exception: ex) { "Unexpected error in deliver loop" } end From fbba1915704f72c4962c8c83ce7d1902cb98dfdd Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 2 Dec 2024 15:09:44 +0100 Subject: [PATCH 178/202] format --- src/lavinmq/mqtt/broker.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 0d2ae52a3c..9352046259 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -11,6 +11,7 @@ module LavinMQ module MQTT class Broker getter vhost, sessions + # The Broker class acts as an intermediary between the MQTT client and the Vhost & Server, # It is initialized when starting a connection and it manages a clients connections, # sessions, and message exchange. @@ -22,6 +23,7 @@ module LavinMQ # - Handling the retain_store # - Interfacing with the virtual host (vhost) and the exchange to route messages. # The Broker class helps keep the MQTT Client concise and focused on the protocol. + def initialize(@vhost : VHost, @replicator : Clustering::Replicator) @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new From 4801c32966ba36cbf24b497e0d16b026ec0d03ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 3 Dec 2024 13:02:48 +0100 Subject: [PATCH 179/202] Fix mqtt unix listener (and some logging) --- src/lavinmq/launcher.cr | 12 ++++++------ src/lavinmq/server.cr | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lavinmq/launcher.cr b/src/lavinmq/launcher.cr index 2af5579f14..cc7089df60 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -115,6 +115,10 @@ module LavinMQ end private def listen # ameba:disable Metrics/CyclomaticComplexity + if clustering_bind = @config.clustering_bind + spawn @amqp_server.listen_clustering(clustering_bind, @config.clustering_port), name: "Clustering listener" + end + if @config.amqp_port > 0 spawn @amqp_server.listen(@config.amqp_bind, @config.amqp_port, Server::Protocol::AMQP), name: "AMQP listening on #{@config.amqp_port}" @@ -127,10 +131,6 @@ module LavinMQ end end - if clustering_bind = @config.clustering_bind - spawn @amqp_server.listen_clustering(clustering_bind, @config.clustering_port), name: "Clustering listener" - end - unless @config.unix_path.empty? spawn @amqp_server.listen_unix(@config.unix_path, Server::Protocol::AMQP), name: "AMQP listening at #{@config.unix_path}" end @@ -163,8 +163,8 @@ module LavinMQ name: "MQTTS listening on #{@config.mqtts_port}" end end - unless @config.unix_path.empty? - spawn @amqp_server.listen_unix(@config.mqtt_unix_path, Server::Protocol::MQTT), name: "MQTT listening at #{@config.unix_path}" + unless @config.mqtt_unix_path.empty? + spawn @amqp_server.listen_unix(@config.mqtt_unix_path, Server::Protocol::MQTT), name: "MQTT listening at #{@config.mqtt_unix_path}" end end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 0825de06d9..5e318f0db9 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -92,7 +92,7 @@ module LavinMQ def listen(s : TCPServer, protocol : Protocol) @listeners[s] = protocol - Log.info { "Listening on #{s.local_address}" } + Log.info { "Listening for #{protocol} on #{s.local_address}" } loop do client = s.accept? || break next client.close if @closed @@ -138,7 +138,7 @@ module LavinMQ def listen(s : UNIXServer, protocol : Protocol) @listeners[s] = protocol - Log.info { "Listening on #{s.local_address}" } + Log.info { "Listening for #{protocol} on #{s.local_address}" } loop do # do not try to use while client = s.accept? || break next client.close if @closed @@ -170,7 +170,7 @@ module LavinMQ def listen_tls(s : TCPServer, context, protocol : Protocol) @listeners[s] = protocol - Log.info { "Listening on #{s.local_address} (TLS)" } + Log.info { "Listening for #{protocol} on #{s.local_address} (TLS)" } loop do # do not try to use while client = s.accept? || break next client.close if @closed From 208c20cd3bb4f3aef5909e8558cfda3f20e8fcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Tue, 3 Dec 2024 13:46:12 +0100 Subject: [PATCH 180/202] Prevent Channel::Closed error from being raised --- src/lavinmq/mqtt/session.cr | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index d73f2f28b9..7d75c5a5fd 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -34,7 +34,10 @@ module LavinMQ loop do break if @closed if @msg_store.empty? || @consumers.empty? - Channel.receive_first(@msg_store.empty_change, @consumers_empty_change) + select + when @msg_store.empty_change.receive? + when @consumers_empty_change.receive? + end next end consumer = consumers.first.as(MQTT::Consumer) From 1f179576a5a94ea1c4aa68f713b37ee409a56210 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 10 Dec 2024 13:03:31 +0100 Subject: [PATCH 181/202] exception handling for mqtt default bindings --- src/lavinmq/http/controller/bindings.cr | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lavinmq/http/controller/bindings.cr b/src/lavinmq/http/controller/bindings.cr index 726f1937f5..8a1ae12a37 100644 --- a/src/lavinmq/http/controller/bindings.cr +++ b/src/lavinmq/http/controller/bindings.cr @@ -50,6 +50,10 @@ module LavinMQ user = user(context) if !user.can_read?(vhost, e.name) access_refused(context, "User doesn't have read permissions to exchange '#{e.name}'") + elsif q.is_a?(LavinMQ::MQTT::Session) + access_refused(context, "Not allowed to bind to an MQTT session") + elsif e.is_a?(LavinMQ::MQTT::Exchange) + access_refused(context, "Not allowed to bind to the default MQTT exchange") elsif !user.can_write?(vhost, q.name) access_refused(context, "User doesn't have write permissions to queue '#{q.name}'") elsif e.name.empty? @@ -132,6 +136,8 @@ module LavinMQ user = user(context) if !user.can_read?(vhost, source.name) access_refused(context, "User doesn't have read permissions to exchange '#{source.name}'") + elsif destination.is_a?(LavinMQ::MQTT::Exchange) || source.is_a?(LavinMQ::MQTT::Exchange) + access_refused(context, "Not allowed to bind to the default MQTT exchange") elsif !user.can_write?(vhost, destination.name) access_refused(context, "User doesn't have write permissions to exchange '#{destination.name}'") elsif source.name.empty? || destination.name.empty? From 96d7df07b6416ddfd846ba480231b92270f2ace5 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 10 Dec 2024 13:04:11 +0100 Subject: [PATCH 182/202] only allow selected policies to be applied for mqtt session --- src/lavinmq/mqtt/exchange.cr | 9 +++++++++ src/lavinmq/mqtt/session.cr | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/lavinmq/mqtt/exchange.cr b/src/lavinmq/mqtt/exchange.cr index 29964560a9..b2e9a0eaa2 100644 --- a/src/lavinmq/mqtt/exchange.cr +++ b/src/lavinmq/mqtt/exchange.cr @@ -120,6 +120,15 @@ module LavinMQ def unbind(destination : Destination, routing_key, headers = nil) : Bool raise LavinMQ::Exchange::AccessRefused.new(self) end + + def apply_policy(policy : Policy?, operator_policy : OperatorPolicy?) + end + + def clear_policy + end + + def handle_arguments + end end end end diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 7d75c5a5fd..4be18a288f 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -156,6 +156,29 @@ module LavinMQ ) end + def apply_policy(policy : Policy?, operator_policy : OperatorPolicy?) # ameba:disable Metrics/CyclomaticComplexity + clear_policy + Policy.merge_definitions(policy, operator_policy).each do |k, v| + @log.debug { "Applying policy #{k}: #{v}" } + case k + when "max-length" + unless @max_length.try &.< v.as_i64 + @max_length = v.as_i64 + drop_overflow + end + when "max-length-bytes" + unless @max_length_bytes.try &.< v.as_i64 + @max_length_bytes = v.as_i64 + drop_overflow + end + when "overflow" + @reject_on_overflow ||= v.as_s == "reject-publish" + end + end + @policy = policy + @operator_policy = operator_policy + end + def ack(packet : MQTT::PubAck) : Nil # TODO: maybe risky to not have lock around this id = packet.packet_id From 028b7311c8a653a2fa27dbc0d1d331cf07bb14c4 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 10 Dec 2024 16:36:56 +0100 Subject: [PATCH 183/202] handle qos 2 at build_packet --- src/lavinmq/mqtt/session.cr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 4be18a288f..7961ea988e 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -145,7 +145,13 @@ module LavinMQ def build_packet(env, packet_id) : MQTT::Publish msg = env.message retained = msg.properties.try &.headers.try &.["mqtt.retain"]? == true - qos = msg.properties.delivery_mode || 0u8 + + qos = case msg.properties.delivery_mode + when 2u8 + 1u8 + else + msg.properties.delivery_mode || 0u8 + end MQTT::Publish.new( packet_id: packet_id, payload: msg.body, From ae2e070b934444268374713d7dfa81ad650f065d Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 11 Dec 2024 14:25:25 +0100 Subject: [PATCH 184/202] dont allow amqp queues or exchanges to bind to mqtt queues or exchanges --- src/lavinmq/amqp/client.cr | 8 ++++++-- src/lavinmq/clustering/follower.cr | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index e40e1c9e2d..51a0590736 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -690,6 +690,10 @@ module LavinMQ send_access_refused(frame, "User doesn't have read permissions to exchange '#{frame.exchange_name}'") elsif !@user.can_write?(@vhost.name, frame.queue_name) send_access_refused(frame, "User doesn't have write permissions to queue '#{frame.queue_name}'") + elsif q.is_a?(LavinMQ::MQTT::Session) + send_access_refused(frame, "Not allowed to bind to an MQTT Session") + elsif @vhost.exchanges[frame.exchange_name].is_a?(LavinMQ::MQTT::Exchange) + send_access_refused(frame, "Not allowed to bind to an MQTT Exchange") elsif queue_exclusive_to_other_client?(q) send_resource_locked(frame, "Exclusive queue") else @@ -753,8 +757,8 @@ module LavinMQ send_access_refused(frame, "User doesn't have read permissions to exchange '#{frame.source}'") elsif !@user.can_write?(@vhost.name, frame.destination) send_access_refused(frame, "User doesn't have write permissions to exchange '#{frame.destination}'") - elsif frame.source.empty? || frame.destination.empty? - send_access_refused(frame, "Not allowed to bind to the default exchange") + elsif source.is_a?(LavinMQ::MQTT::Exchange) || destination.is_a?(LavinMQ::MQTT::Exchange) + send_access_refused(frame, "Not allowed to bind to an MQTT Exchange") else @vhost.apply(frame) send AMQP::Frame::Exchange::BindOk.new(frame.channel) unless frame.no_wait diff --git a/src/lavinmq/clustering/follower.cr b/src/lavinmq/clustering/follower.cr index a9e3d24680..71047742df 100644 --- a/src/lavinmq/clustering/follower.cr +++ b/src/lavinmq/clustering/follower.cr @@ -170,7 +170,6 @@ module LavinMQ private def send_action(action : Action) : Int64 lag_size = action.lag_size @sent_bytes += lag_size - STDOUT.write "Follower #{@remote_address} sent bytes: #{lag_size} acked_bytes=#{@acked_bytes} sent_bytes=#{@sent_bytes}\n".to_slice @actions.send action lag_size end From bd562adb0cfb7b4d3eef75244ade49933d804c4f Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Dec 2024 15:28:02 +0100 Subject: [PATCH 185/202] forbidden to bind AMQP excahnges to the MQTT Session --- src/lavinmq/amqp/client.cr | 8 ++------ src/lavinmq/exchange/direct.cr | 4 ++++ src/lavinmq/exchange/exchange.cr | 1 + src/lavinmq/exchange/fanout.cr | 4 ++++ src/lavinmq/exchange/headers.cr | 4 ++++ src/lavinmq/exchange/topic.cr | 4 ++++ src/lavinmq/http/controller/bindings.cr | 6 ------ 7 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index 51a0590736..ba9a58b2d7 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -690,10 +690,6 @@ module LavinMQ send_access_refused(frame, "User doesn't have read permissions to exchange '#{frame.exchange_name}'") elsif !@user.can_write?(@vhost.name, frame.queue_name) send_access_refused(frame, "User doesn't have write permissions to queue '#{frame.queue_name}'") - elsif q.is_a?(LavinMQ::MQTT::Session) - send_access_refused(frame, "Not allowed to bind to an MQTT Session") - elsif @vhost.exchanges[frame.exchange_name].is_a?(LavinMQ::MQTT::Exchange) - send_access_refused(frame, "Not allowed to bind to an MQTT Exchange") elsif queue_exclusive_to_other_client?(q) send_resource_locked(frame, "Exclusive queue") else @@ -757,8 +753,8 @@ module LavinMQ send_access_refused(frame, "User doesn't have read permissions to exchange '#{frame.source}'") elsif !@user.can_write?(@vhost.name, frame.destination) send_access_refused(frame, "User doesn't have write permissions to exchange '#{frame.destination}'") - elsif source.is_a?(LavinMQ::MQTT::Exchange) || destination.is_a?(LavinMQ::MQTT::Exchange) - send_access_refused(frame, "Not allowed to bind to an MQTT Exchange") + # elsif source.is_a?(LavinMQ::MQTT::Exchange) || destination.is_a?(LavinMQ::MQTT::Exchange) + # send_access_refused(frame, "Not allowed to bind to an MQTT Exchange") else @vhost.apply(frame) send AMQP::Frame::Exchange::BindOk.new(frame.channel) unless frame.no_wait diff --git a/src/lavinmq/exchange/direct.cr b/src/lavinmq/exchange/direct.cr index af559859f1..4cbc96d8a5 100644 --- a/src/lavinmq/exchange/direct.cr +++ b/src/lavinmq/exchange/direct.cr @@ -27,6 +27,10 @@ module LavinMQ true end + def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end + def unbind(destination : Destination, routing_key, headers = nil) : Bool rk_bindings = @bindings[routing_key] return false unless rk_bindings.delete destination diff --git a/src/lavinmq/exchange/exchange.cr b/src/lavinmq/exchange/exchange.cr index 0a271b0395..1ddb5a0e86 100644 --- a/src/lavinmq/exchange/exchange.cr +++ b/src/lavinmq/exchange/exchange.cr @@ -5,6 +5,7 @@ require "../sortable_json" require "../observable" require "./event" require "../amqp/queue" +require "../mqtt/session" module LavinMQ alias Destination = Queue | Exchange diff --git a/src/lavinmq/exchange/fanout.cr b/src/lavinmq/exchange/fanout.cr index 623b59a2a3..14a3729f61 100644 --- a/src/lavinmq/exchange/fanout.cr +++ b/src/lavinmq/exchange/fanout.cr @@ -23,6 +23,10 @@ module LavinMQ true end + def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end + def unbind(destination : Destination, routing_key, headers = nil) return false unless @bindings.delete destination binding_key = BindingKey.new("") diff --git a/src/lavinmq/exchange/headers.cr b/src/lavinmq/exchange/headers.cr index 98165aa930..bd2e358b58 100644 --- a/src/lavinmq/exchange/headers.cr +++ b/src/lavinmq/exchange/headers.cr @@ -36,6 +36,10 @@ module LavinMQ true end + def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end + def unbind(destination : Destination, routing_key, headers) args = headers ? @arguments.clone.merge!(headers) : @arguments bds = @bindings[args] diff --git a/src/lavinmq/exchange/topic.cr b/src/lavinmq/exchange/topic.cr index 117a782143..ad0f67863c 100644 --- a/src/lavinmq/exchange/topic.cr +++ b/src/lavinmq/exchange/topic.cr @@ -27,6 +27,10 @@ module LavinMQ true end + def bind(destination : MQTT::Session, routing_key : String, headers = nil) : Bool + raise LavinMQ::Exchange::AccessRefused.new(self) + end + def unbind(destination : Destination, routing_key, headers = nil) rks = routing_key.split(".") bds = @bindings[routing_key.split(".")] diff --git a/src/lavinmq/http/controller/bindings.cr b/src/lavinmq/http/controller/bindings.cr index 8a1ae12a37..726f1937f5 100644 --- a/src/lavinmq/http/controller/bindings.cr +++ b/src/lavinmq/http/controller/bindings.cr @@ -50,10 +50,6 @@ module LavinMQ user = user(context) if !user.can_read?(vhost, e.name) access_refused(context, "User doesn't have read permissions to exchange '#{e.name}'") - elsif q.is_a?(LavinMQ::MQTT::Session) - access_refused(context, "Not allowed to bind to an MQTT session") - elsif e.is_a?(LavinMQ::MQTT::Exchange) - access_refused(context, "Not allowed to bind to the default MQTT exchange") elsif !user.can_write?(vhost, q.name) access_refused(context, "User doesn't have write permissions to queue '#{q.name}'") elsif e.name.empty? @@ -136,8 +132,6 @@ module LavinMQ user = user(context) if !user.can_read?(vhost, source.name) access_refused(context, "User doesn't have read permissions to exchange '#{source.name}'") - elsif destination.is_a?(LavinMQ::MQTT::Exchange) || source.is_a?(LavinMQ::MQTT::Exchange) - access_refused(context, "Not allowed to bind to the default MQTT exchange") elsif !user.can_write?(vhost, destination.name) access_refused(context, "User doesn't have write permissions to exchange '#{destination.name}'") elsif source.name.empty? || destination.name.empty? From 7795ee40a50165f60b99ec227a2c519311820408 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Dec 2024 15:43:43 +0100 Subject: [PATCH 186/202] format --- src/lavinmq/amqp/client.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/amqp/client.cr b/src/lavinmq/amqp/client.cr index ba9a58b2d7..c8e083a082 100644 --- a/src/lavinmq/amqp/client.cr +++ b/src/lavinmq/amqp/client.cr @@ -753,8 +753,8 @@ module LavinMQ send_access_refused(frame, "User doesn't have read permissions to exchange '#{frame.source}'") elsif !@user.can_write?(@vhost.name, frame.destination) send_access_refused(frame, "User doesn't have write permissions to exchange '#{frame.destination}'") - # elsif source.is_a?(LavinMQ::MQTT::Exchange) || destination.is_a?(LavinMQ::MQTT::Exchange) - # send_access_refused(frame, "Not allowed to bind to an MQTT Exchange") + # elsif source.is_a?(LavinMQ::MQTT::Exchange) || destination.is_a?(LavinMQ::MQTT::Exchange) + # send_access_refused(frame, "Not allowed to bind to an MQTT Exchange") else @vhost.apply(frame) send AMQP::Frame::Exchange::BindOk.new(frame.channel) unless frame.no_wait From a86e57d1bafe798823719da1f284e2ab45808888 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 13 Dec 2024 11:56:43 +0100 Subject: [PATCH 187/202] clean up review comments --- src/lavinmq/clustering/follower.cr | 2 -- src/lavinmq/config.cr | 18 +++++++++--------- src/lavinmq/mqtt/client.cr | 2 +- src/lavinmq/mqtt/retain_store.cr | 5 ++--- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/lavinmq/clustering/follower.cr b/src/lavinmq/clustering/follower.cr index 71047742df..7e72ea6722 100644 --- a/src/lavinmq/clustering/follower.cr +++ b/src/lavinmq/clustering/follower.cr @@ -73,10 +73,8 @@ module LavinMQ end private def read_ack(socket = @socket) : Int64 - # Sometimes when running clustering_spec len is greater than sent_bytes. Causing lag_in_bytes to be negative. len = socket.read_bytes(Int64, IO::ByteFormat::LittleEndian) @acked_bytes += len - STDOUT.write "Follower #{@remote_address} read ack of #{len} acked_bytes=#{@acked_bytes} sent_bytes=#{@sent_bytes}\n".to_slice if @closed && lag_in_bytes.zero? @closed_and_in_sync.close end diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index c99388d2a1..152290557d 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -34,16 +34,16 @@ module LavinMQ property http_unix_path = "" property http_systemd_socket_name = "lavinmq-http.socket" property amqp_systemd_socket_name = "lavinmq-amqp.socket" - property heartbeat = 300_u16 # second - property frame_max = 131_072_u32 # bytes - property channel_max = 2048_u16 # number - property stats_interval = 5000 # millisecond - property stats_log_size = 120 # 10 mins at 5s interval - property? set_timestamp = false # in message headers when receive - property socket_buffer_size = 16384 # bytes - property? tcp_nodelay = false # bool - property max_inflight_messages : UInt16 = 65_535 + property heartbeat = 300_u16 # second + property frame_max = 131_072_u32 # bytes + property channel_max = 2048_u16 # number + property stats_interval = 5000 # millisecond + property stats_log_size = 120 # 10 mins at 5s interval + property? set_timestamp = false # in message headers when receive + property socket_buffer_size = 16384 # bytes + property? tcp_nodelay = false # bool property segment_size : Int32 = 8 * 1024**2 # bytes + property max_inflight_messages : UInt16 = 65_535 property? raise_gc_warn : Bool = false property? data_dir_lock : Bool = true property tcp_keepalive : Tuple(Int32, Int32, Int32)? = {60, 10, 3} # idle, interval, probes/count diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index fd51753cf0..4515a08776 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -146,7 +146,7 @@ module LavinMQ end def close(reason = "") - @log.trace { "Client#close" } + @log.debug { "Client#close" } @closed = true @socket.close end diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index 1642fe68ad..e4e0db3225 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -143,7 +143,6 @@ module LavinMQ block.call(topic, read(file_name)) end end - nil end private def read(file_name : String) : Bytes @@ -164,9 +163,9 @@ module LavinMQ def make_file_name(topic : String) : String @hasher.update topic.to_slice - hash = @hasher.hexfinal + "#{@hasher.hexfinal}#{MESSAGE_FILE_SUFFIX}" + ensure @hasher.reset - "#{hash}#{MESSAGE_FILE_SUFFIX}" end end end From e008a7cac99463c5179ea1022ec80193546d4d6c Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 13 Dec 2024 12:53:26 +0100 Subject: [PATCH 188/202] initialize @broekrs in server instead of in connection_factory --- src/lavinmq/mqtt/connection_factory.cr | 2 +- src/lavinmq/server.cr | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 12cc0d4662..4d0c41c82b 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -11,8 +11,8 @@ module LavinMQ class ConnectionFactory < LavinMQ::ConnectionFactory def initialize(@users : UserStore, @vhosts : VHostStore, + @brokers : Brokers, replicator : Clustering::Replicator) - @brokers = Brokers.new(@vhosts, replicator) end def start(socket : ::IO, connection_info : ConnectionInfo) diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 5e318f0db9..4c5bf6b8e1 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -43,9 +43,10 @@ module LavinMQ Schema.migrate(@data_dir, @replicator) @users = UserStore.new(@data_dir, @replicator) @vhosts = VHostStore.new(@data_dir, @users, @replicator) + @brokers = MQTT::Brokers.new(@vhosts, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) @connection_factories[Protocol::AMQP] = AMQP::ConnectionFactory.new(@users, @vhosts) - @connection_factories[Protocol::MQTT] = MQTT::ConnectionFactory.new(@users, @vhosts, @replicator) + @connection_factories[Protocol::MQTT] = MQTT::ConnectionFactory.new(@users, @vhosts, @brokers, @replicator) apply_parameter spawn stats_loop, name: "Server#stats_loop" end From e82da3042a3defd66af4b4a6e661b55f90ac7a35 Mon Sep 17 00:00:00 2001 From: Viktor Erlingsson Date: Mon, 25 Nov 2024 14:53:10 +0100 Subject: [PATCH 189/202] version bump --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d5a667324..01a0bf5a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [2.0.2] - 2024-11-25 ### Fixed From 5b8332a06b7ea6f47824554cca83a41373d86277 Mon Sep 17 00:00:00 2001 From: Viktor Erlingsson Date: Wed, 4 Dec 2024 13:54:29 +0100 Subject: [PATCH 190/202] Rescue IndexError in first? and move on to next segment (#864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a msg file is a few bytes larger than expected, first? will throw an IndexError and close the queue. This changes that behavior to instead rescue that IndexError and try to move on to the next segment file. Same fix as for shift? in #671 fixes #669 (again 🙂) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a0bf5a08..e7d77f74c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Queues will no longer be closed if file size is incorrect. Fixes [#669](https://github.com/cloudamqp/lavinmq/issues/669) + ## [2.0.2] - 2024-11-25 ### Fixed From 6bed3c7c3a3fcf726bad4a951c5b23288cf2d662 Mon Sep 17 00:00:00 2001 From: Viktor Erlingsson Date: Thu, 5 Dec 2024 09:52:45 +0100 Subject: [PATCH 191/202] java-client dont redeclare exchange in spec (#860) Don't redeclare exchange in test/java/com/rabbitmq/client/test/functional/Tables.java. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d77f74c2..5b05ff66ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Queues will no longer be closed if file size is incorrect. Fixes [#669](https://github.com/cloudamqp/lavinmq/issues/669) +- Dont redeclare exchange in java client test [#860](https://github.com/cloudamqp/lavinmq/pull/860) ## [2.0.2] - 2024-11-25 From 01bdfb5ef8a07b8e5f117589ffa26a3f2ac6453e Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 24 Oct 2024 09:58:51 +0200 Subject: [PATCH 192/202] fetch max_inflight_messages form config --- src/lavinmq/config.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lavinmq/config.cr b/src/lavinmq/config.cr index 152290557d..6b4f6258e4 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -42,6 +42,7 @@ module LavinMQ property? set_timestamp = false # in message headers when receive property socket_buffer_size = 16384 # bytes property? tcp_nodelay = false # bool + property max_inflight_messages : UInt16 = 65_535 property segment_size : Int32 = 8 * 1024**2 # bytes property max_inflight_messages : UInt16 = 65_535 property? raise_gc_warn : Bool = false From b8701bdeb2d22bed5bdd11958a36ee8646a9fa25 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 16 Dec 2024 16:26:25 +0100 Subject: [PATCH 193/202] clean up for review comments --- src/lavinmq/mqtt/broker.cr | 17 +++++----- src/lavinmq/mqtt/connection_factory.cr | 4 +-- src/lavinmq/mqtt/exchange.cr | 18 +++++------ src/lavinmq/mqtt/retain_store.cr | 43 +++++++++----------------- src/lavinmq/server.cr | 8 +++-- 5 files changed, 38 insertions(+), 52 deletions(-) diff --git a/src/lavinmq/mqtt/broker.cr b/src/lavinmq/mqtt/broker.cr index 9352046259..611107e84b 100644 --- a/src/lavinmq/mqtt/broker.cr +++ b/src/lavinmq/mqtt/broker.cr @@ -12,22 +12,21 @@ module LavinMQ class Broker getter vhost, sessions - # The Broker class acts as an intermediary between the MQTT client and the Vhost & Server, - # It is initialized when starting a connection and it manages a clients connections, - # sessions, and message exchange. - # The broker is responsible for: + # The `Broker` class acts as an intermediary between the `Server` and MQTT connections. + # It is initialized by the `Server` and manages client connections, sessions, and message exchange. + # Responsibilities include: # - Handling client connections and disconnections - # - Managing client sessions, including clean and persistent sessions + # - Managing client sessions (clean and persistent) # - Publishing messages to the exchange # - Subscribing and unsubscribing clients to/from topics - # - Handling the retain_store - # - Interfacing with the virtual host (vhost) and the exchange to route messages. - # The Broker class helps keep the MQTT Client concise and focused on the protocol. + # - Handling the retain store + # - Interfacing with the virtual host (vhost) and the exchange to route messages + # The `Broker` class helps keep the MQTT client concise and focused on the protocol. def initialize(@vhost : VHost, @replicator : Clustering::Replicator) @sessions = Sessions.new(@vhost) @clients = Hash(String, Client).new - @retain_store = RetainStore.new(Path[@vhost.data_dir].join("mqtt_reatined_store").to_s, @replicator) + @retain_store = RetainStore.new(File.join(@vhost.data_dir, "mqtt_reatined_store"), @replicator) @exchange = MQTT::Exchange.new(@vhost, EXCHANGE, @retain_store) @vhost.exchanges[EXCHANGE] = @exchange end diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index 4d0c41c82b..cf3067e14c 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -10,9 +10,7 @@ module LavinMQ module MQTT class ConnectionFactory < LavinMQ::ConnectionFactory def initialize(@users : UserStore, - @vhosts : VHostStore, - @brokers : Brokers, - replicator : Clustering::Replicator) + @brokers : Brokers) end def start(socket : ::IO, connection_info : ConnectionInfo) diff --git a/src/lavinmq/mqtt/exchange.cr b/src/lavinmq/mqtt/exchange.cr index b2e9a0eaa2..c79641425f 100644 --- a/src/lavinmq/mqtt/exchange.cr +++ b/src/lavinmq/mqtt/exchange.cr @@ -43,12 +43,10 @@ module LavinMQ def publish(packet : MQTT::Publish) : Int32 @publish_in_count += 1 - headers = AMQP::Table.new.tap do |h| - h[RETAIN_HEADER] = true if packet.retain? - end - properties = AMQP::Properties.new(headers: headers).tap do |p| - p.delivery_mode = packet.qos if packet.responds_to?(:qos) - end + headers = AMQP::Table.new + headers[RETAIN_HEADER] = true if packet.retain? + properties = AMQP::Properties.new(headers: headers) + properties.delivery_mode = packet.qos if packet.responds_to?(:qos) timestamp = RoughTime.unix_ms bodysize = packet.payload.size.to_u64 @@ -56,9 +54,11 @@ module LavinMQ body.write(packet.payload) body.rewind - @retain_store.retain(packet.topic, body, bodysize) if packet.retain? + if packet.retain? + @retain_store.retain(packet.topic, body, bodysize) + body.rewind + end - body.rewind msg = Message.new(timestamp, EXCHANGE, packet.topic, properties, bodysize, body) count = 0 @@ -66,7 +66,7 @@ module LavinMQ msg.properties.delivery_mode = qos if queue.publish(msg) count += 1 - msg.body_io.seek(-msg.bodysize.to_i64, ::IO::Seek::Current) # rewind + msg.body_io.rewind end end @unroutable_count += 1 if count.zero? diff --git a/src/lavinmq/mqtt/retain_store.cr b/src/lavinmq/mqtt/retain_store.cr index e4e0db3225..a4d73214d0 100644 --- a/src/lavinmq/mqtt/retain_store.cr +++ b/src/lavinmq/mqtt/retain_store.cr @@ -14,24 +14,16 @@ module LavinMQ def initialize(@dir : String, @replicator : Clustering::Replicator, @index = IndexTree.new) Dir.mkdir_p @dir @files = Hash(String, File).new do |files, file_name| - file_path = File.join(@dir, file_name) - unless File.exists?(file_path) - File.open(file_path, "w").close - end - f = files[file_name] = File.new(file_path, "r+") - f.sync = true - f + files[file_name] = File.open(File.join(@dir, file_name), "W").tap &.sync = true end @index_file_name = File.join(@dir, INDEX_FILE_NAME) @index_file = File.new(@index_file_name, "a+") @replicator.register_file(@index_file) @lock = Mutex.new - @lock.synchronize do - if @index.empty? - restore_index(@index, @index_file) - write_index - @index_file = File.new(@index_file_name, "a+") - end + if @index.empty? + restore_index(@index, @index_file) + write_index + @index_file = File.new(@index_file_name, "a+") end end @@ -44,7 +36,7 @@ module LavinMQ private def restore_index(index : IndexTree, index_file : ::IO) Log.info { "restoring index" } dir = @dir - msg_count = 0 + msg_count = 0u64 msg_file_segments = Set(String).new( Dir[Path[dir, "*#{MESSAGE_FILE_SUFFIX}"]].compact_map do |fname| File.basename(fname) @@ -95,7 +87,7 @@ module LavinMQ final_file_path = File.join(@dir, msg_file_name) File.rename(tmp_file, final_file_path) @files.delete(final_file_path) - @files[final_file_path] = File.new(final_file_path, "r+") + @files[msg_file_name] = File.new(final_file_path, "r+") @replicator.replace_file(final_file_path) ensure FileUtils.rm_rf tmp_file unless tmp_file.nil? @@ -103,33 +95,28 @@ module LavinMQ end private def write_index - tmp_file = File.join(@dir, "#{INDEX_FILE_NAME}.next") - File.open(tmp_file, "w+") do |f| - @index.each do |topic, _filename| + File.open("#{@index_file_name}.tmp", "w") do |f| + @index.each do |topic| f.puts topic end + f.rename @index_file_name end - File.rename tmp_file, @index_file_name @replicator.replace_file(@index_file_name) - ensure - FileUtils.rm_rf tmp_file unless tmp_file.nil? + rescue ex + FileUtils.rm_rf File.join(@dir, "#{INDEX_FILE_NAME}.tmp") + raise ex end private def add_to_index(topic : String, file_name : String) : Nil @index.insert topic, file_name @index_file.puts topic - @index_file.flush - bytes = Bytes.new(topic.bytesize + 1) - bytes.copy_from(topic.to_slice) - bytes[-1] = 10u8 - @replicator.append(@index_file_name, bytes) + @replicator.append(@index_file_name, "#{topic}\n".to_slice) end private def delete_from_index(topic : String) : Nil if file_name = @index.delete topic Log.trace { "deleted '#{topic}' from index, deleting file #{file_name}" } - if file = @files[file_name] - @files.delete(file) + if file = @files.delete(file_name) file.close file.delete end diff --git a/src/lavinmq/server.cr b/src/lavinmq/server.cr index 4c5bf6b8e1..8d2bafb2fc 100644 --- a/src/lavinmq/server.cr +++ b/src/lavinmq/server.cr @@ -45,8 +45,10 @@ module LavinMQ @vhosts = VHostStore.new(@data_dir, @users, @replicator) @brokers = MQTT::Brokers.new(@vhosts, @replicator) @parameters = ParameterStore(Parameter).new(@data_dir, "parameters.json", @replicator) - @connection_factories[Protocol::AMQP] = AMQP::ConnectionFactory.new(@users, @vhosts) - @connection_factories[Protocol::MQTT] = MQTT::ConnectionFactory.new(@users, @vhosts, @brokers, @replicator) + @connection_factories = { + Protocol::AMQP => AMQP::ConnectionFactory.new(@users, @vhosts), + Protocol::MQTT => MQTT::ConnectionFactory.new(@users, @brokers) + } apply_parameter spawn stats_loop, name: "Server#stats_loop" end @@ -57,7 +59,7 @@ module LavinMQ def amqp_url addr = @listeners - .select { |k, v| k.is_a?(TCPServer) && v.amqp? } + .select { |k, v| v.amqp? } .keys .select(TCPServer) .first From 6cd95610647488a9e7b56f5eb9756a9a9928085f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20B=C3=B6rjesson?= Date: Wed, 18 Dec 2024 09:31:02 +0100 Subject: [PATCH 194/202] Fix some logging --- src/lavinmq/mqtt/client.cr | 2 +- src/lavinmq/mqtt/connection_factory.cr | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lavinmq/mqtt/client.cr b/src/lavinmq/mqtt/client.cr index 4515a08776..1da92ac450 100644 --- a/src/lavinmq/mqtt/client.cr +++ b/src/lavinmq/mqtt/client.cr @@ -16,7 +16,7 @@ module LavinMQ @channels = Hash(UInt16, Client::Channel).new @session : MQTT::Session? rate_stats({"send_oct", "recv_oct"}) - Log = ::Log.for "mqtt.client" + Log = LavinMQ::Log.for "mqtt.client" def initialize(@socket : ::IO, @connection_info : ConnectionInfo, diff --git a/src/lavinmq/mqtt/connection_factory.cr b/src/lavinmq/mqtt/connection_factory.cr index cf3067e14c..4e2777b3dd 100644 --- a/src/lavinmq/mqtt/connection_factory.cr +++ b/src/lavinmq/mqtt/connection_factory.cr @@ -9,14 +9,19 @@ require "../client/connection_factory" module LavinMQ module MQTT class ConnectionFactory < LavinMQ::ConnectionFactory + Log = LavinMQ::Log.for "mqtt.connection_factory" + def initialize(@users : UserStore, @brokers : Brokers) end def start(socket : ::IO, connection_info : ConnectionInfo) + remote_address = connection_info.src + metadata = ::Log::Metadata.build({address: remote_address.to_s}) + logger = Logger.new(Log, metadata) io = MQTT::IO.new(socket) if packet = Packet.from_io(socket).as?(Connect) - Log.trace { "recv #{packet.inspect}" } + logger.trace { "recv #{packet.inspect}" } if user_and_broker = authenticate(io, packet) user, broker = user_and_broker packet = assign_client_id(packet) if packet.client_id.empty? @@ -24,18 +29,18 @@ module LavinMQ connack io, session_present, Connack::ReturnCode::Accepted return broker.connect_client(socket, connection_info, user, packet) else - Log.warn { "Authentication failure for user \"#{packet.username}\"" } + logger.warn { "Authentication failure for user \"#{packet.username}\"" } connack io, false, Connack::ReturnCode::NotAuthorized end end rescue ex : MQTT::Error::Connect - Log.warn { "Connect error #{ex.inspect}" } + (logger || Log).warn { "Connect error #{ex.inspect}" } if io connack io, false, Connack::ReturnCode.new(ex.return_code) end socket.close rescue ex - Log.warn { "Recieved invalid Connect packet: #{ex.inspect}" } + (logger || Log).warn { "Recieved invalid Connect packet: #{ex.inspect}" } socket.close end From 04ac01701f7eebea2f9e407e10ae7ae5b4460302 Mon Sep 17 00:00:00 2001 From: bengtmagnus <131048378+bengtmagnus@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:43:19 +0100 Subject: [PATCH 195/202] Fixed grid error (#883) bugfix: added margin auto to main grid --- static/main.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/main.css b/static/main.css index 5950b6c99f..ac7ba9f1af 100644 --- a/static/main.css +++ b/static/main.css @@ -160,6 +160,8 @@ main { justify-self: center; margin-bottom: 20px; position: relative; + margin-left: auto; + margin-right: auto; } main.main-grid { From 8a1246966b24b80af7792670cd0436bb7586df02 Mon Sep 17 00:00:00 2001 From: Viktor Erlingsson Date: Tue, 17 Dec 2024 11:33:56 +0100 Subject: [PATCH 196/202] log nr of streamed bytes every 30 seconds in follower (#885) log a status message in followers every 30 seconds with total amount of streamed bytes --- CHANGELOG.md | 4 ++++ src/lavinmq/clustering/client.cr | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b05ff66ae..ff927a2381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Queues will no longer be closed if file size is incorrect. Fixes [#669](https://github.com/cloudamqp/lavinmq/issues/669) - Dont redeclare exchange in java client test [#860](https://github.com/cloudamqp/lavinmq/pull/860) +### Added + +- Added some logging for followers [#885](https://github.com/cloudamqp/lavinmq/pull/885) + ## [2.0.2] - 2024-11-25 ### Fixed diff --git a/src/lavinmq/clustering/client.cr b/src/lavinmq/clustering/client.cr index 7f6a75de80..8efc3589dc 100644 --- a/src/lavinmq/clustering/client.cr +++ b/src/lavinmq/clustering/client.cr @@ -16,6 +16,7 @@ module LavinMQ @unix_http_proxy : Proxy? @unix_mqtt_proxy : Proxy? @socket : TCPSocket? + @streamed_bytes = 0_u64 def initialize(@config : Config, @id : Int32, @password : String, proxy = true) System.maximize_fd_limit @@ -219,6 +220,7 @@ module LavinMQ private def stream_changes(socket, lz4) acks = Channel(Int64).new(@config.clustering_max_unsynced_actions) spawn send_ack_loop(acks, socket), name: "Send ack loop" + spawn log_streamed_bytes_loop, name: "Log streamed bytes loop" loop do filename_len = lz4.read_bytes Int32, IO::ByteFormat::LittleEndian next if filename_len.zero? @@ -246,6 +248,7 @@ module LavinMQ @files.delete("#{filename}.tmp").try &.close end ack_bytes = len.abs + sizeof(Int64) + filename_len + sizeof(Int32) + @streamed_bytes &+= ack_bytes acks.send(ack_bytes) end ensure @@ -266,6 +269,13 @@ module LavinMQ socket.close rescue nil end + private def log_streamed_bytes_loop + loop do + sleep 30.seconds + Log.info { "Total streamed bytes: #{@streamed_bytes}" } + end + end + private def authenticate(socket) socket.write Start socket.write_bytes @password.bytesize.to_u8, IO::ByteFormat::LittleEndian From aa2ca469cdefdcafe80a73fd2c9441d1d65d6954 Mon Sep 17 00:00:00 2001 From: Viktor Erlingsson Date: Tue, 17 Dec 2024 11:49:14 +0100 Subject: [PATCH 197/202] update changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff927a2381..896464676e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- New UI for management interface [#821](https://github.com/cloudamqp/lavinmq/pull/821) +- Use sent/received bytes instead of messages to trigger when other tasks can run [#863](https://github.com/cloudamqp/lavinmq/pull/863) + ### Fixed - Queues will no longer be closed if file size is incorrect. Fixes [#669](https://github.com/cloudamqp/lavinmq/issues/669) - Dont redeclare exchange in java client test [#860](https://github.com/cloudamqp/lavinmq/pull/860) +- Removed duplicate metric rss_bytes [#881](https://github.com/cloudamqp/lavinmq/pull/881) +- Release leadership on graceful shutdown [#871](https://github.com/cloudamqp/lavinmq/pull/871) +- Rescue more exceptions while reading msg store on startup [#865](https://github.com/cloudamqp/lavinmq/pull/865) ### Added From a11c9f90aaebff649793e34bb18d7183c1a8d90a Mon Sep 17 00:00:00 2001 From: bengtmagnus <131048378+bengtmagnus@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:48:15 +0100 Subject: [PATCH 198/202] Create uiux_improvements.md --- .github/ISSUE_TEMPLATE/uiux_improvements.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/uiux_improvements.md diff --git a/.github/ISSUE_TEMPLATE/uiux_improvements.md b/.github/ISSUE_TEMPLATE/uiux_improvements.md new file mode 100644 index 0000000000..54998a8b06 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/uiux_improvements.md @@ -0,0 +1,19 @@ +--- +name: UI/UX improvement +about: Create a report to help us improve +title: '' +labels: 'UI/UX' +assignees: '' +--- +**Summary** +Provide a brief description of the issue or improvement. +**Current Behavior** +Describe how the feature or UI element currently works. +**Desired Behavior** +Explain what the ideal experience should be. +**Steps to Reproduce (if reporting an issue)** +List the steps required to replicate the problem. +**Proposed Solution (for improvements)** +Suggest how to resolve the issue or implement the improvement. +**Screenshots/Mockups** +Add visuals to clarify the issue or illustrate your suggestion. From 7842f82c87951556a8d3d0f1daaa925b852601a9 Mon Sep 17 00:00:00 2001 From: bengtmagnus <131048378+bengtmagnus@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:55:05 +0100 Subject: [PATCH 199/202] Update uiux_improvements.md --- .github/ISSUE_TEMPLATE/uiux_improvements.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/uiux_improvements.md b/.github/ISSUE_TEMPLATE/uiux_improvements.md index 54998a8b06..2f45b3a2ab 100644 --- a/.github/ISSUE_TEMPLATE/uiux_improvements.md +++ b/.github/ISSUE_TEMPLATE/uiux_improvements.md @@ -5,15 +5,20 @@ title: '' labels: 'UI/UX' assignees: '' --- -**Summary** +**Summary** Provide a brief description of the issue or improvement. -**Current Behavior** + +**Current Behavior** Describe how the feature or UI element currently works. -**Desired Behavior** + +**Desired Behavior** Explain what the ideal experience should be. -**Steps to Reproduce (if reporting an issue)** + +**Steps to Reproduce (if reporting an issue)** List the steps required to replicate the problem. -**Proposed Solution (for improvements)** + +**Proposed Solution (for improvements)** Suggest how to resolve the issue or implement the improvement. -**Screenshots/Mockups** + +**Screenshots/Mockups** Add visuals to clarify the issue or illustrate your suggestion. From 5273f5700ee37d0b33317aa4c648c19d09e3fefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christina=20Dahl=C3=A9n?= <85930202+kickster97@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:39:38 +0100 Subject: [PATCH 200/202] Update src/lavinmq/mqtt/session.cr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jon Börjesson --- src/lavinmq/mqtt/session.cr | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 7961ea988e..3c7149b5bf 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -146,12 +146,8 @@ module LavinMQ msg = env.message retained = msg.properties.try &.headers.try &.["mqtt.retain"]? == true - qos = case msg.properties.delivery_mode - when 2u8 - 1u8 - else - msg.properties.delivery_mode || 0u8 - end + qos = msg.properties.delivery_mode || 0u8 + qos = 1u8 if qos > 1 MQTT::Publish.new( packet_id: packet_id, payload: msg.body, From 969b194ce090d9e10a171d7950f9dad1d1f24d15 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 18 Dec 2024 11:40:32 +0100 Subject: [PATCH 201/202] review fix --- src/lavinmq/mqtt/session.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 3c7149b5bf..65b3dadef9 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -41,7 +41,7 @@ module LavinMQ next end consumer = consumers.first.as(MQTT::Consumer) - get_packet(false) do |pub_packet| + get_packet do |pub_packet| consumer.deliver(pub_packet) end Fiber.yield if (i &+= 1) % 32768 == 0 @@ -109,7 +109,7 @@ module LavinMQ @vhost.unbind_queue(@name, EXCHANGE, rk, arguments || AMQP::Table.new) end - private def get_packet(no_ack : Bool, & : MQTT::Publish -> Nil) : Bool + private def get_packet(& : MQTT::Publish -> Nil) : Bool raise ClosedError.new if @closed loop do env = @msg_store_lock.synchronize { @msg_store.shift? } || break From a766b9b9f87ea43c944d53505f191eebedb3f30a Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 18 Dec 2024 13:52:19 +0100 Subject: [PATCH 202/202] remove excess write and rewind in exchange --- src/lavinmq/mqtt/exchange.cr | 5 +---- src/lavinmq/mqtt/session.cr | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lavinmq/mqtt/exchange.cr b/src/lavinmq/mqtt/exchange.cr index c79641425f..ceddf3631b 100644 --- a/src/lavinmq/mqtt/exchange.cr +++ b/src/lavinmq/mqtt/exchange.cr @@ -50,10 +50,7 @@ module LavinMQ timestamp = RoughTime.unix_ms bodysize = packet.payload.size.to_u64 - body = ::IO::Memory.new(bodysize) - body.write(packet.payload) - body.rewind - + body = ::IO::Memory.new(packet.payload, false) if packet.retain? @retain_store.retain(packet.topic, body, bodysize) body.rewind diff --git a/src/lavinmq/mqtt/session.cr b/src/lavinmq/mqtt/session.cr index 65b3dadef9..2c03a3b38f 100644 --- a/src/lavinmq/mqtt/session.cr +++ b/src/lavinmq/mqtt/session.cr @@ -145,7 +145,6 @@ module LavinMQ def build_packet(env, packet_id) : MQTT::Publish msg = env.message retained = msg.properties.try &.headers.try &.["mqtt.retain"]? == true - qos = msg.properties.delivery_mode || 0u8 qos = 1u8 if qos > 1 MQTT::Publish.new(