mqtt 0.0.3 → 0.0.4

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