Improved version of luamqtt with fewer bugs

This commit is contained in:
2024-07-20 17:51:48 +02:00
parent c7344711c2
commit 3eee21480b
31 changed files with 3487 additions and 1664 deletions

View File

@@ -0,0 +1,89 @@
-- base connector class for buffered reading.
--
-- Use this base class if the sockets do NOT yield.
-- So LuaSocket for example, when using Copas or OpenResty
-- use the non-buffered base class.
--
-- This base class derives from `non_buffered_base` it implements the
-- `receive` and `buffer_clear` methods. But adds the `plain_receive` method
-- that must be implemented.
--
-- NOTE: the `plain_receive` method is supposed to be non-blocking (see its
-- description), but the `send` method has no such facilities, so is `blocking`
-- in this class. Make sure to set the proper timeouts in either method before
-- starting the send/receive. So for example for LuaSocket call `settimeout(0)`
-- before receiving, and `settimeout(30)` before sending.
--
-- @class mqtt.connector.base.buffered_base
local super = require "mqtt.connector.base.non_buffered_base"
local buffered = setmetatable({}, super)
buffered.__index = buffered
buffered.super = super
buffered.type = "buffered, blocking i/o"
-- debug helper function
-- function buffered:buffer_state(msg)
-- print(string.format("buffer: size = %03d last-byte-done = %03d -- %s",
-- #(self.buffer_string or ""), self.buffer_pointer or 0, msg))
-- end
-- bytes read were handled, clear those
function buffered:buffer_clear()
-- self:buffer_state("before clearing buffer")
self.buffer_string = nil
self.buffer_pointer = nil
end
-- read bytes, first from buffer, remaining from function
-- if function returns "idle" then reset read pointer
function buffered:receive(size)
-- self:buffer_state("receive start "..size.." bytes")
local buf = self.buffer_string or ""
local idx = self.buffer_pointer or 0
while size > (#buf - idx) do
-- buffer is lacking bytes, read more...
local data, err = self:plain_receive(size - (#buf - idx))
if not data then
if err == self.signal_idle then
-- read timedout, retry entire packet later, reset buffer
self.buffer_pointer = 0
end
return data, err
end
-- append received data, and try again
buf = buf .. data
self.buffer_string = buf
-- self:buffer_state("receive added "..#data.." bytes")
end
self.buffer_pointer = idx + size
local data = buf:sub(idx + 1, idx + size)
-- print("data: ", require("mqtt.tools").hex(data))
-- self:buffer_state("receive done "..size.." bytes\n")
return data
end
--- Retrieves the requested number of bytes from the socket, in a non-blocking
-- manner.
-- The implementation MUST read with a timeout that immediately returns if there
-- is no data to read. If there is no data, then it MUST return
-- `nil, self.signal_idle` to indicate it no data was there and we need to retry later.
--
-- If there is partial data, it should return that data (less than the requested
-- number of bytes), with no error/signal.
--
-- If the receive errors, because of a closed connection it should return
-- `nil, self.signal_closed` to indicate this. Any other errors can be returned
-- as a regular `nil, err`.
-- @tparam size int number of bytes to receive.
-- @return data, or `false, err`, where `err` can be a signal.
function buffered:plain_receive(size) -- luacheck: ignore
error("method 'plain_receive' on buffered connector wasn't implemented")
end
return buffered

View File

@@ -0,0 +1,29 @@
-- validates the LuaSec options, and applies defaults
return function(conn)
if conn.secure then
local params = conn.secure_params
if not params then
-- set default LuaSec options
conn.secure_params = {
mode = "client",
protocol = "any",
verify = "none",
options = {"all", "no_sslv2", "no_sslv3", "no_tlsv1"},
}
return
end
local ok, ssl = pcall(require, conn.ssl_module)
assert(ok, "ssl_module '"..tostring(conn.ssl_module).."' not found, secure connections unavailable")
assert(type(params) == "table", "expecting .secure_params to be a table, got: "..type(params))
params.mode = params.mode or "client"
assert(params.mode == "client", "secure parameter 'mode' must be set to 'client' if given, got: "..tostring(params.mode))
local ctx, err = ssl.newcontext(params)
if not ctx then
error("Couldn't create secure context: "..tostring(err))
end
end
end

View File

@@ -0,0 +1,67 @@
-- base connector class for non-buffered reading.
--
-- Use this base class if the sockets DO yield.
-- So Copas or OpenResty for example, when using LuaSocket
-- use the buffered base class.
--
-- NOTE: when the send operation can also yield (as is the case with Copas and
-- OpenResty) you should wrap the `send` handler in a lock to prevent a half-send
-- message from being interleaved by another message send from another thread.
--
-- @class mqtt.connector.base.non_buffered_base
local non_buffered = {
type = "non-buffered, yielding i/o",
timeout = 30 -- default timeout
}
non_buffered.__index = non_buffered
-- we need to specify signals for these conditions such that the client
-- doesn't have to rely on magic strings like "timeout", "wantread", etc.
-- the connector is responsible for translating those connector specific
-- messages to a generic signal
non_buffered.signal_idle = {} -- read timeout occured, so we're idle need to come back later and try again
non_buffered.signal_closed = {} -- remote closed the connection
--- Validate connection options.
function non_buffered:shutdown() -- luacheck: ignore
error("method 'shutdown' on connector wasn't implemented")
end
--- Clears consumed bytes.
-- Called by the mqtt client when the consumed bytes from the buffer are handled
-- and can be cleared from the buffer.
-- A no-op for the non-buffered classes, since the sockets yield when incomplete.
function non_buffered.buffer_clear()
end
--- Retrieves the requested number of bytes from the socket.
-- If the receive errors, because of a closed connection it should return
-- `nil, self.signal_closed` to indicate this. Any other errors can be returned
-- as a regular `nil, err`.
-- @tparam size int number of retrieve to return.
-- @return data, or `false, err`, where `err` can be a signal.
function non_buffered:receive(size) -- luacheck: ignore
error("method 'receive' on non-buffered connector wasn't implemented")
end
--- Open network connection to `self.host` and `self.port`.
-- @return `true` on success, or `false, err` on failure
function non_buffered:connect() -- luacheck: ignore
error("method 'connect' on connector wasn't implemented")
end
--- Shutdown the network connection.
function non_buffered:shutdown() -- luacheck: ignore
error("method 'shutdown' on connector wasn't implemented")
end
--- Shutdown the network connection.
-- @tparam data string data to send
-- @return `true` on success, or `false, err` on failure
function non_buffered:send(data) -- luacheck: ignore
error("method 'send' on connector wasn't implemented")
end
return non_buffered