mqtt-rails 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +210 -0
  8. data/README.md +323 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/lib/mqtt-rails.rb +144 -0
  13. data/lib/mqtt_rails/client.rb +414 -0
  14. data/lib/mqtt_rails/connection_helper.rb +172 -0
  15. data/lib/mqtt_rails/exception.rb +52 -0
  16. data/lib/mqtt_rails/handler.rb +274 -0
  17. data/lib/mqtt_rails/packet.rb +33 -0
  18. data/lib/mqtt_rails/packet/base.rb +315 -0
  19. data/lib/mqtt_rails/packet/connack.rb +102 -0
  20. data/lib/mqtt_rails/packet/connect.rb +183 -0
  21. data/lib/mqtt_rails/packet/disconnect.rb +38 -0
  22. data/lib/mqtt_rails/packet/pingreq.rb +29 -0
  23. data/lib/mqtt_rails/packet/pingresp.rb +38 -0
  24. data/lib/mqtt_rails/packet/puback.rb +44 -0
  25. data/lib/mqtt_rails/packet/pubcomp.rb +44 -0
  26. data/lib/mqtt_rails/packet/publish.rb +148 -0
  27. data/lib/mqtt_rails/packet/pubrec.rb +44 -0
  28. data/lib/mqtt_rails/packet/pubrel.rb +62 -0
  29. data/lib/mqtt_rails/packet/suback.rb +75 -0
  30. data/lib/mqtt_rails/packet/subscribe.rb +124 -0
  31. data/lib/mqtt_rails/packet/unsuback.rb +49 -0
  32. data/lib/mqtt_rails/packet/unsubscribe.rb +84 -0
  33. data/lib/mqtt_rails/publisher.rb +181 -0
  34. data/lib/mqtt_rails/sender.rb +129 -0
  35. data/lib/mqtt_rails/ssl_helper.rb +61 -0
  36. data/lib/mqtt_rails/subscriber.rb +166 -0
  37. data/lib/mqtt_rails/version.rb +3 -0
  38. data/mqtt-rails.gemspec +33 -0
  39. data/samples/client_blocking(reading).rb +29 -0
  40. data/samples/client_blocking(writing).rb +18 -0
  41. data/samples/getting_started.rb +49 -0
  42. data/samples/test_client.rb +69 -0
  43. metadata +126 -0
@@ -0,0 +1,172 @@
1
+ # Copyright (c) 2016-2017 Pierre Goudet <p-goudet@ruby-dev.jp>
2
+ #
3
+ # All rights reserved. This program and the accompanying materials
4
+ # are made available under the terms of the Eclipse Public License v1.0
5
+ # and Eclipse Distribution License v1.0 which accompany this distribution.
6
+ #
7
+ # The Eclipse Public License is available at
8
+ # https://eclipse.org/org/documents/epl-v10.php.
9
+ # and the Eclipse Distribution License is available at
10
+ # https://eclipse.org/org/documents/edl-v10.php.
11
+ #
12
+ # Contributors:
13
+ # Pierre Goudet - initial committer
14
+
15
+ require 'socket'
16
+
17
+ module MqttRails
18
+ class ConnectionHelper
19
+
20
+ attr_accessor :sender
21
+
22
+ def initialize(host, port, ssl, ssl_context, ack_timeout)
23
+ @cs = MQTT_CS_DISCONNECT
24
+ @socket = nil
25
+ @host = host
26
+ @port = port
27
+ @ssl = ssl
28
+ @ssl_context = ssl_context
29
+ @ack_timeout = ack_timeout
30
+ @sender = Sender.new(ack_timeout)
31
+ end
32
+
33
+ def handler=(handler)
34
+ @handler = handler
35
+ end
36
+
37
+ def do_connect(reconnection=false)
38
+ @cs = MQTT_CS_NEW
39
+ @handler.socket = @socket
40
+ # Waiting a Connack packet for "ack_timeout" second from the remote
41
+ connect_timeout = Time.now + @ack_timeout
42
+ while (Time.now <= connect_timeout) && !is_connected? do
43
+ @cs = @handler.receive_packet
44
+ end
45
+ unless is_connected?
46
+ Rails.logger.warn("Connection failed. Couldn't recieve a Connack packet from: #{@host}.")
47
+ raise Exception.new("Connection failed. Check log for more details.") unless reconnection
48
+ end
49
+ @cs
50
+ end
51
+
52
+ def is_connected?
53
+ @cs == MQTT_CS_CONNECTED
54
+ end
55
+
56
+ def do_disconnect(publisher, explicit, mqtt_thread)
57
+ Rails.logger.info("Disconnecting from #{@host}.")
58
+ if explicit
59
+ explicit_disconnect(publisher, mqtt_thread)
60
+ end
61
+ @socket.close unless @socket.nil? || @socket.closed?
62
+ @socket = nil
63
+ end
64
+
65
+ def explicit_disconnect(publisher, mqtt_thread)
66
+ @sender.flush_waiting_packet(false)
67
+ send_disconnect
68
+ mqtt_thread.kill if mqtt_thread && mqtt_thread.alive?
69
+ publisher.flush_publisher unless publisher.nil?
70
+ end
71
+
72
+ def setup_connection
73
+ clean_start(@host, @port)
74
+ config_socket
75
+ unless @socket.nil?
76
+ @sender.socket = @socket
77
+ end
78
+ end
79
+
80
+ def config_socket
81
+ Rails.logger.info("Attempt to connect to host: #{@host}...")
82
+ begin
83
+ tcp_socket = TCPSocket.new(@host, @port)
84
+ if @ssl
85
+ encrypted_socket(tcp_socket, @ssl_context)
86
+ else
87
+ @socket = tcp_socket
88
+ end
89
+ rescue StandardError
90
+ Rails.logger.warn("Could not open a socket with #{@host} on port #{@port}.")
91
+ end
92
+ end
93
+
94
+ def encrypted_socket(tcp_socket, ssl_context)
95
+ unless ssl_context.nil?
96
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
97
+ @socket.sync_close = true
98
+ @socket.connect
99
+ else
100
+ Rails.logger.error("The SSL context was found as nil while the socket's opening.")
101
+ raise Exception
102
+ end
103
+ end
104
+
105
+ def clean_start(host, port)
106
+ self.host = host
107
+ self.port = port
108
+ unless @socket.nil?
109
+ @socket.close unless @socket.closed?
110
+ @socket = nil
111
+ end
112
+ end
113
+
114
+ def host=(host)
115
+ if host.nil? || host == ""
116
+ Rails.logger.error("The host was found as nil while the connection setup.")
117
+ raise ArgumentError
118
+ else
119
+ @host = host
120
+ end
121
+ end
122
+
123
+ def port=(port)
124
+ if port.to_i <= 0
125
+ Rails.logger.error("The port value is invalid (<= 0). Could not setup the connection.")
126
+ raise ArgumentError
127
+ else
128
+ @port = port
129
+ end
130
+ end
131
+
132
+ def send_connect(session_params)
133
+ setup_connection
134
+ packet = MqttRails::Packet::Connect.new(session_params)
135
+ @handler.clean_session = session_params[:clean_session]
136
+ @sender.send_packet(packet)
137
+ MQTT_ERR_SUCCESS
138
+ end
139
+
140
+ def send_disconnect
141
+ packet = MqttRails::Packet::Disconnect.new
142
+ @sender.send_packet(packet)
143
+ MQTT_ERR_SUCCESS
144
+ end
145
+
146
+ # Would return 'true' if ping requset should be sent and 'nil' if not
147
+ def should_send_ping?(now, keep_alive, last_packet_received_at)
148
+ last_pingreq_sent_at = @sender.last_pingreq_sent_at
149
+ last_pingresp_received_at = @handler.last_pingresp_received_at
150
+ if !last_pingreq_sent_at || (last_pingresp_received_at && (last_pingreq_sent_at <= last_pingresp_received_at))
151
+ next_pingreq_at = [@sender.last_packet_sent_at, last_packet_received_at].min + (keep_alive * 0.7).ceil
152
+ return next_pingreq_at <= now
153
+ end
154
+ end
155
+
156
+ def check_keep_alive(persistent, keep_alive)
157
+ now = Time.now
158
+ last_packet_received_at = @handler.last_packet_received_at
159
+ # send a PINGREQ only if we don't already wait for a PINGRESP
160
+ if persistent && should_send_ping?(now, keep_alive, last_packet_received_at)
161
+ Rails.logger.info("Checking if server is still alive...")
162
+ @sender.send_pingreq
163
+ end
164
+ disconnect_timeout_at = last_packet_received_at + (keep_alive * 1.1).ceil
165
+ if disconnect_timeout_at <= now
166
+ Rails.logger.info("No activity is over timeout, disconnecting from #{@host}.")
167
+ @cs = MQTT_CS_DISCONNECT
168
+ end
169
+ @cs
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright (c) 2016-2018 Pierre Goudet <p-goudet@ruby-dev.jp>
2
+ #
3
+ # All rights reserved. This program and the accompanying materials
4
+ # are made available under the terms of the Eclipse Public License v1.0
5
+ # and Eclipse Distribution License v1.0 which accompany this distribution.
6
+ #
7
+ # The Eclipse Public License is available at
8
+ # https://eclipse.org/org/documents/epl-v10.php.
9
+ # and the Eclipse Distribution License is available at
10
+ # https://eclipse.org/org/documents/edl-v10.php.
11
+ #
12
+ # Contributors:
13
+ # Pierre Goudet - initial committer
14
+
15
+
16
+ module MqttRails
17
+ class Exception < ::StandardError
18
+ def initialize(msg="")
19
+ super
20
+ end
21
+ end
22
+
23
+ class ProtocolViolation < Exception
24
+ end
25
+
26
+ class WritingException < Exception
27
+ end
28
+
29
+ class ReadingException < Exception
30
+ end
31
+
32
+ class PacketException < Exception
33
+ end
34
+
35
+ class PacketFormatException < Exception
36
+ end
37
+
38
+ class ProtocolVersionException < Exception
39
+ end
40
+
41
+ class LowVersionException < Exception
42
+ end
43
+
44
+ class FullWritingException < Exception
45
+ end
46
+
47
+ class FullQueueException < Exception
48
+ end
49
+
50
+ class NotSupportedEncryptionException < Exception
51
+ end
52
+ end
@@ -0,0 +1,274 @@
1
+ # Copyright (c) 2016-2017 Pierre Goudet <p-goudet@ruby-dev.jp>
2
+ #
3
+ # All rights reserved. This program and the accompanying materials
4
+ # are made available under the terms of the Eclipse Public License v1.0
5
+ # and Eclipse Distribution License v1.0 which accompany this distribution.
6
+ #
7
+ # The Eclipse Public License is available at
8
+ # https://eclipse.org/org/documents/epl-v10.php.
9
+ # and the Eclipse Distribution License is available at
10
+ # https://eclipse.org/org/documents/edl-v10.php.
11
+ #
12
+ # Contributors:
13
+ # Pierre Goudet - initial committer
14
+
15
+ module MqttRails
16
+ class Handler
17
+
18
+ attr_reader :registered_callback
19
+ attr_reader :last_packet_received_at
20
+ attr_reader :last_pingresp_received_at
21
+ attr_accessor :clean_session
22
+
23
+ def initialize
24
+ @registered_callback = []
25
+ @publisher = nil
26
+ @subscriber = nil
27
+ end
28
+
29
+ def config_pubsub(publisher, subscriber)
30
+ @publisher = publisher
31
+ @subscriber = subscriber
32
+ end
33
+
34
+ def socket=(socket)
35
+ @socket = socket
36
+ end
37
+
38
+ def receive_packet
39
+ result = IO.select([@socket], nil, nil, SELECT_TIMEOUT) unless @socket.nil? || @socket.closed?
40
+ unless result.nil?
41
+ packet = MqttRails::Packet::Base.read(@socket)
42
+ unless packet.nil?
43
+ @last_packet_received_at = Time.now
44
+ if packet.is_a?(MqttRails::Packet::Connack)
45
+ return handle_connack(packet)
46
+ else
47
+ handle_packet(packet)
48
+ end
49
+ end
50
+ end
51
+ result
52
+ end
53
+
54
+ def handle_packet(packet)
55
+ Rails.logger.info("New packet #{packet.class} received.")
56
+ type = packet_type(packet)
57
+ self.send("handle_#{type}", packet)
58
+ end
59
+
60
+ def register_topic_callback(topic, callback, &block)
61
+ if topic.nil?
62
+ Rails.logger.error("The topics where the callback is trying to be registered have been found nil.")
63
+ raise ArgumentError
64
+ end
65
+ clear_topic_callback(topic)
66
+ if block_given?
67
+ @registered_callback.push([topic, block])
68
+ elsif !(callback.nil?) && callback.is_a?(Proc)
69
+ @registered_callback.push([topic, callback])
70
+ end
71
+ MQTT_ERR_SUCCESS
72
+ end
73
+
74
+ def clear_topic_callback(topic)
75
+ if topic.nil?
76
+ Rails.logger.error("The topics where the callback is trying to be unregistered have been found nil.")
77
+ raise ArgumentError
78
+ end
79
+ @registered_callback.delete_if { |pair| pair.first == topic }
80
+ MQTT_ERR_SUCCESS
81
+ end
82
+
83
+ def handle_connack(packet)
84
+ if packet.return_code == 0x00
85
+ Rails.logger.info(packet.return_msg)
86
+ @last_pingresp_received_at = Time.now
87
+ handle_connack_accepted(packet.session_present)
88
+ else
89
+ Rails.logger.warn(packet.return_msg)
90
+ return MQTT_CS_DISCONNECT
91
+ end
92
+ @on_connack.call(packet) unless @on_connack.nil?
93
+ MQTT_CS_CONNECTED
94
+ end
95
+
96
+ def handle_connack_accepted(session_flag)
97
+ clean_session?(session_flag)
98
+ new_session?(session_flag)
99
+ old_session?(session_flag)
100
+ end
101
+
102
+ def new_session?(session_flag)
103
+ if !@clean_session && !session_flag
104
+ Rails.logger.info("New session created for the client.")
105
+ end
106
+ end
107
+
108
+ def clean_session?(session_flag)
109
+ if @clean_session && !session_flag
110
+ Rails.logger.info("No previous session found by server, starting a new one.")
111
+ end
112
+ end
113
+
114
+ def old_session?(session_flag)
115
+ if !@clean_session && session_flag
116
+ Rails.logger.info("Previous session restored by the server.")
117
+ end
118
+ end
119
+
120
+ def handle_pingresp(_packet)
121
+ @last_pingresp_received_at = Time.now
122
+ end
123
+
124
+ def handle_suback(packet)
125
+ max_qos = packet.return_codes
126
+ id = packet.id
127
+ topics = []
128
+ topics = @subscriber.add_subscription(max_qos, id, topics)
129
+ unless topics.empty?
130
+ @on_suback.call(topics) unless @on_suback.nil?
131
+ end
132
+ end
133
+
134
+ def handle_unsuback(packet)
135
+ id = packet.id
136
+ topics = []
137
+ topics = @subscriber.remove_subscription(id, topics)
138
+ unless topics.empty?
139
+ @on_unsuback.call(topics) unless @on_unsuback.nil?
140
+ end
141
+ end
142
+
143
+ def handle_publish(packet)
144
+ id = packet.id
145
+ qos = packet.qos
146
+ if @publisher.do_publish(qos, id) == MQTT_ERR_SUCCESS
147
+ @on_message.call(packet) unless @on_message.nil?
148
+ check_callback(packet)
149
+ end
150
+ end
151
+
152
+ def handle_puback(packet)
153
+ id = packet.id
154
+ if @publisher.do_puback(id) == MQTT_ERR_SUCCESS
155
+ @on_puback.call(packet) unless @on_puback.nil?
156
+ end
157
+ end
158
+
159
+ def handle_pubrec(packet)
160
+ id = packet.id
161
+ if @publisher.do_pubrec(id) == MQTT_ERR_SUCCESS
162
+ @on_pubrec.call(packet) unless @on_pubrec.nil?
163
+ end
164
+ end
165
+
166
+ def handle_pubrel(packet)
167
+ id = packet.id
168
+ if @publisher.do_pubrel(id) == MQTT_ERR_SUCCESS
169
+ @on_pubrel.call(packet) unless @on_pubrel.nil?
170
+ end
171
+ end
172
+
173
+ def handle_pubcomp(packet)
174
+ id = packet.id
175
+ if @publisher.do_pubcomp(id) == MQTT_ERR_SUCCESS
176
+ @on_pubcomp.call(packet) unless @on_pubcomp.nil?
177
+ end
178
+ end
179
+
180
+ def on_connack(&block)
181
+ @on_connack = block if block_given?
182
+ @on_connack
183
+ end
184
+
185
+ def on_suback(&block)
186
+ @on_suback = block if block_given?
187
+ @on_suback
188
+ end
189
+
190
+ def on_unsuback(&block)
191
+ @on_unsuback = block if block_given?
192
+ @on_unsuback
193
+ end
194
+
195
+ def on_puback(&block)
196
+ @on_puback = block if block_given?
197
+ @on_puback
198
+ end
199
+
200
+ def on_pubrec(&block)
201
+ @on_pubrec = block if block_given?
202
+ @on_pubrec
203
+ end
204
+
205
+ def on_pubrel(&block)
206
+ @on_pubrel = block if block_given?
207
+ @on_pubrel
208
+ end
209
+
210
+ def on_pubcomp(&block)
211
+ @on_pubcomp = block if block_given?
212
+ @on_pubcomp
213
+ end
214
+
215
+ def on_message(&block)
216
+ @on_message = block if block_given?
217
+ @on_message
218
+ end
219
+
220
+ def on_connack=(callback)
221
+ @on_connack = callback if callback.is_a?(Proc)
222
+ end
223
+
224
+ def on_suback=(callback)
225
+ @on_suback = callback if callback.is_a?(Proc)
226
+ end
227
+
228
+ def on_unsuback=(callback)
229
+ @on_unsuback = callback if callback.is_a?(Proc)
230
+ end
231
+
232
+ def on_puback=(callback)
233
+ @on_puback = callback if callback.is_a?(Proc)
234
+ end
235
+
236
+ def on_pubrec=(callback)
237
+ @on_pubrec = callback if callback.is_a?(Proc)
238
+ end
239
+
240
+ def on_pubrel=(callback)
241
+ @on_pubrel = callback if callback.is_a?(Proc)
242
+ end
243
+
244
+ def on_pubcomp=(callback)
245
+ @on_pubcomp = callback if callback.is_a?(Proc)
246
+ end
247
+
248
+ def on_message=(callback)
249
+ @on_message = callback if callback.is_a?(Proc)
250
+ end
251
+
252
+ def packet_type(packet)
253
+ type = packet.class
254
+ if MqttRails::PACKET_TYPES[3..13].include?(type)
255
+ type.to_s.split('::').last.downcase
256
+ else
257
+ Rails.logger.error("Received an unexpeceted packet: #{packet}.")
258
+ raise PacketException.new('Invalid packet type id')
259
+ end
260
+ end
261
+
262
+ def check_callback(packet)
263
+ callbacks = []
264
+ @registered_callback.each do |reccord|
265
+ callbacks.push(reccord.last) if MqttRails.match_filter(packet.topic, reccord.first)
266
+ end
267
+ unless callbacks.empty?
268
+ callbacks.each do |callback|
269
+ callback.call(packet)
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end