paho-mqtt 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ # encoding: BINARY
2
+ require "paho_mqtt/packet/base"
3
+ require "paho_mqtt/packet/connect"
4
+ require "paho_mqtt/packet/connack"
5
+ require "paho_mqtt/packet/publish"
6
+ require "paho_mqtt/packet/puback"
7
+ require "paho_mqtt/packet/pubrec"
8
+ require "paho_mqtt/packet/pubrel"
9
+ require "paho_mqtt/packet/pubcomp"
10
+ require "paho_mqtt/packet/subscribe"
11
+ require "paho_mqtt/packet/suback"
12
+ require "paho_mqtt/packet/unsubscribe"
13
+ require "paho_mqtt/packet/unsuback"
14
+ require "paho_mqtt/packet/pingreq"
15
+ require "paho_mqtt/packet/pingresp"
16
+ require "paho_mqtt/packet/disconnect"
17
+
18
+ module PahoMqtt
19
+ end
@@ -0,0 +1,290 @@
1
+ # encoding: BINARY
2
+
3
+ module PahoMqtt
4
+ module Packet
5
+ # Class representing a MQTT Packet
6
+ # Performs binary encoding and decoding of headers
7
+ class Base
8
+ # The version number of the MQTT protocol to use (default 3.1.0)
9
+ attr_accessor :version
10
+
11
+ # Identifier to link related control packets together
12
+ attr_accessor :id
13
+
14
+ # Array of 4 bits in the fixed header
15
+ attr_accessor :flags
16
+
17
+ # The length of the parsed packet body
18
+ attr_reader :body_length
19
+
20
+ # Default attribute values
21
+ ATTR_DEFAULTS = {
22
+ :version => '3.1.0',
23
+ :id => 0,
24
+ :body_length => nil
25
+ }
26
+
27
+ # Read in a packet from a socket
28
+ def self.read(socket)
29
+ # Read in the packet header and create a new packet object
30
+ packet = create_from_header(
31
+ read_byte(socket)
32
+ )
33
+ unless packet.nil?
34
+ packet.validate_flags
35
+
36
+ # Read in the packet length
37
+ multiplier = 1
38
+ body_length = 0
39
+ pos = 1
40
+ begin
41
+ digit = read_byte(socket)
42
+ body_length += ((digit & 0x7F) * multiplier)
43
+ multiplier *= 0x80
44
+ pos += 1
45
+ end while ((digit & 0x80) != 0x00) and pos <= 4
46
+
47
+ # Store the expected body length in the packet
48
+ packet.instance_variable_set('@body_length', body_length)
49
+
50
+ # Read in the packet body
51
+ packet.parse_body( socket.read(body_length) )
52
+ end
53
+ return packet
54
+ end
55
+
56
+ # Parse buffer into new packet object
57
+ def self.parse(buffer)
58
+ packet = parse_header(buffer)
59
+ packet.parse_body(buffer)
60
+ return packet
61
+ end
62
+
63
+ # Parse the header and create a new packet object of the correct type
64
+ # The header is removed from the buffer passed into this function
65
+ def self.parse_header(buffer)
66
+ # Check that the packet is a long as the minimum packet size
67
+ if buffer.bytesize < 2
68
+ raise "Invalid packet: less than 2 bytes long"
69
+ end
70
+
71
+ # Create a new packet object
72
+ bytes = buffer.unpack("C5")
73
+ packet = create_from_header(bytes.first)
74
+ packet.validate_flags
75
+
76
+ # Parse the packet length
77
+ body_length = 0
78
+ multiplier = 1
79
+ pos = 1
80
+ begin
81
+ if buffer.bytesize <= pos
82
+ raise "The packet length header is incomplete"
83
+ end
84
+ digit = bytes[pos]
85
+ body_length += ((digit & 0x7F) * multiplier)
86
+ multiplier *= 0x80
87
+ pos += 1
88
+ end while ((digit & 0x80) != 0x00) and pos <= 4
89
+
90
+ # Store the expected body length in the packet
91
+ packet.instance_variable_set('@body_length', body_length)
92
+
93
+ # Delete the fixed header from the raw packet passed in
94
+ buffer.slice!(0...pos)
95
+
96
+ return packet
97
+ end
98
+
99
+ # Create a new packet object from the first byte of a MQTT packet
100
+ def self.create_from_header(byte)
101
+ unless byte.nil?
102
+ # Work out the class
103
+ type_id = ((byte & 0xF0) >> 4)
104
+ packet_class = PahoMqtt::PACKET_TYPES[type_id]
105
+ if packet_class.nil?
106
+ raise "Invalid packet type identifier: #{type_id}"
107
+ end
108
+
109
+ # Convert the last 4 bits of byte into array of true/false
110
+ flags = (0..3).map { |i| byte & (2 ** i) != 0 }
111
+
112
+ # Create a new packet object
113
+ packet_class.new(:flags => flags)
114
+ end
115
+ end
116
+
117
+ # Create a new empty packet
118
+ def initialize(args={})
119
+ # We must set flags before the other values
120
+ @flags = [false, false, false, false]
121
+ update_attributes(ATTR_DEFAULTS.merge(args))
122
+ end
123
+
124
+ # Set packet attributes from a hash of attribute names and values
125
+ def update_attributes(attr={})
126
+ attr.each_pair do |k,v|
127
+ if v.is_a?(Array) or v.is_a?(Hash)
128
+ send("#{k}=", v.dup)
129
+ else
130
+ send("#{k}=", v)
131
+ end
132
+ end
133
+ end
134
+
135
+ # Get the identifer for this packet type
136
+ def type_id
137
+ index = PahoMqtt::PACKET_TYPES.index(self.class)
138
+ if index.nil?
139
+ raise "Invalid packet type: #{self.class}"
140
+ end
141
+ return index
142
+ end
143
+
144
+ # Get the name of the packet type as a string in capitals
145
+ # (like the MQTT specification uses)
146
+ #
147
+ # Example: CONNACK
148
+ def type_name
149
+ self.class.name.split('::').last.upcase
150
+ end
151
+
152
+ # Set the protocol version number
153
+ def version=(arg)
154
+ @version = arg.to_s
155
+ end
156
+
157
+ # Set the length of the packet body
158
+ def body_length=(arg)
159
+ @body_length = arg.to_i
160
+ end
161
+
162
+ # Parse the body (variable header and payload) of a packet
163
+ def parse_body(buffer)
164
+ if buffer.bytesize != body_length
165
+ raise "Failed to parse packet - input buffer (#{buffer.bytesize}) is not the same as the body length header (#{body_length})"
166
+ end
167
+ end
168
+
169
+ # Get serialisation of packet's body (variable header and payload)
170
+ def encode_body
171
+ '' # No body by default
172
+ end
173
+
174
+ # Serialise the packet
175
+ def to_s
176
+ # Encode the fixed header
177
+ header = [
178
+ ((type_id.to_i & 0x0F) << 4) |
179
+ (flags[3] ? 0x8 : 0x0) |
180
+ (flags[2] ? 0x4 : 0x0) |
181
+ (flags[1] ? 0x2 : 0x0) |
182
+ (flags[0] ? 0x1 : 0x0)
183
+ ]
184
+
185
+ # Get the packet's variable header and payload
186
+ body = self.encode_body
187
+
188
+ # Check that that packet isn't too big
189
+ body_length = body.bytesize
190
+ if body_length > 268435455
191
+ raise "Error serialising packet: body is more than 256MB"
192
+ end
193
+
194
+ # Build up the body length field bytes
195
+ begin
196
+ digit = (body_length % 128)
197
+ body_length = (body_length / 128)
198
+ # if there are more digits to encode, set the top bit of this digit
199
+ digit |= 0x80 if (body_length > 0)
200
+ header.push(digit)
201
+ end while (body_length > 0)
202
+
203
+ # Convert header to binary and add on body
204
+ header.pack('C*') + body
205
+ end
206
+
207
+ # Check that fixed header flags are valid for types that don't use the flags
208
+ # @private
209
+ def validate_flags
210
+ if flags != [false, false, false, false]
211
+ raise "Invalid flags in #{type_name} packet header"
212
+ end
213
+ end
214
+
215
+ # Returns a human readable string
216
+ def inspect
217
+ "\#<#{self.class}>"
218
+ end
219
+
220
+ protected
221
+
222
+ # Encode an array of bytes and return them
223
+ def encode_bytes(*bytes)
224
+ bytes.pack('C*')
225
+ end
226
+
227
+ # Encode an array of bits and return them
228
+ def encode_bits(bits)
229
+ [bits.map{|b| b ? '1' : '0'}.join].pack('b*')
230
+ end
231
+
232
+ # Encode a 16-bit unsigned integer and return it
233
+ def encode_short(val)
234
+ [val.to_i].pack('n')
235
+ end
236
+
237
+ # Encode a UTF-8 string and return it
238
+ # (preceded by the length of the string)
239
+ def encode_string(str)
240
+ str = str.to_s.encode('UTF-8')
241
+
242
+ # Force to binary, when assembling the packet
243
+ str.force_encoding('ASCII-8BIT')
244
+ encode_short(str.bytesize) + str
245
+ end
246
+
247
+ # Remove a 16-bit unsigned integer from the front of buffer
248
+ def shift_short(buffer)
249
+ bytes = buffer.slice!(0..1)
250
+ bytes.unpack('n').first
251
+ end
252
+
253
+ # Remove one byte from the front of the string
254
+ def shift_byte(buffer)
255
+ buffer.slice!(0...1).unpack('C').first
256
+ end
257
+
258
+ # Remove 8 bits from the front of buffer
259
+ def shift_bits(buffer)
260
+ buffer.slice!(0...1).unpack('b8').first.split('').map {|b| b == '1'}
261
+ end
262
+
263
+ # Remove n bytes from the front of buffer
264
+ def shift_data(buffer,bytes)
265
+ buffer.slice!(0...bytes)
266
+ end
267
+
268
+ # Remove string from the front of buffer
269
+ def shift_string(buffer)
270
+ len = shift_short(buffer)
271
+ str = shift_data(buffer,len)
272
+ # Strings in MQTT v3.1 are all UTF-8
273
+ str.force_encoding('UTF-8')
274
+ end
275
+
276
+
277
+ private
278
+
279
+ # Read and unpack a single byte from a socket
280
+ def self.read_byte(socket)
281
+ byte = socket.read(1)
282
+ unless byte.nil?
283
+ byte.unpack('C').first
284
+ else
285
+ nil
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,84 @@
1
+ # encoding: BINARY
2
+
3
+ module PahoMqtt
4
+ module Packet
5
+ class Connack < PahoMqtt::Packet::Base
6
+ # Session Present flag
7
+ attr_accessor :session_present
8
+
9
+ # The return code (defaults to 0 for connection accepted)
10
+ attr_accessor :return_code
11
+
12
+ # Default attribute values
13
+ ATTR_DEFAULTS = {:return_code => 0x00}
14
+
15
+ # Create a new Client Connect packet
16
+ def initialize(args={})
17
+ # We must set flags before other attributes
18
+ @connack_flags = [false, false, false, false, false, false, false, false]
19
+ super(ATTR_DEFAULTS.merge(args))
20
+ end
21
+
22
+ # Get the Session Present flag
23
+ def session_present
24
+ @connack_flags[0]
25
+ end
26
+
27
+ # Set the Session Present flag
28
+ def session_present=(arg)
29
+ if arg.kind_of?(Integer)
30
+ @connack_flags[0] = (arg == 0x1)
31
+ else
32
+ @connack_flags[0] = arg
33
+ end
34
+ end
35
+
36
+ # Get a string message corresponding to a return code
37
+ def return_msg
38
+ case return_code
39
+ when 0x00
40
+ "Connection Accepted"
41
+ when 0x01
42
+ "Connection refused: unacceptable protocol version"
43
+ when 0x02
44
+ "Connection refused: client identifier rejected"
45
+ when 0x03
46
+ "Connection refused: server unavailable"
47
+ when 0x04
48
+ "Connection refused: bad user name or password"
49
+ when 0x05
50
+ "Connection refused: not authorised"
51
+ else
52
+ "Connection refused: error code #{return_code}"
53
+ end
54
+ end
55
+
56
+ # Get serialisation of packet's body
57
+ def encode_body
58
+ body = ''
59
+ body += encode_bits(@connack_flags)
60
+ body += encode_bytes(@return_code.to_i)
61
+ return body
62
+ end
63
+
64
+ # Parse the body (variable header and payload) of a Connect Acknowledgment packet
65
+ def parse_body(buffer)
66
+ super(buffer)
67
+ @connack_flags = shift_bits(buffer)
68
+ unless @connack_flags[1,7] == [false, false, false, false, false, false, false]
69
+ raise "Invalid flags in Connack variable header"
70
+ end
71
+ @return_code = shift_byte(buffer)
72
+ unless buffer.empty?
73
+ raise "Extra bytes at end of Connect Acknowledgment packet"
74
+ end
75
+ end
76
+
77
+ # Returns a human readable string, summarising the properties of the packet
78
+ def inspect
79
+ "\#<#{self.class}: 0x%2.2X>" % return_code
80
+ end
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,149 @@
1
+ # encoding: BINARY
2
+
3
+ module PahoMqtt
4
+ module Packet
5
+ class Connect < PahoMqtt::Packet::Base
6
+ # The name of the protocol
7
+ attr_accessor :protocol_name
8
+
9
+ # The version number of the protocol
10
+ attr_accessor :protocol_level
11
+
12
+ # The client identifier string
13
+ attr_accessor :client_id
14
+
15
+ # Set to false to keep a persistent session with the server
16
+ attr_accessor :clean_session
17
+
18
+ # Period the server should keep connection open for between pings
19
+ attr_accessor :keep_alive
20
+
21
+ # The topic name to send the Will message to
22
+ attr_accessor :will_topic
23
+
24
+ # The QoS level to send the Will message as
25
+ attr_accessor :will_qos
26
+
27
+ # Set to true to make the Will message retained
28
+ attr_accessor :will_retain
29
+
30
+ # The payload of the Will message
31
+ attr_accessor :will_payload
32
+
33
+ # The username for authenticating with the server
34
+ attr_accessor :username
35
+
36
+ # The password for authenticating with the server
37
+ attr_accessor :password
38
+
39
+ # Default attribute values
40
+ ATTR_DEFAULTS = {
41
+ :client_id => nil,
42
+ :clean_session => true,
43
+ :keep_alive => 15,
44
+ :will_topic => nil,
45
+ :will_qos => 0,
46
+ :will_retain => false,
47
+ :will_payload => '',
48
+ :username => nil,
49
+ :password => nil,
50
+ }
51
+
52
+ # Create a new Client Connect packet
53
+ def initialize(args={})
54
+ super(ATTR_DEFAULTS.merge(args))
55
+
56
+ if version == '3.1.0' or version == '3.1'
57
+ self.protocol_name ||= 'MQIsdp'
58
+ self.protocol_level ||= 0x03
59
+ elsif version == '3.1.1'
60
+ self.protocol_name ||= 'MQTT'
61
+ self.protocol_level ||= 0x04
62
+ else
63
+ raise ArgumentError.new("Unsupported protocol version: #{version}")
64
+ end
65
+ end
66
+
67
+ # Get serialisation of packet's body
68
+ def encode_body
69
+ body = ''
70
+ if @version == '3.1.0'
71
+ if @client_id.nil? or @client_id.bytesize < 1
72
+ raise "Client identifier too short while serialising packet"
73
+ elsif @client_id.bytesize > 23
74
+ raise "Client identifier too long when serialising packet"
75
+ end
76
+ end
77
+ body += encode_string(@protocol_name)
78
+ body += encode_bytes(@protocol_level.to_i)
79
+ if @keep_alive < 0
80
+ raise "Invalid keep-alive value: cannot be less than 0"
81
+ end
82
+
83
+ # Set the Connect flags
84
+ @connect_flags = 0
85
+ @connect_flags |= 0x02 if @clean_session
86
+ @connect_flags |= 0x04 unless @will_topic.nil?
87
+ @connect_flags |= ((@will_qos & 0x03) << 3)
88
+ @connect_flags |= 0x20 if @will_retain
89
+ @connect_flags |= 0x40 unless @password.nil?
90
+ @connect_flags |= 0x80 unless @username.nil?
91
+ body += encode_bytes(@connect_flags)
92
+ body += encode_short(@keep_alive)
93
+ body += encode_string(@client_id)
94
+ unless will_topic.nil?
95
+ body += encode_string(@will_topic)
96
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
97
+ body += encode_string(@will_payload)
98
+ end
99
+ body += encode_string(@username) unless @username.nil?
100
+ body += encode_string(@password) unless @password.nil?
101
+ return body
102
+ end
103
+
104
+ # Parse the body (variable header and payload) of a Connect packet
105
+ def parse_body(buffer)
106
+ super(buffer)
107
+ @protocol_name = shift_string(buffer)
108
+ @protocol_level = shift_byte(buffer).to_i
109
+ if @protocol_name == 'MQIsdp' and @protocol_level == 3
110
+ @version = '3.1.0'
111
+ elsif @protocol_name == 'MQTT' and @protocol_level == 4
112
+ @version = '3.1.1'
113
+ else
114
+ raise "Unsupported protocol: #{@protocol_name}/#{@protocol_level}"
115
+ end
116
+
117
+ @connect_flags = shift_byte(buffer)
118
+ @clean_session = ((@connect_flags & 0x02) >> 1) == 0x01
119
+ @keep_alive = shift_short(buffer)
120
+ @client_id = shift_string(buffer)
121
+ if ((@connect_flags & 0x04) >> 2) == 0x01
122
+ # Last Will and Testament
123
+ @will_qos = ((@connect_flags & 0x18) >> 3)
124
+ @will_retain = ((@connect_flags & 0x20) >> 5) == 0x01
125
+ @will_topic = shift_string(buffer)
126
+ # The MQTT v3.1 specification says that the payload is a UTF-8 string
127
+ @will_payload = shift_string(buffer)
128
+ end
129
+ if ((@connect_flags & 0x80) >> 7) == 0x01 and buffer.bytesize > 0
130
+ @username = shift_string(buffer)
131
+ end
132
+ if ((@connect_flags & 0x40) >> 6) == 0x01 and buffer.bytesize > 0
133
+ @password = shift_string(buffer)
134
+ end
135
+ end
136
+
137
+ # Returns a human readable string, summarising the properties of the packet
138
+ def inspect
139
+ str = "\#<#{self.class}: "
140
+ str += "keep_alive=#{keep_alive}"
141
+ str += ", clean" if clean_session
142
+ str += ", client_id='#{client_id}'"
143
+ str += ", username='#{username}'" unless username.nil?
144
+ str += ", password=..." unless password.nil?
145
+ str += ">"
146
+ end
147
+ end
148
+ end
149
+ end