├── .gemtest ├── .gitignore ├── .autotest ├── Manifest.txt ├── .travis.yml ├── Gemfile ├── lib └── net │ └── http │ ├── persistent │ ├── connection.rb │ ├── pool.rb │ └── timed_stack_multi.rb │ └── persistent.rb ├── Rakefile ├── README.rdoc ├── test ├── test_net_http_persistent_timed_stack_multi.rb └── test_net_http_persistent.rb └── History.txt /.gemtest: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /.rdoc 3 | /TAGS 4 | /doc 5 | /pkg 6 | .idea 7 | /Gemfile.lock 8 | -------------------------------------------------------------------------------- /.autotest: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'autotest/restart' 4 | 5 | Autotest.add_hook :initialize do |at| 6 | at.add_exception '.git' 7 | at.add_exception '.rdoc' 8 | end 9 | 10 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | .autotest 2 | .gemtest 3 | .travis.yml 4 | Gemfile 5 | History.txt 6 | Manifest.txt 7 | README.rdoc 8 | Rakefile 9 | lib/net/http/persistent.rb 10 | lib/net/http/persistent/connection.rb 11 | lib/net/http/persistent/pool.rb 12 | lib/net/http/persistent/timed_stack_multi.rb 13 | test/test_net_http_persistent.rb 14 | test/test_net_http_persistent_timed_stack_multi.rb 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | after_script: 3 | - rake travis:after -t 4 | before_script: 5 | - gem install hoe-travis --no-document 6 | - rake travis:before -t 7 | language: ruby 8 | notifications: 9 | email: 10 | - drbrain@segment7.net 11 | rvm: 12 | - 2.3 13 | - 2.4 14 | - 2.5 15 | - 2.6 16 | - 2.7 17 | script: rake travis 18 | install: "" # avoid running default bundler install 19 | 20 | matrix: 21 | include: 22 | - rvm: "2.7" 23 | env: TRAVIS_MATRIX=pipeline 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | # DO NOT EDIT THIS FILE. Instead, edit Rakefile, and run `rake bundler:gemfile`. 4 | 5 | source "https://rubygems.org/" 6 | 7 | gem "connection_pool", "~>2.2" 8 | 9 | gem "minitest", "~>5.11", :group => [:development, :test] 10 | gem "hoe-bundler", "~>1.5", :group => [:development, :test] 11 | gem "hoe-travis", "~>1.4", ">=1.4.1", :group => [:development, :test] 12 | gem "rdoc", ">=4.0", "<7", :group => [:development, :test] 13 | gem "hoe", "~>3.17", :group => [:development, :test] 14 | 15 | # vim: syntax=ruby 16 | -------------------------------------------------------------------------------- /lib/net/http/persistent/connection.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # A Net::HTTP connection wrapper that holds extra information for managing the 3 | # connection's lifetime. 4 | 5 | class Net::HTTP::Persistent::Connection # :nodoc: 6 | 7 | attr_accessor :http 8 | 9 | attr_accessor :last_use 10 | 11 | attr_accessor :requests 12 | 13 | attr_accessor :ssl_generation 14 | 15 | def initialize http_class, http_args, ssl_generation 16 | @http = http_class.new(*http_args) 17 | @ssl_generation = ssl_generation 18 | 19 | reset 20 | end 21 | 22 | def finish 23 | @http.finish 24 | rescue IOError 25 | ensure 26 | reset 27 | end 28 | 29 | def reset 30 | @last_use = Net::HTTP::Persistent::EPOCH 31 | @requests = 0 32 | end 33 | 34 | def ressl ssl_generation 35 | @ssl_generation = ssl_generation 36 | 37 | finish 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'hoe' 4 | 5 | Hoe.plugin :bundler 6 | Hoe.plugin :git 7 | Hoe.plugin :minitest 8 | Hoe.plugin :travis 9 | 10 | Hoe.spec 'net-http-persistent' do 11 | developer 'Eric Hodel', 'drbrain@segment7.net' 12 | 13 | self.readme_file = 'README.rdoc' 14 | self.extra_rdoc_files += Dir['*.rdoc'] 15 | 16 | self.require_ruby_version '>= 2.3' 17 | 18 | license 'MIT' 19 | 20 | rdoc_locations << 21 | 'docs-push.seattlerb.org:/data/www/docs.seattlerb.org/net-http-persistent/' 22 | 23 | dependency 'connection_pool', '~> 2.2' 24 | dependency 'minitest', '~> 5.2', :development 25 | dependency 'hoe-bundler', '~> 1.5', :development 26 | dependency 'hoe-travis', ['~> 1.4', '>= 1.4.1'], :development 27 | dependency 'net-http-pipeline', '~> 1.0' if 28 | ENV['TRAVIS_MATRIX'] == 'pipeline' 29 | end 30 | 31 | # vim: syntax=Ruby 32 | -------------------------------------------------------------------------------- /lib/net/http/persistent/pool.rb: -------------------------------------------------------------------------------- 1 | class Net::HTTP::Persistent::Pool < ConnectionPool # :nodoc: 2 | 3 | attr_reader :available # :nodoc: 4 | attr_reader :key # :nodoc: 5 | 6 | def initialize(options = {}, &block) 7 | super 8 | 9 | @available = Net::HTTP::Persistent::TimedStackMulti.new(@size, &block) 10 | @key = "current-#{@available.object_id}" 11 | end 12 | 13 | def checkin net_http_args 14 | stack = Thread.current[@key][net_http_args] ||= [] 15 | 16 | raise ConnectionPool::Error, 'no connections are checked out' if 17 | stack.empty? 18 | 19 | conn = stack.pop 20 | 21 | if stack.empty? 22 | @available.push conn, connection_args: net_http_args 23 | 24 | Thread.current[@key].delete(net_http_args) 25 | Thread.current[@key] = nil if Thread.current[@key].empty? 26 | end 27 | 28 | nil 29 | end 30 | 31 | def checkout net_http_args 32 | stacks = Thread.current[@key] ||= {} 33 | stack = stacks[net_http_args] ||= [] 34 | 35 | if stack.empty? then 36 | conn = @available.pop connection_args: net_http_args 37 | else 38 | conn = stack.last 39 | end 40 | 41 | stack.push conn 42 | 43 | conn 44 | end 45 | 46 | def shutdown 47 | Thread.current[@key] = nil 48 | super 49 | end 50 | end 51 | 52 | require_relative 'timed_stack_multi' 53 | 54 | -------------------------------------------------------------------------------- /lib/net/http/persistent/timed_stack_multi.rb: -------------------------------------------------------------------------------- 1 | class Net::HTTP::Persistent::TimedStackMulti < ConnectionPool::TimedStack # :nodoc: 2 | 3 | ## 4 | # Returns a new hash that has arrays for keys 5 | # 6 | # Using a class method to limit the bindings referenced by the hash's 7 | # default_proc 8 | 9 | def self.hash_of_arrays # :nodoc: 10 | Hash.new { |h,k| h[k] = [] } 11 | end 12 | 13 | def initialize(size = 0, &block) 14 | super 15 | 16 | @enqueued = 0 17 | @ques = self.class.hash_of_arrays 18 | @lru = {} 19 | @key = :"connection_args-#{object_id}" 20 | end 21 | 22 | def empty? 23 | (@created - @enqueued) >= @max 24 | end 25 | 26 | def length 27 | @max - @created + @enqueued 28 | end 29 | 30 | private 31 | 32 | def connection_stored? options = {} # :nodoc: 33 | !@ques[options[:connection_args]].empty? 34 | end 35 | 36 | def fetch_connection options = {} # :nodoc: 37 | connection_args = options[:connection_args] 38 | 39 | @enqueued -= 1 40 | lru_update connection_args 41 | @ques[connection_args].pop 42 | end 43 | 44 | def lru_update connection_args # :nodoc: 45 | @lru.delete connection_args 46 | @lru[connection_args] = true 47 | end 48 | 49 | def shutdown_connections # :nodoc: 50 | @ques.each_key do |key| 51 | super connection_args: key 52 | end 53 | end 54 | 55 | def store_connection obj, options = {} # :nodoc: 56 | @ques[options[:connection_args]].push obj 57 | @enqueued += 1 58 | end 59 | 60 | def try_create options = {} # :nodoc: 61 | connection_args = options[:connection_args] 62 | 63 | if @created >= @max && @enqueued >= 1 64 | oldest, = @lru.first 65 | @lru.delete oldest 66 | @ques[oldest].pop 67 | 68 | @created -= 1 69 | end 70 | 71 | if @created < @max 72 | @created += 1 73 | lru_update connection_args 74 | return @create_block.call(connection_args) 75 | end 76 | end 77 | 78 | end 79 | 80 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = net-http-persistent 2 | 3 | home :: https://github.com/drbrain/net-http-persistent 4 | rdoc :: http://docs.seattlerb.org/net-http-persistent 5 | 6 | == DESCRIPTION: 7 | 8 | Manages persistent connections using Net::HTTP including a thread pool for 9 | connecting to multiple hosts. 10 | 11 | Using persistent HTTP connections can dramatically increase the speed of HTTP. 12 | Creating a new HTTP connection for every request involves an extra TCP 13 | round-trip and causes TCP congestion avoidance negotiation to start over. 14 | 15 | Net::HTTP supports persistent connections with some API methods but does not 16 | make setting up a single persistent connection or managing multiple 17 | connections easy. Net::HTTP::Persistent wraps Net::HTTP and allows you to 18 | focus on how to make HTTP requests. 19 | 20 | == FEATURES/PROBLEMS: 21 | 22 | * Supports TLS with secure defaults 23 | * Thread-safe 24 | * Pure ruby 25 | 26 | == SYNOPSIS 27 | 28 | The following example will make two requests to the same server. The 29 | connection is kept alive between requests: 30 | 31 | require 'net/http/persistent' 32 | 33 | uri = URI 'http://example.com/awesome/web/service' 34 | 35 | http = Net::HTTP::Persistent.new name: 'my_app_name' 36 | 37 | # perform a GET 38 | response = http.request uri 39 | 40 | # create a POST 41 | post_uri = uri + 'create' 42 | post = Net::HTTP::Post.new post_uri.path 43 | post.set_form_data 'some' => 'cool data' 44 | 45 | # perform the POST, the URI is always required 46 | response = http.request post_uri, post 47 | 48 | # if you are done making http requests, or won't make requests for several 49 | # minutes 50 | http.shutdown 51 | 52 | Please see the documentation on Net::HTTP::Persistent for more information, 53 | including SSL connection verification, header handling and tunable options. 54 | 55 | == INSTALL: 56 | 57 | gem install net-http-persistent 58 | 59 | == LICENSE: 60 | 61 | (The MIT License) 62 | 63 | Copyright (c) Eric Hodel, Aaron Patterson 64 | 65 | Permission is hereby granted, free of charge, to any person obtaining 66 | a copy of this software and associated documentation files (the 67 | 'Software'), to deal in the Software without restriction, including 68 | without limitation the rights to use, copy, modify, merge, publish, 69 | distribute, sublicense, and/or sell copies of the Software, and to 70 | permit persons to whom the Software is furnished to do so, subject to 71 | the following conditions: 72 | 73 | The above copyright notice and this permission notice shall be 74 | included in all copies or substantial portions of the Software. 75 | 76 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 77 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 78 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 79 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 80 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 81 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 82 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 83 | -------------------------------------------------------------------------------- /test/test_net_http_persistent_timed_stack_multi.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'net/http/persistent' 3 | 4 | class TestNetHttpPersistentTimedStackMulti < Minitest::Test 5 | 6 | class Connection 7 | attr_reader :host 8 | 9 | def initialize(host) 10 | @host = host 11 | end 12 | end 13 | 14 | def setup 15 | @stack = Net::HTTP::Persistent::TimedStackMulti.new { Object.new } 16 | end 17 | 18 | def test_empty_eh 19 | stack = Net::HTTP::Persistent::TimedStackMulti.new(1) { Object.new } 20 | 21 | refute_empty stack 22 | 23 | popped = stack.pop 24 | 25 | assert_empty stack 26 | 27 | stack.push connection_args: popped 28 | 29 | refute_empty stack 30 | end 31 | 32 | def test_length 33 | stack = Net::HTTP::Persistent::TimedStackMulti.new(1) { Object.new } 34 | 35 | assert_equal 1, stack.length 36 | 37 | popped = stack.pop 38 | 39 | assert_equal 0, stack.length 40 | 41 | stack.push connection_args: popped 42 | 43 | assert_equal 1, stack.length 44 | end 45 | 46 | def test_pop 47 | object = Object.new 48 | @stack.push object 49 | 50 | popped = @stack.pop 51 | 52 | assert_same object, popped 53 | end 54 | 55 | def test_pop_empty 56 | e = assert_raises Timeout::Error do 57 | @stack.pop timeout: 0 58 | end 59 | 60 | assert_equal 'Waited 0 sec', e.message 61 | end 62 | 63 | def test_pop_full 64 | stack = Net::HTTP::Persistent::TimedStackMulti.new(1) { Object.new } 65 | 66 | popped = stack.pop 67 | 68 | refute_nil popped 69 | assert_empty stack 70 | end 71 | 72 | def test_pop_wait 73 | thread = Thread.start do 74 | @stack.pop 75 | end 76 | 77 | Thread.pass while thread.status == 'run' 78 | 79 | object = Object.new 80 | 81 | @stack.push object 82 | 83 | assert_same object, thread.value 84 | end 85 | 86 | def test_pop_shutdown 87 | @stack.shutdown { } 88 | 89 | assert_raises ConnectionPool::PoolShuttingDownError do 90 | @stack.pop 91 | end 92 | end 93 | 94 | def test_push 95 | stack = Net::HTTP::Persistent::TimedStackMulti.new(1) { Object.new } 96 | 97 | conn = stack.pop 98 | 99 | stack.push connection_args: conn 100 | 101 | refute_empty stack 102 | end 103 | 104 | def test_push_shutdown 105 | called = [] 106 | 107 | @stack.shutdown do |object| 108 | called << object 109 | end 110 | 111 | @stack.push connection_args: Object.new 112 | 113 | refute_empty called 114 | assert_empty @stack 115 | end 116 | 117 | def test_shutdown 118 | @stack.push connection_args: Object.new 119 | 120 | called = [] 121 | 122 | @stack.shutdown do |object| 123 | called << object 124 | end 125 | 126 | refute_empty called 127 | assert_empty @stack 128 | end 129 | 130 | def test_pop_recycle 131 | stack = Net::HTTP::Persistent::TimedStackMulti.new(2) { |host| Connection.new(host) } 132 | 133 | a_conn = stack.pop connection_args: 'a.example' 134 | stack.push a_conn, connection_args: 'a.example' 135 | 136 | b_conn = stack.pop connection_args: 'b.example' 137 | stack.push b_conn, connection_args: 'b.example' 138 | 139 | c_conn = stack.pop connection_args: 'c.example' 140 | 141 | assert_equal 'c.example', c_conn.host 142 | 143 | stack.push c_conn, connection_args: 'c.example' 144 | 145 | recreated = stack.pop connection_args: 'a.example' 146 | 147 | refute_same a_conn, recreated 148 | end 149 | 150 | end 151 | 152 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | === 4.0.1 / 2021-01-12 2 | 3 | Bug fixes: 4 | 5 | * Loosen Ruby version requirement so Ruby 3.0 will work. 6 | 7 | === 4.0.0 / 2020-04-30 8 | 9 | Breaking changes: 10 | 11 | * Removed built-in support for retrying failed requests as Net::HTTP has this 12 | built-in for all supported versions. Pull request #100 by Michael Grosser. 13 | * Dropped support for EoL ruby versions (< 2.4). Future feature releases may 14 | drop support for ruby versions that are at end-of-life or in security-only 15 | maintenance mode with any release. Pull request #113 by David Rodríguez 16 | 17 | New features: 18 | 19 | * Added Net::HTTP::Persistent#max_retries= to configure the number of retries 20 | performed on a request for ruby versions that support it (2.5+). 21 | * URI-ness is determined through #respond_to? to allow compatibility with 22 | Addressable::URI. Pull request #67 by Ryan McKern. 23 | * Use require_relative to reduce patch burden for vendored versions. Pull 24 | Request #106 by David Rodríguez 25 | 26 | Bug fixes: 27 | 28 | * Stop wasting a connection when the keep-alive timeout is less than the idle 29 | timeout. Pull request #115 by Yap Sok Ann. 30 | * Improved use of URI#hostname for IPv6 connections. Pull request #76 by 31 | Tomas Koutsky. 32 | * Improved check for Process::RLIMIT_NOFILE support. Pull request #109 by Vít 33 | Ondruch. 34 | * Fix namespace in comments for escape/unescape wrappers. Pull request #114 35 | by David Rodríguez. 36 | * Fix History.txt timestamp for 3.0.0 release. Pull request #107 by Joe Van 37 | Dyk. 38 | * Fix link to PR #98 in 3.1.0 release notes. Pull request #110 by Justin 39 | Reid. 40 | 41 | Other: 42 | 43 | * Updated Net::HTTP::Persistent#reconnect documentation to indicate that all 44 | connections are reset. Issue #117 by Taisuke Miyazaki. 45 | 46 | === 3.1.0 / 2019-07-24 47 | 48 | New features: 49 | * Support ruby 2.6 Net::HTTP#write_timeout=. Pull request #99 by Víctor 50 | Roldán Betancort. 51 | * Support setting TLS min/max version. Pull request #94 by Bart. 52 | 53 | Bug fixes: 54 | * Reduce potential for memory leak through Hash default proc bindings. Pull 55 | request #64 by Dominic Metzger. 56 | * Test proxy auto detection from no_proxy in ENV. Pull request #60 by 57 | HINOHARA Hiroshi. 58 | * Add missing timestamp for 3.0 release. Pull request #78 by Joe Van Dyk. 59 | * Support IPv6 URLs in proxy checks. Pull request #82 by Nicolás Sanguinetti. 60 | * Use Net::HTTPGenericRequest#method to check idempotence for improved 61 | compatibility. Pull request #83 by Fotos Georgiadis. 62 | * Run net-http-pipeline tests in travis. Pull request #86 by T.J. Schuck. 63 | * Correct +no_proxy+ support to override Net::HTTP proxy fallback. Pull 64 | request #88 by Jared Kauppila. 65 | * Mitigate memory leak when combined with faraday. Pull request #105 by Yohei 66 | Kitamura. 67 | * Set default connection pool size for Windows. Pull request #90 by Jared 68 | Kauppila. 69 | * Fix missing +name:+ argument in documentation. Pull requests #85 by T.J. 70 | Schuck, #84 by James White. 71 | * Fix memory leak from connection pooling. Pull request #98 by Aaron 72 | Patterson. 73 | * Update tests for minitest assert_equal deprecation. Pull request #92 by 74 | Olle Jonsson. 75 | * Fix typo in Net::HTTP::Persistent#pipeline. Pull request #91 by Kazuma 76 | Furuhashi. 77 | 78 | Other: 79 | * Added bundler hoe plugin. Pull request #103 by Michael Grosser. 80 | * Updated ruby versions in Travis CI. Pull request #93 by Olle Jonsson. Pull 81 | request #103 by Michael Grosser. 82 | 83 | === 3.0 / 2016-10-05 84 | 85 | Breaking changes: 86 | 87 | * No longer supports ruby 2.0 and earlier 88 | * Net::HTTP::Persistent::new now uses keyword arguments for +name+ and 89 | +proxy+. 90 | * Removed #max_age, use #expired? 91 | 92 | New features: 93 | 94 | * Uses connection_pool to manage all connections for a Net::HTTP::Persistent 95 | instance. 96 | 97 | Bug fixes: 98 | 99 | * Add missing SSL options ca_path, ciphers, ssl_timeout, verify_depth. 100 | Issue #63 by Johnneylee Jack Rollins. 101 | 102 | === 2.9.4 / 2014-02-10 103 | 104 | * Bug fixes 105 | * Improve proxy escaping from 2.9.2. Pull request #59 by Mislav Marohnić. 106 | 107 | === 2.9.3 / 2014-02-06 108 | 109 | * Bug fixes 110 | * Fix breakage in 2.9.2 for users without proxies. Pull request #56 by 111 | Yoshihiro TAKAHARA (merged), #57 by ChuckLin, #58 by Kenny Meyer. 112 | 113 | === 2.9.2 / 2014-02-05 114 | 115 | * Bug fixes 116 | * Special characters in proxy passwords are now handled correctly. Issue 117 | #48 by Mislav Marohnić. Pull request #54 by Juha Kajava 118 | 119 | === 2.9.1 / 2014-01-22 120 | 121 | * Bug fixes 122 | * Added license to gemspec. Issue #47 by Benjamin Fleischer 123 | * Set Net::HTTP#keep_alive_timeout when supported by ruby. Pull request #53 124 | by Dylan Thacker-Smith. 125 | * The backtrace is preserved for errors in #reset to help with debugging. 126 | Issue #41 by Andrew Cholakian. 127 | 128 | === 2.9 / 2013-07-24 129 | 130 | * Minor enhancement 131 | * Added Net::HTTP::Persistent#max_requests to avoid ECONNRESET for a server 132 | that allows a limited number of requests on a connection. Pull request 133 | #42 by James Tucker. 134 | * Request failures are now raised with the backtrace of the original 135 | exception. This gives better insight into the reason for the failure. 136 | See #41 by Andrew Cholakian. 137 | * OpenSSL is no longer required. If OpenSSL is not available an exception 138 | will be raised when attempting to access HTTPS resources. Feature request 139 | by André Arko 140 | 141 | * Bug fixes 142 | * Explain the proper way of sending parameters depending upon the request 143 | method. Issue #35 by André Arko. 144 | * Handle Errno::ETIMEDOUT by retrying the request. Issue #36 by André Arko. 145 | * Requests retried by ruby 2.x are no longer retried by net-http-persistent. 146 | * Finish the connection if an otherwise unhandled exception happens during a 147 | request. Bug #46 by Mark Oude Veldhuis. 148 | * Net::HTTP::Persistent::detect_idle_timeout now assumes a StandardError 149 | indicates the idle timeout has been found. Bug #43 by James Tucker. 150 | 151 | === 2.8 / 2012-10-17 152 | 153 | * Minor enhancements 154 | * Added Net::HTTP::Persistent::detect_idle_timeout which can be used to 155 | determine the idle timeout for a host. 156 | * The read timeout may now be updated for every request. Issue #33 by 157 | Mislav Marohnić 158 | * Added NO_PROXY support. Pull Request #31 by Laurence Rowe. 159 | * Added #cert and #key aliases for Net::HTTP compatibility. Pull request 160 | #26 by dlee. 161 | * The artifice gem now disables SSL session reuse to prevent breakage of 162 | testing frameworks. Pull Request #29 by Christopher Cooke. 163 | * Disabled Net::HTTP::Persistent::SSLReuse on Ruby 2+. This feature is now 164 | built-in to Net::HTTP. 165 | * Bug fixes 166 | * Socket options are set again following connection reset. Pull request #28 167 | by cmaion. 168 | * #shutdown now works even if no connections were made. Pull Request #24 by 169 | James Tucker. 170 | * Updated test RSA key size to 1024 bits. Bug #25 by Gunnar Wolf. 171 | * The correct host:port are shown in the exception when a proxy connection 172 | fails. Bug #30 by glebtv. 173 | 174 | === 2.7 / 2012-06-06 175 | 176 | * Minor enhancement 177 | * Added JRuby compatibility by default for HTTPS connections. (JRuby lacks 178 | OpenSSL::SSL::Session.) 179 | 180 | === 2.6 / 2012-03-26 181 | 182 | * Minor enhancement 183 | * Net::HTTP::Persistent#idle_timeout may be set to nil to disable expiration 184 | of connections. Pull Request #21 by Aaron Stone 185 | 186 | === 2.5.2 / 2012-02-13 187 | 188 | * Bug fix 189 | * Fix variable shadowing warning. 190 | 191 | === 2.5.1 / 2012-02-10 192 | 193 | * Bug fix 194 | * Reusing SSL connections with HTTP proxies now works. Issue #15 by Paul 195 | Ingham and mcrmfc 196 | 197 | === 2.5 / 2012-02-07 198 | 199 | * Minor enhancements 200 | * The proxy may be changed at any time. 201 | * The allowed SSL version may now be set via #ssl_version. 202 | Issue #16 by astera 203 | * Added Net::HTTP::Persistent#override_headers which allows overriding 204 | * Net::HTTP default headers like User-Agent. See 205 | Net::HTTP::Persistent@Headers for details. Issue #17 by andkerosine 206 | 207 | * Bug fixes 208 | * The ruby 1.8 speed monkeypatch now handles EAGAIN for windows users. 209 | Issue #12 by Alwyn Schoeman 210 | * Fixed POST example in README. Submitted by injekt. 211 | * Fixed various bugs in the shutdown of connections especially cross-thread 212 | (which you shouldn't be doing anyways). 213 | 214 | === 2.4.1 / 2012-02-03 215 | 216 | * Bug fixes 217 | * When FakeWeb or WebMock are loaded SSL sessions will not be reused to 218 | prevent breakage of testing frameworks. Issue #13 by Matt Brictson, pull 219 | request #14 by Zachary Scott 220 | * SSL connections are reset when the SSL parameters change. 221 | Mechanize issue #194 by dsisnero 222 | * Last-use times are now cleaned up in #shutdown. 223 | 224 | === 2.4 / 2012-01-31 225 | 226 | * Minor enhancement 227 | * net-http-persistent now complains if OpenSSL::SSL::VERIFY_PEER is equal to 228 | OpenSSL::SSL::VERIFY_NONE. If you have a platform that is broken this way 229 | you must define the constant: 230 | 231 | I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG = nil 232 | 233 | at the top level of your application to disable the warning. 234 | 235 | * Bug fix 236 | * Fix persisting SSL sessions through HTTP proxies. Mechanize issue #178 by 237 | Robert Poor, net-http-persistent issues #10, #11. 238 | 239 | === 2.3.2 / 2011-12-21 240 | 241 | * Bug fix 242 | * Finish connections that were closed by Net::HTTP so they can be restarted. 243 | 244 | === 2.3.1 / 2011-10-26 245 | 246 | * Bug fix 247 | * If a request object already contains a Connection header it will no longer 248 | be overridden. This allows keep-alive connections to be disabled on a 249 | per-request basis. 250 | 251 | === 2.3 / 2011-10-25 252 | 253 | * Minor Enhancement 254 | * The time since last use for a connection is now recorded in error 255 | messages for the connection. 256 | 257 | === 2.2 / 2011-10-24 258 | 259 | * Minor Enhancements 260 | * Added timeouts for idle connections which are set through #idle_timeout. 261 | The default timeout is 5 seconds. Reducing the idle timeout is preferred 262 | over setting #retry_change_requests to true if you wish to avoid the "too 263 | many connection resets" error when POSTing data. 264 | * Documented tunables and settings in one place in Net::HTTP::Persistent 265 | 266 | === 2.1 / 2011-09-19 267 | 268 | * Minor Enhancement 269 | * For HTTPS connections, SSL sessions are now reused avoiding the extra 270 | round trips and computations of extra SSL handshakes. If you have 271 | problems with SSL session reuse it can be disabled by 272 | Net::HTTP::Persistent#reuse_ssl_sessions 273 | 274 | * Bug Fixes 275 | * The default certificate store is now used even if #verify_mode was not 276 | set. Issue #7, Pull Request #8 by Matthew M. Boedicker 277 | 278 | === 2.0 / 2011-08-26 279 | 280 | * Incompatibility 281 | * Net::HTTP::Persistent#verify_mode now defaults to 282 | OpenSSL::SSL::VERIFY_PEER. This may cause HTTPS request failures if your 283 | default certificate store lacks the correct certificates. 284 | 285 | === 1.9 / 2011-08-26 286 | 287 | * Minor Enhancement 288 | * Added Net::HTTP::Persistent#cert_store to set an SSL certificate store 289 | which defaults to the OpenSSL default certificate store. 290 | 291 | HTTPS server certificates will be validated when this option is combined 292 | with setting Net::HTTP::Persistent#verify_mode to 293 | OpenSSL::SSL::VERIFY_PEER. 294 | 295 | === 1.8.1 / 2011-08-08 296 | 297 | * Bug Fix 298 | * Requests with OpenSSL errors are retried now. Pull Request #5 by James 299 | Tucker. 300 | 301 | === 1.8 / 2011-06-27 302 | 303 | * Minor Enhancement 304 | * Added Net::HTTP::Persistent#retry_change_requests which allows POST and 305 | other non-idempotent requests to be retried automatically. Take care when 306 | enabling this option to ensure the server will handle multiple POSTs with 307 | the same data in a sane manner. 308 | 309 | === 1.7 / 2011-04-17 310 | 311 | * Minor Enhancement 312 | * Added Net::HTTP::Persistent#pipeline which integrates with 313 | net-http-pipeline when it is present. 314 | * Bug Fix 315 | * Perform a case-insensitive check of the URI scheme for HTTPS URIs 316 | 317 | === 1.6.1 / 2011-03-08 318 | 319 | * Bug Fix 320 | * Net::HTTP::Persistent#request now handles Errno::EINVAL as a connection 321 | reset and will be retried for idempotent requests. Reported by Aaron 322 | Qian. 323 | 324 | === 1.6 / 2011-03-01 325 | 326 | * Minor Enhancement 327 | * Added Net::HTTP::Persistent#socket_options to set multiple socket options 328 | at socket startup. 329 | 330 | === 1.5.2 / 2011-02-24 331 | 332 | * Bug Fix 333 | * Only set TCP_NODELAY if the connection has an @socket. Allows 334 | net-http-persistent to be used with fake_web. Reported by Sathish 335 | Pasupunuri. 336 | 337 | === 1.5.1 / 2011-02-10 338 | 339 | * Bug fix 340 | * Only set TCP_NODELAY at connection start. Reported by Brian Henderson. 341 | 342 | === 1.5 / 2011-01-25 343 | 344 | * Minor Enhancements 345 | * Set TCP_NODELAY on created socket if possible. (This will only help for 346 | requests that send bodies.) 347 | 348 | === 1.4.1 / 2010-10-13 349 | 350 | * Bug Fixes 351 | * Don't finish the connection when we're retrying, reset it. Patch by James 352 | Tucker. 353 | 354 | === 1.4 / 2010-09-29 355 | 356 | * Minor Enhancements 357 | * Added the very dangerous #shutdown_in_all_threads. IT IS DANGEROUS!. 358 | Patch by Torsten Schönebaum. 359 | 360 | === 1.3.1 / 2010-09-13 361 | 362 | * Bug Fixes 363 | * #connection_for no longer tries to ssl-enable an existing connection. 364 | Patch by Joseph West. 365 | 366 | === 1.3 / 2010-09-08 367 | 368 | * Minor Enhancements 369 | * HTTP versions are now recorded. This information is not currently used. 370 | 371 | * Bug Fixes 372 | * #shutdown no longer fails when an unstarted HTTP connection is shut down. 373 | 374 | === 1.2.5 / 2010-07-27 375 | 376 | * Bug Fixes 377 | * Fix duplicated test name. Noted by Peter Higgins. 378 | * #shutdown now works even when no connections were made. 379 | 380 | === 1.2.4 / 2010-07-26 381 | 382 | * Bug Fixes 383 | * Actually have #request only finish a connection. Somehow this got 384 | missed. 385 | 386 | === 1.2.3 / 2010-06-29 387 | 388 | * Bug Fixes 389 | * Fix example code (pointed out by Alex Stahl) 390 | 391 | === 1.2.2 / 2010-06-22 392 | 393 | * Bug Fixes 394 | * #request only finishes a connection instead of restarting it. This helps 395 | prevents errors on non-idempotent HTTP requests after errors. 396 | * #connection_for handles EHOSTDOWN like #reset 397 | 398 | === 1.2.1 / 2010-05-25 399 | 400 | * Bug Fixes 401 | * Don't alter Net::BufferedIO#rbuf_fill on 1.9+ 402 | 403 | === 1.2 / 2010-05-20 404 | 405 | * Minor Enhancements 406 | * Net::HTTP#read_timeout is now supported 407 | * Net::HTTP#open_timeout is now supported 408 | * Net::HTTP::Persistent#request now supports a block like Net::HTTP#request 409 | 410 | === 1.1 / 2010-05-18 411 | 412 | * Minor Enhancements 413 | * Proxy support, see Net::HTTP::Persistent::new, 414 | Net::HTTP::Persistent#proxy_from_env 415 | * Added +name+ parameter to Net::HTTP::Persistent::new for separation of 416 | connection pools. 417 | * Added Net::HTTP::Persistent#shutdown so you can clean up after yourself 418 | * Net::HTTP::Persistent now suppresses "peer certificate won't be verified 419 | in this SSL session" warning. 420 | 421 | * Bug Fixes 422 | * Net::HTTP::Persistent retries requests in accordance with RFC 2616. 423 | 424 | === 1.0.1 / 2010-05-05 425 | 426 | * Minor Enhancements 427 | * Added #debug_output 428 | * Now uses Hoe minitest plugin 429 | * Bug Fixes 430 | * Tests pass on 1.9 431 | 432 | === 1.0.0 / 2010-05-04 433 | 434 | * Major Enhancements 435 | * Birthday! 436 | 437 | -------------------------------------------------------------------------------- /lib/net/http/persistent.rb: -------------------------------------------------------------------------------- 1 | require 'net/http' 2 | require 'uri' 3 | require 'cgi' # for escaping 4 | require 'connection_pool' 5 | 6 | begin 7 | require 'net/http/pipeline' 8 | rescue LoadError 9 | end 10 | 11 | autoload :OpenSSL, 'openssl' 12 | 13 | ## 14 | # Persistent connections for Net::HTTP 15 | # 16 | # Net::HTTP::Persistent maintains persistent connections across all the 17 | # servers you wish to talk to. For each host:port you communicate with a 18 | # single persistent connection is created. 19 | # 20 | # Connections will be shared across threads through a connection pool to 21 | # increase reuse of connections. 22 | # 23 | # You can shut down any remaining HTTP connections when done by calling 24 | # #shutdown. 25 | # 26 | # Example: 27 | # 28 | # require 'net/http/persistent' 29 | # 30 | # uri = URI 'http://example.com/awesome/web/service' 31 | # 32 | # http = Net::HTTP::Persistent.new 33 | # 34 | # # perform a GET 35 | # response = http.request uri 36 | # 37 | # # or 38 | # 39 | # get = Net::HTTP::Get.new uri.request_uri 40 | # response = http.request get 41 | # 42 | # # create a POST 43 | # post_uri = uri + 'create' 44 | # post = Net::HTTP::Post.new post_uri.path 45 | # post.set_form_data 'some' => 'cool data' 46 | # 47 | # # perform the POST, the URI is always required 48 | # response http.request post_uri, post 49 | # 50 | # Note that for GET, HEAD and other requests that do not have a body you want 51 | # to use URI#request_uri not URI#path. The request_uri contains the query 52 | # params which are sent in the body for other requests. 53 | # 54 | # == TLS/SSL 55 | # 56 | # TLS connections are automatically created depending upon the scheme of the 57 | # URI. TLS connections are automatically verified against the default 58 | # certificate store for your computer. You can override this by changing 59 | # verify_mode or by specifying an alternate cert_store. 60 | # 61 | # Here are the TLS settings, see the individual methods for documentation: 62 | # 63 | # #certificate :: This client's certificate 64 | # #ca_file :: The certificate-authorities 65 | # #ca_path :: Directory with certificate-authorities 66 | # #cert_store :: An SSL certificate store 67 | # #ciphers :: List of SSl ciphers allowed 68 | # #private_key :: The client's SSL private key 69 | # #reuse_ssl_sessions :: Reuse a previously opened SSL session for a new 70 | # connection 71 | # #ssl_timeout :: Session lifetime 72 | # #ssl_version :: Which specific SSL version to use 73 | # #verify_callback :: For server certificate verification 74 | # #verify_depth :: Depth of certificate verification 75 | # #verify_mode :: How connections should be verified 76 | # 77 | # == Proxies 78 | # 79 | # A proxy can be set through #proxy= or at initialization time by providing a 80 | # second argument to ::new. The proxy may be the URI of the proxy server or 81 | # :ENV which will consult environment variables. 82 | # 83 | # See #proxy= and #proxy_from_env for details. 84 | # 85 | # == Headers 86 | # 87 | # Headers may be specified for use in every request. #headers are appended to 88 | # any headers on the request. #override_headers replace existing headers on 89 | # the request. 90 | # 91 | # The difference between the two can be seen in setting the User-Agent. Using 92 | # http.headers['User-Agent'] = 'MyUserAgent' will send "Ruby, 93 | # MyUserAgent" while http.override_headers['User-Agent'] = 94 | # 'MyUserAgent' will send "MyUserAgent". 95 | # 96 | # == Tuning 97 | # 98 | # === Segregation 99 | # 100 | # Each Net::HTTP::Persistent instance has its own pool of connections. There 101 | # is no sharing with other instances (as was true in earlier versions). 102 | # 103 | # === Idle Timeout 104 | # 105 | # If a connection hasn't been used for this number of seconds it will 106 | # automatically be reset upon the next use to avoid attempting to send to a 107 | # closed connection. The default value is 5 seconds. nil means no timeout. 108 | # Set through #idle_timeout. 109 | # 110 | # Reducing this value may help avoid the "too many connection resets" error 111 | # when sending non-idempotent requests while increasing this value will cause 112 | # fewer round-trips. 113 | # 114 | # === Read Timeout 115 | # 116 | # The amount of time allowed between reading two chunks from the socket. Set 117 | # through #read_timeout 118 | # 119 | # === Max Requests 120 | # 121 | # The number of requests that should be made before opening a new connection. 122 | # Typically many keep-alive capable servers tune this to 100 or less, so the 123 | # 101st request will fail with ECONNRESET. If unset (default), this value has 124 | # no effect, if set, connections will be reset on the request after 125 | # max_requests. 126 | # 127 | # === Open Timeout 128 | # 129 | # The amount of time to wait for a connection to be opened. Set through 130 | # #open_timeout. 131 | # 132 | # === Socket Options 133 | # 134 | # Socket options may be set on newly-created connections. See #socket_options 135 | # for details. 136 | # 137 | # === Connection Termination 138 | # 139 | # If you are done using the Net::HTTP::Persistent instance you may shut down 140 | # all the connections in the current thread with #shutdown. This is not 141 | # recommended for normal use, it should only be used when it will be several 142 | # minutes before you make another HTTP request. 143 | # 144 | # If you are using multiple threads, call #shutdown in each thread when the 145 | # thread is done making requests. If you don't call shutdown, that's OK. 146 | # Ruby will automatically garbage collect and shutdown your HTTP connections 147 | # when the thread terminates. 148 | 149 | class Net::HTTP::Persistent 150 | 151 | ## 152 | # The beginning of Time 153 | 154 | EPOCH = Time.at 0 # :nodoc: 155 | 156 | ## 157 | # Is OpenSSL available? This test works with autoload 158 | 159 | HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc: 160 | 161 | ## 162 | # The default connection pool size is 1/4 the allowed open files 163 | # (ulimit -n) or 256 if your OS does not support file handle 164 | # limits (typically windows). 165 | 166 | if Process.const_defined? :RLIMIT_NOFILE 167 | DEFAULT_POOL_SIZE = Process.getrlimit(Process::RLIMIT_NOFILE).first / 4 168 | else 169 | DEFAULT_POOL_SIZE = 256 170 | end 171 | 172 | ## 173 | # The version of Net::HTTP::Persistent you are using 174 | 175 | VERSION = '4.0.1' 176 | 177 | ## 178 | # Error class for errors raised by Net::HTTP::Persistent. Various 179 | # SystemCallErrors are re-raised with a human-readable message under this 180 | # class. 181 | 182 | class Error < StandardError; end 183 | 184 | ## 185 | # Use this method to detect the idle timeout of the host at +uri+. The 186 | # value returned can be used to configure #idle_timeout. +max+ controls the 187 | # maximum idle timeout to detect. 188 | # 189 | # After 190 | # 191 | # Idle timeout detection is performed by creating a connection then 192 | # performing a HEAD request in a loop until the connection terminates 193 | # waiting one additional second per loop. 194 | # 195 | # NOTE: This may not work on ruby > 1.9. 196 | 197 | def self.detect_idle_timeout uri, max = 10 198 | uri = URI uri unless URI::Generic === uri 199 | uri += '/' 200 | 201 | req = Net::HTTP::Head.new uri.request_uri 202 | 203 | http = new 'net-http-persistent detect_idle_timeout' 204 | 205 | http.connection_for uri do |connection| 206 | sleep_time = 0 207 | 208 | http = connection.http 209 | 210 | loop do 211 | response = http.request req 212 | 213 | $stderr.puts "HEAD #{uri} => #{response.code}" if $DEBUG 214 | 215 | unless Net::HTTPOK === response then 216 | raise Error, "bad response code #{response.code} detecting idle timeout" 217 | end 218 | 219 | break if sleep_time >= max 220 | 221 | sleep_time += 1 222 | 223 | $stderr.puts "sleeping #{sleep_time}" if $DEBUG 224 | sleep sleep_time 225 | end 226 | end 227 | rescue 228 | # ignore StandardErrors, we've probably found the idle timeout. 229 | ensure 230 | return sleep_time unless $! 231 | end 232 | 233 | ## 234 | # This client's OpenSSL::X509::Certificate 235 | 236 | attr_reader :certificate 237 | 238 | ## 239 | # For Net::HTTP parity 240 | 241 | alias cert certificate 242 | 243 | ## 244 | # An SSL certificate authority. Setting this will set verify_mode to 245 | # VERIFY_PEER. 246 | 247 | attr_reader :ca_file 248 | 249 | ## 250 | # A directory of SSL certificates to be used as certificate authorities. 251 | # Setting this will set verify_mode to VERIFY_PEER. 252 | 253 | attr_reader :ca_path 254 | 255 | ## 256 | # An SSL certificate store. Setting this will override the default 257 | # certificate store. See verify_mode for more information. 258 | 259 | attr_reader :cert_store 260 | 261 | ## 262 | # The ciphers allowed for SSL connections 263 | 264 | attr_reader :ciphers 265 | 266 | ## 267 | # Sends debug_output to this IO via Net::HTTP#set_debug_output. 268 | # 269 | # Never use this method in production code, it causes a serious security 270 | # hole. 271 | 272 | attr_accessor :debug_output 273 | 274 | ## 275 | # Current connection generation 276 | 277 | attr_reader :generation # :nodoc: 278 | 279 | ## 280 | # Headers that are added to every request using Net::HTTP#add_field 281 | 282 | attr_reader :headers 283 | 284 | ## 285 | # Maps host:port to an HTTP version. This allows us to enable version 286 | # specific features. 287 | 288 | attr_reader :http_versions 289 | 290 | ## 291 | # Maximum time an unused connection can remain idle before being 292 | # automatically closed. 293 | 294 | attr_accessor :idle_timeout 295 | 296 | ## 297 | # Maximum number of requests on a connection before it is considered expired 298 | # and automatically closed. 299 | 300 | attr_accessor :max_requests 301 | 302 | ## 303 | # Number of retries to perform if a request fails. 304 | # 305 | # See also #max_retries=, Net::HTTP#max_retries=. 306 | 307 | attr_reader :max_retries 308 | 309 | ## 310 | # The value sent in the Keep-Alive header. Defaults to 30. Not needed for 311 | # HTTP/1.1 servers. 312 | # 313 | # This may not work correctly for HTTP/1.0 servers 314 | # 315 | # This method may be removed in a future version as RFC 2616 does not 316 | # require this header. 317 | 318 | attr_accessor :keep_alive 319 | 320 | ## 321 | # The name for this collection of persistent connections. 322 | 323 | attr_reader :name 324 | 325 | ## 326 | # Seconds to wait until a connection is opened. See Net::HTTP#open_timeout 327 | 328 | attr_accessor :open_timeout 329 | 330 | ## 331 | # Headers that are added to every request using Net::HTTP#[]= 332 | 333 | attr_reader :override_headers 334 | 335 | ## 336 | # This client's SSL private key 337 | 338 | attr_reader :private_key 339 | 340 | ## 341 | # For Net::HTTP parity 342 | 343 | alias key private_key 344 | 345 | ## 346 | # The URL through which requests will be proxied 347 | 348 | attr_reader :proxy_uri 349 | 350 | ## 351 | # List of host suffixes which will not be proxied 352 | 353 | attr_reader :no_proxy 354 | 355 | ## 356 | # Test-only accessor for the connection pool 357 | 358 | attr_reader :pool # :nodoc: 359 | 360 | ## 361 | # Seconds to wait until reading one block. See Net::HTTP#read_timeout 362 | 363 | attr_accessor :read_timeout 364 | 365 | ## 366 | # Seconds to wait until writing one block. See Net::HTTP#write_timeout 367 | 368 | attr_accessor :write_timeout 369 | 370 | ## 371 | # By default SSL sessions are reused to avoid extra SSL handshakes. Set 372 | # this to false if you have problems communicating with an HTTPS server 373 | # like: 374 | # 375 | # SSL_connect [...] read finished A: unexpected message (OpenSSL::SSL::SSLError) 376 | 377 | attr_accessor :reuse_ssl_sessions 378 | 379 | ## 380 | # An array of options for Socket#setsockopt. 381 | # 382 | # By default the TCP_NODELAY option is set on sockets. 383 | # 384 | # To set additional options append them to this array: 385 | # 386 | # http.socket_options << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1] 387 | 388 | attr_reader :socket_options 389 | 390 | ## 391 | # Current SSL connection generation 392 | 393 | attr_reader :ssl_generation # :nodoc: 394 | 395 | ## 396 | # SSL session lifetime 397 | 398 | attr_reader :ssl_timeout 399 | 400 | ## 401 | # SSL version to use. 402 | # 403 | # By default, the version will be negotiated automatically between client 404 | # and server. Ruby 1.9 and newer only. Deprecated since Ruby 2.5. 405 | 406 | attr_reader :ssl_version 407 | 408 | ## 409 | # Minimum SSL version to use, e.g. :TLS1_1 410 | # 411 | # By default, the version will be negotiated automatically between client 412 | # and server. Ruby 2.5 and newer only. 413 | 414 | attr_reader :min_version 415 | 416 | ## 417 | # Maximum SSL version to use, e.g. :TLS1_2 418 | # 419 | # By default, the version will be negotiated automatically between client 420 | # and server. Ruby 2.5 and newer only. 421 | 422 | attr_reader :max_version 423 | 424 | ## 425 | # Where this instance's last-use times live in the thread local variables 426 | 427 | attr_reader :timeout_key # :nodoc: 428 | 429 | ## 430 | # SSL verification callback. Used when ca_file or ca_path is set. 431 | 432 | attr_reader :verify_callback 433 | 434 | ## 435 | # Sets the depth of SSL certificate verification 436 | 437 | attr_reader :verify_depth 438 | 439 | ## 440 | # HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER which verifies 441 | # the server certificate. 442 | # 443 | # If no ca_file, ca_path or cert_store is set the default system certificate 444 | # store is used. 445 | # 446 | # You can use +verify_mode+ to override any default values. 447 | 448 | attr_reader :verify_mode 449 | 450 | ## 451 | # Creates a new Net::HTTP::Persistent. 452 | # 453 | # Set a +name+ for fun. Your library name should be good enough, but this 454 | # otherwise has no purpose. 455 | # 456 | # +proxy+ may be set to a URI::HTTP or :ENV to pick up proxy options from 457 | # the environment. See proxy_from_env for details. 458 | # 459 | # In order to use a URI for the proxy you may need to do some extra work 460 | # beyond URI parsing if the proxy requires a password: 461 | # 462 | # proxy = URI 'http://proxy.example' 463 | # proxy.user = 'AzureDiamond' 464 | # proxy.password = 'hunter2' 465 | # 466 | # Set +pool_size+ to limit the maximum number of connections allowed. 467 | # Defaults to 1/4 the number of allowed file handles or 256 if your OS does 468 | # not support a limit on allowed file handles. You can have no more than 469 | # this many threads with active HTTP transactions. 470 | 471 | def initialize name: nil, proxy: nil, pool_size: DEFAULT_POOL_SIZE 472 | @name = name 473 | 474 | @debug_output = nil 475 | @proxy_uri = nil 476 | @no_proxy = [] 477 | @headers = {} 478 | @override_headers = {} 479 | @http_versions = {} 480 | @keep_alive = 30 481 | @open_timeout = nil 482 | @read_timeout = nil 483 | @write_timeout = nil 484 | @idle_timeout = 5 485 | @max_requests = nil 486 | @max_retries = 1 487 | @socket_options = [] 488 | @ssl_generation = 0 # incremented when SSL session variables change 489 | 490 | @socket_options << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if 491 | Socket.const_defined? :TCP_NODELAY 492 | 493 | @pool = Net::HTTP::Persistent::Pool.new size: pool_size do |http_args| 494 | Net::HTTP::Persistent::Connection.new Net::HTTP, http_args, @ssl_generation 495 | end 496 | 497 | @certificate = nil 498 | @ca_file = nil 499 | @ca_path = nil 500 | @ciphers = nil 501 | @private_key = nil 502 | @ssl_timeout = nil 503 | @ssl_version = nil 504 | @min_version = nil 505 | @max_version = nil 506 | @verify_callback = nil 507 | @verify_depth = nil 508 | @verify_mode = nil 509 | @cert_store = nil 510 | 511 | @generation = 0 # incremented when proxy URI changes 512 | 513 | if HAVE_OPENSSL then 514 | @verify_mode = OpenSSL::SSL::VERIFY_PEER 515 | @reuse_ssl_sessions = OpenSSL::SSL.const_defined? :Session 516 | end 517 | 518 | self.proxy = proxy if proxy 519 | end 520 | 521 | ## 522 | # Sets this client's OpenSSL::X509::Certificate 523 | 524 | def certificate= certificate 525 | @certificate = certificate 526 | 527 | reconnect_ssl 528 | end 529 | 530 | # For Net::HTTP parity 531 | alias cert= certificate= 532 | 533 | ## 534 | # Sets the SSL certificate authority file. 535 | 536 | def ca_file= file 537 | @ca_file = file 538 | 539 | reconnect_ssl 540 | end 541 | 542 | ## 543 | # Sets the SSL certificate authority path. 544 | 545 | def ca_path= path 546 | @ca_path = path 547 | 548 | reconnect_ssl 549 | end 550 | 551 | ## 552 | # Overrides the default SSL certificate store used for verifying 553 | # connections. 554 | 555 | def cert_store= store 556 | @cert_store = store 557 | 558 | reconnect_ssl 559 | end 560 | 561 | ## 562 | # The ciphers allowed for SSL connections 563 | 564 | def ciphers= ciphers 565 | @ciphers = ciphers 566 | 567 | reconnect_ssl 568 | end 569 | 570 | ## 571 | # Creates a new connection for +uri+ 572 | 573 | def connection_for uri 574 | use_ssl = uri.scheme.downcase == 'https' 575 | 576 | net_http_args = [uri.hostname, uri.port] 577 | 578 | # I'm unsure if uri.host or uri.hostname should be checked against 579 | # the proxy bypass list. 580 | if @proxy_uri and not proxy_bypass? uri.host, uri.port then 581 | net_http_args.concat @proxy_args 582 | else 583 | net_http_args.concat [nil, nil, nil, nil] 584 | end 585 | 586 | connection = @pool.checkout net_http_args 587 | 588 | http = connection.http 589 | 590 | connection.ressl @ssl_generation if 591 | connection.ssl_generation != @ssl_generation 592 | 593 | if not http.started? then 594 | ssl http if use_ssl 595 | start http 596 | elsif expired? connection then 597 | reset connection 598 | end 599 | 600 | http.keep_alive_timeout = @idle_timeout if @idle_timeout 601 | http.max_retries = @max_retries if http.respond_to?(:max_retries=) 602 | http.read_timeout = @read_timeout if @read_timeout 603 | http.write_timeout = @write_timeout if 604 | @write_timeout && http.respond_to?(:write_timeout=) 605 | 606 | return yield connection 607 | rescue Errno::ECONNREFUSED 608 | address = http.proxy_address || http.address 609 | port = http.proxy_port || http.port 610 | 611 | raise Error, "connection refused: #{address}:#{port}" 612 | rescue Errno::EHOSTDOWN 613 | address = http.proxy_address || http.address 614 | port = http.proxy_port || http.port 615 | 616 | raise Error, "host down: #{address}:#{port}" 617 | ensure 618 | @pool.checkin net_http_args 619 | end 620 | 621 | ## 622 | # CGI::escape wrapper 623 | 624 | def escape str 625 | CGI.escape str if str 626 | end 627 | 628 | ## 629 | # CGI::unescape wrapper 630 | 631 | def unescape str 632 | CGI.unescape str if str 633 | end 634 | 635 | 636 | ## 637 | # Returns true if the connection should be reset due to an idle timeout, or 638 | # maximum request count, false otherwise. 639 | 640 | def expired? connection 641 | return true if @max_requests && connection.requests >= @max_requests 642 | return false unless @idle_timeout 643 | return true if @idle_timeout.zero? 644 | 645 | Time.now - connection.last_use > @idle_timeout 646 | end 647 | 648 | ## 649 | # Starts the Net::HTTP +connection+ 650 | 651 | def start http 652 | http.set_debug_output @debug_output if @debug_output 653 | http.open_timeout = @open_timeout if @open_timeout 654 | 655 | http.start 656 | 657 | socket = http.instance_variable_get :@socket 658 | 659 | if socket then # for fakeweb 660 | @socket_options.each do |option| 661 | socket.io.setsockopt(*option) 662 | end 663 | end 664 | end 665 | 666 | ## 667 | # Finishes the Net::HTTP +connection+ 668 | 669 | def finish connection 670 | connection.finish 671 | 672 | connection.http.instance_variable_set :@last_communicated, nil 673 | connection.http.instance_variable_set :@ssl_session, nil unless 674 | @reuse_ssl_sessions 675 | end 676 | 677 | ## 678 | # Returns the HTTP protocol version for +uri+ 679 | 680 | def http_version uri 681 | @http_versions["#{uri.hostname}:#{uri.port}"] 682 | end 683 | 684 | ## 685 | # Adds "http://" to the String +uri+ if it is missing. 686 | 687 | def normalize_uri uri 688 | (uri =~ /^https?:/) ? uri : "http://#{uri}" 689 | end 690 | 691 | ## 692 | # Set the maximum number of retries for a request. 693 | # 694 | # Defaults to one retry. 695 | # 696 | # Set this to 0 to disable retries. 697 | 698 | def max_retries= retries 699 | retries = retries.to_int 700 | 701 | raise ArgumentError, "max_retries must be positive" if retries < 0 702 | 703 | @max_retries = retries 704 | 705 | reconnect 706 | end 707 | 708 | ## 709 | # Pipelines +requests+ to the HTTP server at +uri+ yielding responses if a 710 | # block is given. Returns all responses received. 711 | # 712 | # See 713 | # Net::HTTP::Pipeline[http://docs.seattlerb.org/net-http-pipeline/Net/HTTP/Pipeline.html] 714 | # for further details. 715 | # 716 | # Only if net-http-pipeline was required before 717 | # net-http-persistent #pipeline will be present. 718 | 719 | def pipeline uri, requests, &block # :yields: responses 720 | connection_for uri do |connection| 721 | connection.http.pipeline requests, &block 722 | end 723 | end 724 | 725 | ## 726 | # Sets this client's SSL private key 727 | 728 | def private_key= key 729 | @private_key = key 730 | 731 | reconnect_ssl 732 | end 733 | 734 | # For Net::HTTP parity 735 | alias key= private_key= 736 | 737 | ## 738 | # Sets the proxy server. The +proxy+ may be the URI of the proxy server, 739 | # the symbol +:ENV+ which will read the proxy from the environment or nil to 740 | # disable use of a proxy. See #proxy_from_env for details on setting the 741 | # proxy from the environment. 742 | # 743 | # If the proxy URI is set after requests have been made, the next request 744 | # will shut-down and re-open all connections. 745 | # 746 | # The +no_proxy+ query parameter can be used to specify hosts which shouldn't 747 | # be reached via proxy; if set it should be a comma separated list of 748 | # hostname suffixes, optionally with +:port+ appended, for example 749 | # example.com,some.host:8080. 750 | 751 | def proxy= proxy 752 | @proxy_uri = case proxy 753 | when :ENV then proxy_from_env 754 | when URI::HTTP then proxy 755 | when nil then # ignore 756 | else raise ArgumentError, 'proxy must be :ENV or a URI::HTTP' 757 | end 758 | 759 | @no_proxy.clear 760 | 761 | if @proxy_uri then 762 | @proxy_args = [ 763 | @proxy_uri.hostname, 764 | @proxy_uri.port, 765 | unescape(@proxy_uri.user), 766 | unescape(@proxy_uri.password), 767 | ] 768 | 769 | @proxy_connection_id = [nil, *@proxy_args].join ':' 770 | 771 | if @proxy_uri.query then 772 | @no_proxy = CGI.parse(@proxy_uri.query)['no_proxy'].join(',').downcase.split(',').map { |x| x.strip }.reject { |x| x.empty? } 773 | end 774 | end 775 | 776 | reconnect 777 | reconnect_ssl 778 | end 779 | 780 | ## 781 | # Creates a URI for an HTTP proxy server from ENV variables. 782 | # 783 | # If +HTTP_PROXY+ is set a proxy will be returned. 784 | # 785 | # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the URI is given the 786 | # indicated user and password unless HTTP_PROXY contains either of these in 787 | # the URI. 788 | # 789 | # The +NO_PROXY+ ENV variable can be used to specify hosts which shouldn't 790 | # be reached via proxy; if set it should be a comma separated list of 791 | # hostname suffixes, optionally with +:port+ appended, for example 792 | # example.com,some.host:8080. When set to * no proxy will 793 | # be returned. 794 | # 795 | # For Windows users, lowercase ENV variables are preferred over uppercase ENV 796 | # variables. 797 | 798 | def proxy_from_env 799 | env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] 800 | 801 | return nil if env_proxy.nil? or env_proxy.empty? 802 | 803 | uri = URI normalize_uri env_proxy 804 | 805 | env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY'] 806 | 807 | # '*' is special case for always bypass 808 | return nil if env_no_proxy == '*' 809 | 810 | if env_no_proxy then 811 | uri.query = "no_proxy=#{escape(env_no_proxy)}" 812 | end 813 | 814 | unless uri.user or uri.password then 815 | uri.user = escape ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER'] 816 | uri.password = escape ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS'] 817 | end 818 | 819 | uri 820 | end 821 | 822 | ## 823 | # Returns true when proxy should by bypassed for host. 824 | 825 | def proxy_bypass? host, port 826 | host = host.downcase 827 | host_port = [host, port].join ':' 828 | 829 | @no_proxy.each do |name| 830 | return true if host[-name.length, name.length] == name or 831 | host_port[-name.length, name.length] == name 832 | end 833 | 834 | false 835 | end 836 | 837 | ## 838 | # Forces reconnection of all HTTP connections, including TLS/SSL 839 | # connections. 840 | 841 | def reconnect 842 | @generation += 1 843 | end 844 | 845 | ## 846 | # Forces reconnection of only TLS/SSL connections. 847 | 848 | def reconnect_ssl 849 | @ssl_generation += 1 850 | end 851 | 852 | ## 853 | # Finishes then restarts the Net::HTTP +connection+ 854 | 855 | def reset connection 856 | http = connection.http 857 | 858 | finish connection 859 | 860 | start http 861 | rescue Errno::ECONNREFUSED 862 | e = Error.new "connection refused: #{http.address}:#{http.port}" 863 | e.set_backtrace $@ 864 | raise e 865 | rescue Errno::EHOSTDOWN 866 | e = Error.new "host down: #{http.address}:#{http.port}" 867 | e.set_backtrace $@ 868 | raise e 869 | end 870 | 871 | ## 872 | # Makes a request on +uri+. If +req+ is nil a Net::HTTP::Get is performed 873 | # against +uri+. 874 | # 875 | # If a block is passed #request behaves like Net::HTTP#request (the body of 876 | # the response will not have been read). 877 | # 878 | # +req+ must be a Net::HTTPGenericRequest subclass (see Net::HTTP for a list). 879 | 880 | def request uri, req = nil, &block 881 | uri = URI uri 882 | req = request_setup req || uri 883 | response = nil 884 | 885 | connection_for uri do |connection| 886 | http = connection.http 887 | 888 | begin 889 | connection.requests += 1 890 | 891 | response = http.request req, &block 892 | 893 | if req.connection_close? or 894 | (response.http_version <= '1.0' and 895 | not response.connection_keep_alive?) or 896 | response.connection_close? then 897 | finish connection 898 | end 899 | rescue Exception # make sure to close the connection when it was interrupted 900 | finish connection 901 | 902 | raise 903 | ensure 904 | connection.last_use = Time.now 905 | end 906 | end 907 | 908 | @http_versions["#{uri.hostname}:#{uri.port}"] ||= response.http_version 909 | 910 | response 911 | end 912 | 913 | ## 914 | # Creates a GET request if +req_or_uri+ is a URI and adds headers to the 915 | # request. 916 | # 917 | # Returns the request. 918 | 919 | def request_setup req_or_uri # :nodoc: 920 | req = if req_or_uri.respond_to? 'request_uri' then 921 | Net::HTTP::Get.new req_or_uri.request_uri 922 | else 923 | req_or_uri 924 | end 925 | 926 | @headers.each do |pair| 927 | req.add_field(*pair) 928 | end 929 | 930 | @override_headers.each do |name, value| 931 | req[name] = value 932 | end 933 | 934 | unless req['Connection'] then 935 | req.add_field 'Connection', 'keep-alive' 936 | req.add_field 'Keep-Alive', @keep_alive 937 | end 938 | 939 | req 940 | end 941 | 942 | ## 943 | # Shuts down all connections 944 | # 945 | # *NOTE*: Calling shutdown for can be dangerous! 946 | # 947 | # If any thread is still using a connection it may cause an error! Call 948 | # #shutdown when you are completely done making requests! 949 | 950 | def shutdown 951 | @pool.shutdown { |http| http.finish } 952 | end 953 | 954 | ## 955 | # Enables SSL on +connection+ 956 | 957 | def ssl connection 958 | connection.use_ssl = true 959 | 960 | connection.ciphers = @ciphers if @ciphers 961 | connection.ssl_timeout = @ssl_timeout if @ssl_timeout 962 | connection.ssl_version = @ssl_version if @ssl_version 963 | connection.min_version = @min_version if @min_version 964 | connection.max_version = @max_version if @max_version 965 | 966 | connection.verify_depth = @verify_depth 967 | connection.verify_mode = @verify_mode 968 | 969 | if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and 970 | not Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then 971 | warn <<-WARNING 972 | !!!SECURITY WARNING!!! 973 | 974 | The SSL HTTP connection to: 975 | 976 | #{connection.address}:#{connection.port} 977 | 978 | !!!MAY NOT BE VERIFIED!!! 979 | 980 | On your platform your OpenSSL implementation is broken. 981 | 982 | There is no difference between the values of VERIFY_NONE and VERIFY_PEER. 983 | 984 | This means that attempting to verify the security of SSL connections may not 985 | work. This exposes you to man-in-the-middle exploits, snooping on the 986 | contents of your connection and other dangers to the security of your data. 987 | 988 | To disable this warning define the following constant at top-level in your 989 | application: 990 | 991 | I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG = nil 992 | 993 | WARNING 994 | end 995 | 996 | connection.ca_file = @ca_file if @ca_file 997 | connection.ca_path = @ca_path if @ca_path 998 | 999 | if @ca_file or @ca_path then 1000 | connection.verify_mode = OpenSSL::SSL::VERIFY_PEER 1001 | connection.verify_callback = @verify_callback if @verify_callback 1002 | end 1003 | 1004 | if @certificate and @private_key then 1005 | connection.cert = @certificate 1006 | connection.key = @private_key 1007 | end 1008 | 1009 | connection.cert_store = if @cert_store then 1010 | @cert_store 1011 | else 1012 | store = OpenSSL::X509::Store.new 1013 | store.set_default_paths 1014 | store 1015 | end 1016 | end 1017 | 1018 | ## 1019 | # SSL session lifetime 1020 | 1021 | def ssl_timeout= ssl_timeout 1022 | @ssl_timeout = ssl_timeout 1023 | 1024 | reconnect_ssl 1025 | end 1026 | 1027 | ## 1028 | # SSL version to use 1029 | 1030 | def ssl_version= ssl_version 1031 | @ssl_version = ssl_version 1032 | 1033 | reconnect_ssl 1034 | end 1035 | 1036 | ## 1037 | # Minimum SSL version to use 1038 | 1039 | def min_version= min_version 1040 | @min_version = min_version 1041 | 1042 | reconnect_ssl 1043 | end 1044 | 1045 | ## 1046 | # maximum SSL version to use 1047 | 1048 | def max_version= max_version 1049 | @max_version = max_version 1050 | 1051 | reconnect_ssl 1052 | end 1053 | 1054 | ## 1055 | # Sets the depth of SSL certificate verification 1056 | 1057 | def verify_depth= verify_depth 1058 | @verify_depth = verify_depth 1059 | 1060 | reconnect_ssl 1061 | end 1062 | 1063 | ## 1064 | # Sets the HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER. 1065 | # 1066 | # Setting this to VERIFY_NONE is a VERY BAD IDEA and should NEVER be used. 1067 | # Securely transfer the correct certificate and update the default 1068 | # certificate store or set the ca file instead. 1069 | 1070 | def verify_mode= verify_mode 1071 | @verify_mode = verify_mode 1072 | 1073 | reconnect_ssl 1074 | end 1075 | 1076 | ## 1077 | # SSL verification callback. 1078 | 1079 | def verify_callback= callback 1080 | @verify_callback = callback 1081 | 1082 | reconnect_ssl 1083 | end 1084 | end 1085 | 1086 | require_relative 'persistent/connection' 1087 | require_relative 'persistent/pool' 1088 | 1089 | -------------------------------------------------------------------------------- /test/test_net_http_persistent.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'minitest/autorun' 3 | require 'net/http/persistent' 4 | require 'stringio' 5 | 6 | HAVE_OPENSSL = defined?(OpenSSL::SSL) 7 | 8 | module Net::HTTP::Persistent::TestConnect 9 | def self.included mod 10 | mod.send :alias_method, :orig_connect, :connect 11 | 12 | def mod.use_connect which 13 | self.send :remove_method, :connect 14 | self.send :alias_method, :connect, which 15 | end 16 | end 17 | 18 | def host_down_connect 19 | raise Errno::EHOSTDOWN 20 | end 21 | 22 | def test_connect 23 | unless use_ssl? then 24 | io = Object.new 25 | def io.setsockopt(*a) @setsockopts ||= []; @setsockopts << a end 26 | 27 | @socket = Net::BufferedIO.new io 28 | 29 | return 30 | end 31 | 32 | io = open '/dev/null' 33 | def io.setsockopt(*a) @setsockopts ||= []; @setsockopts << a end 34 | 35 | @ssl_context ||= OpenSSL::SSL::SSLContext.new 36 | 37 | @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless 38 | @ssl_context.verify_mode 39 | 40 | s = OpenSSL::SSL::SSLSocket.new io, @ssl_context 41 | 42 | @socket = Net::BufferedIO.new s 43 | end 44 | 45 | def refused_connect 46 | raise Errno::ECONNREFUSED 47 | end 48 | end 49 | 50 | class Net::HTTP 51 | include Net::HTTP::Persistent::TestConnect 52 | end 53 | 54 | class TestNetHttpPersistent < Minitest::Test 55 | 56 | def setup 57 | @http = Net::HTTP::Persistent.new 58 | 59 | @uri = URI 'http://example.com/path' 60 | @uri_v6 = URI 'http://[2001:db8::1]/path' 61 | 62 | ENV.delete 'http_proxy' 63 | ENV.delete 'HTTP_PROXY' 64 | ENV.delete 'http_proxy_user' 65 | ENV.delete 'HTTP_PROXY_USER' 66 | ENV.delete 'http_proxy_pass' 67 | ENV.delete 'HTTP_PROXY_PASS' 68 | ENV.delete 'no_proxy' 69 | ENV.delete 'NO_PROXY' 70 | 71 | Net::HTTP.use_connect :test_connect 72 | end 73 | 74 | def teardown 75 | Net::HTTP.use_connect :orig_connect 76 | end 77 | 78 | class BasicConnection 79 | attr_accessor :started, :finished, :address, :port, :use_ssl, 80 | :read_timeout, :open_timeout, :keep_alive_timeout 81 | attr_accessor :ciphers, :ssl_timeout, :ssl_version, :min_version, 82 | :max_version, :verify_depth, :verify_mode, :cert_store, 83 | :ca_file, :ca_path, :cert, :key 84 | attr_reader :req, :debug_output 85 | def initialize 86 | @started, @finished = 0, 0 87 | @address, @port = 'example.com', 80 88 | @use_ssl = false 89 | end 90 | def finish 91 | @finished += 1 92 | @socket = nil 93 | end 94 | def finished? 95 | @finished >= 1 96 | end 97 | def pipeline requests, &block 98 | requests.map { |r| r.path } 99 | end 100 | def reset? 101 | @started == @finished + 1 102 | end 103 | def set_debug_output io 104 | @debug_output = io 105 | end 106 | def start 107 | @started += 1 108 | io = Object.new 109 | def io.setsockopt(*a) @setsockopts ||= []; @setsockopts << a end 110 | @socket = Net::BufferedIO.new io 111 | end 112 | def started? 113 | @started >= 1 114 | end 115 | def proxy_address 116 | end 117 | def proxy_port 118 | end 119 | end 120 | 121 | def basic_connection 122 | raise "#{@uri} is not HTTP" unless @uri.scheme.downcase == 'http' 123 | 124 | net_http_args = [@uri.hostname, @uri.port, nil, nil, nil, nil] 125 | 126 | connection = Net::HTTP::Persistent::Connection.allocate 127 | connection.ssl_generation = @http.ssl_generation 128 | connection.http = BasicConnection.new 129 | connection.reset 130 | 131 | @http.pool.available.push connection, connection_args: net_http_args 132 | 133 | connection 134 | end 135 | 136 | def connection uri = @uri 137 | @uri = uri 138 | 139 | connection = basic_connection 140 | connection.last_use = Time.now 141 | 142 | def (connection.http).request(req) 143 | @req = req 144 | r = Net::HTTPResponse.allocate 145 | r.instance_variable_set :@header, {} 146 | def r.http_version() '1.1' end 147 | def r.read_body() :read_body end 148 | yield r if block_given? 149 | r 150 | end 151 | 152 | connection 153 | end 154 | 155 | def ssl_connection 156 | raise "#{@uri} is not HTTPS" unless @uri.scheme.downcase == 'https' 157 | 158 | net_http_args = [@uri.hostname, @uri.port, nil, nil, nil, nil] 159 | 160 | connection = Net::HTTP::Persistent::Connection.allocate 161 | connection.ssl_generation = @http.ssl_generation 162 | connection.http = BasicConnection.new 163 | connection.reset 164 | 165 | @http.pool.available.push connection, connection_args: net_http_args 166 | 167 | connection 168 | end 169 | 170 | def test_initialize 171 | assert_nil @http.proxy_uri 172 | 173 | assert_empty @http.no_proxy 174 | 175 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 176 | 177 | ssl_session_exists = OpenSSL::SSL.const_defined? :Session 178 | 179 | assert_equal ssl_session_exists, @http.reuse_ssl_sessions 180 | end 181 | 182 | def test_initialize_name 183 | http = Net::HTTP::Persistent.new name: 'name' 184 | assert_equal 'name', http.name 185 | end 186 | 187 | def test_initialize_no_ssl_session 188 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 189 | 190 | skip "OpenSSL::SSL::Session does not exist on #{RUBY_PLATFORM}" unless 191 | OpenSSL::SSL.const_defined? :Session 192 | 193 | ssl_session = OpenSSL::SSL::Session 194 | 195 | OpenSSL::SSL.send :remove_const, :Session 196 | 197 | http = Net::HTTP::Persistent.new 198 | 199 | refute http.reuse_ssl_sessions 200 | ensure 201 | OpenSSL::SSL.const_set :Session, ssl_session if ssl_session 202 | end 203 | 204 | def test_initialize_proxy 205 | proxy_uri = URI.parse 'http://proxy.example' 206 | 207 | http = Net::HTTP::Persistent.new proxy: proxy_uri 208 | 209 | assert_equal proxy_uri, http.proxy_uri 210 | end 211 | 212 | def test_ca_file_equals 213 | @http.ca_file = :ca_file 214 | 215 | assert_equal :ca_file, @http.ca_file 216 | assert_equal 1, @http.ssl_generation 217 | end 218 | 219 | def test_ca_path_equals 220 | @http.ca_path = :ca_path 221 | 222 | assert_equal :ca_path, @http.ca_path 223 | assert_equal 1, @http.ssl_generation 224 | end 225 | 226 | def test_cert_store_equals 227 | @http.cert_store = :cert_store 228 | 229 | assert_equal :cert_store, @http.cert_store 230 | assert_equal 1, @http.ssl_generation 231 | end 232 | 233 | def test_certificate_equals 234 | @http.certificate = :cert 235 | 236 | assert_equal :cert, @http.certificate 237 | assert_equal 1, @http.ssl_generation 238 | end 239 | 240 | def test_ciphers_equals 241 | @http.ciphers = :ciphers 242 | 243 | assert_equal :ciphers, @http.ciphers 244 | assert_equal 1, @http.ssl_generation 245 | end 246 | 247 | def test_connection_for 248 | @http.open_timeout = 123 249 | @http.read_timeout = 321 250 | @http.idle_timeout = 42 251 | @http.max_retries = 5 252 | 253 | used = @http.connection_for @uri do |c| 254 | assert_kind_of Net::HTTP, c.http 255 | 256 | assert c.http.started? 257 | refute c.http.proxy? 258 | 259 | assert_equal 123, c.http.open_timeout 260 | assert_equal 321, c.http.read_timeout 261 | assert_equal 42, c.http.keep_alive_timeout 262 | assert_equal 5, c.http.max_retries if c.http.respond_to?(:max_retries) 263 | 264 | c 265 | end 266 | 267 | stored = @http.pool.checkout ['example.com', 80, nil, nil, nil, nil] 268 | 269 | assert_same used, stored 270 | end 271 | 272 | def test_connection_for_cached 273 | cached = basic_connection 274 | cached.http.start 275 | 276 | @http.read_timeout = 5 277 | 278 | @http.connection_for @uri do |c| 279 | assert c.http.started? 280 | 281 | assert_equal 5, c.http.read_timeout 282 | 283 | assert_same cached, c 284 | end 285 | end 286 | 287 | def test_connection_for_closed 288 | cached = basic_connection 289 | cached.http.start 290 | if Socket.const_defined? :TCP_NODELAY then 291 | io = Object.new 292 | def io.setsockopt(*a) raise IOError, 'closed stream' end 293 | cached.instance_variable_set :@socket, Net::BufferedIO.new(io) 294 | end 295 | 296 | @http.connection_for @uri do |c| 297 | assert c.http.started? 298 | 299 | socket = c.http.instance_variable_get :@socket 300 | 301 | refute_includes socket.io.instance_variables, :@setsockopt 302 | refute_includes socket.io.instance_variables, '@setsockopt' 303 | end 304 | end 305 | 306 | def test_connection_for_debug_output 307 | io = StringIO.new 308 | @http.debug_output = io 309 | 310 | @http.connection_for @uri do |c| 311 | assert c.http.started? 312 | assert_equal io, c.http.instance_variable_get(:@debug_output) 313 | end 314 | end 315 | 316 | def test_connection_for_cached_expire_always 317 | cached = basic_connection 318 | cached.http.start 319 | cached.requests = 10 320 | cached.last_use = Time.now # last used right now 321 | 322 | @http.idle_timeout = 0 323 | 324 | @http.connection_for @uri do |c| 325 | assert c.http.started? 326 | 327 | assert_same cached, c 328 | 329 | assert_equal 0, c.requests, 'connection reset due to timeout' 330 | end 331 | end 332 | 333 | def test_connection_for_cached_expire_never 334 | cached = basic_connection 335 | cached.http.start 336 | cached.requests = 10 337 | cached.last_use = Time.now # last used right now 338 | 339 | @http.idle_timeout = nil 340 | 341 | @http.connection_for @uri do |c| 342 | assert c.http.started? 343 | 344 | assert_same cached, c 345 | 346 | assert_equal 10, c.requests, 'connection reset despite no timeout' 347 | end 348 | end 349 | 350 | def test_connection_for_cached_expired 351 | cached = basic_connection 352 | cached.http.start 353 | cached.requests = 10 354 | cached.last_use = Time.now - 3600 355 | 356 | @http.connection_for @uri do |c| 357 | assert c.http.started? 358 | 359 | assert_same cached, c 360 | assert_equal 0, cached.requests, 'connection not reset due to timeout' 361 | end 362 | end 363 | 364 | def test_connection_for_finished_ssl 365 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 366 | 367 | uri = URI.parse 'https://example.com/path' 368 | 369 | @http.connection_for uri do |c| 370 | assert c.http.started? 371 | assert c.http.use_ssl? 372 | 373 | @http.finish c 374 | 375 | refute c.http.started? 376 | end 377 | 378 | @http.connection_for uri do |c2| 379 | assert c2.http.started? 380 | end 381 | end 382 | 383 | def test_connection_for_ipv6 384 | @http.connection_for @uri_v6 do |c| 385 | assert_equal '2001:db8::1', c.http.address 386 | end 387 | end 388 | 389 | def test_connection_for_host_down 390 | c = basic_connection 391 | def (c.http).start; raise Errno::EHOSTDOWN end 392 | def (c.http).started?; false end 393 | 394 | e = assert_raises Net::HTTP::Persistent::Error do 395 | @http.connection_for @uri do end 396 | end 397 | 398 | assert_equal 'host down: example.com:80', e.message 399 | end 400 | 401 | def test_connection_for_http_class_with_fakeweb 402 | Object.send :const_set, :FakeWeb, nil 403 | 404 | @http.connection_for @uri do |c| 405 | assert_instance_of Net::HTTP, c.http 406 | end 407 | ensure 408 | if Object.const_defined?(:FakeWeb) then 409 | Object.send :remove_const, :FakeWeb 410 | end 411 | end 412 | 413 | def test_connection_for_http_class_with_webmock 414 | Object.send :const_set, :WebMock, nil 415 | @http.connection_for @uri do |c| 416 | assert_instance_of Net::HTTP, c.http 417 | end 418 | ensure 419 | if Object.const_defined?(:WebMock) then 420 | Object.send :remove_const, :WebMock 421 | end 422 | end 423 | 424 | def test_connection_for_http_class_with_artifice 425 | Object.send :const_set, :Artifice, nil 426 | @http.connection_for @uri do |c| 427 | assert_instance_of Net::HTTP, c.http 428 | end 429 | ensure 430 | if Object.const_defined?(:Artifice) then 431 | Object.send :remove_const, :Artifice 432 | end 433 | end 434 | 435 | def test_connection_for_name 436 | http = Net::HTTP::Persistent.new name: 'name' 437 | uri = URI.parse 'http://example/' 438 | 439 | http.connection_for uri do |c| 440 | assert c.http.started? 441 | end 442 | end 443 | 444 | def test_connection_for_proxy 445 | uri = URI.parse 'http://proxy.example' 446 | uri.user = 'johndoe' 447 | uri.password = 'muffins' 448 | 449 | http = Net::HTTP::Persistent.new proxy: uri 450 | 451 | used = http.connection_for @uri do |c| 452 | assert c.http.started? 453 | assert c.http.proxy? 454 | 455 | c 456 | end 457 | 458 | stored = http.pool.checkout ['example.com', 80, 459 | 'proxy.example', 80, 460 | 'johndoe', 'muffins'] 461 | 462 | assert_same used, stored 463 | end 464 | 465 | def test_connection_for_proxy_unescaped 466 | uri = URI.parse 'http://proxy.example' 467 | uri.user = 'john%40doe' 468 | uri.password = 'muf%3Afins' 469 | uri.freeze 470 | 471 | http = Net::HTTP::Persistent.new proxy: uri 472 | 473 | http.connection_for @uri do end 474 | 475 | stored = http.pool.checkout ['example.com', 80, 476 | 'proxy.example', 80, 477 | 'john@doe', 'muf:fins'] 478 | 479 | assert stored 480 | end 481 | 482 | def test_connection_for_proxy_host_down 483 | Net::HTTP.use_connect :host_down_connect 484 | 485 | uri = URI.parse 'http://proxy.example' 486 | uri.user = 'johndoe' 487 | uri.password = 'muffins' 488 | 489 | http = Net::HTTP::Persistent.new proxy: uri 490 | 491 | e = assert_raises Net::HTTP::Persistent::Error do 492 | http.connection_for @uri do end 493 | end 494 | 495 | assert_equal 'host down: proxy.example:80', e.message 496 | end 497 | 498 | def test_connection_for_proxy_refused 499 | Net::HTTP.use_connect :refused_connect 500 | 501 | uri = URI.parse 'http://proxy.example' 502 | uri.user = 'johndoe' 503 | uri.password = 'muffins' 504 | 505 | http = Net::HTTP::Persistent.new proxy: uri 506 | 507 | e = assert_raises Net::HTTP::Persistent::Error do 508 | http.connection_for @uri do end 509 | end 510 | 511 | assert_equal 'connection refused: proxy.example:80', e.message 512 | end 513 | 514 | def test_connection_for_no_proxy 515 | uri = URI.parse 'http://proxy.example' 516 | uri.user = 'johndoe' 517 | uri.password = 'muffins' 518 | uri.query = 'no_proxy=example.com' 519 | 520 | http = Net::HTTP::Persistent.new proxy: uri 521 | 522 | http.connection_for @uri do |c| 523 | assert c.http.started? 524 | refute c.http.proxy? 525 | end 526 | 527 | stored = http.pool.checkout ['example.com', 80] 528 | 529 | assert stored 530 | end 531 | 532 | def test_connection_for_no_proxy_from_env 533 | ENV['http_proxy'] = 'proxy.example' 534 | ENV['no_proxy'] = 'localhost, example.com,' 535 | ENV['proxy_user'] = 'johndoe' 536 | ENV['proxy_password'] = 'muffins' 537 | 538 | http = Net::HTTP::Persistent.new proxy: :ENV 539 | 540 | http.connection_for @uri do |c| 541 | assert c.http.started? 542 | refute c.http.proxy? 543 | refute c.http.proxy_from_env? 544 | end 545 | end 546 | 547 | def test_connection_for_refused 548 | Net::HTTP.use_connect :refused_connect 549 | 550 | e = assert_raises Net::HTTP::Persistent::Error do 551 | @http.connection_for @uri do end 552 | end 553 | 554 | assert_equal 'connection refused: example.com:80', e.message 555 | end 556 | 557 | def test_connection_for_ssl 558 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 559 | 560 | uri = URI.parse 'https://example.com/path' 561 | 562 | @http.connection_for uri do |c| 563 | assert c.http.started? 564 | assert c.http.use_ssl? 565 | end 566 | end 567 | 568 | def test_connection_for_ssl_cached 569 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 570 | 571 | @uri = URI.parse 'https://example.com/path' 572 | 573 | cached = ssl_connection 574 | 575 | @http.connection_for @uri do |c| 576 | assert_same cached, c 577 | end 578 | end 579 | 580 | def test_connection_for_ssl_cached_reconnect 581 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 582 | 583 | @uri = URI.parse 'https://example.com/path' 584 | 585 | cached = ssl_connection 586 | 587 | ssl_generation = @http.ssl_generation 588 | 589 | @http.reconnect_ssl 590 | 591 | @http.connection_for @uri do |c| 592 | assert_same cached, c 593 | refute_equal ssl_generation, c.ssl_generation 594 | end 595 | end 596 | 597 | def test_connection_for_ssl_case 598 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 599 | 600 | uri = URI.parse 'HTTPS://example.com/path' 601 | @http.connection_for uri do |c| 602 | assert c.http.started? 603 | assert c.http.use_ssl? 604 | end 605 | end 606 | 607 | def test_connection_for_timeout 608 | cached = basic_connection 609 | cached.http.start 610 | cached.requests = 10 611 | cached.last_use = Time.now - 6 612 | 613 | @http.connection_for @uri do |c| 614 | assert c.http.started? 615 | assert_equal 0, c.requests 616 | 617 | assert_same cached, c 618 | end 619 | end 620 | 621 | def test_escape 622 | assert_nil @http.escape nil 623 | 624 | assert_equal '+%3F', @http.escape(' ?') 625 | end 626 | 627 | def test_unescape 628 | assert_nil @http.unescape nil 629 | 630 | assert_equal ' ?', @http.unescape('+%3F') 631 | end 632 | 633 | def test_expired_eh 634 | c = basic_connection 635 | c.requests = 0 636 | c.last_use = Time.now - 11 637 | 638 | @http.idle_timeout = 0 639 | assert @http.expired? c 640 | 641 | @http.idle_timeout = 10 642 | assert @http.expired? c 643 | 644 | @http.idle_timeout = 11 645 | assert @http.expired? c 646 | 647 | @http.idle_timeout = 12 648 | refute @http.expired? c 649 | 650 | @http.idle_timeout = nil 651 | refute @http.expired? c 652 | end 653 | 654 | def test_expired_due_to_max_requests 655 | c = basic_connection 656 | c.requests = 0 657 | c.last_use = Time.now 658 | 659 | refute @http.expired? c 660 | 661 | c.requests = 10 662 | refute @http.expired? c 663 | 664 | @http.max_requests = 10 665 | assert @http.expired? c 666 | 667 | c.requests = 9 668 | refute @http.expired? c 669 | end 670 | 671 | def test_finish 672 | c = basic_connection 673 | c.requests = 5 674 | c.http.instance_variable_set(:@last_communicated, Process.clock_gettime(Process::CLOCK_MONOTONIC)) 675 | 676 | @http.finish c 677 | 678 | refute c.http.started? 679 | assert c.http.finished? 680 | 681 | assert_equal 0, c.requests 682 | assert_equal Net::HTTP::Persistent::EPOCH, c.last_use 683 | assert_nil c.http.instance_variable_get(:@last_communicated) 684 | end 685 | 686 | def test_finish_io_error 687 | c = basic_connection 688 | def (c.http).finish; @finished += 1; raise IOError end 689 | c.requests = 5 690 | 691 | @http.finish c 692 | 693 | refute c.http.started? 694 | assert c.http.finished? 695 | end 696 | 697 | def test_finish_ssl_no_session_reuse 698 | http = Net::HTTP.new 'localhost', 443, ssl: true 699 | http.instance_variable_set :@ssl_session, :something 700 | 701 | c = Net::HTTP::Persistent::Connection.allocate 702 | c.instance_variable_set :@http, http 703 | 704 | @http.reuse_ssl_sessions = false 705 | 706 | @http.finish c 707 | 708 | assert_nil c.http.instance_variable_get :@ssl_session 709 | end 710 | 711 | def test_http_version 712 | assert_nil @http.http_version @uri 713 | 714 | connection 715 | 716 | @http.request @uri 717 | 718 | assert_equal '1.1', @http.http_version(@uri) 719 | end 720 | 721 | def test_http_version_IPv6 722 | assert_nil @http.http_version @uri_v6 723 | 724 | connection @uri_v6 725 | 726 | @http.request @uri_v6 727 | 728 | assert_equal '1.1', @http.http_version(@uri_v6) 729 | end 730 | 731 | def test_max_retries_equals 732 | @http.max_retries = 5 733 | 734 | assert_equal 5, @http.max_retries 735 | assert_equal 1, @http.generation 736 | end 737 | 738 | def test_normalize_uri 739 | assert_equal 'http://example', @http.normalize_uri('example') 740 | assert_equal 'http://example', @http.normalize_uri('http://example') 741 | assert_equal 'https://example', @http.normalize_uri('https://example') 742 | end 743 | 744 | def test_override_haeders 745 | assert_empty @http.override_headers 746 | 747 | @http.override_headers['User-Agent'] = 'MyCustomAgent' 748 | 749 | expected = { 'User-Agent' => 'MyCustomAgent' } 750 | 751 | assert_equal expected, @http.override_headers 752 | end 753 | 754 | def test_pipeline 755 | skip 'net-http-pipeline not installed' unless defined?(Net::HTTP::Pipeline) 756 | 757 | cached = basic_connection 758 | cached.http.start 759 | 760 | requests = [ 761 | Net::HTTP::Get.new((@uri + '1').request_uri), 762 | Net::HTTP::Get.new((@uri + '2').request_uri), 763 | ] 764 | 765 | responses = @http.pipeline @uri, requests 766 | 767 | assert_equal 2, responses.length 768 | assert_equal '/1', responses.first 769 | assert_equal '/2', responses.last 770 | end 771 | 772 | def test_private_key_equals 773 | @http.private_key = :private_key 774 | 775 | assert_equal :private_key, @http.private_key 776 | assert_equal 1, @http.ssl_generation 777 | end 778 | 779 | def test_proxy_equals_env 780 | ENV['http_proxy'] = 'proxy.example' 781 | 782 | @http.proxy = :ENV 783 | 784 | assert_equal URI.parse('http://proxy.example'), @http.proxy_uri 785 | 786 | assert_equal 1, @http.generation, 'generation' 787 | assert_equal 1, @http.ssl_generation, 'ssl_generation' 788 | end 789 | 790 | def test_proxy_equals_nil 791 | @http.proxy = nil 792 | 793 | assert_nil @http.proxy_uri 794 | 795 | assert_equal 1, @http.generation, 'generation' 796 | assert_equal 1, @http.ssl_generation, 'ssl_generation' 797 | end 798 | 799 | def test_proxy_equals_uri 800 | proxy_uri = URI.parse 'http://proxy.example' 801 | 802 | @http.proxy = proxy_uri 803 | 804 | assert_equal proxy_uri, @http.proxy_uri 805 | end 806 | 807 | def test_proxy_equals_uri_IPv6 808 | proxy_uri = @uri_v6 809 | 810 | @http.proxy = proxy_uri 811 | 812 | assert_equal proxy_uri, @http.proxy_uri 813 | end 814 | 815 | def test_proxy_from_env 816 | ENV['http_proxy'] = 'proxy.example' 817 | ENV['http_proxy_user'] = 'johndoe' 818 | ENV['http_proxy_pass'] = 'muffins' 819 | ENV['NO_PROXY'] = 'localhost,example.com' 820 | 821 | uri = @http.proxy_from_env 822 | 823 | expected = URI.parse 'http://proxy.example' 824 | expected.user = 'johndoe' 825 | expected.password = 'muffins' 826 | expected.query = 'no_proxy=localhost%2Cexample.com' 827 | 828 | assert_equal expected, uri 829 | end 830 | 831 | def test_proxy_from_env_lower 832 | ENV['http_proxy'] = 'proxy.example' 833 | ENV['http_proxy_user'] = 'johndoe' 834 | ENV['http_proxy_pass'] = 'muffins' 835 | ENV['no_proxy'] = 'localhost,example.com' 836 | 837 | uri = @http.proxy_from_env 838 | 839 | expected = URI.parse 'http://proxy.example' 840 | expected.user = 'johndoe' 841 | expected.password = 'muffins' 842 | expected.query = 'no_proxy=localhost%2Cexample.com' 843 | 844 | assert_equal expected, uri 845 | end 846 | 847 | def test_proxy_from_env_nil 848 | uri = @http.proxy_from_env 849 | 850 | assert_nil uri 851 | 852 | ENV['http_proxy'] = '' 853 | 854 | uri = @http.proxy_from_env 855 | 856 | assert_nil uri 857 | end 858 | 859 | def test_proxy_from_env_no_proxy_star 860 | uri = @http.proxy_from_env 861 | 862 | assert_nil uri 863 | 864 | ENV['http_proxy'] = 'proxy.example' 865 | ENV['no_proxy'] = '*' 866 | 867 | uri = @http.proxy_from_env 868 | 869 | assert_nil uri 870 | end 871 | 872 | def test_proxy_bypass 873 | ENV['http_proxy'] = 'proxy.example' 874 | ENV['no_proxy'] = 'localhost,example.com:80' 875 | 876 | @http.proxy = :ENV 877 | 878 | assert @http.proxy_bypass? 'localhost', 80 879 | assert @http.proxy_bypass? 'localhost', 443 880 | assert @http.proxy_bypass? 'LOCALHOST', 80 881 | assert @http.proxy_bypass? 'example.com', 80 882 | refute @http.proxy_bypass? 'example.com', 443 883 | assert @http.proxy_bypass? 'www.example.com', 80 884 | refute @http.proxy_bypass? 'www.example.com', 443 885 | assert @http.proxy_bypass? 'endingexample.com', 80 886 | refute @http.proxy_bypass? 'example.org', 80 887 | end 888 | 889 | def test_proxy_bypass_space 890 | ENV['http_proxy'] = 'proxy.example' 891 | ENV['no_proxy'] = 'localhost, example.com' 892 | 893 | @http.proxy = :ENV 894 | 895 | assert @http.proxy_bypass? 'example.com', 80 896 | refute @http.proxy_bypass? 'example.org', 80 897 | end 898 | 899 | def test_proxy_bypass_trailing 900 | ENV['http_proxy'] = 'proxy.example' 901 | ENV['no_proxy'] = 'localhost,example.com,' 902 | 903 | @http.proxy = :ENV 904 | 905 | assert @http.proxy_bypass? 'example.com', 80 906 | refute @http.proxy_bypass? 'example.org', 80 907 | end 908 | 909 | def test_proxy_bypass_double_comma 910 | ENV['http_proxy'] = 'proxy.example' 911 | ENV['no_proxy'] = 'localhost,,example.com' 912 | 913 | @http.proxy = :ENV 914 | 915 | assert @http.proxy_bypass? 'example.com', 80 916 | refute @http.proxy_bypass? 'example.org', 80 917 | end 918 | 919 | def test_reconnect 920 | result = @http.reconnect 921 | 922 | assert_equal 1, result 923 | end 924 | 925 | def test_reconnect_ssl 926 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 927 | 928 | @uri = URI 'https://example.com' 929 | now = Time.now 930 | 931 | ssl_http = ssl_connection 932 | 933 | def (ssl_http.http).finish 934 | @started = 0 935 | end 936 | 937 | used1 = @http.connection_for @uri do |c| 938 | c.requests = 1 939 | c.last_use = now 940 | c 941 | end 942 | 943 | assert_equal OpenSSL::SSL::VERIFY_PEER, used1.http.verify_mode 944 | 945 | @http.verify_mode = OpenSSL::SSL::VERIFY_NONE 946 | @http.reconnect_ssl 947 | 948 | used2 = @http.connection_for @uri do |c| 949 | c 950 | end 951 | 952 | assert_same used1, used2 953 | 954 | assert_equal OpenSSL::SSL::VERIFY_NONE, used2.http.verify_mode, 955 | 'verify mode must change' 956 | assert_equal 0, used2.requests 957 | assert_equal Net::HTTP::Persistent::EPOCH, used2.last_use 958 | end 959 | 960 | def test_requestx 961 | @http.override_headers['user-agent'] = 'test ua' 962 | @http.headers['accept'] = 'text/*' 963 | c = connection 964 | 965 | res = @http.request @uri 966 | req = c.http.req 967 | 968 | assert_kind_of Net::HTTPResponse, res 969 | 970 | assert_kind_of Net::HTTP::Get, req 971 | assert_equal '/path', req.path 972 | 973 | assert_equal 'test ua', req['user-agent'] 974 | assert_match %r%text/\*%, req['accept'] 975 | 976 | assert_equal 'keep-alive', req['connection'] 977 | assert_equal '30', req['keep-alive'] 978 | 979 | assert_in_delta Time.now, c.last_use 980 | 981 | assert_equal 1, c.requests 982 | end 983 | 984 | def test_request_block 985 | @http.headers['user-agent'] = 'test ua' 986 | c = connection 987 | body = nil 988 | 989 | res = @http.request @uri do |r| 990 | body = r.read_body 991 | end 992 | 993 | req = c.http.req 994 | 995 | assert_kind_of Net::HTTPResponse, res 996 | refute_nil body 997 | 998 | assert_kind_of Net::HTTP::Get, req 999 | assert_equal '/path', req.path 1000 | assert_equal 'keep-alive', req['connection'] 1001 | assert_equal '30', req['keep-alive'] 1002 | assert_match %r%test ua%, req['user-agent'] 1003 | 1004 | assert_equal 1, c.requests 1005 | end 1006 | 1007 | def test_request_close_1_0 1008 | c = connection 1009 | 1010 | class << c.http 1011 | remove_method :request 1012 | end 1013 | 1014 | def (c.http).request req 1015 | @req = req 1016 | r = Net::HTTPResponse.allocate 1017 | r.instance_variable_set :@header, {} 1018 | def r.http_version() '1.0' end 1019 | def r.read_body() :read_body end 1020 | yield r if block_given? 1021 | r 1022 | end 1023 | 1024 | request = Net::HTTP::Get.new @uri.request_uri 1025 | 1026 | res = @http.request @uri, request 1027 | req = c.http.req 1028 | 1029 | assert_kind_of Net::HTTPResponse, res 1030 | 1031 | assert_kind_of Net::HTTP::Get, req 1032 | assert_equal '/path', req.path 1033 | assert_equal 'keep-alive', req['connection'] 1034 | assert_equal '30', req['keep-alive'] 1035 | 1036 | assert c.http.finished? 1037 | end 1038 | 1039 | def test_request_connection_close_request 1040 | c = connection 1041 | 1042 | request = Net::HTTP::Get.new @uri.request_uri 1043 | request['connection'] = 'close' 1044 | 1045 | res = @http.request @uri, request 1046 | req = c.http.req 1047 | 1048 | assert_kind_of Net::HTTPResponse, res 1049 | 1050 | assert_kind_of Net::HTTP::Get, req 1051 | assert_equal '/path', req.path 1052 | assert_equal 'close', req['connection'] 1053 | assert_nil req['keep-alive'] 1054 | 1055 | assert c.http.finished? 1056 | end 1057 | 1058 | def test_request_connection_close_response 1059 | c = connection 1060 | 1061 | class << c.http 1062 | remove_method :request 1063 | end 1064 | 1065 | def (c.http).request req 1066 | @req = req 1067 | r = Net::HTTPResponse.allocate 1068 | r.instance_variable_set :@header, {} 1069 | r['connection'] = 'close' 1070 | def r.http_version() '1.1' end 1071 | def r.read_body() :read_body end 1072 | yield r if block_given? 1073 | r 1074 | end 1075 | 1076 | request = Net::HTTP::Get.new @uri.request_uri 1077 | 1078 | res = @http.request @uri, request 1079 | req = c.http.req 1080 | 1081 | assert_kind_of Net::HTTPResponse, res 1082 | 1083 | assert_kind_of Net::HTTP::Get, req 1084 | assert_equal '/path', req.path 1085 | assert_equal 'keep-alive', req['connection'] 1086 | assert_equal '30', req['keep-alive'] 1087 | 1088 | assert c.http.finished? 1089 | end 1090 | 1091 | def test_request_exception 1092 | c = basic_connection 1093 | def (c.http).request(*a) 1094 | raise Exception, "very bad things happened" 1095 | end 1096 | 1097 | assert_raises Exception do 1098 | @http.request @uri 1099 | end 1100 | 1101 | assert_equal 0, c.requests 1102 | assert c.http.finished? 1103 | end 1104 | 1105 | def test_request_invalid 1106 | c = basic_connection 1107 | def (c.http).request(*a) raise Errno::EINVAL, "write" end 1108 | 1109 | e = assert_raises Errno::EINVAL do 1110 | @http.request @uri 1111 | end 1112 | 1113 | assert_equal 0, c.requests 1114 | assert_match %r%Invalid argument - write%, e.message 1115 | end 1116 | 1117 | def test_request_post 1118 | c = connection 1119 | 1120 | post = Net::HTTP::Post.new @uri.path 1121 | 1122 | @http.request @uri, post 1123 | req = c.http.req 1124 | 1125 | assert_same post, req 1126 | end 1127 | 1128 | def test_request_setup 1129 | @http.override_headers['user-agent'] = 'test ua' 1130 | @http.headers['accept'] = 'text/*' 1131 | 1132 | input = Net::HTTP::Post.new '/path' 1133 | 1134 | req = @http.request_setup input 1135 | 1136 | assert_same input, req 1137 | assert_equal '/path', req.path 1138 | 1139 | assert_equal 'test ua', req['user-agent'] 1140 | assert_match %r%text/\*%, req['accept'] 1141 | 1142 | assert_equal 'keep-alive', req['connection'] 1143 | assert_equal '30', req['keep-alive'] 1144 | end 1145 | 1146 | def test_request_string 1147 | @http.override_headers['user-agent'] = 'test ua' 1148 | @http.headers['accept'] = 'text/*' 1149 | c = connection 1150 | 1151 | res = @http.request @uri.to_s 1152 | req = c.http.req 1153 | 1154 | assert_kind_of Net::HTTPResponse, res 1155 | 1156 | assert_kind_of Net::HTTP::Get, req 1157 | assert_equal '/path', req.path 1158 | 1159 | assert_equal 1, c.requests 1160 | end 1161 | 1162 | def test_request_setup_uri 1163 | uri = @uri + '?a=b' 1164 | 1165 | req = @http.request_setup uri 1166 | 1167 | assert_kind_of Net::HTTP::Get, req 1168 | assert_equal '/path?a=b', req.path 1169 | end 1170 | 1171 | def test_reset 1172 | c = basic_connection 1173 | c.http.start 1174 | c.last_use = Time.now 1175 | c.requests = 5 1176 | 1177 | @http.reset c 1178 | 1179 | assert c.http.started? 1180 | assert c.http.finished? 1181 | assert c.http.reset? 1182 | assert_equal 0, c.requests 1183 | assert_equal Net::HTTP::Persistent::EPOCH, c.last_use 1184 | end 1185 | 1186 | def test_reset_host_down 1187 | c = basic_connection 1188 | c.last_use = Time.now 1189 | def (c.http).start; raise Errno::EHOSTDOWN end 1190 | c.requests = 5 1191 | 1192 | e = assert_raises Net::HTTP::Persistent::Error do 1193 | @http.reset c 1194 | end 1195 | 1196 | assert_match %r%host down%, e.message 1197 | assert_match __FILE__, e.backtrace.first 1198 | end 1199 | 1200 | def test_reset_io_error 1201 | c = basic_connection 1202 | c.last_use = Time.now 1203 | c.requests = 5 1204 | 1205 | @http.reset c 1206 | 1207 | assert c.http.started? 1208 | assert c.http.finished? 1209 | end 1210 | 1211 | def test_reset_refused 1212 | c = basic_connection 1213 | c.last_use = Time.now 1214 | def (c.http).start; raise Errno::ECONNREFUSED end 1215 | c.requests = 5 1216 | 1217 | e = assert_raises Net::HTTP::Persistent::Error do 1218 | @http.reset c 1219 | end 1220 | 1221 | assert_match %r%connection refused%, e.message 1222 | assert_match __FILE__, e.backtrace.first 1223 | end 1224 | 1225 | def test_shutdown 1226 | c = connection 1227 | 1228 | orig = @http 1229 | @http = Net::HTTP::Persistent.new name: 'name' 1230 | c2 = connection 1231 | 1232 | orig.shutdown 1233 | 1234 | @http = orig 1235 | 1236 | assert c.http.finished?, 'last-generation connection must be finished' 1237 | refute c2.http.finished?, 'present generation connection must not be finished' 1238 | end 1239 | 1240 | def test_ssl 1241 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1242 | 1243 | @http.verify_callback = :callback 1244 | c = Net::HTTP.new 'localhost', 80 1245 | 1246 | @http.ssl c 1247 | 1248 | assert c.use_ssl? 1249 | assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode 1250 | assert_kind_of OpenSSL::X509::Store, c.cert_store 1251 | assert_nil c.verify_callback 1252 | end 1253 | 1254 | def test_ssl_ca_file 1255 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1256 | 1257 | @http.ca_file = 'ca_file' 1258 | @http.verify_callback = :callback 1259 | c = Net::HTTP.new 'localhost', 80 1260 | 1261 | @http.ssl c 1262 | 1263 | assert c.use_ssl? 1264 | assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode 1265 | assert_equal :callback, c.verify_callback 1266 | end 1267 | 1268 | def test_ssl_ca_path 1269 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1270 | 1271 | @http.ca_path = 'ca_path' 1272 | @http.verify_callback = :callback 1273 | c = Net::HTTP.new 'localhost', 80 1274 | 1275 | @http.ssl c 1276 | 1277 | assert c.use_ssl? 1278 | assert_equal OpenSSL::SSL::VERIFY_PEER, c.verify_mode 1279 | assert_equal :callback, c.verify_callback 1280 | end 1281 | 1282 | def test_ssl_cert_store 1283 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1284 | 1285 | store = OpenSSL::X509::Store.new 1286 | @http.cert_store = store 1287 | 1288 | c = Net::HTTP.new 'localhost', 80 1289 | 1290 | @http.ssl c 1291 | 1292 | assert c.use_ssl? 1293 | assert_equal store, c.cert_store 1294 | end 1295 | 1296 | def test_ssl_cert_store_default 1297 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1298 | 1299 | @http.verify_mode = OpenSSL::SSL::VERIFY_PEER 1300 | 1301 | c = Net::HTTP.new 'localhost', 80 1302 | 1303 | @http.ssl c 1304 | 1305 | assert c.use_ssl? 1306 | assert c.cert_store 1307 | end 1308 | 1309 | def test_ssl_certificate 1310 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1311 | 1312 | @http.certificate = :cert 1313 | @http.private_key = :key 1314 | c = Net::HTTP.new 'localhost', 80 1315 | 1316 | @http.ssl c 1317 | 1318 | assert c.use_ssl? 1319 | assert_equal :cert, c.cert 1320 | assert_equal :key, c.key 1321 | end 1322 | 1323 | def test_ssl_verify_mode 1324 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1325 | 1326 | @http.verify_mode = OpenSSL::SSL::VERIFY_NONE 1327 | c = Net::HTTP.new 'localhost', 80 1328 | 1329 | @http.ssl c 1330 | 1331 | assert c.use_ssl? 1332 | assert_equal OpenSSL::SSL::VERIFY_NONE, c.verify_mode 1333 | end 1334 | 1335 | def test_ssl_warning 1336 | skip 'OpenSSL is missing' unless HAVE_OPENSSL 1337 | 1338 | begin 1339 | orig_verify_peer = OpenSSL::SSL::VERIFY_PEER 1340 | OpenSSL::SSL.send :remove_const, :VERIFY_PEER 1341 | OpenSSL::SSL.send :const_set, :VERIFY_PEER, OpenSSL::SSL::VERIFY_NONE 1342 | 1343 | c = Net::HTTP.new 'localhost', 80 1344 | 1345 | out, err = capture_io do 1346 | @http.ssl c 1347 | end 1348 | 1349 | assert_empty out 1350 | 1351 | assert_match %r%localhost:80%, err 1352 | assert_match %r%I_KNOW_THAT_OPENSSL%, err 1353 | 1354 | Object.send :const_set, :I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG, nil 1355 | 1356 | assert_silent do 1357 | @http.ssl c 1358 | end 1359 | ensure 1360 | OpenSSL::SSL.send :remove_const, :VERIFY_PEER 1361 | OpenSSL::SSL.send :const_set, :VERIFY_PEER, orig_verify_peer 1362 | if Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then 1363 | Object.send :remove_const, :I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG 1364 | end 1365 | end 1366 | end 1367 | 1368 | def test_ssl_timeout_equals 1369 | @http.ssl_timeout = :ssl_timeout 1370 | 1371 | assert_equal :ssl_timeout, @http.ssl_timeout 1372 | assert_equal 1, @http.ssl_generation 1373 | end 1374 | 1375 | def test_ssl_version_equals 1376 | @http.ssl_version = :ssl_version 1377 | 1378 | assert_equal :ssl_version, @http.ssl_version 1379 | assert_equal 1, @http.ssl_generation 1380 | end 1381 | 1382 | def test_min_version_equals 1383 | @http.min_version = :min_version 1384 | 1385 | assert_equal :min_version, @http.min_version 1386 | assert_equal 1, @http.ssl_generation 1387 | end 1388 | 1389 | def test_max_version_equals 1390 | @http.max_version = :max_version 1391 | 1392 | assert_equal :max_version, @http.max_version 1393 | assert_equal 1, @http.ssl_generation 1394 | end 1395 | 1396 | def test_start 1397 | c = basic_connection 1398 | c = c.http 1399 | 1400 | @http.socket_options << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1] 1401 | @http.debug_output = $stderr 1402 | @http.open_timeout = 6 1403 | 1404 | @http.start c 1405 | 1406 | assert_equal $stderr, c.debug_output 1407 | assert_equal 6, c.open_timeout 1408 | 1409 | socket = c.instance_variable_get :@socket 1410 | 1411 | expected = [] 1412 | expected << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if 1413 | Socket.const_defined? :TCP_NODELAY 1414 | expected << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1] 1415 | 1416 | assert_equal expected, socket.io.instance_variable_get(:@setsockopts) 1417 | end 1418 | 1419 | def test_verify_callback_equals 1420 | @http.verify_callback = :verify_callback 1421 | 1422 | assert_equal :verify_callback, @http.verify_callback 1423 | assert_equal 1, @http.ssl_generation 1424 | end 1425 | 1426 | def test_verify_depth_equals 1427 | @http.verify_depth = :verify_depth 1428 | 1429 | assert_equal :verify_depth, @http.verify_depth 1430 | assert_equal 1, @http.ssl_generation 1431 | end 1432 | 1433 | def test_verify_mode_equals 1434 | @http.verify_mode = :verify_mode 1435 | 1436 | assert_equal :verify_mode, @http.verify_mode 1437 | assert_equal 1, @http.ssl_generation 1438 | end 1439 | 1440 | end 1441 | 1442 | --------------------------------------------------------------------------------