go-mqtt 0.0.1
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/LICENSE.md +21 -0
- data/NEWS.md +150 -0
- data/README.md +237 -0
- data/lib/mqtt/client.rb +755 -0
- data/lib/mqtt/openssl_fix.rb +29 -0
- data/lib/mqtt/packet.rb +1049 -0
- data/lib/mqtt/proxy.rb +115 -0
- data/lib/mqtt/sn/packet.rb +720 -0
- data/lib/mqtt/version.rb +4 -0
- data/lib/mqtt.rb +48 -0
- data/spec/mqtt_client_spec.rb +1124 -0
- data/spec/mqtt_packet_spec.rb +2012 -0
- data/spec/mqtt_proxy_spec.rb +8 -0
- data/spec/mqtt_sn_packet_spec.rb +1721 -0
- data/spec/mqtt_version_spec.rb +23 -0
- data/spec/zz_client_integration_spec.rb +177 -0
- metadata +152 -0
data/lib/mqtt/packet.rb
ADDED
@@ -0,0 +1,1049 @@
|
|
1
|
+
# encoding: BINARY
|
2
|
+
|
3
|
+
module MQTT
|
4
|
+
# Class representing a MQTT Packet
|
5
|
+
# Performs binary encoding and decoding of headers
|
6
|
+
class Packet
|
7
|
+
# The version number of the MQTT protocol to use (default 3.1.0)
|
8
|
+
attr_accessor :version
|
9
|
+
|
10
|
+
# Identifier to link related control packets together
|
11
|
+
attr_accessor :id
|
12
|
+
|
13
|
+
# Array of 4 bits in the fixed header
|
14
|
+
attr_accessor :flags
|
15
|
+
|
16
|
+
# The length of the parsed packet body
|
17
|
+
attr_reader :body_length
|
18
|
+
|
19
|
+
# Default attribute values
|
20
|
+
ATTR_DEFAULTS = {
|
21
|
+
:version => '3.1.0',
|
22
|
+
:id => 0,
|
23
|
+
:body_length => nil
|
24
|
+
}
|
25
|
+
|
26
|
+
# Read in a packet from a socket
|
27
|
+
def self.read(socket)
|
28
|
+
# Read in the packet header and create a new packet object
|
29
|
+
packet = create_from_header(
|
30
|
+
read_byte(socket)
|
31
|
+
)
|
32
|
+
packet.validate_flags
|
33
|
+
|
34
|
+
# Read in the packet length
|
35
|
+
multiplier = 1
|
36
|
+
body_length = 0
|
37
|
+
pos = 1
|
38
|
+
|
39
|
+
loop do
|
40
|
+
digit = read_byte(socket)
|
41
|
+
body_length += ((digit & 0x7F) * multiplier)
|
42
|
+
multiplier *= 0x80
|
43
|
+
pos += 1
|
44
|
+
break if (digit & 0x80).zero? || pos > 4
|
45
|
+
end
|
46
|
+
|
47
|
+
# Store the expected body length in the packet
|
48
|
+
packet.instance_variable_set('@body_length', body_length)
|
49
|
+
|
50
|
+
# Read in the packet body
|
51
|
+
packet.parse_body(socket.read(body_length))
|
52
|
+
|
53
|
+
packet
|
54
|
+
end
|
55
|
+
|
56
|
+
# Parse buffer into new packet object
|
57
|
+
def self.parse(buffer)
|
58
|
+
packet = parse_header(buffer)
|
59
|
+
packet.parse_body(buffer)
|
60
|
+
packet
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parse the header and create a new packet object of the correct type
|
64
|
+
# The header is removed from the buffer passed into this function
|
65
|
+
def self.parse_header(buffer)
|
66
|
+
# Check that the packet is a long as the minimum packet size
|
67
|
+
if buffer.bytesize < 2
|
68
|
+
raise ProtocolException, 'Invalid packet: less than 2 bytes long'
|
69
|
+
end
|
70
|
+
|
71
|
+
# Create a new packet object
|
72
|
+
bytes = buffer.unpack('C5')
|
73
|
+
packet = create_from_header(bytes.first)
|
74
|
+
packet.validate_flags
|
75
|
+
|
76
|
+
# Parse the packet length
|
77
|
+
body_length = 0
|
78
|
+
multiplier = 1
|
79
|
+
pos = 1
|
80
|
+
|
81
|
+
loop do
|
82
|
+
if buffer.bytesize <= pos
|
83
|
+
raise ProtocolException, 'The packet length header is incomplete'
|
84
|
+
end
|
85
|
+
|
86
|
+
digit = bytes[pos]
|
87
|
+
body_length += ((digit & 0x7F) * multiplier)
|
88
|
+
multiplier *= 0x80
|
89
|
+
pos += 1
|
90
|
+
break if (digit & 0x80).zero? || pos > 4
|
91
|
+
end
|
92
|
+
|
93
|
+
# Store the expected body length in the packet
|
94
|
+
packet.instance_variable_set('@body_length', body_length)
|
95
|
+
|
96
|
+
# Delete the fixed header from the raw packet passed in
|
97
|
+
buffer.slice!(0...pos)
|
98
|
+
|
99
|
+
packet
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create a new packet object from the first byte of a MQTT packet
|
103
|
+
def self.create_from_header(byte)
|
104
|
+
# Work out the class
|
105
|
+
type_id = ((byte & 0xF0) >> 4)
|
106
|
+
packet_class = MQTT::PACKET_TYPES[type_id]
|
107
|
+
if packet_class.nil?
|
108
|
+
raise ProtocolException, "Invalid packet type identifier: #{type_id}"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Convert the last 4 bits of byte into array of true/false
|
112
|
+
flags = (0..3).map { |i| byte & (2**i) != 0 }
|
113
|
+
|
114
|
+
# Create a new packet object
|
115
|
+
packet_class.new(:flags => flags)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Create a new empty packet
|
119
|
+
def initialize(args = {})
|
120
|
+
# We must set flags before the other values
|
121
|
+
@flags = [false, false, false, false]
|
122
|
+
update_attributes(ATTR_DEFAULTS.merge(args))
|
123
|
+
end
|
124
|
+
|
125
|
+
# Set packet attributes from a hash of attribute names and values
|
126
|
+
def update_attributes(attr = {})
|
127
|
+
attr.each_pair do |k, v|
|
128
|
+
if v.is_a?(Array) || v.is_a?(Hash)
|
129
|
+
send("#{k}=", v.dup)
|
130
|
+
else
|
131
|
+
send("#{k}=", v)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Get the identifer for this packet type
|
137
|
+
def type_id
|
138
|
+
index = MQTT::PACKET_TYPES.index(self.class)
|
139
|
+
raise "Invalid packet type: #{self.class}" if index.nil?
|
140
|
+
index
|
141
|
+
end
|
142
|
+
|
143
|
+
# Get the name of the packet type as a string in capitals
|
144
|
+
# (like the MQTT specification uses)
|
145
|
+
#
|
146
|
+
# Example: CONNACK
|
147
|
+
def type_name
|
148
|
+
self.class.name.split('::').last.upcase
|
149
|
+
end
|
150
|
+
|
151
|
+
# Set the protocol version number
|
152
|
+
def version=(arg)
|
153
|
+
@version = arg.to_s
|
154
|
+
end
|
155
|
+
|
156
|
+
# Set the length of the packet body
|
157
|
+
def body_length=(arg)
|
158
|
+
@body_length = arg.to_i
|
159
|
+
end
|
160
|
+
|
161
|
+
# Parse the body (variable header and payload) of a packet
|
162
|
+
def parse_body(buffer)
|
163
|
+
return if buffer.bytesize == body_length
|
164
|
+
|
165
|
+
raise ProtocolException, "Failed to parse packet - input buffer (#{buffer.bytesize}) is not the same as the body length header (#{body_length})"
|
166
|
+
end
|
167
|
+
|
168
|
+
# Get serialisation of packet's body (variable header and payload)
|
169
|
+
def encode_body
|
170
|
+
'' # No body by default
|
171
|
+
end
|
172
|
+
|
173
|
+
# Serialise the packet
|
174
|
+
def to_s
|
175
|
+
# Encode the fixed header
|
176
|
+
header = [
|
177
|
+
((type_id.to_i & 0x0F) << 4) |
|
178
|
+
(flags[3] ? 0x8 : 0x0) |
|
179
|
+
(flags[2] ? 0x4 : 0x0) |
|
180
|
+
(flags[1] ? 0x2 : 0x0) |
|
181
|
+
(flags[0] ? 0x1 : 0x0)
|
182
|
+
]
|
183
|
+
|
184
|
+
# Get the packet's variable header and payload
|
185
|
+
body = encode_body
|
186
|
+
|
187
|
+
# Check that that packet isn't too big
|
188
|
+
body_length = body.bytesize
|
189
|
+
if body_length > 268_435_455
|
190
|
+
raise 'Error serialising packet: body is more than 256MB'
|
191
|
+
end
|
192
|
+
|
193
|
+
# Build up the body length field bytes
|
194
|
+
loop do
|
195
|
+
digit = (body_length % 128)
|
196
|
+
body_length = body_length.div(128)
|
197
|
+
# if there are more digits to encode, set the top bit of this digit
|
198
|
+
digit |= 0x80 if body_length > 0
|
199
|
+
header.push(digit)
|
200
|
+
break if body_length <= 0
|
201
|
+
end
|
202
|
+
|
203
|
+
# Convert header to binary and add on body
|
204
|
+
header.pack('C*') + body
|
205
|
+
end
|
206
|
+
|
207
|
+
# Check that fixed header flags are valid for types that don't use the flags
|
208
|
+
# @private
|
209
|
+
def validate_flags
|
210
|
+
return if flags == [false, false, false, false]
|
211
|
+
|
212
|
+
raise ProtocolException, "Invalid flags in #{type_name} packet header"
|
213
|
+
end
|
214
|
+
|
215
|
+
# Returns a human readable string
|
216
|
+
def inspect
|
217
|
+
"\#<#{self.class}>"
|
218
|
+
end
|
219
|
+
|
220
|
+
# Read and unpack a single byte from a socket
|
221
|
+
def self.read_byte(socket)
|
222
|
+
byte = socket.getbyte
|
223
|
+
raise ProtocolException, 'Failed to read byte from socket' if byte.nil?
|
224
|
+
|
225
|
+
byte
|
226
|
+
end
|
227
|
+
|
228
|
+
protected
|
229
|
+
|
230
|
+
# Encode an array of bytes and return them
|
231
|
+
def encode_bytes(*bytes)
|
232
|
+
bytes.pack('C*')
|
233
|
+
end
|
234
|
+
|
235
|
+
# Encode an array of bits and return them
|
236
|
+
def encode_bits(bits)
|
237
|
+
[bits.map { |b| b ? '1' : '0' }.join].pack('b*')
|
238
|
+
end
|
239
|
+
|
240
|
+
# Encode a 16-bit unsigned integer and return it
|
241
|
+
def encode_short(val)
|
242
|
+
raise 'Value too big for short' if val > 0xffff
|
243
|
+
[val.to_i].pack('n')
|
244
|
+
end
|
245
|
+
|
246
|
+
# Encode a UTF-8 string and return it
|
247
|
+
# (preceded by the length of the string)
|
248
|
+
def encode_string(str)
|
249
|
+
str = str.to_s.encode('UTF-8')
|
250
|
+
|
251
|
+
# Force to binary, when assembling the packet
|
252
|
+
str.force_encoding('ASCII-8BIT')
|
253
|
+
encode_short(str.bytesize) + str
|
254
|
+
end
|
255
|
+
|
256
|
+
# Remove a 16-bit unsigned integer from the front of buffer
|
257
|
+
def shift_short(buffer)
|
258
|
+
bytes = buffer.slice!(0..1)
|
259
|
+
bytes.unpack('n').first
|
260
|
+
end
|
261
|
+
|
262
|
+
# Remove one byte from the front of the string
|
263
|
+
def shift_byte(buffer)
|
264
|
+
buffer.slice!(0...1).unpack('C').first
|
265
|
+
end
|
266
|
+
|
267
|
+
# Remove 8 bits from the front of buffer
|
268
|
+
def shift_bits(buffer)
|
269
|
+
buffer.slice!(0...1).unpack('b8').first.split('').map { |b| b == '1' }
|
270
|
+
end
|
271
|
+
|
272
|
+
# Remove n bytes from the front of buffer
|
273
|
+
def shift_data(buffer, bytes)
|
274
|
+
buffer.slice!(0...bytes)
|
275
|
+
end
|
276
|
+
|
277
|
+
# Remove string from the front of buffer
|
278
|
+
def shift_string(buffer)
|
279
|
+
len = shift_short(buffer)
|
280
|
+
str = shift_data(buffer, len)
|
281
|
+
# Strings in MQTT v3.1 are all UTF-8
|
282
|
+
str.force_encoding('UTF-8')
|
283
|
+
end
|
284
|
+
|
285
|
+
## PACKET SUBCLASSES ##
|
286
|
+
|
287
|
+
# Class representing an MQTT Publish message
|
288
|
+
class Publish < MQTT::Packet
|
289
|
+
# Duplicate delivery flag
|
290
|
+
attr_accessor :duplicate
|
291
|
+
|
292
|
+
# Retain flag
|
293
|
+
attr_accessor :retain
|
294
|
+
|
295
|
+
# Quality of Service level (0, 1, 2)
|
296
|
+
attr_accessor :qos
|
297
|
+
|
298
|
+
# The topic name to publish to
|
299
|
+
attr_accessor :topic
|
300
|
+
|
301
|
+
# The data to be published
|
302
|
+
attr_accessor :payload
|
303
|
+
|
304
|
+
# Default attribute values
|
305
|
+
ATTR_DEFAULTS = {
|
306
|
+
:topic => nil,
|
307
|
+
:payload => ''
|
308
|
+
}
|
309
|
+
|
310
|
+
# Create a new Publish packet
|
311
|
+
def initialize(args = {})
|
312
|
+
super(ATTR_DEFAULTS.merge(args))
|
313
|
+
end
|
314
|
+
|
315
|
+
def duplicate
|
316
|
+
@flags[3]
|
317
|
+
end
|
318
|
+
|
319
|
+
# Set the DUP flag (true/false)
|
320
|
+
def duplicate=(arg)
|
321
|
+
@flags[3] = arg.is_a?(Integer) ? (arg == 0x1) : arg
|
322
|
+
end
|
323
|
+
|
324
|
+
def retain
|
325
|
+
@flags[0]
|
326
|
+
end
|
327
|
+
|
328
|
+
# Set the retain flag (true/false)
|
329
|
+
def retain=(arg)
|
330
|
+
@flags[0] = arg.is_a?(Integer) ? (arg == 0x1) : arg
|
331
|
+
end
|
332
|
+
|
333
|
+
def qos
|
334
|
+
(@flags[1] ? 0x01 : 0x00) | (@flags[2] ? 0x02 : 0x00)
|
335
|
+
end
|
336
|
+
|
337
|
+
# Set the Quality of Service level (0/1/2)
|
338
|
+
def qos=(arg)
|
339
|
+
@qos = arg.to_i
|
340
|
+
raise "Invalid QoS value: #{@qos}" if @qos < 0 || @qos > 2
|
341
|
+
|
342
|
+
@flags[1] = (arg & 0x01 == 0x01)
|
343
|
+
@flags[2] = (arg & 0x02 == 0x02)
|
344
|
+
end
|
345
|
+
|
346
|
+
# Get serialisation of packet's body
|
347
|
+
def encode_body
|
348
|
+
body = ''
|
349
|
+
if @topic.nil? || @topic.to_s.empty?
|
350
|
+
raise 'Invalid topic name when serialising packet'
|
351
|
+
end
|
352
|
+
body += encode_string(@topic)
|
353
|
+
body += encode_short(@id) unless qos.zero?
|
354
|
+
body += payload.to_s.dup.force_encoding('ASCII-8BIT')
|
355
|
+
body
|
356
|
+
end
|
357
|
+
|
358
|
+
# Parse the body (variable header and payload) of a Publish packet
|
359
|
+
def parse_body(buffer)
|
360
|
+
super(buffer)
|
361
|
+
@topic = shift_string(buffer)
|
362
|
+
@id = shift_short(buffer) unless qos.zero?
|
363
|
+
@payload = buffer
|
364
|
+
end
|
365
|
+
|
366
|
+
# Check that fixed header flags are valid for this packet type
|
367
|
+
# @private
|
368
|
+
def validate_flags
|
369
|
+
raise ProtocolException, 'Invalid packet: QoS value of 3 is not allowed' if qos == 3
|
370
|
+
raise ProtocolException, 'Invalid packet: DUP cannot be set for QoS 0' if qos.zero? && duplicate
|
371
|
+
end
|
372
|
+
|
373
|
+
# Returns a human readable string, summarising the properties of the packet
|
374
|
+
def inspect
|
375
|
+
"\#<#{self.class}: " \
|
376
|
+
"d#{duplicate ? '1' : '0'}, " \
|
377
|
+
"q#{qos}, " \
|
378
|
+
"r#{retain ? '1' : '0'}, " \
|
379
|
+
"m#{id}, " \
|
380
|
+
"'#{topic}', " \
|
381
|
+
"#{inspect_payload}>"
|
382
|
+
end
|
383
|
+
|
384
|
+
protected
|
385
|
+
|
386
|
+
def inspect_payload
|
387
|
+
str = payload.to_s
|
388
|
+
if str.bytesize < 16 && str =~ /^[ -~]*$/
|
389
|
+
"'#{str}'"
|
390
|
+
else
|
391
|
+
"... (#{str.bytesize} bytes)"
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# Class representing an MQTT Connect Packet
|
397
|
+
class Connect < MQTT::Packet
|
398
|
+
# The name of the protocol
|
399
|
+
attr_accessor :protocol_name
|
400
|
+
|
401
|
+
# The version number of the protocol
|
402
|
+
attr_accessor :protocol_level
|
403
|
+
|
404
|
+
# The client identifier string
|
405
|
+
attr_accessor :client_id
|
406
|
+
|
407
|
+
# Set to false to keep a persistent session with the server
|
408
|
+
attr_accessor :clean_session
|
409
|
+
|
410
|
+
# Period the server should keep connection open for between pings
|
411
|
+
attr_accessor :keep_alive
|
412
|
+
|
413
|
+
# The topic name to send the Will message to
|
414
|
+
attr_accessor :will_topic
|
415
|
+
|
416
|
+
# The QoS level to send the Will message as
|
417
|
+
attr_accessor :will_qos
|
418
|
+
|
419
|
+
# Set to true to make the Will message retained
|
420
|
+
attr_accessor :will_retain
|
421
|
+
|
422
|
+
# The payload of the Will message
|
423
|
+
attr_accessor :will_payload
|
424
|
+
|
425
|
+
# The username for authenticating with the server
|
426
|
+
attr_accessor :username
|
427
|
+
|
428
|
+
# The password for authenticating with the server
|
429
|
+
attr_accessor :password
|
430
|
+
|
431
|
+
# Default attribute values
|
432
|
+
ATTR_DEFAULTS = {
|
433
|
+
:client_id => nil,
|
434
|
+
:clean_session => true,
|
435
|
+
:keep_alive => 15,
|
436
|
+
:will_topic => nil,
|
437
|
+
:will_qos => 0,
|
438
|
+
:will_retain => false,
|
439
|
+
:will_payload => '',
|
440
|
+
:username => nil,
|
441
|
+
:password => nil
|
442
|
+
}
|
443
|
+
|
444
|
+
# Create a new Client Connect packet
|
445
|
+
def initialize(args = {})
|
446
|
+
super(ATTR_DEFAULTS.merge(args))
|
447
|
+
|
448
|
+
if ['3.1.0', '3.1'].include?(version)
|
449
|
+
self.protocol_name ||= 'MQIsdp'
|
450
|
+
self.protocol_level ||= 0x03
|
451
|
+
elsif version == '3.1.1'
|
452
|
+
self.protocol_name ||= 'MQTT'
|
453
|
+
self.protocol_level ||= 0x04
|
454
|
+
else
|
455
|
+
raise ArgumentError, "Unsupported protocol version: #{version}"
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Get serialisation of packet's body
|
460
|
+
def encode_body
|
461
|
+
body = ''
|
462
|
+
|
463
|
+
if @version == '3.1.0'
|
464
|
+
raise 'Client identifier too short while serialising packet' if @client_id.nil? || @client_id.bytesize < 1
|
465
|
+
raise 'Client identifier too long when serialising packet' if @client_id.bytesize > 23
|
466
|
+
end
|
467
|
+
|
468
|
+
body += encode_string(@protocol_name)
|
469
|
+
body += encode_bytes(@protocol_level.to_i)
|
470
|
+
|
471
|
+
if @keep_alive < 0
|
472
|
+
raise 'Invalid keep-alive value: cannot be less than 0'
|
473
|
+
end
|
474
|
+
|
475
|
+
# Set the Connect flags
|
476
|
+
@connect_flags = 0
|
477
|
+
@connect_flags |= 0x02 if @clean_session
|
478
|
+
@connect_flags |= 0x04 unless @will_topic.nil?
|
479
|
+
@connect_flags |= ((@will_qos & 0x03) << 3)
|
480
|
+
@connect_flags |= 0x20 if @will_retain
|
481
|
+
@connect_flags |= 0x40 unless @password.nil?
|
482
|
+
@connect_flags |= 0x80 unless @username.nil?
|
483
|
+
body += encode_bytes(@connect_flags)
|
484
|
+
|
485
|
+
body += encode_short(@keep_alive)
|
486
|
+
body += encode_string(@client_id)
|
487
|
+
unless will_topic.nil?
|
488
|
+
body += encode_string(@will_topic)
|
489
|
+
# The MQTT v3.1 specification says that the payload is a UTF-8 string
|
490
|
+
body += encode_string(@will_payload)
|
491
|
+
end
|
492
|
+
body += encode_string(@username) unless @username.nil?
|
493
|
+
body += encode_string(@password) unless @password.nil?
|
494
|
+
body
|
495
|
+
end
|
496
|
+
|
497
|
+
# Parse the body (variable header and payload) of a Connect packet
|
498
|
+
def parse_body(buffer)
|
499
|
+
super(buffer)
|
500
|
+
@protocol_name = shift_string(buffer)
|
501
|
+
@protocol_level = shift_byte(buffer).to_i
|
502
|
+
if @protocol_name == 'MQIsdp' && @protocol_level == 3
|
503
|
+
@version = '3.1.0'
|
504
|
+
elsif @protocol_name == 'MQTT' && @protocol_level == 4
|
505
|
+
@version = '3.1.1'
|
506
|
+
else
|
507
|
+
raise ProtocolException, "Unsupported protocol: #{@protocol_name}/#{@protocol_level}"
|
508
|
+
end
|
509
|
+
|
510
|
+
@connect_flags = shift_byte(buffer)
|
511
|
+
@clean_session = ((@connect_flags & 0x02) >> 1) == 0x01
|
512
|
+
@keep_alive = shift_short(buffer)
|
513
|
+
@client_id = shift_string(buffer)
|
514
|
+
if ((@connect_flags & 0x04) >> 2) == 0x01
|
515
|
+
# Last Will and Testament
|
516
|
+
@will_qos = ((@connect_flags & 0x18) >> 3)
|
517
|
+
@will_retain = ((@connect_flags & 0x20) >> 5) == 0x01
|
518
|
+
@will_topic = shift_string(buffer)
|
519
|
+
# The MQTT v3.1 specification says that the payload is a UTF-8 string
|
520
|
+
@will_payload = shift_string(buffer)
|
521
|
+
end
|
522
|
+
if ((@connect_flags & 0x80) >> 7) == 0x01 && buffer.bytesize > 0
|
523
|
+
@username = shift_string(buffer)
|
524
|
+
end
|
525
|
+
if ((@connect_flags & 0x40) >> 6) == 0x01 && buffer.bytesize > 0 # rubocop: disable Style/GuardClause
|
526
|
+
@password = shift_string(buffer)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# Returns a human readable string, summarising the properties of the packet
|
531
|
+
def inspect
|
532
|
+
str = "\#<#{self.class}: " \
|
533
|
+
"keep_alive=#{keep_alive}"
|
534
|
+
str += ', clean' if clean_session
|
535
|
+
str += ", client_id='#{client_id}'"
|
536
|
+
str += ", username='#{username}'" unless username.nil?
|
537
|
+
str += ', password=...' unless password.nil?
|
538
|
+
str + '>'
|
539
|
+
end
|
540
|
+
|
541
|
+
# ---- Deprecated attributes and methods ---- #
|
542
|
+
|
543
|
+
# @deprecated Please use {#protocol_level} instead
|
544
|
+
def protocol_version
|
545
|
+
protocol_level
|
546
|
+
end
|
547
|
+
|
548
|
+
# @deprecated Please use {#protocol_level=} instead
|
549
|
+
def protocol_version=(args)
|
550
|
+
self.protocol_level = args
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
# Class representing an MQTT Connect Acknowledgment Packet
|
555
|
+
class Connack < MQTT::Packet
|
556
|
+
# Session Present flag
|
557
|
+
attr_accessor :session_present
|
558
|
+
|
559
|
+
# The return code (defaults to 0 for connection accepted)
|
560
|
+
attr_accessor :return_code
|
561
|
+
|
562
|
+
# Default attribute values
|
563
|
+
ATTR_DEFAULTS = { :return_code => 0x00 }
|
564
|
+
|
565
|
+
# Create a new Client Connect packet
|
566
|
+
def initialize(args = {})
|
567
|
+
# We must set flags before other attributes
|
568
|
+
@connack_flags = [false, false, false, false, false, false, false, false]
|
569
|
+
super(ATTR_DEFAULTS.merge(args))
|
570
|
+
end
|
571
|
+
|
572
|
+
# Get the Session Present flag
|
573
|
+
def session_present
|
574
|
+
@connack_flags[0]
|
575
|
+
end
|
576
|
+
|
577
|
+
# Set the Session Present flag
|
578
|
+
def session_present=(arg)
|
579
|
+
@connack_flags[0] = arg.is_a?(Integer) ? (arg == 0x1) : arg
|
580
|
+
end
|
581
|
+
|
582
|
+
# Get a string message corresponding to a return code
|
583
|
+
def return_msg
|
584
|
+
case return_code
|
585
|
+
when 0x00
|
586
|
+
'Connection Accepted'
|
587
|
+
when 0x01
|
588
|
+
'Connection refused: unacceptable protocol version'
|
589
|
+
when 0x02
|
590
|
+
'Connection refused: client identifier rejected'
|
591
|
+
when 0x03
|
592
|
+
'Connection refused: server unavailable'
|
593
|
+
when 0x04
|
594
|
+
'Connection refused: bad user name or password'
|
595
|
+
when 0x05
|
596
|
+
'Connection refused: not authorised'
|
597
|
+
else
|
598
|
+
"Connection refused: error code #{return_code}"
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# Get serialisation of packet's body
|
603
|
+
def encode_body
|
604
|
+
body = ''
|
605
|
+
body += encode_bits(@connack_flags)
|
606
|
+
body += encode_bytes(@return_code.to_i)
|
607
|
+
body
|
608
|
+
end
|
609
|
+
|
610
|
+
# Parse the body (variable header and payload) of a Connect Acknowledgment packet
|
611
|
+
def parse_body(buffer)
|
612
|
+
super(buffer)
|
613
|
+
@connack_flags = shift_bits(buffer)
|
614
|
+
unless @connack_flags[1, 7] == [false, false, false, false, false, false, false]
|
615
|
+
raise ProtocolException, 'Invalid flags in Connack variable header'
|
616
|
+
end
|
617
|
+
@return_code = shift_byte(buffer)
|
618
|
+
|
619
|
+
return if buffer.empty?
|
620
|
+
raise ProtocolException, 'Extra bytes at end of Connect Acknowledgment packet'
|
621
|
+
end
|
622
|
+
|
623
|
+
# Returns a human readable string, summarising the properties of the packet
|
624
|
+
def inspect
|
625
|
+
"\#<#{self.class}: 0x%2.2X>" % return_code
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
# Class representing an MQTT Publish Acknowledgment packet
|
630
|
+
class Puback < MQTT::Packet
|
631
|
+
# Get serialisation of packet's body
|
632
|
+
def encode_body
|
633
|
+
encode_short(@id)
|
634
|
+
end
|
635
|
+
|
636
|
+
# Parse the body (variable header and payload) of a packet
|
637
|
+
def parse_body(buffer)
|
638
|
+
super(buffer)
|
639
|
+
@id = shift_short(buffer)
|
640
|
+
|
641
|
+
return if buffer.empty?
|
642
|
+
raise ProtocolException, 'Extra bytes at end of Publish Acknowledgment packet'
|
643
|
+
end
|
644
|
+
|
645
|
+
# Returns a human readable string, summarising the properties of the packet
|
646
|
+
def inspect
|
647
|
+
"\#<#{self.class}: 0x%2.2X>" % id
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
# Class representing an MQTT Publish Received packet
|
652
|
+
class Pubrec < MQTT::Packet
|
653
|
+
# Get serialisation of packet's body
|
654
|
+
def encode_body
|
655
|
+
encode_short(@id)
|
656
|
+
end
|
657
|
+
|
658
|
+
# Parse the body (variable header and payload) of a packet
|
659
|
+
def parse_body(buffer)
|
660
|
+
super(buffer)
|
661
|
+
@id = shift_short(buffer)
|
662
|
+
|
663
|
+
return if buffer.empty?
|
664
|
+
raise ProtocolException, 'Extra bytes at end of Publish Received packet'
|
665
|
+
end
|
666
|
+
|
667
|
+
# Returns a human readable string, summarising the properties of the packet
|
668
|
+
def inspect
|
669
|
+
"\#<#{self.class}: 0x%2.2X>" % id
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
# Class representing an MQTT Publish Release packet
|
674
|
+
class Pubrel < MQTT::Packet
|
675
|
+
# Default attribute values
|
676
|
+
ATTR_DEFAULTS = {
|
677
|
+
:flags => [false, true, false, false]
|
678
|
+
}
|
679
|
+
|
680
|
+
# Create a new Pubrel packet
|
681
|
+
def initialize(args = {})
|
682
|
+
super(ATTR_DEFAULTS.merge(args))
|
683
|
+
end
|
684
|
+
|
685
|
+
# Get serialisation of packet's body
|
686
|
+
def encode_body
|
687
|
+
encode_short(@id)
|
688
|
+
end
|
689
|
+
|
690
|
+
# Parse the body (variable header and payload) of a packet
|
691
|
+
def parse_body(buffer)
|
692
|
+
super(buffer)
|
693
|
+
@id = shift_short(buffer)
|
694
|
+
|
695
|
+
return if buffer.empty?
|
696
|
+
raise ProtocolException, 'Extra bytes at end of Publish Release packet'
|
697
|
+
end
|
698
|
+
|
699
|
+
# Check that fixed header flags are valid for this packet type
|
700
|
+
# @private
|
701
|
+
def validate_flags
|
702
|
+
return if @flags == [false, true, false, false]
|
703
|
+
raise ProtocolException, 'Invalid flags in PUBREL packet header'
|
704
|
+
end
|
705
|
+
|
706
|
+
# Returns a human readable string, summarising the properties of the packet
|
707
|
+
def inspect
|
708
|
+
"\#<#{self.class}: 0x%2.2X>" % id
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
# Class representing an MQTT Publish Complete packet
|
713
|
+
class Pubcomp < MQTT::Packet
|
714
|
+
# Get serialisation of packet's body
|
715
|
+
def encode_body
|
716
|
+
encode_short(@id)
|
717
|
+
end
|
718
|
+
|
719
|
+
# Parse the body (variable header and payload) of a packet
|
720
|
+
def parse_body(buffer)
|
721
|
+
super(buffer)
|
722
|
+
@id = shift_short(buffer)
|
723
|
+
|
724
|
+
return if buffer.empty?
|
725
|
+
raise ProtocolException, 'Extra bytes at end of Publish Complete packet'
|
726
|
+
end
|
727
|
+
|
728
|
+
# Returns a human readable string, summarising the properties of the packet
|
729
|
+
def inspect
|
730
|
+
"\#<#{self.class}: 0x%2.2X>" % id
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
# Class representing an MQTT Client Subscribe packet
|
735
|
+
class Subscribe < MQTT::Packet
|
736
|
+
# One or more topic filters to subscribe to
|
737
|
+
attr_accessor :topics
|
738
|
+
|
739
|
+
# Default attribute values
|
740
|
+
ATTR_DEFAULTS = {
|
741
|
+
:topics => [],
|
742
|
+
:flags => [false, true, false, false]
|
743
|
+
}
|
744
|
+
|
745
|
+
# Create a new Subscribe packet
|
746
|
+
def initialize(args = {})
|
747
|
+
super(ATTR_DEFAULTS.merge(args))
|
748
|
+
end
|
749
|
+
|
750
|
+
# Set one or more topic filters for the Subscribe packet
|
751
|
+
# The topics parameter should be one of the following:
|
752
|
+
# * String: subscribe to one topic with QoS 0
|
753
|
+
# * Array: subscribe to multiple topics with QoS 0
|
754
|
+
# * Hash: subscribe to multiple topics where the key is the topic and the value is the QoS level
|
755
|
+
#
|
756
|
+
# For example:
|
757
|
+
# packet.topics = 'a/b'
|
758
|
+
# packet.topics = ['a/b', 'c/d']
|
759
|
+
# packet.topics = [['a/b',0], ['c/d',1]]
|
760
|
+
# packet.topics = {'a/b' => 0, 'c/d' => 1}
|
761
|
+
#
|
762
|
+
def topics=(value)
|
763
|
+
# Get input into a consistent state
|
764
|
+
input = value.is_a?(Array) ? value.flatten : [value]
|
765
|
+
|
766
|
+
@topics = []
|
767
|
+
until input.empty?
|
768
|
+
item = input.shift
|
769
|
+
if item.is_a?(Hash)
|
770
|
+
# Convert hash into an ordered array of arrays
|
771
|
+
@topics += item.sort
|
772
|
+
elsif item.is_a?(String)
|
773
|
+
# Peek at the next item in the array, and remove it if it is an integer
|
774
|
+
if input.first.is_a?(Integer)
|
775
|
+
qos = input.shift
|
776
|
+
@topics << [item, qos]
|
777
|
+
else
|
778
|
+
@topics << [item, 0]
|
779
|
+
end
|
780
|
+
else
|
781
|
+
# Meh?
|
782
|
+
raise "Invalid topics input: #{value.inspect}"
|
783
|
+
end
|
784
|
+
end
|
785
|
+
@topics
|
786
|
+
end
|
787
|
+
|
788
|
+
# Get serialisation of packet's body
|
789
|
+
def encode_body
|
790
|
+
raise 'no topics given when serialising packet' if @topics.empty?
|
791
|
+
body = encode_short(@id)
|
792
|
+
topics.each do |item|
|
793
|
+
body += encode_string(item[0])
|
794
|
+
body += encode_bytes(item[1])
|
795
|
+
end
|
796
|
+
body
|
797
|
+
end
|
798
|
+
|
799
|
+
# Parse the body (variable header and payload) of a packet
|
800
|
+
def parse_body(buffer)
|
801
|
+
super(buffer)
|
802
|
+
@id = shift_short(buffer)
|
803
|
+
@topics = []
|
804
|
+
while buffer.bytesize > 0
|
805
|
+
topic_name = shift_string(buffer)
|
806
|
+
topic_qos = shift_byte(buffer)
|
807
|
+
@topics << [topic_name, topic_qos]
|
808
|
+
end
|
809
|
+
end
|
810
|
+
|
811
|
+
# Check that fixed header flags are valid for this packet type
|
812
|
+
# @private
|
813
|
+
def validate_flags
|
814
|
+
return if @flags == [false, true, false, false]
|
815
|
+
raise ProtocolException, 'Invalid flags in SUBSCRIBE packet header'
|
816
|
+
end
|
817
|
+
|
818
|
+
# Returns a human readable string, summarising the properties of the packet
|
819
|
+
def inspect
|
820
|
+
_str = "\#<#{self.class}: 0x%2.2X, %s>" % [
|
821
|
+
id,
|
822
|
+
topics.map { |t| "'#{t[0]}':#{t[1]}" }.join(', ')
|
823
|
+
]
|
824
|
+
end
|
825
|
+
end
|
826
|
+
|
827
|
+
# Class representing an MQTT Subscribe Acknowledgment packet
|
828
|
+
class Suback < MQTT::Packet
|
829
|
+
# An array of return codes, ordered by the topics that were subscribed to
|
830
|
+
attr_accessor :return_codes
|
831
|
+
|
832
|
+
# Default attribute values
|
833
|
+
ATTR_DEFAULTS = {
|
834
|
+
:return_codes => []
|
835
|
+
}
|
836
|
+
|
837
|
+
# Create a new Subscribe Acknowledgment packet
|
838
|
+
def initialize(args = {})
|
839
|
+
super(ATTR_DEFAULTS.merge(args))
|
840
|
+
end
|
841
|
+
|
842
|
+
# Set the granted QoS value for each of the topics that were subscribed to
|
843
|
+
# Can either be an integer or an array or integers.
|
844
|
+
def return_codes=(value)
|
845
|
+
if value.is_a?(Array)
|
846
|
+
@return_codes = value
|
847
|
+
elsif value.is_a?(Integer)
|
848
|
+
@return_codes = [value]
|
849
|
+
else
|
850
|
+
raise 'return_codes should be an integer or an array of return codes'
|
851
|
+
end
|
852
|
+
end
|
853
|
+
|
854
|
+
# Get serialisation of packet's body
|
855
|
+
def encode_body
|
856
|
+
if @return_codes.empty?
|
857
|
+
raise 'no granted QoS given when serialising packet'
|
858
|
+
end
|
859
|
+
body = encode_short(@id)
|
860
|
+
return_codes.each { |qos| body += encode_bytes(qos) }
|
861
|
+
body
|
862
|
+
end
|
863
|
+
|
864
|
+
# Parse the body (variable header and payload) of a packet
|
865
|
+
def parse_body(buffer)
|
866
|
+
super(buffer)
|
867
|
+
@id = shift_short(buffer)
|
868
|
+
@return_codes << shift_byte(buffer) while buffer.bytesize > 0
|
869
|
+
end
|
870
|
+
|
871
|
+
# Returns a human readable string, summarising the properties of the packet
|
872
|
+
def inspect
|
873
|
+
"\#<#{self.class}: 0x%2.2X, rc=%s>" % [id, return_codes.map { |rc| '0x%2.2X' % rc }.join(',')]
|
874
|
+
end
|
875
|
+
|
876
|
+
# ---- Deprecated attributes and methods ---- #
|
877
|
+
|
878
|
+
# @deprecated Please use {#return_codes} instead
|
879
|
+
def granted_qos
|
880
|
+
return_codes
|
881
|
+
end
|
882
|
+
|
883
|
+
# @deprecated Please use {#return_codes=} instead
|
884
|
+
def granted_qos=(args)
|
885
|
+
self.return_codes = args
|
886
|
+
end
|
887
|
+
end
|
888
|
+
|
889
|
+
# Class representing an MQTT Client Unsubscribe packet
|
890
|
+
class Unsubscribe < MQTT::Packet
|
891
|
+
# One or more topic paths to unsubscribe from
|
892
|
+
attr_accessor :topics
|
893
|
+
|
894
|
+
# Default attribute values
|
895
|
+
ATTR_DEFAULTS = {
|
896
|
+
:topics => [],
|
897
|
+
:flags => [false, true, false, false]
|
898
|
+
}
|
899
|
+
|
900
|
+
# Create a new Unsubscribe packet
|
901
|
+
def initialize(args = {})
|
902
|
+
super(ATTR_DEFAULTS.merge(args))
|
903
|
+
end
|
904
|
+
|
905
|
+
# Set one or more topic paths to unsubscribe from
|
906
|
+
def topics=(value)
|
907
|
+
@topics = value.is_a?(Array) ? value : [value]
|
908
|
+
end
|
909
|
+
|
910
|
+
# Get serialisation of packet's body
|
911
|
+
def encode_body
|
912
|
+
raise 'no topics given when serialising packet' if @topics.empty?
|
913
|
+
body = encode_short(@id)
|
914
|
+
topics.each { |topic| body += encode_string(topic) }
|
915
|
+
body
|
916
|
+
end
|
917
|
+
|
918
|
+
# Parse the body (variable header and payload) of a packet
|
919
|
+
def parse_body(buffer)
|
920
|
+
super(buffer)
|
921
|
+
@id = shift_short(buffer)
|
922
|
+
@topics << shift_string(buffer) while buffer.bytesize > 0
|
923
|
+
end
|
924
|
+
|
925
|
+
# Check that fixed header flags are valid for this packet type
|
926
|
+
# @private
|
927
|
+
def validate_flags
|
928
|
+
return if @flags == [false, true, false, false]
|
929
|
+
raise ProtocolException, 'Invalid flags in UNSUBSCRIBE packet header'
|
930
|
+
end
|
931
|
+
|
932
|
+
# Returns a human readable string, summarising the properties of the packet
|
933
|
+
def inspect
|
934
|
+
"\#<#{self.class}: 0x%2.2X, %s>" % [
|
935
|
+
id,
|
936
|
+
topics.map { |t| "'#{t}'" }.join(', ')
|
937
|
+
]
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
# Class representing an MQTT Unsubscribe Acknowledgment packet
|
942
|
+
class Unsuback < MQTT::Packet
|
943
|
+
# Create a new Unsubscribe Acknowledgment packet
|
944
|
+
def initialize(args = {})
|
945
|
+
super(args)
|
946
|
+
end
|
947
|
+
|
948
|
+
# Get serialisation of packet's body
|
949
|
+
def encode_body
|
950
|
+
encode_short(@id)
|
951
|
+
end
|
952
|
+
|
953
|
+
# Parse the body (variable header and payload) of a packet
|
954
|
+
def parse_body(buffer)
|
955
|
+
super(buffer)
|
956
|
+
@id = shift_short(buffer)
|
957
|
+
|
958
|
+
return if buffer.empty?
|
959
|
+
raise ProtocolException, 'Extra bytes at end of Unsubscribe Acknowledgment packet'
|
960
|
+
end
|
961
|
+
|
962
|
+
# Returns a human readable string, summarising the properties of the packet
|
963
|
+
def inspect
|
964
|
+
"\#<#{self.class}: 0x%2.2X>" % id
|
965
|
+
end
|
966
|
+
end
|
967
|
+
|
968
|
+
# Class representing an MQTT Ping Request packet
|
969
|
+
class Pingreq < MQTT::Packet
|
970
|
+
# Create a new Ping Request packet
|
971
|
+
def initialize(args = {})
|
972
|
+
super(args)
|
973
|
+
end
|
974
|
+
|
975
|
+
# Check the body
|
976
|
+
def parse_body(buffer)
|
977
|
+
super(buffer)
|
978
|
+
|
979
|
+
return if buffer.empty?
|
980
|
+
raise ProtocolException, 'Extra bytes at end of Ping Request packet'
|
981
|
+
end
|
982
|
+
end
|
983
|
+
|
984
|
+
# Class representing an MQTT Ping Response packet
|
985
|
+
class Pingresp < MQTT::Packet
|
986
|
+
# Create a new Ping Response packet
|
987
|
+
def initialize(args = {})
|
988
|
+
super(args)
|
989
|
+
end
|
990
|
+
|
991
|
+
# Check the body
|
992
|
+
def parse_body(buffer)
|
993
|
+
super(buffer)
|
994
|
+
|
995
|
+
return if buffer.empty?
|
996
|
+
raise ProtocolException, 'Extra bytes at end of Ping Response packet'
|
997
|
+
end
|
998
|
+
end
|
999
|
+
|
1000
|
+
# Class representing an MQTT Client Disconnect packet
|
1001
|
+
class Disconnect < MQTT::Packet
|
1002
|
+
# Create a new Client Disconnect packet
|
1003
|
+
def initialize(args = {})
|
1004
|
+
super(args)
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
# Check the body
|
1008
|
+
def parse_body(buffer)
|
1009
|
+
super(buffer)
|
1010
|
+
|
1011
|
+
return if buffer.empty?
|
1012
|
+
raise ProtocolException, 'Extra bytes at end of Disconnect packet'
|
1013
|
+
end
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
# ---- Deprecated attributes and methods ---- #
|
1017
|
+
public
|
1018
|
+
|
1019
|
+
# @deprecated Please use {#id} instead
|
1020
|
+
def message_id
|
1021
|
+
id
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
# @deprecated Please use {#id=} instead
|
1025
|
+
def message_id=(args)
|
1026
|
+
self.id = args
|
1027
|
+
end
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
# An enumeration of the MQTT packet types
|
1031
|
+
PACKET_TYPES = [
|
1032
|
+
nil,
|
1033
|
+
MQTT::Packet::Connect,
|
1034
|
+
MQTT::Packet::Connack,
|
1035
|
+
MQTT::Packet::Publish,
|
1036
|
+
MQTT::Packet::Puback,
|
1037
|
+
MQTT::Packet::Pubrec,
|
1038
|
+
MQTT::Packet::Pubrel,
|
1039
|
+
MQTT::Packet::Pubcomp,
|
1040
|
+
MQTT::Packet::Subscribe,
|
1041
|
+
MQTT::Packet::Suback,
|
1042
|
+
MQTT::Packet::Unsubscribe,
|
1043
|
+
MQTT::Packet::Unsuback,
|
1044
|
+
MQTT::Packet::Pingreq,
|
1045
|
+
MQTT::Packet::Pingresp,
|
1046
|
+
MQTT::Packet::Disconnect,
|
1047
|
+
nil
|
1048
|
+
]
|
1049
|
+
end
|