mqtt-ccutrer 1.0.0

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