mqtt 0.0.9 → 0.1.0

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