mqtt 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/lib/mqtt/proxy.rb CHANGED
@@ -1,121 +1,109 @@
1
- #!/usr/bin/env ruby
1
+ # Class for implementing a proxy to filter/mangle MQTT packets.
2
+ class MQTT::Proxy
3
+ attr_reader :local_host
4
+ attr_reader :local_port
5
+ attr_reader :broker_host
6
+ attr_reader :broker_port
7
+ attr_reader :listen_queue
8
+ attr_reader :select_timeout
9
+ attr_reader :logger
2
10
 
3
- require 'mqtt'
4
- require 'mqtt/packet'
5
- require 'thread'
6
- require 'logger'
7
- require 'socket'
11
+ # Create a new MQTT Proxy instance.
12
+ #
13
+ # Possible argument keys:
14
+ #
15
+ # :local_host Address to bind listening socket to.
16
+ # :local_port Port to bind listening socket to.
17
+ # :broker_host Address of upstream broker to send packets upstream to.
18
+ # :broker_port Port of upstream broker to send packets upstream to.
19
+ # :select_timeout Time in seconds before disconnecting a connection.
20
+ # :logger Ruby Logger object to send informational messages to.
21
+ #
22
+ # NOTE: be careful not to connect to yourself!
23
+ def initialize(args={})
24
+ @local_host = args[:local_host] || '0.0.0.0'
25
+ @local_port = args[:local_port] || MQTT::DEFAULT_PORT
26
+ @broker_host = args[:broker_host] || MQTT::DEFAULT_HOST
27
+ @broker_port = args[:broker_port] || 18830
28
+ @select_timeout = args[:select_timeout] || 60
8
29
 
30
+ # Setup a logger
31
+ @logger = args[:logger]
32
+ if @logger.nil?
33
+ @logger = Logger.new(STDOUT)
34
+ @logger.level = Logger::INFO
35
+ end
9
36
 
10
- module MQTT
37
+ # Default is not to have any filters
38
+ @client_filter = nil
39
+ @broker_filter = nil
11
40
 
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
41
+ # Create TCP server socket
42
+ @server = TCPServer.open(@local_host,@local_port)
43
+ @logger.info "MQTT::Proxy listening on #{@local_host}:#{@local_port}"
44
+ end
45
+
46
+ # Set a filter Proc for packets coming from the client (to the broker).
47
+ def client_filter=(proc)
48
+ @client_filter = proc
49
+ end
50
+
51
+ # Set a filter Proc for packets coming from the broker (to the client).
52
+ def broker_filter=(proc)
53
+ @broker_filter = proc
54
+ end
55
+
56
+ # Start accepting connections and processing packets.
57
+ def run
58
+ loop do
59
+ # Wait for a client to connect and then create a thread for it
60
+ Thread.new(@server.accept) do |client_socket|
61
+ logger.info "Accepted client: #{client_socket.peeraddr.join(':')}"
62
+ broker_socket = TCPSocket.new(@broker_host,@broker_port)
63
+ begin
64
+ process_packets(client_socket,broker_socket)
65
+ rescue Exception => exp
66
+ logger.error exp.to_s
82
67
  end
68
+ logger.info "Disconnected: #{client_socket.peeraddr.join(':')}"
69
+ broker_socket.close
70
+ client_socket.close
83
71
  end
84
72
  end
73
+ end
85
74
 
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"
75
+ private
76
+
77
+ def process_packets(client_socket,broker_socket)
78
+ loop do
79
+ # Wait for some data on either socket
80
+ selected = IO.select([client_socket,broker_socket], nil, nil, @select_timeout)
81
+ if selected.nil?
82
+ # Timeout
83
+ raise "Timeout in select"
84
+ else
85
+ # Iterate through each of the sockets with data to read
86
+ if selected[0].include?(client_socket)
87
+ packet = MQTT::Packet.read(client_socket)
88
+ logger.debug "client -> <#{packet.type}>"
89
+ packet = @client_filter.call(packet) unless @client_filter.nil?
90
+ unless packet.nil?
91
+ broker_socket.write(packet)
92
+ logger.debug "<#{packet.type}> -> broker"
115
93
  end
94
+ elsif selected[0].include?(broker_socket)
95
+ packet = MQTT::Packet.read(broker_socket)
96
+ logger.debug "broker -> <#{packet.type}>"
97
+ packet = @broker_filter.call(packet) unless @broker_filter.nil?
98
+ unless packet.nil?
99
+ client_socket.write(packet)
100
+ logger.debug "<#{packet.type}> -> client"
101
+ end
102
+ else
103
+ logger.error "Problem with select: socket is neither broker or client"
116
104
  end
117
105
  end
118
106
  end
119
-
120
107
  end
121
- end
108
+
109
+ end
@@ -0,0 +1,3 @@
1
+ module MQTT
2
+ VERSION = "0.0.5"
3
+ end
@@ -0,0 +1,303 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'spec_helper'
4
+ require 'mqtt'
5
+
6
+ describe MQTT::Client do
7
+
8
+ before(:each) do
9
+ @client = MQTT::Client.new
10
+ @socket = StringIO.new
11
+ end
12
+
13
+ describe "when calling the 'connect' method" do
14
+ before(:each) do
15
+ TCPSocket.stubs(:new).returns(@socket)
16
+ Thread.stubs(:new)
17
+ @client.stubs(:receive_connack)
18
+ end
19
+
20
+ it "should create a TCP Socket if not connected" do
21
+ TCPSocket.expects(:new).once.returns(@socket)
22
+ @client.connect('myclient')
23
+ end
24
+
25
+ it "should not create a new TCP Socket if connected" do
26
+ @client.stubs(:connected?).returns(true)
27
+ TCPSocket.expects(:new).never
28
+ @client.connect('myclient')
29
+ end
30
+
31
+ it "should start the reader thread if not connected" do
32
+ Thread.expects(:new).once
33
+ @client.connect('myclient')
34
+ end
35
+
36
+ it "should write a valid CONNECT packet to the socket if not connected" do
37
+ @client.connect('myclient')
38
+ @socket.string.should == "\020\026\x00\x06MQIsdp\x03\x02\x00\x0a\x00\x08myclient"
39
+ end
40
+
41
+ it "should try and read an acknowledgement packet to the socket if not connected" do
42
+ @client.expects(:receive_connack).once
43
+ @client.connect('myclient')
44
+ end
45
+
46
+ it "should disconnect after connecting, if a block is given" do
47
+ @client.expects(:disconnect).once
48
+ @client.connect('myclient') { nil }
49
+ end
50
+
51
+ it "should not disconnect after connecting, if no block is given" do
52
+ @client.expects(:disconnect).never
53
+ @client.connect('myclient')
54
+ end
55
+
56
+ it "should include the username and password for an authenticated connection" do
57
+ @client.username = 'username'
58
+ @client.password = 'password'
59
+ @client.connect('myclient')
60
+ @socket.string.should ==
61
+ "\x10\x2A"+
62
+ "\x00\x06MQIsdp"+
63
+ "\x03\xC2\x00\x0a\x00"+
64
+ "\x08myclient"+
65
+ "\x00\x08username"+
66
+ "\x00\x08password"
67
+ end
68
+
69
+ it "should reset clean session flag to true, if no client id is given" do
70
+ @client.client_id = nil
71
+ @client.clean_session = false
72
+ @client.connect
73
+ @client.clean_session.should be_true
74
+ end
75
+ end
76
+
77
+ describe "when calling the 'receive_connack' method" do
78
+ before(:each) do
79
+ @client.instance_variable_set(:@socket, @socket)
80
+ IO.stubs(:select).returns([[@socket], [], []])
81
+ end
82
+
83
+ it "should not throw an exception for a successful CONNACK packet" do
84
+ @socket.write("\x20\x02\x00\x00")
85
+ @socket.rewind
86
+ lambda { @client.send(:receive_connack) }.should_not raise_error
87
+ end
88
+
89
+ it "should throw an exception if the packet type isn't CONNACK" do
90
+ @socket.write("\xD0\x00")
91
+ @socket.rewind
92
+ lambda { @client.send(:receive_connack) }.should raise_error(MQTT::ProtocolException)
93
+ end
94
+
95
+ it "should throw an exception if the CONNACK packet return code is 'unacceptable protocol version'" do
96
+ @socket.write("\x20\x02\x00\x01")
97
+ @socket.rewind
98
+ lambda { @client.send(:receive_connack) }.should raise_error(MQTT::ProtocolException, /unacceptable protocol version/i)
99
+ end
100
+
101
+ it "should throw an exception if the CONNACK packet return code is 'client identifier rejected'" do
102
+ @socket.write("\x20\x02\x00\x02")
103
+ @socket.rewind
104
+ lambda { @client.send(:receive_connack) }.should raise_error(MQTT::ProtocolException, /client identifier rejected/i)
105
+ end
106
+
107
+ it "should throw an exception if the CONNACK packet return code is 'broker unavailable'" do
108
+ @socket.write("\x20\x02\x00\x03")
109
+ @socket.rewind
110
+ lambda { @client.send(:receive_connack) }.should raise_error(MQTT::ProtocolException, /broker unavailable/i)
111
+ end
112
+
113
+ it "should throw an exception if the CONNACK packet return code is an unknown" do
114
+ @socket.write("\x20\x02\x00\xAA")
115
+ @socket.rewind
116
+ lambda { @client.send(:receive_connack) }.should raise_error(MQTT::ProtocolException, /connection refused/i)
117
+ end
118
+ end
119
+
120
+ describe "when calling the 'disconnect' method" do
121
+ before(:each) do
122
+ @client.instance_variable_set(:@socket, @socket)
123
+ @client.instance_variable_set(:@read_thread, stub_everything('Read Thread'))
124
+ end
125
+
126
+ it "should not do anything if the socket is already disconnected" do
127
+ @client.stubs(:connected?).returns(false)
128
+ @client.disconnect(true)
129
+ @socket.string.should == ""
130
+ end
131
+
132
+ it "should write a valid DISCONNECT packet to the socket if connected and the send_msg=true an" do
133
+ @client.stubs(:connected?).returns(true)
134
+ @client.disconnect(true)
135
+ @socket.string.should == "\xE0\x00"
136
+ end
137
+
138
+ it "should not write anything to the socket if the send_msg=false" do
139
+ @client.stubs(:connected?).returns(true)
140
+ @client.disconnect(false)
141
+ @socket.string.should be_empty
142
+ end
143
+
144
+ it "should call the close method on the socket" do
145
+ @socket.expects(:close)
146
+ @client.disconnect
147
+ end
148
+ end
149
+
150
+ describe "when calling the 'ping' method" do
151
+ before(:each) do
152
+ @client.instance_variable_set(:@socket, @socket)
153
+ end
154
+
155
+ it "should write a valid PINGREQ packet to the socket" do
156
+ @client.ping
157
+ @socket.string.should == "\xC0\x00"
158
+ end
159
+
160
+ it "should update the time a ping was last sent" do
161
+ @client.instance_variable_set(:@last_pingreq, 0)
162
+ @client.ping
163
+ @client.instance_variable_get(:@last_pingreq).should_not == 0
164
+ end
165
+ end
166
+
167
+ describe "when calling the 'publish' method" do
168
+ before(:each) do
169
+ @client.instance_variable_set(:@socket, @socket)
170
+ end
171
+
172
+ it "should write a valid PUBLISH packet to the socket without the retain flag" do
173
+ @client.publish('topic','payload', false, 0)
174
+ @socket.string.should == "\x30\x0e\x00\x05topicpayload"
175
+ end
176
+
177
+ it "should write a valid PUBLISH packet to the socket with the retain flag set" do
178
+ @client.publish('topic','payload', true, 0)
179
+ @socket.string.should == "\x31\x0e\x00\x05topicpayload"
180
+ end
181
+
182
+ it "should write a valid PUBLISH packet to the socket with the QOS set to 1" do
183
+ @client.publish('topic','payload', false, 1)
184
+ @socket.string.should == "\x32\x10\x00\x05topic\x00\x01payload"
185
+ end
186
+
187
+ it "should write a valid PUBLISH packet to the socket with the QOS set to 2" do
188
+ @client.publish('topic','payload', false, 2)
189
+ @socket.string.should == "\x34\x10\x00\x05topic\x00\x01payload"
190
+ end
191
+ end
192
+
193
+ describe "when calling the 'subscribe' method" do
194
+ before(:each) do
195
+ @client.instance_variable_set(:@socket, @socket)
196
+ end
197
+
198
+ it "should write a valid SUBSCRIBE packet to the socket if given a single topic String" do
199
+ @client.subscribe('a/b')
200
+ @socket.string.should == "\x82\x08\x00\x01\x00\x03a/b\x00"
201
+ end
202
+
203
+ it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings in an Array" do
204
+ @client.subscribe('a/b','c/d')
205
+ @socket.string.should == "\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x00"
206
+ end
207
+
208
+ it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings with QoS in an Array" do
209
+ @client.subscribe(['a/b',0],['c/d',1])
210
+ @socket.string.should == "\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x01"
211
+ end
212
+
213
+ it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings with QoS in a Hash" do
214
+ @client.subscribe('a/b' => 0,'c/d' => 1)
215
+ @socket.string.should == "\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x01"
216
+ end
217
+ end
218
+
219
+ describe "when calling the 'get' method" do
220
+ before(:each) do
221
+ @client.instance_variable_set(:@socket, @socket)
222
+ end
223
+
224
+ def inject_packet(opts={})
225
+ packet = MQTT::Packet::Publish.new(opts)
226
+ @client.instance_variable_get('@read_queue').push(packet)
227
+ end
228
+
229
+ it "should successfull receive a valid PUBLISH packet with a QoS 0" do
230
+ inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
231
+ topic,payload = @client.get
232
+ topic.should == 'topic0'
233
+ payload.should == 'payload0'
234
+ end
235
+
236
+ it "should successfull receive a valid PUBLISH packet with a QoS 1" do
237
+ inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
238
+ topic,payload = @client.get
239
+ topic.should == 'topic1'
240
+ payload.should == 'payload1'
241
+ end
242
+ end
243
+
244
+ describe "when calling the 'unsubscribe' method" do
245
+ before(:each) do
246
+ @client.instance_variable_set(:@socket, @socket)
247
+ end
248
+
249
+ it "should write a valid UNSUBSCRIBE packet to the socket if given a single topic String" do
250
+ @client.unsubscribe('a/b')
251
+ @socket.string.should == "\xa2\x07\x00\x01\x00\x03a/b"
252
+ end
253
+
254
+ it "should write a valid UNSUBSCRIBE packet to the socket if given a two topic Strings" do
255
+ @client.unsubscribe('a/b','c/d')
256
+ @socket.string.should == "\xa2\x0c\x00\x01\x00\x03a/b\x00\x03c/d"
257
+ end
258
+ end
259
+
260
+ describe "when calling the 'receive_packet' method" do
261
+ before(:each) do
262
+ @client.instance_variable_set(:@socket, @socket)
263
+ IO.stubs(:select).returns([[@socket], [], []])
264
+ @read_queue = @client.instance_variable_get(:@read_queue)
265
+ @parent_thread = Thread.current[:parent] = stub_everything('Parent Thread')
266
+ end
267
+
268
+ it "should put PUBLISH messages on to the read queue" do
269
+ @socket.write("\x30\x0e\x00\x05topicpayload")
270
+ @socket.rewind
271
+ @client.send(:receive_packet)
272
+ @read_queue.size.should == 1
273
+ end
274
+
275
+ it "should not put other messages on to the read queue" do
276
+ @socket.write("\x20\x02\x00\x00")
277
+ @socket.rewind
278
+ @client.send(:receive_packet)
279
+ @read_queue.size.should == 0
280
+ end
281
+
282
+ it "should send a ping packet if one is due" do
283
+ IO.expects(:select).returns(nil)
284
+ @client.instance_variable_set(:@last_pingreq, Time.at(0))
285
+ @client.expects(:ping).once
286
+ @client.send(:receive_packet)
287
+ end
288
+
289
+ it "should close the socket if there is an exception" do
290
+ @socket.expects(:close).once
291
+ MQTT::Packet.stubs(:read).raises(MQTT::Exception)
292
+ @client.send(:receive_packet)
293
+ end
294
+
295
+ it "should pass exceptions up to parent thread" do
296
+ @parent_thread.expects(:raise).once
297
+ MQTT::Packet.stubs(:read).raises(MQTT::Exception)
298
+ @client.send(:receive_packet)
299
+ end
300
+
301
+ end
302
+
303
+ end