mqtt 0.0.3 → 0.0.4

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/NEWS CHANGED
@@ -1,5 +1,10 @@
1
1
  = Ruby MQTT NEWS
2
2
 
3
+ == Ruby MQTT Version 0.0.4 (2009-02-22)
4
+
5
+ Re-factored packet encoding/decoding into one class per packet type.
6
+ Added MQTT::Proxy class for implementing an MQTT proxy.
7
+
3
8
  == Ruby MQTT Version 0.0.3 (2009-02-08)
4
9
 
5
10
  Added checking of Connection Acknowledgement.
data/README CHANGED
@@ -13,7 +13,7 @@ You may get the latest stable version from Rubyforge. Source gems are also avail
13
13
  == Synopsis
14
14
 
15
15
  require 'rubygems'
16
- require 'mqtt'
16
+ require 'mqtt/client'
17
17
 
18
18
  # Publish example
19
19
  mqtt = MQTT::Client.new('mqtt.example.com')
@@ -34,16 +34,17 @@ You may get the latest stable version from Rubyforge. Source gems are also avail
34
34
 
35
35
  == TODO
36
36
 
37
- * Process acknowledgement packets
38
- * Create classes for each type of packet?
39
- * More validations of data/parameters
40
- * Implement exception throwing
41
37
  * Implement Will and Testament
38
+ * Process acknowledgement packets / Implement QOS 1 in client
39
+ * More validations of data/parameters
40
+ * More error checking and exception throwing
41
+ - Check that packet data is valid - don't blindly read values
42
+ - Subscribe and Unsubscribe packets should always have QOS of 1
42
43
  * More examples
43
44
  * Integration tests
44
45
  * Refactor to add callbacks that are called from seperate thread
45
- * Implement QOS Level 1
46
- * Implement QOS Level 2
46
+ * Implement QOS Level 2 in client
47
+ * Prevent proxy from connecting to itself
47
48
  * Add support for binding socket to specific local address
48
49
 
49
50
  == Resources
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ require 'spec/rake/verify_rcov'
8
8
 
9
9
 
10
10
  NAME = "mqtt"
11
- VERS = "0.0.3"
11
+ VERS = "0.0.4"
12
12
  CLEAN.include ['pkg', 'rdoc']
13
13
 
14
14
  spec = Gem::Specification.new do |s|
data/examples/livetext.rb CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  $:.unshift File.dirname(__FILE__)+'/../lib'
4
4
 
5
- require 'mqtt'
5
+ require 'mqtt/client'
6
6
 
7
- client = MQTT::Client.new('hadrian.aelius.com')
7
+ client = MQTT::Client.new('mqtt.example.com')
8
8
  client.connect do
9
9
  client.subscribe('livetext/#')
10
10
  loop do
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__)+'/../lib'
4
+
5
+ require 'mqtt/proxy'
6
+
7
+ proxy = MQTT::Proxy.new(
8
+ :local_host => '0.0.0.0',
9
+ :local_port => 1883,
10
+ :broker_host => 'mqtt.example.com',
11
+ :broker_port => 1883
12
+ )
13
+
14
+ proxy.client_filter = lambda { |packet|
15
+ puts "From client: #{packet.inspect}"
16
+ return packet
17
+ }
18
+
19
+ proxy.broker_filter = lambda { |packet|
20
+ puts "From broker: #{packet.inspect}"
21
+ return packet
22
+ }
23
+
24
+ # Start the proxy
25
+ proxy.run
@@ -2,7 +2,7 @@
2
2
 
3
3
  $:.unshift File.dirname(__FILE__)+'/../lib'
4
4
 
5
- require 'mqtt'
5
+ require 'mqtt/client'
6
6
 
7
7
  client = MQTT::Client.new('mqtt.example.com')
8
8
  client.connect do
@@ -2,7 +2,7 @@
2
2
 
3
3
  $:.unshift File.dirname(__FILE__)+'/../lib'
4
4
 
5
- require 'mqtt'
5
+ require 'mqtt/client'
6
6
 
7
7
  client = MQTT::Client.new('mqtt.example.com')
8
8
  client.connect do |c|
data/lib/mqtt.rb CHANGED
@@ -1,29 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'mqtt/client'
4
-
5
3
  # Pure-ruby implementation of the MQTT protocol
6
4
  module MQTT
7
5
 
8
- PACKET_TYPES = [
9
- nil,
10
- :connect, # Client request to connect to Broker
11
- :connack, # Connect Acknowledgment
12
- :publish, # Publish message
13
- :puback, # Publish Acknowledgment
14
- :pubrec, # Publish Received (assured delivery part 1)
15
- :pubrel, # Publish Release (assured delivery part 2)
16
- :pubcomp, # Publish Complete (assured delivery part 3)
17
- :subscribe, # Client Subscribe request
18
- :suback, # Subscribe Acknowledgment
19
- :unsubscribe, # Client Unsubscribe request
20
- :unsuback, # Unsubscribe Acknowledgment
21
- :pingreq, # PING Request
22
- :pingresp, # PING Response
23
- :disconnect, # Client is Disconnecting
24
- nil
25
- ]
26
-
27
6
  class Exception < Exception
28
7
 
29
8
  end
data/lib/mqtt/client.rb CHANGED
@@ -48,22 +48,12 @@ module MQTT
48
48
  @socket = TCPSocket.new(@remote_host,@remote_port)
49
49
 
50
50
  # Protocol name and version
51
- packet = MQTT::Packet.new(:type => :connect)
52
- packet.add_string('MQIsdp')
53
- packet.add_bytes(0x03)
54
-
55
- # Connect flags
56
- connect_flags = 0x00
57
- connect_flags ||= 0x02 if @clean_start
58
- # FIXME: implement Will and Testament
59
- packet.add_bytes(connect_flags)
60
-
61
- # Keep Alive timer: 10 seconds
62
- packet.add_short(@keep_alive)
63
-
64
- # Add the client identifier
65
- packet.add_string(@client_id)
66
-
51
+ packet = MQTT::Packet::Connect.new(
52
+ :clean_start => @clean_start,
53
+ :keep_alive => @keep_alive,
54
+ :client_id => @client_id
55
+ )
56
+
67
57
  # Send packet
68
58
  send_packet(packet)
69
59
 
@@ -89,7 +79,7 @@ module MQTT
89
79
  def disconnect(send_msg=true)
90
80
  if connected?
91
81
  if send_msg
92
- packet = MQTT::Packet.new(:type => :disconnect)
82
+ packet = MQTT::Packet::Disconnect.new
93
83
  send_packet(packet)
94
84
  end
95
85
  @read_thread.kill if @read_thread and @read_thread.alive?
@@ -106,30 +96,21 @@ module MQTT
106
96
 
107
97
  # Send a MQTT ping message to indicate that the MQTT client is alive.
108
98
  def ping
109
- packet = MQTT::Packet.new(:type => :pingreq)
99
+ packet = MQTT::Packet::Pingreq.new
110
100
  send_packet(packet)
111
101
  @last_pingreq = Time.now
112
102
  end
113
103
 
114
104
  # Publish a message on a particular topic to the MQTT broker.
115
105
  def publish(topic, payload, retain=false, qos=0)
116
- packet = MQTT::Packet.new(
117
- :type => :publish,
106
+ packet = MQTT::Packet::Publish.new(
118
107
  :qos => qos,
119
- :retain => retain
108
+ :retain => retain,
109
+ :topic => topic,
110
+ :payload => payload,
111
+ :message_id => @message_id.next
120
112
  )
121
-
122
- # Add the topic name
123
- packet.add_string(topic)
124
-
125
- # Add Message ID for qos1 and qos2
126
- unless qos == 0
127
- packet.add_short(@message_id.next)
128
- end
129
-
130
- # Add the packet payload
131
- packet.add_data(payload)
132
-
113
+
133
114
  # Send the packet
134
115
  send_packet(packet)
135
116
  end
@@ -147,27 +128,10 @@ module MQTT
147
128
  # client.subscribe( 'a/b' => 0, 'c/d' => 1 )
148
129
  #
149
130
  def subscribe(*topics)
150
- array = []
151
- topics.each do |item|
152
- if item.is_a?(Hash)
153
- # Convert hash into an ordered array of arrays
154
- array += item.sort
155
- elsif item.is_a?(Array)
156
- # Already in [topic,qos] format
157
- array.push item
158
- else
159
- # Default to QOS 0
160
- array.push [item.to_s,0]
161
- end
162
- end
163
-
164
- # Create the packet
165
- packet = MQTT::Packet.new(:type => :subscribe, :qos => 1)
166
- packet.add_short(@message_id.next)
167
- array.each do |item|
168
- packet.add_string(item[0])
169
- packet.add_bytes(item[1])
170
- end
131
+ packet = MQTT::Packet::Subscribe.new(
132
+ :topics => topics,
133
+ :message_id => @message_id.next
134
+ )
171
135
  send_packet(packet)
172
136
  end
173
137
 
@@ -180,17 +144,17 @@ module MQTT
180
144
  def get
181
145
  # Wait for a packet to be available
182
146
  packet = @read_queue.pop
183
-
184
- # Parse the variable header
185
- topic = packet.shift_string
186
- msg_id = packet.shift_short unless (packet.qos == 0)
187
- return topic,packet.body
147
+ topic = packet.topic
148
+ payload = packet.payload
149
+ return topic,payload
188
150
  end
189
151
 
190
152
  # Send a unsubscribe message for one or more topics on the MQTT broker
191
153
  def unsubscribe(*topics)
192
- packet = MQTT::Packet.new(:type => :unsubscribe, :qos => 1)
193
- topics.each { |topic| packet.add_string(topic) }
154
+ packet = MQTT::Packet::Unsubscribe.new(
155
+ :topics => topics,
156
+ :message_id => @message_id.next
157
+ )
194
158
  send_packet(packet)
195
159
  end
196
160
 
@@ -205,7 +169,7 @@ module MQTT
205
169
  unless result.nil?
206
170
  # Yes - read in the packet
207
171
  packet = MQTT::Packet.read(@socket)
208
- if packet.type == :publish
172
+ if packet.class == MQTT::Packet::Publish
209
173
  # Add to queue
210
174
  @read_queue.push(packet)
211
175
  else
@@ -236,23 +200,13 @@ module MQTT
236
200
  def receive_connack
237
201
  Timeout.timeout(@ack_timeout) do
238
202
  packet = MQTT::Packet.read(@socket)
239
- if packet.type != :connack
240
- raise MQTT::ProtocolException.new("Response wan't a connection acknowledgement: #{packet.type}")
203
+ if packet.class != MQTT::Packet::Connack
204
+ raise MQTT::ProtocolException.new("Response wan't a connection acknowledgement: #{packet.class}")
241
205
  end
242
206
 
243
- # Read in the return code
244
- byte1, return_code = packet.shift_bytes(2)
245
- if return_code == 0x00
246
- # Success
247
- nil
248
- elsif return_code == 0x01
249
- raise MQTT::ProtocolException.new("Connection refused: unacceptable protocol version")
250
- elsif return_code == 0x02
251
- raise MQTT::ProtocolException.new("Connection refused: client identifier rejected")
252
- elsif return_code == 0x03
253
- raise MQTT::ProtocolException.new("Connection refused: broker unavailable")
254
- else
255
- raise MQTT::ProtocolException.new("Connection refused: #{return_code}")
207
+ # Check the return code
208
+ if packet.return_code != 0x00
209
+ raise MQTT::ProtocolException.new(packet.return_msg)
256
210
  end
257
211
  end
258
212
  end
data/lib/mqtt/packet.rb CHANGED
@@ -7,19 +7,19 @@ module MQTT
7
7
  # Class representing a MQTT Packet
8
8
  # Performs binary encoding and decoding of headers
9
9
  class Packet
10
- attr_reader :type # The packet type
11
10
  attr_reader :dup # Duplicate delivery flag
12
11
  attr_reader :retain # Retain flag
13
12
  attr_reader :qos # Quality of Service level
14
- attr_reader :body # Packet's body (everything after fixed header)
15
13
 
16
14
  # Read in a packet from a socket
17
15
  def self.read(socket)
18
-
19
- # Create a packet object
16
+ # Read in the packet header and work out the class
20
17
  header = read_byte(socket)
21
- packet = MQTT::Packet.new(
22
- :type => ((header & 0xF0) >> 4),
18
+ type_id = ((header & 0xF0) >> 4)
19
+ packet_class = MQTT::PACKET_TYPES[type_id]
20
+
21
+ # Create a new packet object
22
+ packet = packet_class.new(
23
23
  :dup => ((header & 0x08) >> 3),
24
24
  :qos => ((header & 0x06) >> 1),
25
25
  :retain => ((header & 0x01) >> 0)
@@ -36,39 +36,22 @@ module MQTT
36
36
  # FIXME: only allow 4 bytes?
37
37
 
38
38
  # Read in the packet body
39
- packet.body = socket.read(body_len)
39
+ packet.parse_body( socket.read(body_len) )
40
40
 
41
41
  return packet
42
42
  end
43
43
 
44
44
  # Create a new empty packet
45
45
  def initialize(args={})
46
- self.type = args[:type] || :invalid
47
46
  self.dup = args[:dup] || false
48
47
  self.qos = args[:qos] || 0
49
48
  self.retain = args[:retain] || false
50
- self.body = args[:body] || ''
51
49
  end
52
50
 
53
- # Set the packet type
54
- # Can either by the packet type id (integer)
55
- # Or the packet type as a symbol/string
56
- # See the MQTT module for an enumeration of packet types.
57
- def type=(arg)
58
- if arg.kind_of?(Integer)
59
- # Convert type identifier to symbol
60
- @type = MQTT::PACKET_TYPES[arg]
61
- else
62
- @type = arg.to_sym
63
- # FIXME: raise exception if packet type is invalid?
64
- end
65
- end
66
-
67
- # Return the identifer for this packet type
51
+ # Get the identifer for this packet type
68
52
  def type_id
69
- raise "No packet type set for this packet" if @type.nil?
70
- index = MQTT::PACKET_TYPES.index(@type)
71
- raise "Invalid packet type: #{@type}" if index.nil?
53
+ index = MQTT::PACKET_TYPES.index(self.class)
54
+ raise "Invalid packet type: #{self.class}" if index.nil?
72
55
  return index
73
56
  end
74
57
 
@@ -94,62 +77,17 @@ module MQTT
94
77
  def qos=(arg)
95
78
  @qos = arg.to_i
96
79
  end
97
-
98
- # Set (replace) the packet body
99
- def body=(arg)
100
- # FIXME: only allow 268435455 bytes?
101
- @body = arg.to_s
102
- end
103
-
104
-
105
-
106
-
107
-
108
- # Add an array of bytes to the end of the packet's body
109
- def add_bytes(*bytes)
110
- @body += bytes.pack('C*')
111
- end
112
80
 
113
- # Add a 16-bit unsigned integer to the end of the packet's body
114
- def add_short(val)
115
- @body += [val.to_i].pack('n')
116
- end
117
-
118
- # Add some raw data to the end of the packet's body
119
- def add_data(data)
120
- data = data.to_s unless data.is_a?(String)
121
- @body += data
122
- end
123
-
124
- # Add a string to the end of the packet's body
125
- # (preceded by the length of the string)
126
- def add_string(str)
127
- str = str.to_s unless str.is_a?(String)
128
- add_short(str.size)
129
- add_data(str)
130
- end
131
-
132
-
133
- # Remove a 16-bit unsigned integer from the front on the body
134
- def shift_short
135
- bytes = @body.slice!(0..1)
136
- bytes.unpack('n').first
137
- end
138
-
139
- # Remove n bytes from the front on the body
140
- def shift_bytes(bytes)
141
- @body.slice!(0...bytes).unpack('C*')
142
- end
143
-
144
- # Remove n bytes from the front on the body
145
- def shift_data(bytes)
146
- @body.slice!(0...bytes)
81
+ # Parse the body (variable header and payload) of a packet
82
+ def parse_body(buffer)
83
+ unless buffer.size == 0
84
+ raise MQTT::ProtocolException.new("Error: parse_body was not sub-classed for a packet with a payload")
85
+ end
147
86
  end
148
87
 
149
- # Remove string from the front on the body
150
- def shift_string
151
- len = shift_short
152
- shift_data(len)
88
+ # Get serialisation of packet's body (variable header and payload)
89
+ def encode_body
90
+ '' # No body by default
153
91
  end
154
92
 
155
93
 
@@ -163,8 +101,11 @@ module MQTT
163
101
  (retain ? 0x1 : 0x0)
164
102
  ]
165
103
 
104
+ # Get the packet's variable header and payload
105
+ body = self.encode_body
106
+
166
107
  # Build up the body length field bytes
167
- body_size = @body.size
108
+ body_size = body.size
168
109
  begin
169
110
  digit = (body_size % 128)
170
111
  body_size = (body_size / 128)
@@ -174,24 +115,493 @@ module MQTT
174
115
  end while (body_size > 0)
175
116
 
176
117
  # Convert header to binary and add on body
177
- header.pack('C*') + @body
118
+ header.pack('C*') + body
119
+ end
120
+
121
+
122
+ protected
123
+
124
+ # Encode an array of bytes and return them
125
+ def encode_bytes(*bytes)
126
+ bytes.pack('C*')
127
+ end
128
+
129
+ # Encode a 16-bit unsigned integer and return it
130
+ def encode_short(val)
131
+ [val.to_i].pack('n')
132
+ end
133
+
134
+ # Encode a string and return it
135
+ # (preceded by the length of the string)
136
+ def encode_string(str)
137
+ str = str.to_s unless str.is_a?(String)
138
+ encode_short(str.size) + str
139
+ end
140
+
141
+ # Remove a 16-bit unsigned integer from the front of buffer
142
+ def shift_short(buffer)
143
+ bytes = buffer.slice!(0..1)
144
+ bytes.unpack('n').first
178
145
  end
179
146
 
180
- def inspect
181
- format("#<MQTT::Packet:0x%01x ", object_id)+
182
- "type=#{@type}, dup=#{@dup}, retain=#{@retain}, "+
183
- "qos=#{@qos}, body.size=#{@body.size}>"
147
+ # Remove one byte from the front of the string
148
+ def shift_byte(buffer)
149
+ buffer.slice!(0...1).unpack('C').first
184
150
  end
185
151
 
152
+ # Remove n bytes from the front of buffer
153
+ def shift_data(buffer,bytes)
154
+ buffer.slice!(0...bytes)
155
+ end
156
+
157
+ # Remove string from the front of buffer
158
+ def shift_string(buffer)
159
+ len = shift_short(buffer)
160
+ shift_data(buffer,len)
161
+ end
162
+
163
+
186
164
  private
187
165
 
188
- # Read and unpack a single byte from socket
166
+ # Read and unpack a single byte from a socket
189
167
  def self.read_byte(socket)
190
168
  byte = socket.read(1)
191
169
  raise MQTT::ProtocolException if byte.nil?
192
170
  byte.unpack('C').first
193
171
  end
172
+
173
+
174
+
175
+ ## PACKET SUBCLASSES ##
176
+
177
+
178
+ # Class representing an MQTT Publish message
179
+ class Publish < MQTT::Packet
180
+ attr_accessor :topic
181
+ attr_accessor :message_id
182
+ attr_accessor :payload
183
+
184
+ # Create a new Unsubscribe Acknowledgment packet
185
+ def initialize(args={})
186
+ super(args)
187
+ self.topic = args[:topic] || nil
188
+ self.message_id = args[:message_id] || 0
189
+ self.payload = args[:payload] || ''
190
+ end
191
+
192
+ # Get serialisation of packet's body
193
+ def encode_body
194
+ body = ''
195
+ raise "Invalid topic name when serialising packet" if @topic.nil?
196
+ body += encode_string(@topic)
197
+ body += encode_short(@message_id) unless qos == 0
198
+ body += payload.to_s
199
+ return body
200
+ end
201
+
202
+ # Parse the body (variable header and payload) of a Publish packet
203
+ def parse_body(buffer)
204
+ @topic = shift_string(buffer)
205
+ @message_id = shift_short(buffer) unless qos == 0
206
+ @payload = buffer.dup
207
+ end
208
+ end
209
+
210
+ # Class representing an MQTT Connect Packet
211
+ class Connect < MQTT::Packet
212
+ attr_accessor :protocol_name
213
+ attr_accessor :protocol_version
214
+ attr_accessor :client_id
215
+ attr_accessor :clean_start
216
+ attr_accessor :keep_alive
217
+ attr_accessor :will_topic
218
+ attr_accessor :will_qos
219
+ attr_accessor :will_retain
220
+ attr_accessor :will_payload
221
+
222
+ # Create a new Client Connect packet
223
+ def initialize(args={})
224
+ super(args)
225
+ self.protocol_name = args[:protocol_name] || 'MQIsdp'
226
+ self.protocol_version = args[:protocol_version] || 0x03
227
+ self.client_id = args[:client_id] || nil
228
+ self.clean_start = args[:clean_start] || true
229
+ self.keep_alive = args[:keep_alive] || 10
230
+ self.will_topic = args[:will_topic] || nil
231
+ self.will_qos = args[:will_qos] || 0
232
+ self.will_retain = args[:will_retain] || false
233
+ self.will_payload = args[:will_payload] || ''
234
+ end
235
+
236
+ # Get serialisation of packet's body
237
+ def encode_body
238
+ body = ''
239
+ raise "Invalid client identifier when serialising packet" if @client_id.nil?
240
+ body += encode_string(@protocol_name)
241
+ body += encode_bytes(@protocol_version.to_i)
242
+ body += encode_bytes(0) # Connect Flags
243
+ body += encode_short(@keep_alive) # Keep Alive timer
244
+ body += encode_string(@client_id)
245
+ # FIXME: implement Will
246
+ #unless @will_topic.nil?
247
+ # body += encode_string(@will_topic)
248
+ # body += will_payload.to_s
249
+ #end
250
+ return body
251
+ end
252
+
253
+ # Parse the body (variable header and payload) of a Connect packet
254
+ def parse_body(buffer)
255
+ @protocol_name = shift_string(buffer)
256
+ @protocol_version = shift_byte(buffer)
257
+ flags = shift_byte(buffer)
258
+ @keep_alive = shift_short(buffer)
259
+ @client_id = shift_string(buffer)
260
+ # FIXME: implement Will
261
+ end
262
+ end
263
+
264
+ # Class representing an MQTT Connect Acknowledgment Packet
265
+ class Connack < MQTT::Packet
266
+ attr_accessor :return_code
267
+
268
+ # Create a new Client Connect packet
269
+ def initialize(args={})
270
+ super(args)
271
+ self.return_code = args[:return_code] || 0
272
+ end
273
+
274
+ # Get a string message corresponding to a return code
275
+ def return_msg
276
+ case return_code
277
+ when 0x00
278
+ "Connection Accepted"
279
+ when 0x01
280
+ "Connection refused: unacceptable protocol version"
281
+ when 0x02
282
+ "Connection refused: client identifier rejected"
283
+ when 0x03
284
+ "Connection refused: broker unavailable"
285
+ else
286
+ "Connection refused: error code #{return_code}"
287
+ end
288
+ end
289
+
290
+ # Get serialisation of packet's body
291
+ def encode_body
292
+ body = ''
293
+ body += encode_bytes(0) # Unused
294
+ body += encode_bytes(@return_code.to_i) # Return Code
295
+ return body
296
+ end
297
+
298
+ # Parse the body (variable header and payload) of a Connect Acknowledgment packet
299
+ def parse_body(buffer)
300
+ unused = shift_byte(buffer)
301
+ @return_code = shift_byte(buffer)
302
+ end
303
+ end
304
+
305
+ # Class representing an MQTT Publish Acknowledgment packet
306
+ class Puback < MQTT::Packet
307
+ attr_accessor :message_id
308
+
309
+ # Create a new Unsubscribe Acknowledgment packet
310
+ def initialize(args={})
311
+ super(args)
312
+ self.message_id = args[:message_id] || 0
313
+ end
314
+
315
+ # Get serialisation of packet's body
316
+ def encode_body
317
+ encode_short(@message_id)
318
+ end
319
+
320
+ # Parse the body (variable header and payload) of a packet
321
+ def parse_body(buffer)
322
+ @message_id = shift_short(buffer)
323
+ end
324
+ end
325
+
326
+ # Class representing an MQTT Publish Received packet
327
+ class Pubrec < MQTT::Packet
328
+ attr_accessor :message_id
329
+
330
+ # Create a new Unsubscribe Acknowledgment packet
331
+ def initialize(args={})
332
+ super(args)
333
+ self.message_id = args[:message_id] || 0
334
+ end
335
+
336
+ # Get serialisation of packet's body
337
+ def encode_body
338
+ encode_short(@message_id)
339
+ end
340
+
341
+ # Parse the body (variable header and payload) of a packet
342
+ def parse_body(buffer)
343
+ @message_id = shift_short(buffer)
344
+ end
345
+ end
346
+
347
+ # Class representing an MQTT Publish Release packet
348
+ class Pubrel < MQTT::Packet
349
+ attr_accessor :message_id
350
+
351
+ # Create a new Unsubscribe Acknowledgment packet
352
+ def initialize(args={})
353
+ super(args)
354
+ self.message_id = args[:message_id] || 0
355
+ end
356
+
357
+ # Get serialisation of packet's body
358
+ def encode_body
359
+ encode_short(@message_id)
360
+ end
361
+
362
+ # Parse the body (variable header and payload) of a packet
363
+ def parse_body(buffer)
364
+ @message_id = shift_short(buffer)
365
+ end
366
+ end
367
+
368
+ # Class representing an MQTT Publish Complete packet
369
+ class Pubcomp < MQTT::Packet
370
+ attr_accessor :message_id
371
+
372
+ # Create a new Unsubscribe Acknowledgment packet
373
+ def initialize(args={})
374
+ super(args)
375
+ self.message_id = args[:message_id] || 0
376
+ end
377
+
378
+ # Get serialisation of packet's body
379
+ def encode_body
380
+ encode_short(@message_id)
381
+ end
382
+
383
+ # Parse the body (variable header and payload) of a packet
384
+ def parse_body(buffer)
385
+ @message_id = shift_short(buffer)
386
+ end
387
+ end
388
+
389
+ # Class representing an MQTT Client Subscribe packet
390
+ class Subscribe < MQTT::Packet
391
+ attr_reader :topics
392
+ attr_accessor :message_id
393
+
394
+ # Create a new Unsubscribe Acknowledgment packet
395
+ def initialize(args={})
396
+ super(args)
397
+ self.topics = args[:topics] || []
398
+ self.message_id = args[:message_id] || 0
399
+ self.qos = 1 # Force a QOS of 1
400
+ end
401
+
402
+ # Set one or more topics for the Subscrible packet
403
+ # The topics parameter should be one of the following:
404
+ # * String: subscribe to one topic with QOS 0
405
+ # * Array: subscribe to multiple topics with QOS 0
406
+ # * Hash: subscribe to multiple topics where the key is the topic and the value is the QOS level
407
+ #
408
+ # For example:
409
+ # packet.topics = 'a/b'
410
+ # packet.topics = ['a/b', 'c/d']
411
+ # packet.topics = [['a/b',0], ['c/d',1]]
412
+ # packet.topics = {'a/b' => 0, 'c/d' => 1}
413
+ #
414
+ def topics=(value)
415
+ # Get input into a consistent state
416
+ if value.is_a?(Array)
417
+ input = value.flatten
418
+ else
419
+ input = [value]
420
+ end
421
+
422
+ @topics = []
423
+ while(input.size>0)
424
+ item = input.shift
425
+ if item.is_a?(Hash)
426
+ # Convert hash into an ordered array of arrays
427
+ @topics += item.sort
428
+ elsif item.is_a?(String)
429
+ # Peek at the next item in the array, and remove it if it is an integer
430
+ if input.first.is_a?(Integer)
431
+ qos = input.shift
432
+ @topics << [item,qos]
433
+ else
434
+ @topics << [item,0]
435
+ end
436
+ else
437
+ # Meh?
438
+ raise "Invalid topics input: #{value.inspect}"
439
+ end
440
+ end
441
+ @topics
442
+ end
443
+
444
+ # Get serialisation of packet's body
445
+ def encode_body
446
+ raise "no topics given when serialising packet" if @topics.empty?
447
+ body = encode_short(@message_id)
448
+ topics.each do |item|
449
+ body += encode_string(item[0])
450
+ body += encode_bytes(item[1])
451
+ end
452
+ return body
453
+ end
454
+
455
+ # Parse the body (variable header and payload) of a packet
456
+ def parse_body(buffer)
457
+ @message_id = shift_short(buffer)
458
+ @topics = []
459
+ while(buffer.size>0)
460
+ topic_name = shift_string(buffer)
461
+ topic_qos = shift_byte(buffer)
462
+ @topics << [topic_name,topic_qos]
463
+ end
464
+ end
465
+ end
466
+
467
+ # Class representing an MQTT Subscribe Acknowledgment packet
468
+ class Suback < MQTT::Packet
469
+ attr_accessor :message_id
470
+ attr_reader :granted_qos
471
+
472
+ # Create a new Unsubscribe Acknowledgment packet
473
+ def initialize(args={})
474
+ super(args)
475
+ self.message_id = args[:message_id] || 0
476
+ self.granted_qos = args[:granted_qos] || []
477
+ end
478
+
479
+ def granted_qos=(value)
480
+ raise "granted QOS should be an array of arrays" unless value.is_a?(Array)
481
+ @granted_qos = value
482
+ end
483
+
484
+ # Get serialisation of packet's body
485
+ def encode_body
486
+ raise "no granted QOS given when serialising packet" if @granted_qos.empty?
487
+ body = encode_short(@message_id)
488
+ granted_qos.flatten.each { |qos| body += encode_bytes(qos) }
489
+ return body
490
+ end
491
+
492
+ # Parse the body (variable header and payload) of a packet
493
+ def parse_body(buffer)
494
+ @message_id = shift_short(buffer)
495
+ while(buffer.size>0)
496
+ @granted_qos << [shift_byte(buffer),shift_byte(buffer)]
497
+ end
498
+ end
499
+ end
500
+
501
+ # Class representing an MQTT Client Unsubscribe packet
502
+ class Unsubscribe < MQTT::Packet
503
+ attr_reader :topics
504
+ attr_accessor :message_id
505
+
506
+ # Create a new Unsubscribe Acknowledgment packet
507
+ def initialize(args={})
508
+ super(args)
509
+ self.topics = args[:topics] || []
510
+ self.message_id = args[:message_id] || 0
511
+ self.qos = 1 # Force a QOS of 1
512
+ end
513
+
514
+ def topics=(value)
515
+ if value.is_a?(Array)
516
+ @topics = value
517
+ else
518
+ @topics = [value]
519
+ end
520
+ end
521
+
522
+ # Get serialisation of packet's body
523
+ def encode_body
524
+ raise "no topics given when serialising packet" if @topics.empty?
525
+ body = encode_short(@message_id)
526
+ topics.each { |topic| body += encode_string(topic) }
527
+ return body
528
+ end
529
+
530
+ # Parse the body (variable header and payload) of a packet
531
+ def parse_body(buffer)
532
+ @message_id = shift_short(buffer)
533
+ while(buffer.size>0)
534
+ @topics << shift_string(buffer)
535
+ end
536
+ end
537
+ end
538
+
539
+ # Class representing an MQTT Unsubscribe Acknowledgment packet
540
+ class Unsuback < MQTT::Packet
541
+ attr_accessor :message_id
542
+
543
+ # Create a new Unsubscribe Acknowledgment packet
544
+ def initialize(args={})
545
+ super(args)
546
+ self.message_id = args[:message_id] || 0
547
+ end
548
+
549
+ # Get serialisation of packet's body
550
+ def encode_body
551
+ encode_short(@message_id)
552
+ end
553
+
554
+ # Parse the body (variable header and payload) of a packet
555
+ def parse_body(buffer)
556
+ @message_id = shift_short(buffer)
557
+ end
558
+ end
559
+
560
+ # Class representing an MQTT Ping Request packet
561
+ class Pingreq < MQTT::Packet
562
+ # Create a new Ping Request packet
563
+ def initialize(args={})
564
+ super(args)
565
+ end
566
+ end
567
+
568
+ # Class representing an MQTT Ping Response packet
569
+ class Pingresp < MQTT::Packet
570
+ # Create a new Ping Response packet
571
+ def initialize(args={})
572
+ super(args)
573
+ end
574
+ end
575
+
576
+ # Class representing an MQTT Client Disconnect packet
577
+ class Disconnect < MQTT::Packet
578
+ # Create a new Client Disconnect packet
579
+ def initialize(args={})
580
+ super(args)
581
+ end
582
+ end
194
583
 
195
584
  end
585
+
586
+
587
+ # An enumeration of the MQTT packet types
588
+ PACKET_TYPES = [
589
+ nil,
590
+ MQTT::Packet::Connect,
591
+ MQTT::Packet::Connack,
592
+ MQTT::Packet::Publish,
593
+ MQTT::Packet::Puback,
594
+ MQTT::Packet::Pubrec,
595
+ MQTT::Packet::Pubrel,
596
+ MQTT::Packet::Pubcomp,
597
+ MQTT::Packet::Subscribe,
598
+ MQTT::Packet::Suback,
599
+ MQTT::Packet::Unsubscribe,
600
+ MQTT::Packet::Unsuback,
601
+ MQTT::Packet::Pingreq,
602
+ MQTT::Packet::Pingresp,
603
+ MQTT::Packet::Disconnect,
604
+ nil
605
+ ]
196
606
 
197
607
  end
data/lib/mqtt/proxy.rb ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mqtt'
4
+ require 'mqtt/packet'
5
+ require 'thread'
6
+ require 'logger'
7
+ require 'socket'
8
+
9
+
10
+ module MQTT
11
+
12
+ # Class for implementing a proxy to filter/mangle MQTT packets.
13
+ class Proxy
14
+ attr_reader :local_host
15
+ attr_reader :local_port
16
+ attr_reader :broker_host
17
+ attr_reader :broker_port
18
+ attr_reader :listen_queue
19
+ attr_reader :select_timeout
20
+ attr_reader :logger
21
+
22
+ # Create a new MQTT Proxy instance.
23
+ #
24
+ # Possible argument keys:
25
+ #
26
+ # :local_host Address to bind listening socket to.
27
+ # :local_port Port to bind listening socket to.
28
+ # :broker_host Address of upstream broker to send packets upstream to.
29
+ # :broker_port Port of upstream broker to send packets upstream to.
30
+ # :select_timeout Time in seconds before disconnecting a connection.
31
+ # :logger Ruby Logger object to send informational messages to.
32
+ #
33
+ # NOTE: be careful not to connect to yourself!
34
+ def initialize(args={})
35
+ @local_host = args[:local_host] || '0.0.0.0'
36
+ @local_port = args[:local_port] || 1883
37
+ @broker_host = args[:broker_host] || 'localhost'
38
+ @broker_port = args[:broker_port] || 18830
39
+ @select_timeout = args[:select_timeout] || 60
40
+
41
+ # Setup a logger
42
+ @logger = args[:logger]
43
+ if @logger.nil?
44
+ @logger = Logger.new(STDOUT)
45
+ @logger.level = Logger::INFO
46
+ end
47
+
48
+ # Default is not to have any filters
49
+ @client_filter = nil
50
+ @broker_filter = nil
51
+
52
+ # Create TCP server socket
53
+ @server = TCPServer.open(@local_host,@local_port)
54
+ @logger.info "MQTT::Proxy listening on #{@local_host}:#{@local_port}"
55
+ end
56
+
57
+ # Set a filter Proc for packets coming from the client (to the broker).
58
+ def client_filter=(proc)
59
+ @client_filter = proc
60
+ end
61
+
62
+ # Set a filter Proc for packets coming from the broker (to the client).
63
+ def broker_filter=(proc)
64
+ @broker_filter = proc
65
+ end
66
+
67
+ # Start accepting connections and processing packets.
68
+ def run
69
+ loop do
70
+ # Wait for a client to connect and then create a thread for it
71
+ Thread.new(@server.accept) do |client_socket|
72
+ logger.info "Accepted client: #{client_socket.peeraddr.join(':')}"
73
+ broker_socket = TCPSocket.new(@broker_host,@broker_port)
74
+ begin
75
+ process_packets(client_socket,broker_socket)
76
+ rescue Exception => exp
77
+ logger.error exp.to_s
78
+ end
79
+ logger.info "Disconnected: #{client_socket.peeraddr.join(':')}"
80
+ broker_socket.close
81
+ client_socket.close
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def process_packets(client_socket,broker_socket)
89
+ loop do
90
+ # Wait for some data on either socket
91
+ selected = IO.select([client_socket,broker_socket], nil, nil, @select_timeout)
92
+ if selected.nil?
93
+ # Timeout
94
+ raise "Timeout in select"
95
+ else
96
+ # Iterate through each of the sockets with data to read
97
+ if selected[0].include?(client_socket)
98
+ packet = MQTT::Packet.read(client_socket)
99
+ logger.debug "client -> <#{packet.type}>"
100
+ packet = @client_filter.call(packet) unless @client_filter.nil?
101
+ unless packet.nil?
102
+ broker_socket.write(packet)
103
+ logger.debug "<#{packet.type}> -> broker"
104
+ end
105
+ elsif selected[0].include?(broker_socket)
106
+ packet = MQTT::Packet.read(broker_socket)
107
+ logger.debug "broker -> <#{packet.type}>"
108
+ packet = @broker_filter.call(packet) unless @broker_filter.nil?
109
+ unless packet.nil?
110
+ client_socket.write(packet)
111
+ logger.debug "<#{packet.type}> -> client"
112
+ end
113
+ else
114
+ logger.error "Problem with select: socket is neither broker or client"
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ end
121
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mqtt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicholas J Humfrey
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-08 00:00:00 +00:00
12
+ date: 2009-02-22 00:00:00 +00:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -28,7 +28,9 @@ files:
28
28
  - lib/mqtt.rb
29
29
  - lib/mqtt/client.rb
30
30
  - lib/mqtt/packet.rb
31
+ - lib/mqtt/proxy.rb
31
32
  - examples/livetext.rb
33
+ - examples/readonly_proxy.rb
32
34
  - examples/simple_get.rb
33
35
  - examples/simple_publish.rb
34
36
  - README