mqtt-ccutrer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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