mqtt 0.6.0 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 910045c2af52ae8eaca56e2a86b936d91a4dc1c17f9c93756a9ea54a42f1b005
4
- data.tar.gz: 7e5da868a348396b21357df0b735973438f3919cd40bc5b9e7dca79141e181fb
3
+ metadata.gz: 1cd5b00cfda7ebb05929c0cc4319b7400b5a8e787a7e11bfeba265017cd31cc8
4
+ data.tar.gz: b08388a375e26c252ebe829ea191f51c85693cb0e2b2b986f36b567a5dd710c0
5
5
  SHA512:
6
- metadata.gz: 57cde9d9792ca557c90d28349f0d4352aec8c861488ef67de1c9d40ccba7ee7ee984235340c18b68940273fd518177bc91f4b07c4be832934fc9e72c02656cef
7
- data.tar.gz: fe71c84c97918687930830606bd1fc9214e05d92914062b7521f52b69e688953ae9230681531354b9837f99850c1d226dc4bb8f20961e0c4970fe1d0d82fb419
6
+ metadata.gz: 17a6d4df69038d28566c9c4921e82a4eb363aafc0d1f811ca94ad538337a05460ea7ca8bfc5e8769d00e958443ea72c53c56bd483dc4ebbaf5f89b5d7afc918b
7
+ data.tar.gz: 71a683f038ec5df05ade1f70cf536ab1174700da82eb9ef810b59d8c06af64588bd0166f7e9012f344f4af7f1326afdd9f0f462a744e340ac3f860f89bf9b0b1
data/NEWS.md CHANGED
@@ -1,6 +1,32 @@
1
1
  Ruby MQTT NEWS
2
2
  ==============
3
3
 
4
+ Ruby MQTT Version 0.7.0 (2025-10-29)
5
+ ------------------------------------
6
+
7
+ * TLS hostname verification (#125)
8
+ * Supporting private keys other than `RSA` (#148)
9
+ * Removed support for ruby 1.8 and 1.9 (#156)
10
+ * Handle instantaneous and unsolicited PUBACKs (#158)
11
+ * Prevent deadlock due to system call error (#159)
12
+ * Added support for timeout when opening a TCPSocket (#163)
13
+ * Switch to URI::DEFAULT_PARSER.unescape() instead of CGI.unescape()
14
+
15
+
16
+ Ruby MQTT Version 0.6.0 (2023-02-17)
17
+ ------------------------------------
18
+
19
+ * Add Rubocop and performed code cleanup
20
+ * Updated yard minimum version to
21
+ * Added support for clearing the queue of waiting incoming messages (#117)
22
+ * Wrap packet id after 0xffff (#118)
23
+ * Use a queue to wait for Puback packets rather than polling (#120)
24
+ * Filter out spec files from the coverage report
25
+ * Improvements `Packet.read_byte` performance (#134)
26
+ * Use monotonic clock instead of realtime for keep_alive (#132)
27
+ * Switch to CGI.unescape instead of the deprecated URI.unescape (#136)
28
+
29
+
4
30
  Ruby MQTT Version 0.5.0 (2017-04-16)
5
31
  ------------------------------------
6
32
 
data/README.md CHANGED
@@ -25,16 +25,16 @@ You may get the latest stable version from [Rubygems]:
25
25
 
26
26
  $ gem install mqtt
27
27
 
28
- Alternatively, to use a development snapshot from GitHub using [Bundler]:
29
-
30
- gem 'mqtt', :git => 'https://github.com/njh/ruby-mqtt.git'
28
+ Alternatively, to use a development snapshot from GitHub using [Bundler], add this to your `Gemfile`:
31
29
 
30
+ ~~~ ruby
31
+ gem 'mqtt', :github => 'njh/ruby-mqtt'
32
+ ~~~
32
33
 
33
34
  Quick Start
34
35
  -----------
35
36
 
36
37
  ~~~ ruby
37
- require 'rubygems'
38
38
  require 'mqtt'
39
39
 
40
40
  # Publish example
@@ -89,6 +89,15 @@ client.ca_file = path_to('root-ca.pem')
89
89
  client.connect()
90
90
  ~~~
91
91
 
92
+ The default timeout when opening a TCP Socket is 30 seconds. To specify it explicitly, use 'connect_timeout =>':
93
+
94
+ ~~~ ruby
95
+ client = MQTT::Client.connect(
96
+ :host => 'myserver.example.com',
97
+ :connect_timeout => 15
98
+ )
99
+ ~~~
100
+
92
101
  The connection can either be made without the use of a block:
93
102
 
94
103
  ~~~ ruby
data/lib/mqtt/client.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  autoload :OpenSSL, 'openssl'
2
2
  autoload :URI, 'uri'
3
- autoload :CGI, 'cgi'
4
3
 
5
4
  # Client class for talking to an MQTT server
6
5
  module MQTT
@@ -24,6 +23,9 @@ module MQTT
24
23
  # @see OpenSSL::SSL::SSLContext::METHODS
25
24
  attr_accessor :ssl
26
25
 
26
+ # Set to false to skip tls hostname verification
27
+ attr_accessor :verify_host
28
+
27
29
  # Time (in seconds) between pings to remote server (default is 15 seconds)
28
30
  attr_accessor :keep_alive
29
31
 
@@ -36,6 +38,9 @@ module MQTT
36
38
  # Number of seconds to wait for acknowledgement packets (default is 5 seconds)
37
39
  attr_accessor :ack_timeout
38
40
 
41
+ # Number of seconds to connect to the server (default is 90 seconds)
42
+ attr_accessor :connect_timeout
43
+
39
44
  # Username to authenticate to the server with
40
45
  attr_accessor :username
41
46
 
@@ -69,13 +74,15 @@ module MQTT
69
74
  :clean_session => true,
70
75
  :client_id => nil,
71
76
  :ack_timeout => 5,
77
+ :connect_timeout => 30,
72
78
  :username => nil,
73
79
  :password => nil,
74
80
  :will_topic => nil,
75
81
  :will_payload => nil,
76
82
  :will_qos => 0,
77
83
  :will_retain => false,
78
- :ssl => false
84
+ :ssl => false,
85
+ :verify_host => true
79
86
  }
80
87
 
81
88
  # Create and connect a new MQTT Client
@@ -193,13 +200,13 @@ module MQTT
193
200
  # Set a path to a file containing a PEM-format client private key
194
201
  def key_file=(*args)
195
202
  path, passphrase = args.flatten
196
- ssl_context.key = OpenSSL::PKey::RSA.new(File.open(path), passphrase)
203
+ ssl_context.key = OpenSSL::PKey.read(File.binread(path), passphrase)
197
204
  end
198
205
 
199
206
  # Set to a PEM-format client private key
200
207
  def key=(*args)
201
208
  cert, passphrase = args.flatten
202
- ssl_context.key = OpenSSL::PKey::RSA.new(cert, passphrase)
209
+ ssl_context.key = OpenSSL::PKey.read(cert, passphrase)
203
210
  end
204
211
 
205
212
  # Set a path to a file containing a PEM-format CA certificate and enable peer verification
@@ -235,7 +242,7 @@ module MQTT
235
242
 
236
243
  unless connected?
237
244
  # Create network socket
238
- tcp_socket = TCPSocket.new(@host, @port)
245
+ tcp_socket = open_tcp_socket
239
246
 
240
247
  if @ssl
241
248
  # Set the protocol version
@@ -248,6 +255,8 @@ module MQTT
248
255
  @socket.hostname = @host if @socket.respond_to?(:hostname=)
249
256
 
250
257
  @socket.connect
258
+
259
+ @socket.post_connection_check(@host) if @verify_host
251
260
  else
252
261
  @socket = tcp_socket
253
262
  end
@@ -326,14 +335,11 @@ module MQTT
326
335
  :payload => payload
327
336
  )
328
337
 
329
- # Send the packet
330
- res = send_packet(packet)
331
-
332
- return if qos.zero?
338
+ queue = qos.zero? ? nil : wait_for_puback(packet.id)
333
339
 
334
- queue = Queue.new
340
+ res = send_packet(packet)
335
341
 
336
- wait_for_puback packet.id, queue
342
+ return unless queue
337
343
 
338
344
  deadline = current_time + @ack_timeout
339
345
 
@@ -473,7 +479,7 @@ module MQTT
473
479
  end
474
480
  keep_alive!
475
481
  # Pass exceptions up to parent thread
476
- rescue Exception => exp
482
+ rescue ::Exception => exp
477
483
  unless @socket.nil?
478
484
  @socket.close
479
485
  @socket = nil
@@ -482,9 +488,9 @@ module MQTT
482
488
  Thread.current[:parent].raise(exp)
483
489
  end
484
490
 
485
- def wait_for_puback(id, queue)
491
+ def wait_for_puback(id)
486
492
  @pubacks_semaphore.synchronize do
487
- @pubacks[id] = queue
493
+ @pubacks[id] = Queue.new
488
494
  end
489
495
  end
490
496
 
@@ -496,7 +502,7 @@ module MQTT
496
502
  @last_ping_response = current_time
497
503
  elsif packet.class == MQTT::Packet::Puback
498
504
  @pubacks_semaphore.synchronize do
499
- @pubacks[packet.id] << packet
505
+ @pubacks[packet.id] << packet if @pubacks[packet.id]
500
506
  end
501
507
  end
502
508
  # Ignore all other packets
@@ -585,8 +591,8 @@ module MQTT
585
591
  {
586
592
  :host => uri.host,
587
593
  :port => uri.port || nil,
588
- :username => uri.user ? CGI.unescape(uri.user) : nil,
589
- :password => uri.password ? CGI.unescape(uri.password) : nil,
594
+ :username => uri.user ? URI::DEFAULT_PARSER.unescape(uri.user) : nil,
595
+ :password => uri.password ? URI::DEFAULT_PARSER.unescape(uri.password) : nil,
590
596
  :ssl => ssl
591
597
  }
592
598
  end
@@ -597,6 +603,19 @@ module MQTT
597
603
  @last_packet_id
598
604
  end
599
605
 
606
+ def open_tcp_socket
607
+ return TCPSocket.new @host, @port, connect_timeout: @connect_timeout if RUBY_VERSION.to_f >= 3.0
608
+
609
+ begin
610
+ Timeout.timeout(@connect_timeout) do
611
+ return TCPSocket.new(@host, @port)
612
+ end
613
+ rescue Timeout::Error
614
+ raise IO::TimeoutError, "Connection timed out for \"#{@host}\" port #{@port}" if defined? IO::TimeoutError
615
+ raise Errno::ETIMEDOUT, "Connection timed out for \"#{@host}\" port #{@port}"
616
+ end
617
+ end
618
+
600
619
  # ---- Deprecated attributes and methods ---- #
601
620
  public
602
621
 
data/lib/mqtt/packet.rb CHANGED
@@ -445,7 +445,7 @@ module MQTT
445
445
  def initialize(args = {})
446
446
  super(ATTR_DEFAULTS.merge(args))
447
447
 
448
- if version == '3.1.0' || version == '3.1'
448
+ if ['3.1.0', '3.1'].include?(version)
449
449
  self.protocol_name ||= 'MQIsdp'
450
450
  self.protocol_level ||= 0x03
451
451
  elsif version == '3.1.1'
data/lib/mqtt/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module MQTT
2
2
  # The version number of the MQTT gem
3
- VERSION = '0.6.0'
3
+ VERSION = '0.7.0'
4
4
  end
data/lib/mqtt.rb CHANGED
@@ -7,11 +7,6 @@ require 'timeout'
7
7
 
8
8
  require 'mqtt/version'
9
9
 
10
- # String encoding monkey patch for Ruby 1.8
11
- unless String.method_defined?(:force_encoding)
12
- require 'mqtt/patches/string_encoding.rb'
13
- end
14
-
15
10
  module MQTT
16
11
  # Default port number for unencrypted connections
17
12
  DEFAULT_PORT = 1883
@@ -232,10 +232,46 @@ describe MQTT::Client do
232
232
  describe "setting an encrypted client private key, w/an incorrect passphrase" do
233
233
  let(:key_pass) { 'ttqm' }
234
234
 
235
- it "should raise an OpenSSL::PKey::RSAError exception" do
235
+ it "should raise an exception" do
236
236
  expect(client.ssl_context.key).to be_nil
237
237
  expect { client.key_file = [fixture_path('client.pass.key'), key_pass] }.to(
238
- raise_error(OpenSSL::PKey::RSAError, /Neither PUB key nor PRIV key/))
238
+ raise_error(/Could not parse PKey/))
239
+ end
240
+ end
241
+
242
+ describe "setting a client private EC key file path" do
243
+ it "should add a certificate to the SSL context" do
244
+ expect(client.ssl_context.key).to be_nil
245
+ client.key_file = fixture_path('ec.key')
246
+ expect(client.ssl_context.key).to be_a(OpenSSL::PKey::EC)
247
+ end
248
+ end
249
+
250
+ describe "setting a client private EC key directly" do
251
+ it "should add a certificate to the SSL context" do
252
+ expect(client.ssl_context.key).to be_nil
253
+ client.key = File.read(fixture_path('ec.key'))
254
+ expect(client.ssl_context.key).to be_a(OpenSSL::PKey::EC)
255
+ end
256
+ end
257
+
258
+ describe "setting an encrypted client private EC key, w/the correct passphrase" do
259
+ let(:key_pass) { 'mqtt' }
260
+
261
+ it "should add the decrypted certificate to the SSL context" do
262
+ expect(client.ssl_context.key).to be_nil
263
+ client.key_file = [fixture_path('ec.pass.key'), key_pass]
264
+ expect(client.ssl_context.key).to be_a(OpenSSL::PKey::EC)
265
+ end
266
+ end
267
+
268
+ describe "setting an encrypted client private EC key, w/an incorrect passphrase" do
269
+ let(:key_pass) { 'ttqm' }
270
+
271
+ it "should raise an exception" do
272
+ expect(client.ssl_context.key).to be_nil
273
+ expect { client.key_file = [fixture_path('ec.pass.key'), key_pass] }.to(
274
+ raise_error(/Could not parse PKey/))
239
275
  end
240
276
  end
241
277
 
@@ -406,6 +442,7 @@ describe MQTT::Client do
406
442
  it "should use ssl if it enabled using the :ssl => true parameter" do
407
443
  expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
408
444
  expect(ssl_socket).to receive(:connect)
445
+ expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
409
446
 
410
447
  client = MQTT::Client.new('mqtt.example.com', :ssl => true)
411
448
  allow(client).to receive(:receive_connack)
@@ -415,6 +452,7 @@ describe MQTT::Client do
415
452
  it "should use ssl if it enabled using the mqtts:// scheme" do
416
453
  expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
417
454
  expect(ssl_socket).to receive(:connect)
455
+ expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
418
456
 
419
457
  client = MQTT::Client.new('mqtts://mqtt.example.com')
420
458
  allow(client).to receive(:receive_connack)
@@ -424,6 +462,7 @@ describe MQTT::Client do
424
462
  it "should use set the SSL version, if the :ssl parameter is a symbol" do
425
463
  expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
426
464
  expect(ssl_socket).to receive(:connect)
465
+ expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
427
466
 
428
467
  client = MQTT::Client.new('mqtt.example.com', :ssl => :TLSv1)
429
468
  expect(client.ssl_context).to receive('ssl_version=').with(:TLSv1)
@@ -434,11 +473,21 @@ describe MQTT::Client do
434
473
  it "should use set hostname on the SSL socket for SNI" do
435
474
  expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
436
475
  expect(ssl_socket).to receive(:hostname=).with('mqtt.example.com')
476
+ expect(ssl_socket).to receive(:post_connection_check).with('mqtt.example.com')
437
477
 
438
478
  client = MQTT::Client.new('mqtts://mqtt.example.com')
439
479
  allow(client).to receive(:receive_connack)
440
480
  client.connect
441
481
  end
482
+
483
+ it "should skip host verification" do
484
+ expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
485
+ expect(ssl_socket).to receive(:connect)
486
+
487
+ client = MQTT::Client.new('mqtt.example.com', :ssl => true, :verify_host => false)
488
+ allow(client).to receive(:receive_connack)
489
+ client.connect
490
+ end
442
491
  end
443
492
 
444
493
  context "with a last will and testament set" do
@@ -592,11 +641,11 @@ describe MQTT::Client do
592
641
  @injected_pubacks[packet.id] = packet
593
642
  end
594
643
 
595
- def wait_for_puback(id, queue)
644
+ def wait_for_puback(id)
596
645
  packet = @injected_pubacks.fetch(id) {
597
646
  return super
598
647
  }
599
- queue << packet
648
+ Queue.new << packet
600
649
  end
601
650
  end
602
651
 
@@ -606,6 +655,19 @@ describe MQTT::Client do
606
655
  client.instance_variable_set('@socket', socket)
607
656
  end
608
657
 
658
+ it "should respect connect_timeout" do
659
+ client = MQTT::Client.new(:host => '198.51.100.1', :connect_timeout => 0.1)
660
+ expect {
661
+ client.connect
662
+ }.to raise_error(
663
+ if defined? IO::TimeoutError
664
+ IO::TimeoutError
665
+ else
666
+ Errno::ETIMEDOUT
667
+ end
668
+ )
669
+ end
670
+
609
671
  it "should respect timeouts" do
610
672
  require "socket"
611
673
  rd, wr = UNIXSocket.pair
@@ -696,6 +758,20 @@ describe MQTT::Client do
696
758
  expect(client).to receive(:send_packet) { |packet| expect(packet.id).to eq(2) }
697
759
  client.publish "topic", "message", false, 1
698
760
  end
761
+
762
+ it "does not crash when receiving a PUBACK for a packet it never sent" do
763
+ expect { client.send(:handle_packet, MQTT::Packet::Puback.new(:id => 666)) }.to_not raise_error
764
+ end
765
+
766
+ it "does not crash with QoS 1 when the broker sends the PUBACK instantly" do
767
+ allow(client).to receive(:send_packet).and_wrap_original do |send_packet, packet, *args, &block|
768
+ send_packet.call(packet, *args, &block).tap do
769
+ client.send(:handle_packet, MQTT::Packet::Puback.new(:id => packet.id))
770
+ end
771
+ end
772
+
773
+ expect { client.publish("topic", "message", false, 1) }.to_not raise_error
774
+ end
699
775
  end
700
776
 
701
777
  describe "when calling the 'subscribe' method" do
@@ -923,15 +999,29 @@ describe MQTT::Client do
923
999
  expect(@read_queue.size).to eq(0)
924
1000
  end
925
1001
 
926
- it "should close the socket if there is an exception" do
1002
+ it "should close the socket if there is an MQTT exception" do
927
1003
  expect(socket).to receive(:close).once
928
1004
  allow(MQTT::Packet).to receive(:read).and_raise(MQTT::Exception)
929
1005
  client.send(:receive_packet)
930
1006
  end
931
1007
 
1008
+ it "should close the socket if there is a system call error" do
1009
+ expect(socket).to receive(:close).once
1010
+ allow(MQTT::Packet).to receive(:read).and_raise(Errno::ECONNRESET)
1011
+ client.send(:receive_packet)
1012
+ end
1013
+
932
1014
  it "should pass exceptions up to parent thread" do
933
- expect(@parent_thread).to receive(:raise).once
934
- allow(MQTT::Packet).to receive(:read).and_raise(MQTT::Exception)
1015
+ e = MQTT::Exception.new
1016
+ expect(@parent_thread).to receive(:raise).with(e).once
1017
+ allow(MQTT::Packet).to receive(:read).and_raise(e)
1018
+ client.send(:receive_packet)
1019
+ end
1020
+
1021
+ it "should pass a system call error up to parent thread" do
1022
+ e = Errno::ECONNRESET.new
1023
+ expect(@parent_thread).to receive(:raise).with(e).once
1024
+ allow(MQTT::Packet).to receive(:read).and_raise(e)
935
1025
  client.send(:receive_packet)
936
1026
  end
937
1027
 
@@ -382,8 +382,7 @@ describe MQTT::Packet::Publish do
382
382
  expect(packet.topic.bytesize).to eq(8)
383
383
  end
384
384
 
385
- it "should have the correct topic string length", :unless => RUBY_VERSION =~ /^1\.8/ do
386
- # Ruby 1.8 doesn't support UTF-8 properly
385
+ it "should have the correct topic string length" do
387
386
  expect(packet.topic.length).to eq(6)
388
387
  end
389
388
 
@@ -391,8 +390,7 @@ describe MQTT::Packet::Publish do
391
390
  expect(packet.payload.bytesize).to eq(12)
392
391
  end
393
392
 
394
- it "should have the correct payload string length", :unless => RUBY_VERSION =~ /^1\.8/ do
395
- # Ruby 1.8 doesn't support UTF-8 properly
393
+ it "should have the correct payload string length" do
396
394
  expect(packet.payload.length).to eq(10)
397
395
  end
398
396
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mqtt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicholas J Humfrey
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-17 00:00:00.000000000 Z
11
+ date: 2025-10-29 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logger
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -108,7 +122,6 @@ files:
108
122
  - lib/mqtt/client.rb
109
123
  - lib/mqtt/openssl_fix.rb
110
124
  - lib/mqtt/packet.rb
111
- - lib/mqtt/patches/string_encoding.rb
112
125
  - lib/mqtt/proxy.rb
113
126
  - lib/mqtt/sn/packet.rb
114
127
  - lib/mqtt/version.rb
@@ -137,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
150
  - !ruby/object:Gem::Version
138
151
  version: '0'
139
152
  requirements: []
140
- rubygems_version: 3.3.7
153
+ rubygems_version: 3.5.3
141
154
  signing_key:
142
155
  specification_version: 4
143
156
  summary: Implementation of the MQTT protocol
@@ -1,32 +0,0 @@
1
- # Monkey patch to add stubbed string encoding functions to Ruby 1.8
2
-
3
- class String
4
- def force_encoding(encoding)
5
- @encoding = encoding
6
- self
7
- end
8
-
9
- def encoding
10
- @encoding ||= Encoding::ASCII_8BIT
11
- end
12
-
13
- def encode(encoding)
14
- new = dup
15
- new.force_encoding(encoding)
16
- end
17
- end
18
-
19
- class Encoding
20
- attr_reader :name
21
-
22
- def initialize(name)
23
- @name = name
24
- end
25
-
26
- def to_s
27
- @name
28
- end
29
-
30
- UTF_8 = Encoding.new('UTF-8')
31
- ASCII_8BIT = Encoding.new('ASCII-8BIT')
32
- end