From 505588b601ef82b13fbec9b6aa20b7fcd5b76916 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 26 Aug 2024 16:40:43 +0200 Subject: [PATCH 001/188] 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 1f6944db97..255637522c 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 8709ab05c1..18a31fd4f7 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 0d55b88b0c..c6ff07a1d2 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 a683bd9cd2..6c52e03793 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -118,13 +118,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 @@ -134,7 +134,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 @@ -153,6 +153,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 45ba0f9dd49fde2490767c63f625f31bca555d3e Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 27 Aug 2024 14:57:42 +0200 Subject: [PATCH 002/188] 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 6492763fa013cb273458118c64d65cec53a769ec Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 28 Aug 2024 10:10:05 +0200 Subject: [PATCH 003/188] 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 c6ff07a1d2..e87b413a89 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 4d57ff0fea62e6a5169d51c05974f6e376d26ef9 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 6 Sep 2024 13:18:26 +0200 Subject: [PATCH 004/188] 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 8e6c6cf77869cec683180621285baecbe98e6503 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Fri, 6 Sep 2024 13:50:00 +0200 Subject: [PATCH 005/188] 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 7347ed8833f26a6e11427d37b220fc2fc4c96173 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 6 Sep 2024 13:57:55 +0200 Subject: [PATCH 006/188] 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 2165723c8abcbc18e8f7bca9fc4818da44989980 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Fri, 6 Sep 2024 14:53:50 +0200 Subject: [PATCH 007/188] 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 fdad4d536fc40b581b6cf1e908a224d179f625dd Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Fri, 6 Sep 2024 15:01:31 +0200 Subject: [PATCH 008/188] 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 77963a95978b52bdc4670ce2d6ec3c660a1e5394 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 10 Sep 2024 13:20:42 +0200 Subject: [PATCH 009/188] 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 48dd0592c60eb94391836c2937f102de1213b122 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 10 Sep 2024 13:58:18 +0200 Subject: [PATCH 010/188] 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 c702b013e84deabe9006a7564bd57890e53fc0fe Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 10 Sep 2024 14:23:50 +0200 Subject: [PATCH 011/188] 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 2a381b624db4a47e3a1382e8347c41b098484489 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Wed, 11 Sep 2024 21:52:37 +0200 Subject: [PATCH 012/188] 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 8076cddc9c2342fe691e18f87fbe0b06bac770fd Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Sep 2024 09:25:05 +0200 Subject: [PATCH 013/188] 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 88ef61365d1b169f0a36d00564a99abe4f6014a3 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Sep 2024 09:27:43 +0200 Subject: [PATCH 014/188] 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 21df3509e522a0a85b7301011ca1358c31750d76 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 12 Sep 2024 10:04:24 +0200 Subject: [PATCH 015/188] 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 ab83ec2e5f1a1e0f9c43736577bad3bd63374aa3 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 16 Sep 2024 10:51:55 +0200 Subject: [PATCH 016/188] 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 0d402a8fbb2d45de5470391b651e961be57d0ecf Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 16 Sep 2024 13:59:31 +0200 Subject: [PATCH 017/188] 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 8b08d117163c28a656c4b3cdc49edc76596c2532 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 16 Sep 2024 15:41:55 +0200 Subject: [PATCH 018/188] 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 d0ba3551d2123d605593ae5d42a90a96c1dbdcf8 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 16 Sep 2024 22:28:00 +0200 Subject: [PATCH 019/188] 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 b6d3996cc48730598beedac2675b0b51087ccae3 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 17 Sep 2024 16:55:17 +0200 Subject: [PATCH 020/188] 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 db6abeeb211f41ecf43e7e8982068d9ba430d94c Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 17 Sep 2024 16:56:33 +0200 Subject: [PATCH 021/188] 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 6a00891ad456f7eb5d9c9a1afe12e072a1d121ac Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 17 Sep 2024 16:57:23 +0200 Subject: [PATCH 022/188] 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 592600c85fcefdb23dc514ca7864b7409b578a96 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 23 Sep 2024 10:13:17 +0200 Subject: [PATCH 023/188] 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 e87b413a89..04f448f067 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 4f20533a535ceb660ba4c94bf992b727eccb40da Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 23 Sep 2024 11:47:39 +0200 Subject: [PATCH 024/188] 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 5f588ab2c147bdc1a887dae6be12d4c9f14c8255 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 23 Sep 2024 16:05:36 +0200 Subject: [PATCH 025/188] 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 6042f6f48d22dd127cc67b8e9c82a90cc577a06f Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 24 Sep 2024 13:55:25 +0200 Subject: [PATCH 026/188] 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 47be1ee77b0a57a3b1e62d17a655bf992479bf90 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 25 Sep 2024 10:33:44 +0200 Subject: [PATCH 027/188] 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 622ac3cef79e44c561338b9d338e51ae36c9392d Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 25 Sep 2024 10:44:16 +0200 Subject: [PATCH 028/188] 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 421a09893679712c61e966ad9444a3949bcfee04 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 25 Sep 2024 11:01:05 +0200 Subject: [PATCH 029/188] 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 44b791d775fa0d4f0f1d831d705d53e8cb13ed17 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 27 Sep 2024 09:18:50 +0200 Subject: [PATCH 030/188] 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 c931ce201c8079185cfed3648972b74f1678bb9e Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 1 Oct 2024 14:03:32 +0200 Subject: [PATCH 031/188] 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 b5d3a98c95382fac3aa9c7cc99df7507e7f3dd7e Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Wed, 18 Sep 2024 15:14:46 +0200 Subject: [PATCH 032/188] 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 dafc567fb885b9460280f079e8a336dcec1ccf81 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 1 Oct 2024 15:18:15 +0200 Subject: [PATCH 033/188] 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 8baf983d65e32007296669202796a3a5fa7216ef Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 2 Oct 2024 11:51:24 +0200 Subject: [PATCH 034/188] 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 585cb6bd250731209abb211512e9917511d3f3e4 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 2 Oct 2024 11:51:51 +0200 Subject: [PATCH 035/188] 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 5d763396492bba1b995a91f159facfb8207956a4 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 3 Oct 2024 13:09:38 +0200 Subject: [PATCH 036/188] 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 81467de9f184aa5161205b3f27ea4c5cbb5c5006 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 3 Oct 2024 13:57:36 +0200 Subject: [PATCH 037/188] 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 7f238e736af71c2fe230eae0e95aea3b6c7ebc9c Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 10 Oct 2024 09:37:15 +0200 Subject: [PATCH 038/188] 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 5bfa97751d899078be736821d07ed54643b8f44a Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 10 Oct 2024 09:52:59 +0200 Subject: [PATCH 039/188] 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 4ecf4a9c8a42b1fe68a19f709765d20301f49587 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 10 Oct 2024 11:41:45 +0200 Subject: [PATCH 040/188] 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 57e0ac860fedb5ce99ffe4a39f4854e20fd0e035 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 10 Oct 2024 12:50:40 +0200 Subject: [PATCH 041/188] 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 0650194ec7d0ce60d5a2faf70d960eff3e1b14a6 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 14 Oct 2024 10:44:07 +0200 Subject: [PATCH 042/188] 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 e1a2b8298f9e2f102c780485bb77f88a7976650a Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 14 Oct 2024 10:52:49 +0200 Subject: [PATCH 043/188] 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 bd96dcfbd34ca123690b97913f18650096f2b338 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Mon, 14 Oct 2024 12:59:32 +0200 Subject: [PATCH 044/188] 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 9fc032b263b7e110b309e2745002fbf1aad437c6 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 09:51:12 +0200 Subject: [PATCH 045/188] 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 151860df932d1504d96d0773b1b775be86838ef9 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 09:52:21 +0200 Subject: [PATCH 046/188] 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 578aca198875ed8f4c918ee9d1958711009a5fc0 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 15:33:09 +0200 Subject: [PATCH 047/188] 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 42ce5ee547d35adb41f0e3eaaf1e4d49342fce0d Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 15 Oct 2024 15:49:23 +0200 Subject: [PATCH 048/188] 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 de90cb76a65114489b890afe5589484d3fd432a1 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 15:49:19 +0200 Subject: [PATCH 049/188] 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 41a2798f710004680e624284353f18284cd2d103 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 15:53:27 +0200 Subject: [PATCH 050/188] 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 db83ff8816bbe1c649dd50ddc1b3a4af262d14f4 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/188] 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 daa13e3b566ed3f14d9522f89cbf621023ef7b66 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/188] 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 1daac5996ece30bf2a5f2bec2c1820bce816ff16 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/188] 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 f431ac4c840907ddae953e9d25f007092ef7bc01 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/188] 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 275231b192388580a1c5b19ddab3364330849c7d 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/188] 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 e82506a6a79dfeeb9e2c5c03d4172cba9abce431 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/188] 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 32d4e9a1b0bd3b8f5a2b1e9d229cb59e78518761 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/188] 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 c036d47f22a28c329c48ec155a74fc6f90ce842b 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/188] 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 46c8a39495dcc8d2d2b4ea8bf4409845d616ab66 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/188] 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 ce245c523744e7236dc85baa8b3781693602ad38 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/188] 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 e245bcbf7e4788c56f4344a3ea1ebb3f7a7d5b03 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/188] 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 2ae36c2c58c04c027ffa05410f43d7428f620499 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 15 Oct 2024 16:23:50 +0200 Subject: [PATCH 062/188] 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 8062e812637bef3cfb647f99e8fdfa3d956ce6b9 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Tue, 15 Oct 2024 16:42:42 +0200 Subject: [PATCH 063/188] 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 f021fdaf61717935c25a593159e0c8da469e00bd Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 16 Oct 2024 10:51:51 +0200 Subject: [PATCH 064/188] 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 2dd49d06bac2f43c72e157864b75949eb6510db2 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 16 Oct 2024 11:18:58 +0200 Subject: [PATCH 065/188] 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 06d8175763c6c4fa61b03468ed04b93665302bf2 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 16 Oct 2024 16:39:40 +0200 Subject: [PATCH 066/188] 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 fd19bff85f234ec1bfb932e250dcb15fdc27335c Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 17 Oct 2024 13:31:29 +0200 Subject: [PATCH 067/188] 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 f0f8f391a6a237371044287a3a8a881d9360fc5d Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 17 Oct 2024 16:44:11 +0200 Subject: [PATCH 068/188] 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 ec59be49b95b642c1fa0df04a843d5e5a9924479 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/188] 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 7d765ad407b08036bdae0c5705a11ad39d1855a8 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 18 Oct 2024 11:47:09 +0200 Subject: [PATCH 070/188] 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 7453bc5c7b3becc3c76c022f04d260d2ea05566c 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/188] 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 1d1cfd05f2769aad4547043e38b5ce62fbecc28e Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:09:42 +0200 Subject: [PATCH 072/188] 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 5c16b7f89a808623fab48cae2ab8db5244cfc6e3 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:12:04 +0200 Subject: [PATCH 073/188] 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 00250bb66f49c77551603f3a651840e179063ca2 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:22:42 +0200 Subject: [PATCH 074/188] 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 6b989cc3beca43b761c42646f5aee97d97613a99 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:42:24 +0200 Subject: [PATCH 075/188] 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 81722257e07173774d99a6ea4835776fdcc0b1c8 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 13:55:50 +0200 Subject: [PATCH 076/188] 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 5580ea4a9de78b679a3c8384185049a22f8995ee Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 14:40:05 +0200 Subject: [PATCH 077/188] 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 9226687928dd004d18ae96ba342d5c8ed6097145 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 14:42:04 +0200 Subject: [PATCH 078/188] 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 0d7a268567f162854cea3a337c18601db068af56 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 15:07:26 +0200 Subject: [PATCH 079/188] 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 25b1aefe0d63d839d56ebd6fc6462c285ed1a627 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 21 Oct 2024 15:31:05 +0200 Subject: [PATCH 080/188] 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 cfe5eebe881c65ce62f67edc4fb6014ed621bc05 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 09:49:35 +0200 Subject: [PATCH 081/188] 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 183609a72f2df73e87b27eb52ce7941789272cdc 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/188] 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 11c7f30157734ae88fed509032b5bbaea2a7aa27 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/188] 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 4626c1fb7675c4b4a75034409ad0a3fbcafe50d6 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/188] 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 dd18f2f0edc222a149ee135511601cce56cb39e4 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/188] 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 02ecf2e4d1f94ed44826244a1ededa436ed1cdc3 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/188] 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 21ea60fd60240293058e0510c92667f618e0169d 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/188] 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 7863af6f51e18bcfd6a3351865b0a383e585dd68 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/188] 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 17dd34b5e8fd20e12c51aa880c7a0abf9b591ca7 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 15:38:55 +0200 Subject: [PATCH 089/188] 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 b44990c18cb4eff5bcecba7729eb3816788eccfc Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 16:07:39 +0200 Subject: [PATCH 090/188] 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 2e496818dbcc4c4871b3682cb1455b88e89913c0 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 16:16:23 +0200 Subject: [PATCH 091/188] 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 f2630096acec94ef5252320c7ee2ec68ebbf7e8a Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 22 Oct 2024 16:42:13 +0200 Subject: [PATCH 092/188] 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 750dd6a3083f1158fe235ea57a2cd16180163375 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/188] 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 8f15518083d49f6f72e22c901e5611441682595d 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/188] 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 d4f9ad3fa50bc895d7d92cf13224e061768e8d9c 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/188] 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 861883973b9e84fc62f390f6ed2c9c60b3222cb8 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/188] 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 62625f8d24dec8d6b7c6ef0aa0e8907d18191b2d 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/188] 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 c95f65e2ac8f74c01e7ff3583d6e93970c2ac535 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Oct 2024 09:25:59 +0200 Subject: [PATCH 098/188] 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 d1dcbbdf6c311904b068e6280b9a69c60276791c Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Oct 2024 10:12:26 +0200 Subject: [PATCH 099/188] 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 7ed81b383c6dde96ea97caa29015451f1bdd647f 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/188] 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 3cdf6aab4dfa7e4c7fc4ca1ece26f9917834e89d Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 23 Oct 2024 15:23:48 +0200 Subject: [PATCH 101/188] 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 358523cb180383e573b403d7ddf37c9433cf4ca3 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/188] 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 2c9ea27de87cb63f3d16a52d706c82a1ff96dd37 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/188] 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 42a9f9fc4c773a0e628ce1b494e255d7e26e4f8a Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 24 Oct 2024 09:18:28 +0200 Subject: [PATCH 104/188] 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 9e1ddb6ebce9ffb7ba79e1d0b706ab816584d8d2 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 24 Oct 2024 09:35:49 +0200 Subject: [PATCH 105/188] 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 7dd27fa7965220a8e91f869895b5c44ed7bf9e03 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 24 Oct 2024 09:58:51 +0200 Subject: [PATCH 106/188] 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 04f448f067..61e35c3a2b 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 @@ -168,6 +169,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 "replication" then abort("#{file}: [replication] is deprecated and replaced with [clustering], see the README for more information") @@ -278,6 +280,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 03e4e9d50e224741b165b09794eb658023a2206e 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/188] 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 5d7798502a70e26bd0805590aed358f24f0b9a1a 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/188] 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 2b1b38af52222a1354905367e2d629afebc00172 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/188] 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 7f44080a6f72ec9c81c2d7ee3bd91b9fb5666658 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 25 Oct 2024 14:44:29 +0200 Subject: [PATCH 110/188] 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 e4879495da3ee69b19e28ad199c6cf594d4b72bc Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 25 Oct 2024 14:44:50 +0200 Subject: [PATCH 111/188] 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 95ac073c1c6db53bea02fa8f3ee73b39592f0003 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/188] 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 f290486a7fdf3f0dd871aa0dc8e9b947e8fedc14 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/188] 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 ea924f5b7555eabcf3713b6b66177e9940b63374 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 30 Oct 2024 11:23:23 +0100 Subject: [PATCH 114/188] 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 96d5b5422c777e07417522954e4c7608ce1d7704 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 30 Oct 2024 13:16:46 +0100 Subject: [PATCH 115/188] 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 c990e8eb961dec435593e9ca60f549890d3ca9d1 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 30 Oct 2024 13:58:27 +0100 Subject: [PATCH 116/188] 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 bc06320cea0d94597c0d28ca5539bb9a12ff3a8f Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 31 Oct 2024 11:42:23 +0100 Subject: [PATCH 117/188] 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 23fc2063d7..ae69521e44 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 db41e9d19d286a3227b9a66b1cf0bc04cbc9ddd7 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 31 Oct 2024 13:10:54 +0100 Subject: [PATCH 118/188] 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 d86b677988e3f0087528dccb60eec29f1d13e853 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 31 Oct 2024 15:01:50 +0100 Subject: [PATCH 119/188] 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 d0f787941f0fc413a48e66a451ee499889fcbf5d Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 31 Oct 2024 15:07:26 +0100 Subject: [PATCH 120/188] 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 09241eed4bf81bb918d5f2f46f8ab7dbb1eb62fc Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 1 Nov 2024 08:51:28 +0100 Subject: [PATCH 121/188] 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 ae69521e44..8d7499079e 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 61e35c3a2b..2797846d5e 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 @@ -294,7 +294,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 08a196383e08184a9b2a1f17d26d14a86743e7e6 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 1 Nov 2024 10:15:33 +0100 Subject: [PATCH 122/188] 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 20343caa75af9a55945538d9fc9db91a37f6c4a6 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 1 Nov 2024 14:51:08 +0100 Subject: [PATCH 123/188] 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 6c52e03793..7e0830d75e 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -184,7 +184,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 06a68faa56e9e67577b0dd82687e8bf788aaf2d4 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 10:47:10 +0100 Subject: [PATCH 124/188] 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 0a1f49d1e0e224f3c551efb83e84998392553185 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:04:57 +0100 Subject: [PATCH 125/188] 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 8d7499079e..8d3de2ab4b 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 28650de95256ee5c1c4fd43276f7e4c69233a485 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:20:08 +0100 Subject: [PATCH 126/188] 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 024ff9c00babc3caf5ca35653af3bc9e87d37b65 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:31:34 +0100 Subject: [PATCH 127/188] 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 76ab2c0baf2a49d652ac117f6aa35c9c093bb46c Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:45:41 +0100 Subject: [PATCH 128/188] 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 56829e94ef55fe047d7b438dc0c8a8326727f006 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 11:49:24 +0100 Subject: [PATCH 129/188] 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 dbc41b6d70b87ae79e6d5aedb245a17528eb95b6 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 13:43:35 +0100 Subject: [PATCH 130/188] 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 8b063ff28e521e00c5a256f46ce10591fbabc272 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 14:20:17 +0100 Subject: [PATCH 131/188] 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 809626ad41686a184d618bfca21a0ad3da75a298 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 14:27:47 +0100 Subject: [PATCH 132/188] 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 d6c9d7b053c1d744f25bc51719d559869a5623d0 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 4 Nov 2024 14:53:32 +0100 Subject: [PATCH 133/188] 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 813c0810a23aa01446b5ee159fdb1bcaf8b5d6c0 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 09:24:57 +0100 Subject: [PATCH 134/188] 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 8d3de2ab4b..50eae42183 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 df2cb43cd11656820724b4a2b5f013d18ba81e1a Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 14:46:24 +0100 Subject: [PATCH 135/188] 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 fc1d221613..32a1ea4302 100644 --- a/src/lavinmq/amqp/queue/queue.cr +++ b/src/lavinmq/amqp/queue/queue.cr @@ -744,6 +744,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 2797846d5e..60150812e0 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 4b9be56f0094e2cc076a0102db76cccf0f5c70c5 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 14:47:33 +0100 Subject: [PATCH 136/188] 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 32a1ea4302..fb66d2b33b 100644 --- a/src/lavinmq/amqp/queue/queue.cr +++ b/src/lavinmq/amqp/queue/queue.cr @@ -745,7 +745,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 f7ac14d8f093e0c1341671f4b4b44fc38d80ae4b Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 6 Nov 2024 15:00:05 +0100 Subject: [PATCH 137/188] 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 50eae42183..e73de9647d 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 19272da915876fc26c2b5e5eab698e9aea408d29 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 7 Nov 2024 13:37:47 +0100 Subject: [PATCH 138/188] 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 7e0151024e5f7d1cd0705692c27435e32231d9dc Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 7 Nov 2024 13:45:45 +0100 Subject: [PATCH 139/188] 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 84e548b96471fe912ee57672961d479230f267b2 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 7 Nov 2024 13:49:43 +0100 Subject: [PATCH 140/188] 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 e71ab369f5905d74c993ce6c0453e8ae03915224 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 8 Nov 2024 15:21:50 +0100 Subject: [PATCH 141/188] 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 d3396cf76fd109e1ad3f2f9c7502d4982773ab4e Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 11 Nov 2024 21:20:44 +0100 Subject: [PATCH 142/188] 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 60150812e0..00f99b5c52 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 @@ -90,6 +91,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 @@ -285,6 +292,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 7e0830d75e..8b22d39b12 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -158,6 +158,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 6d5ad0c6449ac96d88e81ee36be01c482d00feb7 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 11 Nov 2024 21:38:38 +0100 Subject: [PATCH 143/188] 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 f0ef2d289d27ce2d75959d27cc34ee6957f2c454 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 12 Nov 2024 15:55:35 +0100 Subject: [PATCH 144/188] 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 031aec7586a6aae4ad86e717b844486e336816bc Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 09:29:33 +0100 Subject: [PATCH 145/188] 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 00f99b5c52..518c2ff39c 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 @@ -112,6 +113,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 } @@ -295,6 +299,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 e33d52809fe7690b6a2ce5e3201c641d1618e387 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 10:00:56 +0100 Subject: [PATCH 146/188] 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 16e48be1fd5d284d8c635160a01774081471f0dd Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 10:05:06 +0100 Subject: [PATCH 147/188] 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 149112043d995378d2500c2fb2078d05ce5dc0fe Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 11:42:47 +0100 Subject: [PATCH 148/188] 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 fb66d2b33b..6f120ac4b4 100644 --- a/src/lavinmq/amqp/queue/queue.cr +++ b/src/lavinmq/amqp/queue/queue.cr @@ -753,7 +753,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 8b22d39b12..e62ba8e5af 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -116,7 +116,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}" @@ -165,6 +165,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 4c3480ba7b3e53db9f58b557bbd8355f6c095c97 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 11:49:16 +0100 Subject: [PATCH 149/188] 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 068a174f5dd357f74102f6a6ad9bfa5b9fcfe9ac Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 11:53:48 +0100 Subject: [PATCH 150/188] 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 dd1f1106892c3b7c5ef6459454550325948471d0 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 12:02:21 +0100 Subject: [PATCH 151/188] 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 ccde82783bf1442add529715ac03f9fa71fa2b48 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/188] 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 e62ba8e5af..5cdca71244 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -118,13 +118,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 @@ -134,7 +134,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 @@ -155,13 +155,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 aeeb4cbb23e60d4b13e1efcfcaa9c40abaa1e757 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/188] 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 5cdca71244..3081bc4e19 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -166,7 +166,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 cac11638f1130074ca5fae9a64e63498ba107d78 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 13 Nov 2024 13:48:27 +0100 Subject: [PATCH 154/188] 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 6b6188fdccac79b39a723624e6e91196bca71a1a Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 08:50:59 +0100 Subject: [PATCH 155/188] 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 71561acb9c20a554f74f956b1c98c57f4c9d2246 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 08:51:14 +0100 Subject: [PATCH 156/188] 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 9815c63f7a52eb8a0814bb568b7c7cb75801a48f Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 09:00:20 +0100 Subject: [PATCH 157/188] 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 1de72a1e85b62895c419ef8260e00663d7f0c4ba 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/188] 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 43bf65fd0bd3b4324351874a04349d3f4ed723e6 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 11:47:43 +0100 Subject: [PATCH 159/188] 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 ef2c7f36be1b6e5d6a6e7175ecf7f2ed62626933 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 12:00:03 +0100 Subject: [PATCH 160/188] 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 6f0eedddabdc47550f3f005d98415bc2b26b617d Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 12:00:55 +0100 Subject: [PATCH 161/188] 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 61a15e2770cfda9d6c46e91533117b6d67d194e4 Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 14 Nov 2024 15:26:33 +0100 Subject: [PATCH 162/188] 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 0525c767e92911e53abecb3e990688b692d4d900 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 22:11:53 +0100 Subject: [PATCH 163/188] 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 131043d56ac354e174c073d4cf289ac86277a0d5 Mon Sep 17 00:00:00 2001 From: Magnus Landerblom Date: Thu, 14 Nov 2024 22:34:23 +0100 Subject: [PATCH 164/188] 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 bf18a56bac2545c64459b7f519c384ba5796dbab Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 15 Nov 2024 10:17:12 +0100 Subject: [PATCH 165/188] 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 9410a139b19cab0c8f636588d905776fda8ef6b4 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 15 Nov 2024 11:25:37 +0100 Subject: [PATCH 166/188] 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 7ac09abd016e6c35af27e0133bbc4319201c9a3c 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/188] 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 cabf44514535f5ce75b872a8fab71db49ce2b047 Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 15 Nov 2024 12:09:08 +0100 Subject: [PATCH 168/188] 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 b29bc61daa0b12870d9bf467295717a0946aa98c Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 17:45:10 +0100 Subject: [PATCH 169/188] 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 e73de9647d..2b5425716d 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 518c2ff39c..b400ae4533 100644 --- a/src/lavinmq/config.cr +++ b/src/lavinmq/config.cr @@ -297,8 +297,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 3081bc4e19..26da568a3a 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -194,7 +194,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 d19e4d192109b61d455e8ea950b6a16e35440302 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 17:46:41 +0100 Subject: [PATCH 170/188] 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 1b87d0e0958e02e490964ae4d884a18d436c03e3 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 20:11:57 +0100 Subject: [PATCH 171/188] 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 1af2f13844d5118b426db804e15888ce703bd344 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 20:55:51 +0100 Subject: [PATCH 172/188] 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 9345b85cc66e274037871068f6420add53547949 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 18 Nov 2024 20:59:28 +0100 Subject: [PATCH 173/188] 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 f097cc92681933c9f020af15f4cf2bc7d14a9a29 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/188] 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 69871d29b1454a118c0bace895205b9f37b4c654 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 20 Nov 2024 16:41:11 +0100 Subject: [PATCH 175/188] 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 1dc8cb6b68a66084c212151dd923fc3893500e5f Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 21 Nov 2024 10:25:39 +0100 Subject: [PATCH 176/188] 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 b6b1059eb035aaa05b1ef78d6bb383b3c05e3999 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 2 Dec 2024 11:46:41 +0100 Subject: [PATCH 177/188] 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 c67089d2ad26cb169166d83f7dc8d872a0757140 Mon Sep 17 00:00:00 2001 From: Christina Date: Mon, 2 Dec 2024 15:09:44 +0100 Subject: [PATCH 178/188] 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 53854ce5af563b9db606ba0dd42e14f4cc5145aa 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/188] 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 26da568a3a..b6c888989c 100644 --- a/src/lavinmq/launcher.cr +++ b/src/lavinmq/launcher.cr @@ -117,6 +117,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}" @@ -129,10 +133,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 @@ -165,8 +165,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 88a2f2821891c56deddcc2e311d33526264f76d8 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/188] 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 7eecf477dfaf4436d5703316e7addb87e3511eaa Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 10 Dec 2024 13:03:31 +0100 Subject: [PATCH 181/188] 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 b516e519d6..3893725389 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? @@ -130,6 +134,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 d84133782a2b9bd0c5a14c502beda705f32138c3 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 10 Dec 2024 13:04:11 +0100 Subject: [PATCH 182/188] 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 06162d9bab709e3a7e3ecf6112df217b7de1ed92 Mon Sep 17 00:00:00 2001 From: Christina Date: Tue, 10 Dec 2024 16:36:56 +0100 Subject: [PATCH 183/188] 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 56a71dee5e80cf3b8b212f090131e398c755af62 Mon Sep 17 00:00:00 2001 From: Christina Date: Wed, 11 Dec 2024 14:25:25 +0100 Subject: [PATCH 184/188] 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 2b5425716d..39b7e5ab34 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 95be31fe6c5ec06f5af4c8e35165a160dc46e09a Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Dec 2024 15:28:02 +0100 Subject: [PATCH 185/188] 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 39b7e5ab34..47f4c47c73 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 3893725389..b516e519d6 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? @@ -134,8 +130,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 d3d6cb1f6a0d1980278d0524089de1e86c706c6e Mon Sep 17 00:00:00 2001 From: Christina Date: Thu, 12 Dec 2024 15:43:43 +0100 Subject: [PATCH 186/188] 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 47f4c47c73..9588e04bbe 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 81ee5fdf5c6880afbf21395c748a5a6e799a72ea Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 13 Dec 2024 11:56:43 +0100 Subject: [PATCH 187/188] 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 b400ae4533..64d26c6121 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 2bbbcd98721765cdcb1d4d44e952a0ef373ffcae Mon Sep 17 00:00:00 2001 From: Christina Date: Fri, 13 Dec 2024 12:53:26 +0100 Subject: [PATCH 188/188] 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