mqtt-core 0.0.1.ci.release
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/mqtt/core/client/acknowledgement.rb +31 -0
- data/lib/mqtt/core/client/client_id_generator.rb +19 -0
- data/lib/mqtt/core/client/connection.rb +193 -0
- data/lib/mqtt/core/client/enumerable_subscription.rb +224 -0
- data/lib/mqtt/core/client/filesystem_session_store.rb +197 -0
- data/lib/mqtt/core/client/memory_session_store.rb +84 -0
- data/lib/mqtt/core/client/qos0_session_store.rb +56 -0
- data/lib/mqtt/core/client/qos2_session_store.rb +45 -0
- data/lib/mqtt/core/client/qos_tracker.rb +119 -0
- data/lib/mqtt/core/client/retry_strategy.rb +63 -0
- data/lib/mqtt/core/client/session.rb +195 -0
- data/lib/mqtt/core/client/session_store.rb +128 -0
- data/lib/mqtt/core/client/socket_factory.rb +268 -0
- data/lib/mqtt/core/client/subscription.rb +109 -0
- data/lib/mqtt/core/client/uri.rb +77 -0
- data/lib/mqtt/core/client.rb +700 -0
- data/lib/mqtt/core/packet/connect.rb +39 -0
- data/lib/mqtt/core/packet/publish.rb +45 -0
- data/lib/mqtt/core/packet/subscribe.rb +190 -0
- data/lib/mqtt/core/packet/unsubscribe.rb +21 -0
- data/lib/mqtt/core/packet.rb +168 -0
- data/lib/mqtt/core/type/binary.rb +35 -0
- data/lib/mqtt/core/type/bit_flags.rb +89 -0
- data/lib/mqtt/core/type/boolean_byte.rb +48 -0
- data/lib/mqtt/core/type/fixed_int.rb +43 -0
- data/lib/mqtt/core/type/list.rb +41 -0
- data/lib/mqtt/core/type/password.rb +36 -0
- data/lib/mqtt/core/type/properties.rb +124 -0
- data/lib/mqtt/core/type/reason_codes.rb +60 -0
- data/lib/mqtt/core/type/remaining.rb +30 -0
- data/lib/mqtt/core/type/shape.rb +177 -0
- data/lib/mqtt/core/type/sub_type.rb +34 -0
- data/lib/mqtt/core/type/utf8_string.rb +39 -0
- data/lib/mqtt/core/type/utf8_string_pair.rb +29 -0
- data/lib/mqtt/core/type/var_int.rb +56 -0
- data/lib/mqtt/core/version.rb +10 -0
- data/lib/mqtt/core.rb +5 -0
- data/lib/mqtt/errors.rb +39 -0
- data/lib/mqtt/logger.rb +92 -0
- data/lib/mqtt/open.rb +239 -0
- data/lib/mqtt/options.rb +59 -0
- data/lib/mqtt/version.rb +6 -0
- data/lib/mqtt.rb +3 -0
- data/lib/patches/openssl.rb +19 -0
- metadata +98 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# list of type
|
|
7
|
+
class List
|
|
8
|
+
attr_reader :type
|
|
9
|
+
|
|
10
|
+
def initialize(type)
|
|
11
|
+
@type = type
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def item_type
|
|
15
|
+
type
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def default_value
|
|
19
|
+
[]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read(io)
|
|
23
|
+
result = []
|
|
24
|
+
result << type.read(io) until io.eof?
|
|
25
|
+
result.freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write(value, io)
|
|
29
|
+
value&.each { |v| type.write(v, io) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Array<#type>]
|
|
33
|
+
def from(arr, **)
|
|
34
|
+
arr ||= []
|
|
35
|
+
arr = [arr] unless arr.respond_to?(:map)
|
|
36
|
+
arr.map { |v| type.from(v, **) }.freeze
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# Password data - input can be a proc to pull a password
|
|
7
|
+
# converts to String with binary encoding
|
|
8
|
+
module Password
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# return [String with binary encoding]
|
|
12
|
+
def read(io)
|
|
13
|
+
size = io.read(2)&.unpack1('S>')
|
|
14
|
+
io.read(size)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def write(value, io)
|
|
18
|
+
value ||= default_value
|
|
19
|
+
io.write([value.size, value].pack('S>A*'))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [String(binary)]
|
|
23
|
+
def from(value, **)
|
|
24
|
+
return nil if value.nil?
|
|
25
|
+
|
|
26
|
+
value = value.call if value.respond_to?(:call)
|
|
27
|
+
value.to_s.b
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def default_value
|
|
31
|
+
@default_value ||= ''.b.freeze
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# Properties Map (used since MQTT 5.0)
|
|
7
|
+
class Properties
|
|
8
|
+
# @!parse class PropertyType < Data; end
|
|
9
|
+
PropertyType = Data.define(:id, :name, :type, :packet_types) do
|
|
10
|
+
class << self
|
|
11
|
+
def create(id, name, type, packet_types = :all, types: {})
|
|
12
|
+
new(id, name, resolve_type(type, types), packet_types)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resolve_type(input_type, types)
|
|
16
|
+
case input_type
|
|
17
|
+
when Symbol
|
|
18
|
+
types.fetch(input_type) { raise ProtocolError, "Unknown property type #{input_type}" }
|
|
19
|
+
when Array
|
|
20
|
+
List.new(resolve_type(input_type.first, types))
|
|
21
|
+
else
|
|
22
|
+
input_type
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(id:, name:, type:, packet_types: [:all])
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def from(value)
|
|
32
|
+
type.from(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read(io, into)
|
|
36
|
+
if type.is_a?(Type::List)
|
|
37
|
+
(into[name] ||= []) << type.item_type.read(io)
|
|
38
|
+
else
|
|
39
|
+
into[name] = type.read(io)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write(value, io, write_type: type)
|
|
44
|
+
if write_type.is_a?(Type::List)
|
|
45
|
+
value.each { |item| write(item, io, write_type: type.item_type) }
|
|
46
|
+
else
|
|
47
|
+
VarInt.write(id, io)
|
|
48
|
+
write_type.write(value, io)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def for_packet?(packet_type_name)
|
|
53
|
+
return true if packet_types == :all
|
|
54
|
+
|
|
55
|
+
packet_types.include?(packet_type_name)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize(packet_type_name, property_types)
|
|
60
|
+
@property_types = property_types.select { |pt| pt.for_packet?(packet_type_name) }.freeze
|
|
61
|
+
@property_map =
|
|
62
|
+
Hash.new { |_h, k| raise ProtocolError, "Unknown property type #{k} for #{packet_type_name}" }
|
|
63
|
+
.merge!(@property_types.to_h { |pt| [pt.name, pt] })
|
|
64
|
+
.merge!(@property_types.to_h { |pt| [pt.id, pt] })
|
|
65
|
+
.freeze
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sub_properties
|
|
69
|
+
@property_types.to_h { |pt| [pt.name, pt.type] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def read(io)
|
|
73
|
+
default_value.tap do |output|
|
|
74
|
+
# Where properties is the last thing in a packet and there are no properties, the length property
|
|
75
|
+
# is not required to be set
|
|
76
|
+
next if io.eof?
|
|
77
|
+
|
|
78
|
+
Packet.read_sio(io) do |sio|
|
|
79
|
+
until sio.eof?
|
|
80
|
+
id = VarInt.read(sio)
|
|
81
|
+
property_type = @property_map.fetch(id) do
|
|
82
|
+
raise ProtocolError, format('Unknown property id 0x%02x', id)
|
|
83
|
+
end
|
|
84
|
+
property_type.read(sio, output)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def write(values, io)
|
|
91
|
+
Packet.write_sio(io) do |sio|
|
|
92
|
+
values.each_pair do |name, value|
|
|
93
|
+
property_type = @property_map.fetch(name) { raise ProtocolError, "Unknown property name #{name}" }
|
|
94
|
+
property_type.write(value, sio)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @param [Hash|nil] values
|
|
100
|
+
def from(values, data:)
|
|
101
|
+
values ||= default_value
|
|
102
|
+
raise ArgumentError 'Properties values must be a Hash' unless values.respond_to?(:each_pair)
|
|
103
|
+
|
|
104
|
+
# properties with entries at the top level of the data structure override the underlying hash value
|
|
105
|
+
@property_types.filter_map do |property_type|
|
|
106
|
+
property_type.name.tap { |name| values[name] = data.delete(name) if data.key?(name) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# now make sure every entry in values is valid and converted to type
|
|
110
|
+
values.filter_map do |(name, value)|
|
|
111
|
+
property_type = @property_map.fetch(name) { raise ArgumentError, "Unknown property name #{name}" }
|
|
112
|
+
next nil unless (filled_value = property_type.from(value))
|
|
113
|
+
|
|
114
|
+
[name, filled_value]
|
|
115
|
+
end.to_h.freeze
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def default_value
|
|
119
|
+
{}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'fixed_int'
|
|
4
|
+
|
|
5
|
+
module MQTT
|
|
6
|
+
module Core
|
|
7
|
+
module Type
|
|
8
|
+
# A type that resolves to a reason code
|
|
9
|
+
class ReasonCodes
|
|
10
|
+
# @!parse class ReasonCode < ::Data; end
|
|
11
|
+
ReasonCode = Data.define(:code, :name, :packet_types, :error) do
|
|
12
|
+
def for_packet?(packet_type_name)
|
|
13
|
+
code == 0xff || packet_types.include?(packet_type_name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def success?
|
|
17
|
+
!failed?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def failed?
|
|
21
|
+
code >= 0x80
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
format '%<name>s(0x%02<code>x)', name:, code:
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(valid_codes)
|
|
30
|
+
@reason_code_map = valid_codes.to_h { |rc| [rc.code, rc] }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(...)
|
|
34
|
+
@reason_code_map.fetch(...)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read(io)
|
|
38
|
+
# if (s)io is at eof then the reason code is assumed to be success
|
|
39
|
+
fetch(Int8.read(io) || 0x00) { |rc| raise ProtocolError, "Invalid Reason Code #{rc}" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def write(value, io)
|
|
43
|
+
# use from here as sometimes the code is applied as a default
|
|
44
|
+
Int8.write(from(value).code, io)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def from(input_value, **)
|
|
48
|
+
input_value = input_value.code if input_value.is_a?(ReasonCode)
|
|
49
|
+
raise ArgumentError, "Invalid Reason Code #{input_value}" unless input_value.respond_to?(:to_i)
|
|
50
|
+
|
|
51
|
+
fetch(input_value.to_i) { |rc| raise ArgumentError, "Invalid Reason Code #{rc}" }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def default_value
|
|
55
|
+
fetch(0x00)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# remaining payload, no length prefix, reads to EOF (StringIO) and writes the whole value as binary
|
|
7
|
+
# string
|
|
8
|
+
module Remaining
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def read(io)
|
|
12
|
+
# read to end of file (which only works because it is only ever used with a StringIO
|
|
13
|
+
io.read
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write(value, io)
|
|
17
|
+
io.write(value.to_s.b)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def from(value, **)
|
|
21
|
+
value.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def default_value
|
|
25
|
+
''.b
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# Base module for structured MQTT types {Packet} and {SubType}
|
|
7
|
+
module Shape
|
|
8
|
+
Field = Data.define(:name, :type, :condition) do
|
|
9
|
+
def write(obj, io)
|
|
10
|
+
type.write(obj.send(name), io) if obj.instance_exec(&condition)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(obj, io)
|
|
14
|
+
value = type.read(io) if obj.instance_exec(&condition)
|
|
15
|
+
value = type.default_value if value.nil? && type.respond_to?(:default_value)
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# methods for defining fields
|
|
21
|
+
module Definition
|
|
22
|
+
attr_reader :fields
|
|
23
|
+
|
|
24
|
+
def flags(*flags)
|
|
25
|
+
Type::BitFlags.new(*flags)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def list(type, **fields, &)
|
|
29
|
+
if fields.any?
|
|
30
|
+
klass = Class.new(Type::SubType)
|
|
31
|
+
klass.instance_variable_set(
|
|
32
|
+
:@fields, resolve_fields(fields).tap { |resolved| define_field_methods(resolved, klass:) }
|
|
33
|
+
)
|
|
34
|
+
# Define additional methods and aliases (after field methods are defined!)
|
|
35
|
+
klass.class_eval(&) if block_given?
|
|
36
|
+
type = const_set(type.to_s.split('_').map(&:capitalize).join, klass)
|
|
37
|
+
elsif type.is_a?(Symbol)
|
|
38
|
+
type = self::VALUE_TYPES[type]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Type::List.new(type)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def resolve_fields(fields)
|
|
45
|
+
fields.map do |(name, type)|
|
|
46
|
+
type, condition = field_type_for(type)
|
|
47
|
+
Field.new(name:, type:, condition:)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def field_type_for(type_info)
|
|
52
|
+
return field_type_for({ type: type_info, if: true }) unless type_info.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
type, type_if = type_info.values_at(:type, :if)
|
|
55
|
+
type = self::VALUE_TYPES[type] if type.is_a?(Symbol)
|
|
56
|
+
condition =
|
|
57
|
+
case type_if
|
|
58
|
+
when Symbol
|
|
59
|
+
-> { send(type_if) }
|
|
60
|
+
when Proc
|
|
61
|
+
type_if
|
|
62
|
+
else
|
|
63
|
+
-> { true }
|
|
64
|
+
end
|
|
65
|
+
[type, condition]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Avoid name clashes
|
|
69
|
+
def sub_property_method(_name, property_name)
|
|
70
|
+
property_name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def define_field_methods(fields, klass: self)
|
|
74
|
+
fields.map do |field|
|
|
75
|
+
name, type, _condition = field.deconstruct
|
|
76
|
+
|
|
77
|
+
if type.respond_to?(:sub_properties)
|
|
78
|
+
define_property_readers(name, type, klass:)
|
|
79
|
+
define_hash_reader(name, type, klass:)
|
|
80
|
+
else
|
|
81
|
+
klass.define_method(name) { @data[name] }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def define_property_readers(name, type, klass:)
|
|
87
|
+
type.sub_properties.each_key do |property_name|
|
|
88
|
+
method = klass.sub_property_method(name, property_name) if klass.respond_to?(:sub_property_method)
|
|
89
|
+
klass.define_method(method || property_name) { @data.dig(name, property_name) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def define_hash_reader(name, type, klass:)
|
|
94
|
+
klass.define_method(name) do
|
|
95
|
+
type.sub_properties.keys.filter_map do |property_name|
|
|
96
|
+
method = klass.sub_property_method(name, property_name) if klass.respond_to?(:sub_property_method)
|
|
97
|
+
value = send(method || property_name)
|
|
98
|
+
[property_name, value] unless value.nil?
|
|
99
|
+
end.to_h.freeze
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def initialize(*deserialize_args, **data)
|
|
105
|
+
@data = {}
|
|
106
|
+
apply_data(defaults)
|
|
107
|
+
if deserialize_args.any?
|
|
108
|
+
deserialize(*deserialize_args)
|
|
109
|
+
else
|
|
110
|
+
apply_data(data) if data.any?
|
|
111
|
+
apply_overrides(@data)
|
|
112
|
+
end
|
|
113
|
+
validate if respond_to?(:validate, true)
|
|
114
|
+
@data.freeze
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def serialize(io)
|
|
118
|
+
serialize_fields(self.class.fields, io)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_h
|
|
122
|
+
@data
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def deconstruct_keys(*keys)
|
|
126
|
+
to_h.slice(keys)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# rubocop:disable Metrics/AbcSize
|
|
132
|
+
def apply_data(data)
|
|
133
|
+
self.class.fields.each do |f|
|
|
134
|
+
# source @data (defaults)
|
|
135
|
+
# data[f.name] if data.key?(f.name) - overwrites or merges with defaults
|
|
136
|
+
# properties if data[sub_property_name]
|
|
137
|
+
if f.type.respond_to?(:sub_properties)
|
|
138
|
+
result = @data[f.name] ||= {}
|
|
139
|
+
result.merge!(data.delete(f.name)) if data.key?(f.name)
|
|
140
|
+
f.type.sub_properties.each do |property_name, property_type|
|
|
141
|
+
result[property_name] = property_type.from(data.delete(property_name)) if data.key?(property_name)
|
|
142
|
+
end
|
|
143
|
+
elsif data.key?(f.name)
|
|
144
|
+
@data[f.name] = f.type.from(data.delete(f.name))
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
raise ArgumentError, "Unused data for #{self.class.name} - #{data}" unless data.empty?
|
|
148
|
+
end
|
|
149
|
+
# rubocop:enable Metrics/AbcSize
|
|
150
|
+
|
|
151
|
+
# applied in the same way as user supplied data
|
|
152
|
+
def defaults
|
|
153
|
+
{}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def apply_overrides(data)
|
|
157
|
+
# do nothing by default
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def serialize_fields(fields, io)
|
|
161
|
+
fields.each { |f| f.write(self, io) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def deserialize_fields(fields, io)
|
|
165
|
+
fields.filter_map do |f|
|
|
166
|
+
value = f.read(self, io)
|
|
167
|
+
[f.name, value] if value
|
|
168
|
+
end.to_h
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def deserialize(io)
|
|
172
|
+
@data.merge!(deserialize_fields(self.class.fields, io))
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'shape'
|
|
4
|
+
|
|
5
|
+
module MQTT
|
|
6
|
+
module Core
|
|
7
|
+
module Type
|
|
8
|
+
# Class representing an MQTT SubType (defined for structured lists)
|
|
9
|
+
class SubType
|
|
10
|
+
include Shape
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# @!attribute [r] fields
|
|
14
|
+
# @return [Array] fields injected by List
|
|
15
|
+
attr_reader :fields
|
|
16
|
+
|
|
17
|
+
def read(io)
|
|
18
|
+
new(io)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(value, io)
|
|
22
|
+
value.serialize(io)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# TODO: if we had a subtype that was not within a list
|
|
26
|
+
# then we'd probably list the fields and pull top level content from the data
|
|
27
|
+
def from(value, **)
|
|
28
|
+
new(**value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# UTF8 Encoded String written with length as prefix
|
|
7
|
+
module UTF8String
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def read(io)
|
|
11
|
+
size = io.read(2)&.unpack1('S>')
|
|
12
|
+
io.read(size).force_encoding(Encoding::UTF_8).tap { |s| raise EncodingError unless valid_utf8?(s) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def from(value, **)
|
|
16
|
+
return nil if value.nil?
|
|
17
|
+
|
|
18
|
+
value = value.to_s
|
|
19
|
+
return value if valid_utf8?(value)
|
|
20
|
+
|
|
21
|
+
value.encode(Encoding::UTF_8).tap { |s| raise EncodingError unless valid_utf8?(s) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def write(value, io)
|
|
25
|
+
value ||= default_value
|
|
26
|
+
io.write([value.size, value].pack('S>A*'))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def default_value
|
|
30
|
+
@default_value ||= ''.encode(Encoding::UTF_8).freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid_utf8?(str)
|
|
34
|
+
str.encoding == Encoding::UTF_8 && str.valid_encoding? && !str.include?("\u0000")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# Key value pair as UTF8 encoded strings
|
|
7
|
+
module UTF8StringPair
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def read(io)
|
|
11
|
+
[UTF8String.read(io), UTF8String.read(io)].freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def from(value, **)
|
|
15
|
+
raise ArgumentError, 'Value must be Array<String,String>' unless value.is_a?(Array) && value.size == 2
|
|
16
|
+
|
|
17
|
+
value.map { |v| UTF8String.from(v) }.freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def write(value, io)
|
|
21
|
+
raise ProtocolError, 'Value must be Array<String,String>' unless value.is_a?(Array) && value.size == 2
|
|
22
|
+
|
|
23
|
+
UTF8String.write(value[0], io)
|
|
24
|
+
UTF8String.write(value[1], io)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module Core
|
|
5
|
+
module Type
|
|
6
|
+
# Variable Byte Integer
|
|
7
|
+
module VarInt
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Maximum variable integer (hence maximum packet size = 256MB)
|
|
11
|
+
MAX_INTVAR = 268_435_455
|
|
12
|
+
|
|
13
|
+
def read(io)
|
|
14
|
+
value = 0
|
|
15
|
+
multiplier = 1
|
|
16
|
+
|
|
17
|
+
loop do
|
|
18
|
+
encoded_byte = io.readbyte
|
|
19
|
+
return value unless encoded_byte
|
|
20
|
+
|
|
21
|
+
value += (encoded_byte & 0x7F) * multiplier
|
|
22
|
+
|
|
23
|
+
raise ProtocolError, 'Malformed Variable Byte Integer' if multiplier > 0x80**3
|
|
24
|
+
|
|
25
|
+
multiplier *= 0x80
|
|
26
|
+
|
|
27
|
+
return value if encoded_byte.nobits?(128)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def from(value)
|
|
32
|
+
value ||= 0
|
|
33
|
+
raise ArgumentError, 'Vaiue must be Integer' unless value.respond_to?(:to_int)
|
|
34
|
+
|
|
35
|
+
value = value.to_i
|
|
36
|
+
raise ArgumentError, "Value out of range (0 - #{MAX_INTVAR})" unless value.between?(0, MAX_INTVAR)
|
|
37
|
+
|
|
38
|
+
value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def write(value, io)
|
|
42
|
+
value ||= 0
|
|
43
|
+
raise ProtocolError, "Value out of range (0 - #{MAX_INTVAR})" unless value.between?(0, MAX_INTVAR)
|
|
44
|
+
|
|
45
|
+
loop do
|
|
46
|
+
encoded_byte = value % 0x80
|
|
47
|
+
value /= 0x80
|
|
48
|
+
encoded_byte |= 0x80 if value.positive?
|
|
49
|
+
io.write([encoded_byte].pack('C'))
|
|
50
|
+
break if value.zero?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/mqtt/core.rb
ADDED
data/lib/mqtt/errors.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
|
|
5
|
+
module MQTT
|
|
6
|
+
# Super-class for other MQTT related exceptions
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
# included in protocol specific errors that are retriable
|
|
9
|
+
module Retriable
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Raised if there is a problem with data received from a remote host
|
|
14
|
+
class ProtocolError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when trying to perform a function but no connection is available
|
|
17
|
+
class ConnectionError < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised in the disconnect handler when the client expects a session, but the broker does not have one.
|
|
20
|
+
class SessionNotPresent < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised in the disconnect handler when the client session has expired before it can reconnect to the broker.
|
|
23
|
+
class SessionExpired < Error; end
|
|
24
|
+
|
|
25
|
+
# A ResponseError will be raised from packet acknowledgements
|
|
26
|
+
class ResponseError < Error; end
|
|
27
|
+
|
|
28
|
+
RETRIABLE_NETWORK_ERRORS = [
|
|
29
|
+
Errno::ECONNABORTED, # Connection aborted
|
|
30
|
+
Errno::ECONNRESET, # Connection reset
|
|
31
|
+
Errno::EHOSTUNREACH, # No route to host
|
|
32
|
+
Errno::ENETUNREACH, # Network unreachable
|
|
33
|
+
Errno::EPIPE, # Broken pipe
|
|
34
|
+
Errno::ETIMEDOUT, # Connection timed out
|
|
35
|
+
Errno::EINVAL, # Invalid argument (Windows-specific)
|
|
36
|
+
SocketError, # DNS and basic socket errors
|
|
37
|
+
IOError # Generic IO errors (including IO::TimeoutError)
|
|
38
|
+
].freeze
|
|
39
|
+
end
|