diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 02fa71f..ebca0f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,8 +12,8 @@ jobs: strategy: fail-fast: false matrix: - ruby: [2.4, 2.5, 2.6, 2.7, '3.0', 3.1, jruby-9.2] - os: [ubuntu-20.04, windows-2019] + ruby: [2.4, 2.5, 2.6, 2.7, '3.0', 3.1, 3.2, jruby-9.2] + os: [ubuntu-20.04, windows-2022] include: - { ruby: 3.1, os: ubuntu-20.04, matrix: pipeline } @@ -23,13 +23,12 @@ jobs: CI_MATRIX: ${{ matrix.matrix }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - rubygems: 3.3.6 bundler-cache: true - name: Test things diff --git a/History.txt b/History.txt index 36743ba..353ccfd 100644 --- a/History.txt +++ b/History.txt @@ -1,3 +1,27 @@ +=== 4.0.5 / 2024-12-04 + +Bug fixes: + +* Allow setting extra_chain_cert= + +=== 4.0.4 / 2024-09-09 + +Bug fixes: + +* Allow setting verify_hostname to false + +=== 4.0.3 / 2024-09-09 + +Bug fixes: + +* Handle Net::HTTP#verify_hostname was added in Ruby 3.0 or later. #120 + +=== 4.0.2 / 2023-03-29 + +Bug fixes: + +* Fix compatibility with `connection_pool 2.4+` + === 4.0.1 / 2021-01-12 Bug fixes: diff --git a/README.rdoc b/README.rdoc index 9d7b0d1..88ffc7a 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,7 +1,7 @@ = net-http-persistent home :: https://github.com/drbrain/net-http-persistent -rdoc :: https://rdoc.info/gems/net-http-persistent +rdoc :: https://rubydoc.info/gems/net-http-persistent == DESCRIPTION: diff --git a/lib/net/http/persistent.rb b/lib/net/http/persistent.rb index 4658650..10b6a72 100644 --- a/lib/net/http/persistent.rb +++ b/lib/net/http/persistent.rb @@ -65,6 +65,7 @@ # #ca_path :: Directory with certificate-authorities # #cert_store :: An SSL certificate store # #ciphers :: List of SSl ciphers allowed +# #extra_chain_cert :: Extra certificates to be added to the certificate chain # #private_key :: The client's SSL private key # #reuse_ssl_sessions :: Reuse a previously opened SSL session for a new # connection @@ -73,6 +74,8 @@ # #verify_callback :: For server certificate verification # #verify_depth :: Depth of certificate verification # #verify_mode :: How connections should be verified +# #verify_hostname :: Use hostname verification for server certificate +# during the handshake # # == Proxies # @@ -179,7 +182,7 @@ class Net::HTTP::Persistent ## # The version of Net::HTTP::Persistent you are using - VERSION = '4.0.1' + VERSION = '4.0.5' ## # Error class for errors raised by Net::HTTP::Persistent. Various @@ -270,6 +273,11 @@ def self.detect_idle_timeout uri, max = 10 attr_reader :ciphers + ## + # Extra certificates to be added to the certificate chain + + attr_reader :extra_chain_cert + ## # Sends debug_output to this IO via Net::HTTP#set_debug_output. # @@ -454,6 +462,21 @@ def self.detect_idle_timeout uri, max = 10 attr_reader :verify_mode + ## + # HTTPS verify_hostname. + # + # If a client sets this to true and enables SNI with SSLSocket#hostname=, + # the hostname verification on the server certificate is performed + # automatically during the handshake using + # OpenSSL::SSL.verify_certificate_identity(). + # + # You can set +verify_hostname+ as true to use hostname verification + # during the handshake. + # + # NOTE: This works with Ruby > 3.0. + + attr_reader :verify_hostname + ## # Creates a new Net::HTTP::Persistent. # @@ -513,6 +536,7 @@ def initialize name: nil, proxy: nil, pool_size: DEFAULT_POOL_SIZE @verify_callback = nil @verify_depth = nil @verify_mode = nil + @verify_hostname = nil @cert_store = nil @generation = 0 # incremented when proxy URI changes @@ -574,6 +598,21 @@ def ciphers= ciphers reconnect_ssl end + if Net::HTTP.method_defined?(:extra_chain_cert=) + ## + # Extra certificates to be added to the certificate chain. + # It is only supported starting from Net::HTTP version 0.1.1 + def extra_chain_cert= extra_chain_cert + @extra_chain_cert = extra_chain_cert + + reconnect_ssl + end + else + def extra_chain_cert= _extra_chain_cert + raise "extra_chain_cert= is not supported by this version of Net::HTTP" + end + end + ## # Creates a new connection for +uri+ @@ -612,13 +651,23 @@ def connection_for uri return yield connection rescue Errno::ECONNREFUSED - address = http.proxy_address || http.address - port = http.proxy_port || http.port + if http.proxy? + address = http.proxy_address + port = http.proxy_port + else + address = http.address + port = http.port + end raise Error, "connection refused: #{address}:#{port}" rescue Errno::EHOSTDOWN - address = http.proxy_address || http.address - port = http.proxy_port || http.port + if http.proxy? + address = http.proxy_address + port = http.proxy_port + else + address = http.address + port = http.port + end raise Error, "host down: #{address}:#{port}" ensure @@ -982,8 +1031,10 @@ def ssl connection connection.min_version = @min_version if @min_version connection.max_version = @max_version if @max_version - connection.verify_depth = @verify_depth - connection.verify_mode = @verify_mode + connection.verify_depth = @verify_depth + connection.verify_mode = @verify_mode + connection.verify_hostname = @verify_hostname if + @verify_hostname != nil && connection.respond_to?(:verify_hostname=) if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and not Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then @@ -1025,6 +1076,10 @@ def ssl connection connection.key = @private_key end + if defined?(@extra_chain_cert) and @extra_chain_cert + connection.extra_chain_cert = @extra_chain_cert + end + connection.cert_store = if @cert_store then @cert_store else @@ -1092,6 +1147,15 @@ def verify_mode= verify_mode reconnect_ssl end + ## + # Sets the HTTPS verify_hostname. + + def verify_hostname= verify_hostname + @verify_hostname = verify_hostname + + reconnect_ssl + end + ## # SSL verification callback. @@ -1104,4 +1168,3 @@ def verify_callback= callback require_relative 'persistent/connection' require_relative 'persistent/pool' - diff --git a/lib/net/http/persistent/connection.rb b/lib/net/http/persistent/connection.rb index 4e1c601..aa95fc2 100644 --- a/lib/net/http/persistent/connection.rb +++ b/lib/net/http/persistent/connection.rb @@ -25,6 +25,7 @@ def finish ensure reset end + alias_method :close, :finish def reset @last_use = Net::HTTP::Persistent::EPOCH diff --git a/lib/net/http/persistent/pool.rb b/lib/net/http/persistent/pool.rb index 21371f5..220c70e 100644 --- a/lib/net/http/persistent/pool.rb +++ b/lib/net/http/persistent/pool.rb @@ -11,20 +11,32 @@ def initialize(options = {}, &block) end def checkin net_http_args - stack = Thread.current[@key][net_http_args] ||= [] + if net_http_args.is_a?(Hash) && net_http_args.size == 1 && net_http_args[:force] + # ConnectionPool 2.4+ calls `checkin(force: true)` after fork. + # When this happens, we should remove all connections from Thread.current + if stacks = Thread.current[@key] + stacks.each do |http_args, connections| + connections.each do |conn| + @available.push conn, connection_args: http_args + end + connections.clear + end + end + else + stack = Thread.current[@key][net_http_args] ||= [] - raise ConnectionPool::Error, 'no connections are checked out' if - stack.empty? + raise ConnectionPool::Error, 'no connections are checked out' if + stack.empty? - conn = stack.pop + conn = stack.pop - if stack.empty? - @available.push conn, connection_args: net_http_args + if stack.empty? + @available.push conn, connection_args: net_http_args - Thread.current[@key].delete(net_http_args) - Thread.current[@key] = nil if Thread.current[@key].empty? + Thread.current[@key].delete(net_http_args) + Thread.current[@key] = nil if Thread.current[@key].empty? + end end - nil end diff --git a/lib/net/http/persistent/timed_stack_multi.rb b/lib/net/http/persistent/timed_stack_multi.rb index d242e9d..9924a0a 100644 --- a/lib/net/http/persistent/timed_stack_multi.rb +++ b/lib/net/http/persistent/timed_stack_multi.rb @@ -63,7 +63,8 @@ def try_create options = {} # :nodoc: if @created >= @max && @enqueued >= 1 oldest, = @lru.first @lru.delete oldest - @ques[oldest].pop + connection = @ques[oldest].pop + connection.close if connection.respond_to?(:close) @created -= 1 end diff --git a/test/test_net_http_persistent.rb b/test/test_net_http_persistent.rb index ecd5426..8ba4570 100644 --- a/test/test_net_http_persistent.rb +++ b/test/test_net_http_persistent.rb @@ -116,6 +116,9 @@ def proxy_address end def proxy_port end + def proxy? + false + end end def basic_connection @@ -244,6 +247,14 @@ def test_ciphers_equals assert_equal 1, @http.ssl_generation end + def test_extra_chain_cert_equals + skip 'extra_chain_cert is not supported by Net::HTTP' unless Net::HTTP.method_defined?(:extra_chain_cert) + @http.extra_chain_cert = :extra_chain_cert + + assert_equal :extra_chain_cert, @http.extra_chain_cert + assert_equal 1, @http.ssl_generation + end + def test_connection_for @http.open_timeout = 123 @http.read_timeout = 321 @@ -1267,6 +1278,7 @@ def test_ssl assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode assert_kind_of OpenSSL::X509::Store, c.cert_store assert_nil c.verify_callback + assert_nil c.verify_hostname if c.respond_to?(:verify_hostname) end def test_ssl_ca_file @@ -1350,6 +1362,49 @@ def test_ssl_verify_mode assert_equal OpenSSL::SSL::VERIFY_NONE, c.verify_mode end + def test_ssl_enable_verify_hostname + skip 'OpenSSL is missing' unless HAVE_OPENSSL + + @http.verify_hostname = true + c = Net::HTTP.new 'localhost', 80 + + skip 'net/http doesn\'t provide verify_hostname= method' unless + c.respond_to?(:verify_hostname=) + + @http.ssl c + + assert c.use_ssl? + assert c.verify_hostname + end + + def test_ssl_disable_verify_hostname + skip 'OpenSSL is missing' unless HAVE_OPENSSL + + @http.verify_hostname = false + c = Net::HTTP.new 'localhost', 80 + + skip 'net/http doesn\'t provide verify_hostname= method' unless + c.respond_to?(:verify_hostname=) + + @http.ssl c + + assert c.use_ssl? + assert c.verify_hostname == false + end + + def test_ssl_extra_chain_cert + skip 'OpenSSL is missing' unless HAVE_OPENSSL + skip 'extra_chain_cert is not supported by Net::HTTP' unless Net::HTTP.method_defined?(:extra_chain_cert) + + @http.extra_chain_cert = :extra_chain_cert + c = Net::HTTP.new 'localhost', 80 + + @http.ssl c + + assert c.use_ssl? + assert_equal :extra_chain_cert, c.extra_chain_cert + end + def test_ssl_warning skip 'OpenSSL is missing' unless HAVE_OPENSSL @@ -1455,5 +1510,14 @@ def test_verify_mode_equals assert_equal 1, @http.ssl_generation end -end + def test_connection_pool_after_fork + # ConnectionPool 2.4+ calls `checkin(force: true)` after fork + @http.pool.checkin(force: true) + @http.pool.checkout ['example.com', 80, nil, nil, nil, nil] + @http.pool.checkin(force: true) + @http.pool.reload do |connection| + connection.close + end + end +end diff --git a/test/test_net_http_persistent_timed_stack_multi.rb b/test/test_net_http_persistent_timed_stack_multi.rb index 834d0ae..38e191c 100644 --- a/test/test_net_http_persistent_timed_stack_multi.rb +++ b/test/test_net_http_persistent_timed_stack_multi.rb @@ -4,10 +4,15 @@ class TestNetHttpPersistentTimedStackMulti < Minitest::Test class Connection - attr_reader :host + attr_reader :host, :closed def initialize(host) @host = host + @closed = false + end + + def close + @closed = true end end @@ -69,6 +74,18 @@ def test_pop_full assert_empty stack end + def test_pop_closes_extra_connections + stack = Net::HTTP::Persistent::TimedStackMulti.new(1) { |host| Connection.new(host) } + + a_conn = stack.pop connection_args: 'a.example' + stack.push a_conn, connection_args: 'a.example' + + b_conn = stack.pop connection_args: 'b.example' + + assert a_conn.closed + refute b_conn.closed + end + def test_pop_wait thread = Thread.start do @stack.pop