mqtt 0.0.9 → 0.1.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.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2009-2013 Nicholas J Humfrey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/NEWS CHANGED
@@ -1,6 +1,25 @@
1
1
  Ruby MQTT NEWS
2
2
  ==============
3
3
 
4
+ Ruby MQTT Version 0.1.0 (2013-09-07)
5
+ ------------------------------------
6
+
7
+ * Changed license to MIT, to simplify licensing concerns
8
+ * Improvements for UTF-8 handling under Ruby 1.9
9
+ * Added ```get_packet``` method
10
+ * Added support for a keep-alive value of 0
11
+ * Added a #inspect method to the Packet classes
12
+ * Added checks for the protocol name and version
13
+ * Added check to ensure that packet body isn't too big
14
+ * Added validation of QoS value
15
+ * Added example of using authentication
16
+ * Fixed 'unused variable' warnings
17
+ * Reduced duplicated code in packet parsing
18
+ * Improved testing
19
+ - Created fake server and integration tests
20
+ - Better test coverage
21
+ - Added more tests for error states
22
+
4
23
 
5
24
  Ruby MQTT Version 0.0.9 (2012-12-21)
6
25
  ------------------------------------
data/README CHANGED
@@ -46,6 +46,11 @@ Resources
46
46
  * GitHub Project: http://github.com/njh/ruby-mqtt
47
47
  * API Documentation: http://rubydoc.info/gems/mqtt/frames
48
48
 
49
+ License
50
+ -------
51
+
52
+ The ruby-mqtt gem is licensed under the terms of the MIT license.
53
+ See the file LICENSE for details.
49
54
 
50
55
  Contact
51
56
  -------
@@ -53,4 +58,3 @@ Contact
53
58
  * Author: Nicholas J Humfrey
54
59
  * Email: njh@aelius.com
55
60
  * Home Page: http://www.aelius.com/njh/
56
- * License: Distributes under the same terms as Ruby
@@ -5,7 +5,12 @@ require 'socket'
5
5
  require 'thread'
6
6
  require 'timeout'
7
7
 
8
- require "mqtt/version"
8
+ require 'mqtt/version'
9
+
10
+ # String encoding monkey patch for Ruby 1.8
11
+ unless String.method_defined?(:force_encoding)
12
+ require 'mqtt/patches/string_encoding.rb'
13
+ end
9
14
 
10
15
  module MQTT
11
16
 
@@ -220,7 +220,7 @@ class MQTT::Client
220
220
  send_packet(packet)
221
221
  end
222
222
 
223
- # Return the next message recieved from the MQTT broker.
223
+ # Return the next message received from the MQTT broker.
224
224
  # An optional topic can be given to subscribe to.
225
225
  #
226
226
  # The method either returns the topic and message as an array:
@@ -248,6 +248,34 @@ class MQTT::Client
248
248
  end
249
249
  end
250
250
 
251
+ # Return the next packet object received from the MQTT broker.
252
+ # An optional topic can be given to subscribe to.
253
+ #
254
+ # The method either returns a single packet:
255
+ # packet = client.get_packet
256
+ # puts packet.topic
257
+ #
258
+ # Or can be used with a block to keep processing messages:
259
+ # client.get_packet('test') do |packet|
260
+ # # Do stuff here
261
+ # puts packet.topic
262
+ # end
263
+ #
264
+ def get_packet(topic=nil)
265
+ # Subscribe to a topic, if an argument is given
266
+ subscribe(topic) unless topic.nil?
267
+
268
+ if block_given?
269
+ # Loop forever!
270
+ loop do
271
+ yield(@read_queue.pop)
272
+ end
273
+ else
274
+ # Wait for one packet to be available
275
+ return @read_queue.pop
276
+ end
277
+ end
278
+
251
279
  # Returns true if the incoming message queue is empty.
252
280
  def queue_empty?
253
281
  @read_queue.empty?
@@ -289,7 +317,7 @@ private
289
317
  end
290
318
 
291
319
  # Time to send a keep-alive ping request?
292
- if Time.now > @last_pingreq + @keep_alive
320
+ if @keep_alive > 0 and Time.now > @last_pingreq + @keep_alive
293
321
  ping
294
322
  end
295
323
 
@@ -1,3 +1,5 @@
1
+ # encoding: BINARY
2
+
1
3
  module MQTT
2
4
 
3
5
  # Class representing a MQTT Packet
@@ -17,27 +19,21 @@ module MQTT
17
19
 
18
20
  # Read in a packet from a socket
19
21
  def self.read(socket)
20
- # Read in the packet header and work out the class
21
- header = read_byte(socket)
22
- type_id = ((header & 0xF0) >> 4)
23
- packet_class = MQTT::PACKET_TYPES[type_id]
24
-
25
- # Create a new packet object
26
- packet = packet_class.new(
27
- :duplicate => ((header & 0x08) >> 3),
28
- :qos => ((header & 0x06) >> 1),
29
- :retain => ((header & 0x01) >> 0)
22
+ # Read in the packet header and create a new packet object
23
+ packet = create_from_header(
24
+ read_byte(socket)
30
25
  )
31
26
 
32
27
  # Read in the packet length
33
28
  multiplier = 1
34
29
  body_length = 0
30
+ pos = 1
35
31
  begin
36
32
  digit = read_byte(socket)
37
33
  body_length += ((digit & 0x7F) * multiplier)
38
34
  multiplier *= 0x80
39
- end while ((digit & 0x80) != 0x00)
40
- # FIXME: only allow 4 bytes?
35
+ pos += 1
36
+ end while ((digit & 0x80) != 0x00) and pos <= 4
41
37
 
42
38
  # Store the expected body length in the packet
43
39
  packet.instance_variable_set('@body_length', body_length)
@@ -58,29 +54,24 @@ module MQTT
58
54
  # Parse the header and create a new packet object of the correct type
59
55
  # The header is removed from the buffer passed into this function
60
56
  def self.parse_header(buffer)
61
- # Work out the class
62
- type_id = ((buffer.unpack("C*")[0] & 0xF0) >> 4)
63
- packet_class = MQTT::PACKET_TYPES[type_id]
64
- if packet_class.nil?
65
- raise ProtocolException.new("Invalid packet type identifier: #{type_id}")
57
+ # Check that the packet is a long as the minimum packet size
58
+ if buffer.bytesize < 2
59
+ raise ProtocolException.new("Invalid packet: less than 2 bytes long")
66
60
  end
67
61
 
68
62
  # Create a new packet object
69
- packet = packet_class.new(
70
- :duplicate => ((buffer.unpack("C*")[0] & 0x08) >> 3) == 0x01,
71
- :qos => ((buffer.unpack("C*")[0] & 0x06) >> 1),
72
- :retain => ((buffer.unpack("C*")[0] & 0x01) >> 0) == 0x01
73
- )
63
+ bytes = buffer.unpack("C5")
64
+ packet = create_from_header(bytes.first)
74
65
 
75
66
  # Parse the packet length
76
67
  body_length = 0
77
68
  multiplier = 1
78
69
  pos = 1
79
70
  begin
80
- if buffer.length <= pos
71
+ if buffer.bytesize <= pos
81
72
  raise ProtocolException.new("The packet length header is incomplete")
82
73
  end
83
- digit = buffer.unpack("C*")[pos]
74
+ digit = bytes[pos]
84
75
  body_length += ((digit & 0x7F) * multiplier)
85
76
  multiplier *= 0x80
86
77
  pos += 1
@@ -89,12 +80,28 @@ module MQTT
89
80
  # Store the expected body length in the packet
90
81
  packet.instance_variable_set('@body_length', body_length)
91
82
 
92
- # Delete the variable length header from the raw packet passed in
83
+ # Delete the fixed header from the raw packet passed in
93
84
  buffer.slice!(0...pos)
94
85
 
95
86
  return packet
96
87
  end
97
88
 
89
+ # Create a new packet object from the first byte of a MQTT packet
90
+ def self.create_from_header(byte)
91
+ # Work out the class
92
+ type_id = ((byte & 0xF0) >> 4)
93
+ packet_class = MQTT::PACKET_TYPES[type_id]
94
+ if packet_class.nil?
95
+ raise ProtocolException.new("Invalid packet type identifier: #{type_id}")
96
+ end
97
+
98
+ # Create a new packet object
99
+ packet_class.new(
100
+ :duplicate => ((byte & 0x08) >> 3) == 0x01,
101
+ :qos => ((byte & 0x06) >> 1),
102
+ :retain => ((byte & 0x01) >> 0) == 0x01
103
+ )
104
+ end
98
105
 
99
106
  # Create a new empty packet
100
107
  def initialize(args={})
@@ -137,6 +144,9 @@ module MQTT
137
144
  # Set the Quality of Service level (0/1/2)
138
145
  def qos=(arg)
139
146
  @qos = arg.to_i
147
+ if @qos < 0 or @qos > 2
148
+ raise "Invalid QoS value: #{@qos}"
149
+ end
140
150
  end
141
151
 
142
152
  # Set the length of the packet body
@@ -146,9 +156,9 @@ module MQTT
146
156
 
147
157
  # Parse the body (variable header and payload) of a packet
148
158
  def parse_body(buffer)
149
- if buffer.length != body_length
159
+ if buffer.bytesize != body_length
150
160
  raise ProtocolException.new(
151
- "Failed to parse packet - input buffer (#{buffer.length}) is not the same as the body length buffer (#{body_length})"
161
+ "Failed to parse packet - input buffer (#{buffer.bytesize}) is not the same as the body length header (#{body_length})"
152
162
  )
153
163
  end
154
164
  end
@@ -172,8 +182,13 @@ module MQTT
172
182
  # Get the packet's variable header and payload
173
183
  body = self.encode_body
174
184
 
185
+ # Check that that packet isn't too big
186
+ body_length = body.bytesize
187
+ if body_length > 268435455
188
+ raise "Error serialising packet: body is more than 256MB"
189
+ end
190
+
175
191
  # Build up the body length field bytes
176
- body_length = body.length
177
192
  begin
178
193
  digit = (body_length % 128)
179
194
  body_length = (body_length / 128)
@@ -186,6 +201,9 @@ module MQTT
186
201
  header.pack('C*') + body
187
202
  end
188
203
 
204
+ def inspect
205
+ "\#<#{self.class}>"
206
+ end
189
207
 
190
208
  protected
191
209
 
@@ -199,11 +217,14 @@ module MQTT
199
217
  [val.to_i].pack('n')
200
218
  end
201
219
 
202
- # Encode a string and return it
220
+ # Encode a UTF-8 string and return it
203
221
  # (preceded by the length of the string)
204
222
  def encode_string(str)
205
- str = str.to_s unless str.is_a?(String)
206
- encode_short(str.length) + str
223
+ str = str.to_s.encode('UTF-8')
224
+
225
+ # Force to binary, when assembling the packet
226
+ str.force_encoding('ASCII-8BIT')
227
+ encode_short(str.bytesize) + str
207
228
  end
208
229
 
209
230
  # Remove a 16-bit unsigned integer from the front of buffer
@@ -225,7 +246,9 @@ module MQTT
225
246
  # Remove string from the front of buffer
226
247
  def shift_string(buffer)
227
248
  len = shift_short(buffer)
228
- shift_data(buffer,len)
249
+ str = shift_data(buffer,len)
250
+ # Strings in MQTT v3.1 are all UTF-8
251
+ str.force_encoding('UTF-8')
229
252
  end
230
253
 
231
254
 
@@ -235,7 +258,7 @@ module MQTT
235
258
  def self.read_byte(socket)
236
259
  byte = socket.read(1)
237
260
  if byte.nil?
238
- raise ProtocolException
261
+ raise ProtocolException.new("Failed to read byte from socket")
239
262
  end
240
263
  byte.unpack('C').first
241
264
  end
@@ -265,12 +288,12 @@ module MQTT
265
288
  # Get serialisation of packet's body
266
289
  def encode_body
267
290
  body = ''
268
- if @topic.nil?
291
+ if @topic.nil? or @topic.to_s.empty?
269
292
  raise "Invalid topic name when serialising packet"
270
293
  end
271
294
  body += encode_string(@topic)
272
295
  body += encode_short(@message_id) unless qos == 0
273
- body += payload.to_s
296
+ body += payload.to_s.force_encoding('ASCII-8BIT')
274
297
  return body
275
298
  end
276
299
 
@@ -279,7 +302,27 @@ module MQTT
279
302
  super(buffer)
280
303
  @topic = shift_string(buffer)
281
304
  @message_id = shift_short(buffer) unless qos == 0
282
- @payload = buffer.dup
305
+ @payload = buffer
306
+ end
307
+
308
+ def inspect
309
+ "\#<#{self.class}: " +
310
+ "d#{duplicate ? '1' : '0'}, " +
311
+ "q#{qos}, " +
312
+ "r#{retain ? '1' : '0'}, " +
313
+ "m#{message_id}, " +
314
+ "'#{topic}', " +
315
+ "#{inspect_payload}>"
316
+ end
317
+
318
+ protected
319
+ def inspect_payload
320
+ str = payload.to_s
321
+ if str.bytesize < 16
322
+ "'#{str}'"
323
+ else
324
+ "... (#{str.bytesize} bytes)"
325
+ end
283
326
  end
284
327
  end
285
328
 
@@ -323,12 +366,16 @@ module MQTT
323
366
  # Get serialisation of packet's body
324
367
  def encode_body
325
368
  body = ''
326
- if @client_id.nil? or @client_id.length < 1 or @client_id.length > 23
369
+ if @client_id.nil? or @client_id.bytesize < 1 or @client_id.bytesize > 23
327
370
  raise "Invalid client identifier when serialising packet"
328
371
  end
329
372
  body += encode_string(@protocol_name)
330
373
  body += encode_bytes(@protocol_version.to_i)
331
374
 
375
+ if @keep_alive < 0
376
+ raise "Invalid keep-alive value: cannot be less than 0"
377
+ end
378
+
332
379
  # Set the Connect flags
333
380
  @connect_flags = 0
334
381
  @connect_flags |= 0x02 if @clean_session
@@ -343,6 +390,7 @@ module MQTT
343
390
  body += encode_string(@client_id)
344
391
  unless will_topic.nil?
345
392
  body += encode_string(@will_topic)
393
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
346
394
  body += encode_string(@will_payload)
347
395
  end
348
396
  body += encode_string(@username) unless @username.nil?
@@ -354,7 +402,20 @@ module MQTT
354
402
  def parse_body(buffer)
355
403
  super(buffer)
356
404
  @protocol_name = shift_string(buffer)
357
- @protocol_version = shift_byte(buffer)
405
+ @protocol_version = shift_byte(buffer).to_i
406
+
407
+ if @protocol_name != 'MQIsdp'
408
+ raise ProtocolException.new(
409
+ "Unsupported protocol name: #{@protocol_name}"
410
+ )
411
+ end
412
+
413
+ if @protocol_version != 3
414
+ raise ProtocolException.new(
415
+ "Unsupported protocol version: #{@protocol_version}"
416
+ )
417
+ end
418
+
358
419
  @connect_flags = shift_byte(buffer)
359
420
  @clean_session = ((@connect_flags & 0x02) >> 1) == 0x01
360
421
  @keep_alive = shift_short(buffer)
@@ -364,15 +425,26 @@ module MQTT
364
425
  @will_qos = ((@connect_flags & 0x18) >> 3)
365
426
  @will_retain = ((@connect_flags & 0x20) >> 5) == 0x01
366
427
  @will_topic = shift_string(buffer)
428
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
367
429
  @will_payload = shift_string(buffer)
368
430
  end
369
- if ((@connect_flags & 0x80) >> 7) == 0x01 and buffer.length > 0
431
+ if ((@connect_flags & 0x80) >> 7) == 0x01 and buffer.bytesize > 0
370
432
  @username = shift_string(buffer)
371
433
  end
372
- if ((@connect_flags & 0x40) >> 6) == 0x01 and buffer.length > 0
434
+ if ((@connect_flags & 0x40) >> 6) == 0x01 and buffer.bytesize > 0
373
435
  @password = shift_string(buffer)
374
436
  end
375
437
  end
438
+
439
+ def inspect
440
+ str = "\#<#{self.class}: "
441
+ str += "keep_alive=#{keep_alive}"
442
+ str += ", clean" if clean_session
443
+ str += ", client_id='#{client_id}'"
444
+ str += ", username='#{username}'" unless username.nil?
445
+ str += ", password=..." unless password.nil?
446
+ str += ">"
447
+ end
376
448
  end
377
449
 
378
450
  # Class representing an MQTT Connect Acknowledgment Packet
@@ -416,12 +488,16 @@ module MQTT
416
488
  # Parse the body (variable header and payload) of a Connect Acknowledgment packet
417
489
  def parse_body(buffer)
418
490
  super(buffer)
419
- unused = shift_byte(buffer)
491
+ _unused = shift_byte(buffer)
420
492
  @return_code = shift_byte(buffer)
421
493
  unless buffer.empty?
422
494
  raise ProtocolException.new("Extra bytes at end of Connect Acknowledgment packet")
423
495
  end
424
496
  end
497
+
498
+ def inspect
499
+ "\#<#{self.class}: 0x%2.2X>" % return_code
500
+ end
425
501
  end
426
502
 
427
503
  # Class representing an MQTT Publish Acknowledgment packet
@@ -447,6 +523,10 @@ module MQTT
447
523
  raise ProtocolException.new("Extra bytes at end of Publish Acknowledgment packet")
448
524
  end
449
525
  end
526
+
527
+ def inspect
528
+ "\#<#{self.class}: 0x%2.2X>" % message_id
529
+ end
450
530
  end
451
531
 
452
532
  # Class representing an MQTT Publish Received packet
@@ -472,6 +552,10 @@ module MQTT
472
552
  raise ProtocolException.new("Extra bytes at end of Publish Received packet")
473
553
  end
474
554
  end
555
+
556
+ def inspect
557
+ "\#<#{self.class}: 0x%2.2X>" % message_id
558
+ end
475
559
  end
476
560
 
477
561
  # Class representing an MQTT Publish Release packet
@@ -497,6 +581,10 @@ module MQTT
497
581
  raise ProtocolException.new("Extra bytes at end of Publish Release packet")
498
582
  end
499
583
  end
584
+
585
+ def inspect
586
+ "\#<#{self.class}: 0x%2.2X>" % message_id
587
+ end
500
588
  end
501
589
 
502
590
  # Class representing an MQTT Publish Complete packet
@@ -522,6 +610,10 @@ module MQTT
522
610
  raise ProtocolException.new("Extra bytes at end of Publish Complete packet")
523
611
  end
524
612
  end
613
+
614
+ def inspect
615
+ "\#<#{self.class}: 0x%2.2X>" % message_id
616
+ end
525
617
  end
526
618
 
527
619
  # Class representing an MQTT Client Subscribe packet
@@ -597,12 +689,19 @@ module MQTT
597
689
  super(buffer)
598
690
  @message_id = shift_short(buffer)
599
691
  @topics = []
600
- while(buffer.length>0)
692
+ while(buffer.bytesize>0)
601
693
  topic_name = shift_string(buffer)
602
694
  topic_qos = shift_byte(buffer)
603
695
  @topics << [topic_name,topic_qos]
604
696
  end
605
697
  end
698
+
699
+ def inspect
700
+ str = "\#<#{self.class}: 0x%2.2X, %s>" % [
701
+ message_id,
702
+ topics.map {|t| "'#{t[0]}':#{t[1]}"}.join(', ')
703
+ ]
704
+ end
606
705
  end
607
706
 
608
707
  # Class representing an MQTT Subscribe Acknowledgment packet
@@ -643,10 +742,14 @@ module MQTT
643
742
  def parse_body(buffer)
644
743
  super(buffer)
645
744
  @message_id = shift_short(buffer)
646
- while(buffer.length>0)
745
+ while(buffer.bytesize>0)
647
746
  @granted_qos << shift_byte(buffer)
648
747
  end
649
748
  end
749
+
750
+ def inspect
751
+ "\#<#{self.class}: 0x%2.2X, qos=%s>" % [message_id, granted_qos.join(',')]
752
+ end
650
753
  end
651
754
 
652
755
  # Class representing an MQTT Client Unsubscribe packet
@@ -684,10 +787,17 @@ module MQTT
684
787
  def parse_body(buffer)
685
788
  super(buffer)
686
789
  @message_id = shift_short(buffer)
687
- while(buffer.length>0)
790
+ while(buffer.bytesize>0)
688
791
  @topics << shift_string(buffer)
689
792
  end
690
793
  end
794
+
795
+ def inspect
796
+ str = "\#<#{self.class}: 0x%2.2X, %s>" % [
797
+ message_id,
798
+ topics.map {|t| "'#{t}'"}.join(', ')
799
+ ]
800
+ end
691
801
  end
692
802
 
693
803
  # Class representing an MQTT Unsubscribe Acknowledgment packet
@@ -713,6 +823,10 @@ module MQTT
713
823
  raise ProtocolException.new("Extra bytes at end of Unsubscribe Acknowledgment packet")
714
824
  end
715
825
  end
826
+
827
+ def inspect
828
+ "\#<#{self.class}: 0x%2.2X>" % message_id
829
+ end
716
830
  end
717
831
 
718
832
  # Class representing an MQTT Ping Request packet
@@ -762,7 +876,6 @@ module MQTT
762
876
  end
763
877
  end
764
878
  end
765
-
766
879
  end
767
880
 
768
881