qubitro-mqtt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1050 @@
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.read(1)
223
+ raise ProtocolException, 'Failed to read byte from socket' if byte.nil?
224
+
225
+ byte.unpack('C').first
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 version == '3.1.0' || version == '3.1'
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
1050
+