paho-mqtt 0.0.2 → 1.0.0

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.
@@ -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