excon 0.62.0 → 0.79.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.md +1 -1
  3. data/README.md +5 -4
  4. data/data/cacert.pem +769 -1496
  5. data/excon.gemspec +17 -2
  6. data/lib/excon.rb +25 -17
  7. data/lib/excon/connection.rb +193 -119
  8. data/lib/excon/constants.rb +36 -13
  9. data/lib/excon/error.rb +15 -0
  10. data/lib/excon/headers.rb +4 -3
  11. data/lib/excon/instrumentors/logging_instrumentor.rb +4 -15
  12. data/lib/excon/instrumentors/standard_instrumentor.rb +2 -9
  13. data/lib/excon/middlewares/base.rb +6 -0
  14. data/lib/excon/middlewares/capture_cookies.rb +1 -1
  15. data/lib/excon/middlewares/decompress.rb +2 -2
  16. data/lib/excon/middlewares/expects.rb +7 -1
  17. data/lib/excon/middlewares/idempotent.rb +20 -3
  18. data/lib/excon/middlewares/instrumentor.rb +8 -0
  19. data/lib/excon/middlewares/mock.rb +12 -3
  20. data/lib/excon/middlewares/redirect_follower.rb +25 -3
  21. data/lib/excon/middlewares/response_parser.rb +3 -0
  22. data/lib/excon/pretty_printer.rb +1 -8
  23. data/lib/excon/response.rb +12 -9
  24. data/lib/excon/socket.rb +38 -21
  25. data/lib/excon/ssl_socket.rb +24 -13
  26. data/lib/excon/test/plugin/server/exec.rb +2 -2
  27. data/lib/excon/test/server.rb +1 -1
  28. data/lib/excon/unix_socket.rb +1 -0
  29. data/lib/excon/utils.rb +41 -5
  30. data/lib/excon/version.rb +1 -1
  31. metadata +27 -98
  32. data/.document +0 -5
  33. data/.gitignore +0 -13
  34. data/.rspec +0 -3
  35. data/.travis.yml +0 -29
  36. data/Gemfile +0 -19
  37. data/Rakefile +0 -41
  38. data/benchmarks/class_vs_lambda.rb +0 -50
  39. data/benchmarks/concat_vs_insert.rb +0 -21
  40. data/benchmarks/concat_vs_interpolate.rb +0 -22
  41. data/benchmarks/cr_lf.rb +0 -21
  42. data/benchmarks/downcase-eq-eq_vs_casecmp.rb +0 -169
  43. data/benchmarks/excon.rb +0 -69
  44. data/benchmarks/excon_vs.rb +0 -165
  45. data/benchmarks/for_vs_array_each.rb +0 -27
  46. data/benchmarks/for_vs_hash_each.rb +0 -27
  47. data/benchmarks/has_key-vs-lookup.rb +0 -177
  48. data/benchmarks/headers_case_sensitivity.rb +0 -83
  49. data/benchmarks/headers_split_vs_match.rb +0 -34
  50. data/benchmarks/implicit_block-vs-explicit_block.rb +0 -98
  51. data/benchmarks/merging.rb +0 -21
  52. data/benchmarks/single_vs_double_quotes.rb +0 -21
  53. data/benchmarks/string_ranged_index.rb +0 -87
  54. data/benchmarks/strip_newline.rb +0 -115
  55. data/benchmarks/vs_stdlib.rb +0 -82
  56. data/changelog.txt +0 -1083
  57. data/spec/excon/error_spec.rb +0 -139
  58. data/spec/excon/test/server_spec.rb +0 -28
  59. data/spec/excon_spec.rb +0 -7
  60. data/spec/helpers/file_path_helpers.rb +0 -22
  61. data/spec/requests/basic_spec.rb +0 -40
  62. data/spec/requests/eof_requests_spec.rb +0 -36
  63. data/spec/requests/unix_socket_spec.rb +0 -46
  64. data/spec/spec_helper.rb +0 -24
  65. data/spec/support/shared_contexts/test_server_context.rb +0 -83
  66. data/spec/support/shared_examples/shared_example_for_clients.rb +0 -218
  67. data/spec/support/shared_examples/shared_example_for_streaming_clients.rb +0 -20
  68. data/spec/support/shared_examples/shared_example_for_test_servers.rb +0 -16
  69. data/tests/authorization_header_tests.rb +0 -29
  70. data/tests/bad_tests.rb +0 -47
  71. data/tests/basic_tests.rb +0 -351
  72. data/tests/batch_requests.rb +0 -133
  73. data/tests/complete_responses.rb +0 -31
  74. data/tests/data/127.0.0.1.cert.crt +0 -20
  75. data/tests/data/127.0.0.1.cert.key +0 -27
  76. data/tests/data/excon.cert.crt +0 -20
  77. data/tests/data/excon.cert.key +0 -27
  78. data/tests/data/xs +0 -1
  79. data/tests/error_tests.rb +0 -145
  80. data/tests/header_tests.rb +0 -119
  81. data/tests/middlewares/canned_response_tests.rb +0 -34
  82. data/tests/middlewares/capture_cookies_tests.rb +0 -34
  83. data/tests/middlewares/decompress_tests.rb +0 -157
  84. data/tests/middlewares/escape_path_tests.rb +0 -36
  85. data/tests/middlewares/idempotent_tests.rb +0 -206
  86. data/tests/middlewares/instrumentation_tests.rb +0 -315
  87. data/tests/middlewares/mock_tests.rb +0 -304
  88. data/tests/middlewares/redirect_follower_tests.rb +0 -112
  89. data/tests/pipeline_tests.rb +0 -40
  90. data/tests/proxy_tests.rb +0 -306
  91. data/tests/query_string_tests.rb +0 -87
  92. data/tests/rackups/basic.rb +0 -41
  93. data/tests/rackups/basic.ru +0 -3
  94. data/tests/rackups/basic_auth.ru +0 -14
  95. data/tests/rackups/deflater.ru +0 -4
  96. data/tests/rackups/proxy.ru +0 -18
  97. data/tests/rackups/query_string.ru +0 -13
  98. data/tests/rackups/redirecting.ru +0 -23
  99. data/tests/rackups/redirecting_with_cookie.ru +0 -40
  100. data/tests/rackups/request_headers.ru +0 -15
  101. data/tests/rackups/request_methods.ru +0 -21
  102. data/tests/rackups/response_header.ru +0 -18
  103. data/tests/rackups/ssl.ru +0 -16
  104. data/tests/rackups/ssl_mismatched_cn.ru +0 -15
  105. data/tests/rackups/ssl_verify_peer.ru +0 -16
  106. data/tests/rackups/streaming.ru +0 -30
  107. data/tests/rackups/thread_safety.ru +0 -17
  108. data/tests/rackups/timeout.ru +0 -14
  109. data/tests/rackups/webrick_patch.rb +0 -34
  110. data/tests/request_headers_tests.rb +0 -21
  111. data/tests/request_method_tests.rb +0 -47
  112. data/tests/request_tests.rb +0 -59
  113. data/tests/response_tests.rb +0 -197
  114. data/tests/servers/bad.rb +0 -20
  115. data/tests/servers/eof.rb +0 -17
  116. data/tests/servers/error.rb +0 -20
  117. data/tests/servers/good.rb +0 -350
  118. data/tests/test_helper.rb +0 -306
  119. data/tests/thread_safety_tests.rb +0 -39
  120. data/tests/timeout_tests.rb +0 -12
  121. data/tests/utils_tests.rb +0 -81
data/excon.gemspec CHANGED
@@ -11,8 +11,13 @@ Gem::Specification.new do |s|
11
11
  s.license = 'MIT'
12
12
  s.rdoc_options = ["--charset=UTF-8"]
13
13
  s.extra_rdoc_files = %w[README.md CONTRIBUTORS.md CONTRIBUTING.md]
14
- s.files = `git ls-files -z`.split("\x0")
15
- s.test_files = s.files.select { |path| path =~ /^[spec|tests]\/.*_[spec|tests]\.rb/ }
14
+ s.files = `git ls-files -- data/* lib/*`.split("\n") + [
15
+ "CONTRIBUTING.md",
16
+ "CONTRIBUTORS.md",
17
+ "LICENSE.md",
18
+ "README.md",
19
+ "excon.gemspec"
20
+ ]
16
21
 
17
22
  s.add_development_dependency('rspec', '>= 3.5.0')
18
23
  s.add_development_dependency('activesupport')
@@ -26,4 +31,14 @@ Gem::Specification.new do |s|
26
31
  s.add_development_dependency('sinatra-contrib')
27
32
  s.add_development_dependency('json', '>= 1.8.5')
28
33
  s.add_development_dependency('puma')
34
+ s.add_development_dependency('webrick')
35
+
36
+ s.metadata = {
37
+ 'homepage_uri' => 'https://github.com/excon/excon',
38
+ 'bug_tracker_uri' => 'https://github.com/excon/excon/issues',
39
+ 'changelog_uri' => 'https://github.com/excon/excon/blob/master/changelog.txt',
40
+ 'documentation_uri' => 'https://github.com/excon/excon/blob/master/README.md',
41
+ 'source_code_uri' => 'https://github.com/excon/excon',
42
+ 'wiki_uri' => 'https://github.com/excon/excon/wiki'
43
+ }
29
44
  end
data/lib/excon.rb CHANGED
@@ -23,11 +23,11 @@ require 'excon/middlewares/instrumentor'
23
23
  require 'excon/middlewares/mock'
24
24
  require 'excon/middlewares/response_parser'
25
25
 
26
+ require 'excon/error'
26
27
  require 'excon/constants'
27
28
  require 'excon/utils'
28
29
 
29
30
  require 'excon/connection'
30
- require 'excon/error'
31
31
  require 'excon/headers'
32
32
  require 'excon/response'
33
33
  require 'excon/middlewares/decompress'
@@ -61,6 +61,14 @@ module Excon
61
61
  if $VERBOSE || ENV['EXCON_DEBUG']
62
62
  $stderr.puts "[excon][WARNING] #{warning}\n#{ caller.join("\n") }"
63
63
  end
64
+
65
+ if @raise_on_warnings
66
+ raise Error::Warning.new(warning)
67
+ end
68
+ end
69
+
70
+ def set_raise_on_warnings!(should_raise)
71
+ @raise_on_warnings = should_raise
64
72
  end
65
73
 
66
74
  # Status of mocking
@@ -105,9 +113,9 @@ module Excon
105
113
 
106
114
  # @see Connection#initialize
107
115
  # Initializes a new keep-alive session for a given remote host
108
- # @param [String] url The destination URL
109
- # @param [Hash<Symbol, >] params One or more option params to set on the Connection instance
110
- # @return [Connection] A new Excon::Connection instance
116
+ # @param [String] url The destination URL
117
+ # @param [Hash<Symbol, >] params One or more option params to set on the Connection instance
118
+ # @return [Connection] A new Excon::Connection instance
111
119
  def new(url, params = {})
112
120
  uri_parser = params[:uri_parser] || defaults[:uri_parser]
113
121
  uri = uri_parser.parse(url)
@@ -135,13 +143,13 @@ module Excon
135
143
  end
136
144
 
137
145
  # push an additional stub onto the list to check for mock requests
138
- # @param [Hash<Symbol, >] request params to match against, omitted params match all
139
- # @param [Hash<Symbol, >] response params to return from matched request or block to call with params
140
- def stub(request_params = {}, response_params = nil)
141
- if method = request_params.delete(:method)
146
+ # @param request_params [Hash<Symbol, >] request params to match against, omitted params match all
147
+ # @param response_params [Hash<Symbol, >] response params to return from matched request or block to call with params
148
+ def stub(request_params = {}, response_params = nil, &block)
149
+ if (method = request_params.delete(:method))
142
150
  request_params[:method] = method.to_s.downcase.to_sym
143
151
  end
144
- if url = request_params.delete(:url)
152
+ if (url = request_params.delete(:url))
145
153
  uri = URI.parse(url)
146
154
  request_params = {
147
155
  :host => uri.host,
@@ -167,7 +175,7 @@ module Excon
167
175
  if response_params
168
176
  raise(ArgumentError.new("stub requires either response_params OR a block"))
169
177
  else
170
- stub = [request_params, Proc.new]
178
+ stub = [request_params, block]
171
179
  end
172
180
  elsif response_params
173
181
  stub = [request_params, response_params]
@@ -179,10 +187,10 @@ module Excon
179
187
  end
180
188
 
181
189
  # get a stub matching params or nil
182
- # @param [Hash<Symbol, >] request params to match against, omitted params match all
183
- # @return [Hash<Symbol, >] response params to return from matched request or block to call with params
190
+ # @param request_params [Hash<Symbol, >] request params to match against, omitted params match all
191
+ # @return [Hash<Symbol, >] response params to return from matched request or block to call with params
184
192
  def stub_for(request_params={})
185
- if method = request_params.delete(:method)
193
+ if (method = request_params.delete(:method))
186
194
  request_params[:method] = method.to_s.downcase.to_sym
187
195
  end
188
196
  Excon.stubs.each do |stub, response_params|
@@ -190,7 +198,7 @@ module Excon
190
198
  headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
191
199
  case value = stub[:headers][key]
192
200
  when Regexp
193
- if match = value.match(request_params[:headers][key])
201
+ if (match = value.match(request_params[:headers][key]))
194
202
  captures[:headers][key] = match.captures
195
203
  end
196
204
  match
@@ -201,7 +209,7 @@ module Excon
201
209
  non_headers_match = (stub.keys - [:headers]).all? do |key|
202
210
  case value = stub[key]
203
211
  when Regexp
204
- if match = value.match(request_params[key])
212
+ if (match = value.match(request_params[key]))
205
213
  captures[key] = match.captures
206
214
  end
207
215
  match
@@ -228,8 +236,8 @@ module Excon
228
236
  end
229
237
 
230
238
  # remove first/oldest stub matching request_params
231
- # @param [Hash<Symbol, >] request params to match against, omitted params match all
232
- # @return [Hash<Symbol, >] response params from deleted stub
239
+ # @param request_params [Hash<Symbol, >] request params to match against, omitted params match all
240
+ # @return [Hash<Symbol, >] response params from deleted stub
233
241
  def unstub(request_params = {})
234
242
  stub = stub_for(request_params)
235
243
  Excon.stubs.delete_at(Excon.stubs.index(stub))
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'ipaddr'
3
+
2
4
  module Excon
3
5
  class Connection
4
6
  include Utils
@@ -38,27 +40,27 @@ module Excon
38
40
  end
39
41
  end
40
42
  def logger=(logger)
41
- Excon::LoggingInstrumentor.logger = logger
42
43
  @data[:instrumentor] = Excon::LoggingInstrumentor
44
+ @data[:logger] = logger
43
45
  end
44
46
 
45
47
  # Initializes a new Connection instance
46
- # @param [Hash<Symbol, >] params One or more optional params
47
- # @option params [String] :body Default text to be sent over a socket. Only used if :body absent in Connection#request params
48
- # @option params [Hash<Symbol, String>] :headers The default headers to supply in a request. Only used if params[:headers] is not supplied to Connection#request
49
- # @option params [String] :host The destination host's reachable DNS name or IP, in the form of a String. IPv6 addresses must be wrapped (e.g. [::1]). See URI#host.
50
- # @option params [String] :hostname Same as host, but usable for socket connections. IPv6 addresses must not be wrapped (e.g. ::1). See URI#hostname.
51
- # @option params [String] :path Default path; appears after 'scheme://host:port/'. Only used if params[:path] is not supplied to Connection#request
52
- # @option params [Fixnum] :port The port on which to connect, to the destination host
53
- # @option params [Hash] :query Default query; appended to the 'scheme://host:port/path/' in the form of '?key=value'. Will only be used if params[:query] is not supplied to Connection#request
54
- # @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
55
- # @option params [String] :socket The path to the unix socket (required for 'unix://' connections)
56
- # @option params [String] :ciphers Only use the specified SSL/TLS cipher suites; use OpenSSL cipher spec format e.g. 'HIGH:!aNULL:!3DES' or 'AES256-SHA:DES-CBC3-SHA'
57
- # @option params [String] :proxy Proxy server; e.g. 'http://myproxy.com:8888'
58
- # @option params [Fixnum] :retry_limit Set how many times we'll retry a failed request. (Default 4)
59
- # @option params [Fixnum] :retry_interval Set how long to wait between retries. (Default 0)
60
- # @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
61
- # @option params [String] :instrumentor_name Name prefix for #instrument events. Defaults to 'excon'
48
+ # @param [Hash<Symbol, >] params One or more optional params
49
+ # @option params [String] :body Default text to be sent over a socket. Only used if :body absent in Connection#request params
50
+ # @option params [Hash<Symbol, String>] :headers The default headers to supply in a request. Only used if params[:headers] is not supplied to Connection#request
51
+ # @option params [String] :host The destination host's reachable DNS name or IP, in the form of a String. IPv6 addresses must be wrapped (e.g. [::1]). See URI#host.
52
+ # @option params [String] :hostname Same as host, but usable for socket connections. IPv6 addresses must not be wrapped (e.g. ::1). See URI#hostname.
53
+ # @option params [String] :path Default path; appears after 'scheme://host:port/'. Only used if params[:path] is not supplied to Connection#request
54
+ # @option params [Fixnum] :port The port on which to connect, to the destination host
55
+ # @option params [Hash] :query Default query; appended to the 'scheme://host:port/path/' in the form of '?key=value'. Will only be used if params[:query] is not supplied to Connection#request
56
+ # @option params [String] :scheme The protocol; 'https' causes OpenSSL to be used
57
+ # @option params [String] :socket The path to the unix socket (required for 'unix://' connections)
58
+ # @option params [String] :ciphers Only use the specified SSL/TLS cipher suites; use OpenSSL cipher spec format e.g. 'HIGH:!aNULL:!3DES' or 'AES256-SHA:DES-CBC3-SHA'
59
+ # @option params [String] :proxy Proxy server; e.g. 'http://myproxy.com:8888'
60
+ # @option params [Fixnum] :retry_limit Set how many times we'll retry a failed request. (Default 4)
61
+ # @option params [Fixnum] :retry_interval Set how long to wait between retries. (Default 0)
62
+ # @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
63
+ # @option params [String] :instrumentor_name Name prefix for #instrument events. Defaults to 'excon'
62
64
  def initialize(params = {})
63
65
  @data = Excon.defaults.dup
64
66
  # merge does not deep-dup, so make sure headers is not the original
@@ -67,12 +69,17 @@ module Excon
67
69
  # the same goes for :middlewares
68
70
  @data[:middlewares] = @data[:middlewares].dup
69
71
 
70
- params = validate_params(:connection, params)
71
72
  @data.merge!(params)
73
+ validate_params(:connection, @data, @data[:middlewares])
74
+
75
+ if @data.key?(:host) && !@data.key?(:hostname)
76
+ Excon.display_warning('hostname is missing! For IPv6 support, provide both host and hostname: Excon::Connection#new(:host => uri.host, :hostname => uri.hostname, ...).')
77
+ @data[:hostname] = @data[:host]
78
+ end
72
79
 
73
80
  setup_proxy
74
81
 
75
- if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
82
+ if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
76
83
  @data[:instrumentor] = Excon::StandardInstrumentor
77
84
  end
78
85
 
@@ -138,7 +145,13 @@ module Excon
138
145
 
139
146
  # add headers to request
140
147
  datum[:headers].each do |key, values|
148
+ if key.to_s.match(/[\r\n]/)
149
+ raise Excon::Errors::InvalidHeaderKey.new(key.to_s.inspect + ' contains forbidden "\r" or "\n"')
150
+ end
141
151
  [values].flatten.each do |value|
152
+ if value.to_s.match(/[\r\n]/)
153
+ raise Excon::Errors::InvalidHeaderValue.new(value.to_s.inspect + ' contains forbidden "\r" or "\n"')
154
+ end
142
155
  request << key.to_s << ': ' << value.to_s << CR_NL
143
156
  end
144
157
  end
@@ -150,9 +163,7 @@ module Excon
150
163
  socket.write(request) # write out request + headers
151
164
  while true # write out body with chunked encoding
152
165
  chunk = datum[:request_block].call
153
- if FORCE_ENC
154
- chunk.force_encoding('BINARY')
155
- end
166
+ chunk = binary_encode(chunk)
156
167
  if chunk.length > 0
157
168
  socket.write(chunk.length.to_s(16) << CR_NL << chunk << CR_NL)
158
169
  else
@@ -171,27 +182,23 @@ module Excon
171
182
  end
172
183
 
173
184
  # if request + headers is less than chunk size, fill with body
174
- if FORCE_ENC
175
- request.force_encoding('BINARY')
176
- end
185
+ request = binary_encode(request)
177
186
  chunk = body.read([datum[:chunk_size] - request.length, 0].max)
178
187
  if chunk
179
- if FORCE_ENC
180
- chunk.force_encoding('BINARY')
181
- end
188
+ chunk = binary_encode(chunk)
182
189
  socket.write(request << chunk)
183
190
  else
184
191
  socket.write(request) # write out request + headers
185
192
  end
186
193
 
187
- while chunk = body.read(datum[:chunk_size])
194
+ while (chunk = body.read(datum[:chunk_size]))
188
195
  socket.write(chunk)
189
196
  end
190
197
  end
191
198
  end
192
199
  rescue => error
193
200
  case error
194
- when Excon::Errors::StubNotFound, Excon::Errors::Timeout
201
+ when Excon::Errors::InvalidHeaderKey, Excon::Errors::InvalidHeaderValue, Excon::Errors::StubNotFound, Excon::Errors::Timeout
195
202
  raise(error)
196
203
  else
197
204
  raise_socket_error(error)
@@ -216,18 +223,24 @@ module Excon
216
223
  end
217
224
 
218
225
  # Sends the supplied request to the destination host.
219
- # @yield [chunk] @see Response#self.parse
220
- # @param [Hash<Symbol, >] params One or more optional params, override defaults set in Connection.new
221
- # @option params [String] :body text to be sent over a socket
222
- # @option params [Hash<Symbol, String>] :headers The default headers to supply in a request
223
- # @option params [String] :path appears after 'scheme://host:port/'
224
- # @option params [Hash] :query appended to the 'scheme://host:port/path/' in the form of '?key=value'
226
+ # @yield [chunk] @see Response#self.parse
227
+ # @param [Hash<Symbol, >] params One or more optional params, override defaults set in Connection.new
228
+ # @option params [String] :body text to be sent over a socket
229
+ # @option params [Hash<Symbol, String>] :headers The default headers to supply in a request
230
+ # @option params [String] :path appears after 'scheme://host:port/'
231
+ # @option params [Hash] :query appended to the 'scheme://host:port/path/' in the form of '?key=value'
225
232
  def request(params={}, &block)
226
- params = validate_params(:request, params)
227
233
  # @data has defaults, merge in new params to override
228
234
  datum = @data.merge(params)
229
235
  datum[:headers] = @data[:headers].merge(datum[:headers] || {})
230
236
 
237
+ validate_params(:request, params, datum[:middlewares])
238
+ # If the user passed in new middleware, we want to validate that the original connection parameters
239
+ # are still valid with the provided middleware.
240
+ if params[:middlewares]
241
+ validate_params(:connection, @data, datum[:middlewares])
242
+ end
243
+
231
244
  if datum[:user] || datum[:password]
232
245
  user, pass = Utils.unescape_uri(datum[:user].to_s), Utils.unescape_uri(datum[:password].to_s)
233
246
  datum[:headers]['Authorization'] ||= 'Basic ' + ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
@@ -238,7 +251,12 @@ module Excon
238
251
  else
239
252
  datum[:headers]['Host'] ||= datum[:host] + port_string(datum)
240
253
  end
241
- datum[:retries_remaining] ||= datum[:retry_limit]
254
+
255
+ # RFC 7230, section 5.4, states that the Host header SHOULD be the first one # to be present.
256
+ # Some web servers will reject the request if it comes too late, so let's hoist it to the top.
257
+ if (host = datum[:headers].delete('Host'))
258
+ datum[:headers] = { 'Host' => host }.merge(datum[:headers])
259
+ end
242
260
 
243
261
  # if path is empty or doesn't start with '/', insert one
244
262
  unless datum[:path][0, 1] == '/'
@@ -247,11 +265,16 @@ module Excon
247
265
 
248
266
  if block_given?
249
267
  Excon.display_warning('Excon requests with a block are deprecated, pass :response_block instead.')
250
- datum[:response_block] = Proc.new
268
+ datum[:response_block] = block
251
269
  end
252
270
 
253
271
  datum[:connection] = self
254
272
 
273
+ # cleanup data left behind on persistent connection after interrupt
274
+ if datum[:persistent] && !@persistent_socket_reusable
275
+ reset
276
+ end
277
+
255
278
  datum[:stack] = datum[:middlewares].map do |middleware|
256
279
  lambda {|stack| middleware.new(stack)}
257
280
  end.reverse.inject(self) do |middlewares, middleware|
@@ -260,10 +283,12 @@ module Excon
260
283
  datum = datum[:stack].request_call(datum)
261
284
 
262
285
  unless datum[:pipeline]
286
+ @persistent_socket_reusable = false
263
287
  datum = response(datum)
288
+ @persistent_socket_reusable = true
264
289
 
265
290
  if datum[:persistent]
266
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
291
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
267
292
  if datum[:response][:headers][key].casecmp('close') == 0
268
293
  reset
269
294
  end
@@ -278,6 +303,10 @@ module Excon
278
303
  end
279
304
  rescue => error
280
305
  reset
306
+
307
+ # If we didn't get far enough to initialize datum and the middleware stack, just raise
308
+ raise error if !datum
309
+
281
310
  datum[:error] = error
282
311
  if datum[:stack]
283
312
  datum[:stack].error_call(datum)
@@ -287,7 +316,7 @@ module Excon
287
316
  end
288
317
 
289
318
  # Sends the supplied requests to the destination host using pipelining.
290
- # @pipeline_params [Array<Hash>] pipeline_params An array of one or more optional params, override defaults set in Connection.new, see #request for details
319
+ # @param pipeline_params [Array<Hash>] An array of one or more optional params, override defaults set in Connection.new, see #request for details
291
320
  def requests(pipeline_params)
292
321
  pipeline_params.each {|params| params.merge!(:pipeline => true, :persistent => true) }
293
322
  pipeline_params.last.merge!(:persistent => @data[:persistent])
@@ -299,7 +328,7 @@ module Excon
299
328
  end
300
329
 
301
330
  if @data[:persistent]
302
- if key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
331
+ if (key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
303
332
  if responses.last[:headers][key].casecmp('close') == 0
304
333
  reset
305
334
  end
@@ -314,7 +343,7 @@ module Excon
314
343
  # Sends the supplied requests to the destination host using pipelining in
315
344
  # batches of @limit [Numeric] requests. This is your soft file descriptor
316
345
  # limit by default, typically 256.
317
- # @pipeline_params [Array<Hash>] pipeline_params An array of one or more optional params, override defaults set in Connection.new, see #request for details
346
+ # @param pipeline_params [Array<Hash>] An array of one or more optional params, override defaults set in Connection.new, see #request for details
318
347
  def batch_requests(pipeline_params, limit = nil)
319
348
  limit ||= Process.respond_to?(:getrlimit) ? Process.getrlimit(:NOFILE).first : 256
320
349
  responses = []
@@ -327,9 +356,10 @@ module Excon
327
356
  end
328
357
 
329
358
  def reset
330
- if old_socket = sockets.delete(@socket_key)
359
+ if (old_socket = sockets.delete(@socket_key))
331
360
  old_socket.close rescue nil
332
361
  end
362
+ @persistent_socket_reusable = true
333
363
  end
334
364
 
335
365
  # Generate HTTP request verb methods
@@ -355,15 +385,7 @@ module Excon
355
385
  vars = instance_variables.inject({}) do |accum, var|
356
386
  accum.merge!(var.to_sym => instance_variable_get(var))
357
387
  end
358
- if vars[:'@data'][:headers].has_key?('Authorization')
359
- vars[:'@data'] = vars[:'@data'].dup
360
- vars[:'@data'][:headers] = vars[:'@data'][:headers].dup
361
- vars[:'@data'][:headers]['Authorization'] = REDACTED
362
- end
363
- if vars[:'@data'][:password]
364
- vars[:'@data'] = vars[:'@data'].dup
365
- vars[:'@data'][:password] = REDACTED
366
- end
388
+ vars[:'@data'] = Utils.redact(vars[:'@data'])
367
389
  inspection = '#<Excon::Connection:'
368
390
  inspection += (object_id << 1).to_s(16)
369
391
  vars.each do |key, value|
@@ -373,6 +395,10 @@ module Excon
373
395
  inspection
374
396
  end
375
397
 
398
+ def valid_request_keys(middlewares)
399
+ valid_middleware_keys(middlewares) + Excon::VALID_REQUEST_KEYS
400
+ end
401
+
376
402
  private
377
403
 
378
404
  def detect_content_length(body)
@@ -387,34 +413,50 @@ module Excon
387
413
  end
388
414
  end
389
415
 
390
- def validate_params(validation, params)
416
+ def valid_middleware_keys(middlewares)
417
+ middlewares.flat_map do |middleware|
418
+ if middleware.respond_to?(:valid_parameter_keys)
419
+ middleware.valid_parameter_keys
420
+ else
421
+ Excon.display_warning(
422
+ "Excon middleware #{middleware} does not define #valid_parameter_keys"
423
+ )
424
+ []
425
+ end
426
+ end
427
+ end
428
+
429
+ def validate_params(validation, params, middlewares)
391
430
  valid_keys = case validation
392
431
  when :connection
393
- Excon::VALID_CONNECTION_KEYS
432
+ valid_middleware_keys(middlewares) + Excon::VALID_CONNECTION_KEYS
394
433
  when :request
395
- Excon::VALID_REQUEST_KEYS
434
+ valid_request_keys(middlewares)
435
+ else
436
+ raise ArgumentError.new("Invalid validation type '#{validation}'")
396
437
  end
438
+
397
439
  invalid_keys = params.keys - valid_keys
398
440
  unless invalid_keys.empty?
399
441
  Excon.display_warning("Invalid Excon #{validation} keys: #{invalid_keys.map(&:inspect).join(', ')}")
400
- # FIXME: for now, just warn, don't mutate, give things (ie fog) a chance to catch up
401
- #params = params.dup
402
- #invalid_keys.each {|key| params.delete(key) }
403
- end
404
442
 
405
- if validation == :connection && params.key?(:host) && !params.key?(:hostname)
406
- Excon.display_warning('hostname is missing! For IPv6 support, provide both host and hostname: Excon::Connection#new(:host => uri.host, :hostname => uri.hostname, ...).')
407
- params[:hostname] = params[:host]
443
+ if validation == :request
444
+ deprecated_keys = invalid_keys & Excon::DEPRECATED_VALID_REQUEST_KEYS.keys
445
+ mw_msg = deprecated_keys.map do |k|
446
+ "#{k}: #{Excon::DEPRECATED_VALID_REQUEST_KEYS[k]}"
447
+ end.join(', ')
448
+ Excon.display_warning(
449
+ "The following request keys are only valid with the associated middleware: #{mw_msg}"
450
+ )
451
+ end
408
452
  end
409
-
410
- params
411
453
  end
412
454
 
413
455
  def response(datum={})
414
456
  datum[:stack].response_call(datum)
415
457
  rescue => error
416
458
  case error
417
- when Excon::Errors::HTTPStatusError, Excon::Errors::Timeout
459
+ when Excon::Errors::HTTPStatusError, Excon::Errors::Timeout, Excon::Errors::TooManyRedirects
418
460
  raise(error)
419
461
  else
420
462
  raise_socket_error(error)
@@ -434,13 +476,14 @@ module Excon
434
476
 
435
477
  def sockets
436
478
  @_excon_sockets ||= {}
479
+ @_excon_sockets.compare_by_identity
437
480
 
438
481
  if @data[:thread_safe_sockets]
439
482
  # In a multi-threaded world, if the same connection is used by multiple
440
483
  # threads at the same time to connect to the same destination, they may
441
484
  # stomp on each other's sockets. This ensures every thread gets their
442
485
  # own socket cache, within the context of a single connection.
443
- @_excon_sockets[Thread.current.object_id] ||= {}
486
+ @_excon_sockets[Thread.current] ||= {}
444
487
  else
445
488
  @_excon_sockets
446
489
  end
@@ -454,6 +497,47 @@ module Excon
454
497
  end
455
498
  end
456
499
 
500
+ def proxy_match_host_port(host, port)
501
+ host_match = if host.is_a? IPAddr
502
+ begin
503
+ host.include? @data[:host]
504
+ rescue IPAddr::Error
505
+ false
506
+ end
507
+ else
508
+ /(^|\.)#{host}$/.match(@data[:host])
509
+ end
510
+ host_match && (port.nil? || port.to_i == @data[:port])
511
+ end
512
+
513
+ def proxy_from_env
514
+ if (no_proxy_env = ENV['no_proxy'] || ENV['NO_PROXY'])
515
+ no_proxy_list = no_proxy_env.scan(/\s*(?:\[([\dA-Fa-f:\/]+)\]|\*?\.?([^\s,:]+))(?::(\d+))?\s*/i).map { |e|
516
+ if e[0]
517
+ begin
518
+ [IPAddr.new(e[0]), e[2]]
519
+ rescue IPAddr::Error
520
+ nil
521
+ end
522
+ else
523
+ begin
524
+ [IPAddr.new(e[1]), e[2]]
525
+ rescue IPAddr::Error
526
+ [e[1], e[2]]
527
+ end
528
+ end
529
+ }.reject { |e| e.nil? || e[0].nil? }
530
+ end
531
+
532
+ unless no_proxy_env && no_proxy_list.index { |h| proxy_match_host_port(h[0], h[1]) }
533
+ if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY'))
534
+ @data[:proxy] = ENV['https_proxy'] || ENV['HTTPS_PROXY']
535
+ elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY'))
536
+ @data[:proxy] = ENV['http_proxy'] || ENV['HTTP_PROXY']
537
+ end
538
+ end
539
+ end
540
+
457
541
  def setup_proxy
458
542
  if @data[:disable_proxy]
459
543
  if @data[:proxy]
@@ -462,64 +546,54 @@ module Excon
462
546
  return
463
547
  end
464
548
 
465
- unless @data[:scheme] == UNIX
466
- if no_proxy_env = ENV["no_proxy"] || ENV["NO_PROXY"]
467
- no_proxy_list = no_proxy_env.scan(/\*?\.?([^\s,:]+)(?::(\d+))?/i).map { |s| [s[0], s[1]] }
549
+ return if @data[:scheme] == UNIX
550
+
551
+ proxy_from_env
552
+
553
+ case @data[:proxy]
554
+ when nil
555
+ @data.delete(:proxy)
556
+ when ''
557
+ @data.delete(:proxy)
558
+ when Hash
559
+ # no processing needed
560
+ when String, URI
561
+ uri = @data[:proxy].is_a?(String) ? URI.parse(@data[:proxy]) : @data[:proxy]
562
+ @data[:proxy] = {
563
+ :host => uri.host,
564
+ :hostname => uri.hostname,
565
+ # path is only sensible for a Unix socket proxy
566
+ :path => uri.scheme == UNIX ? uri.path : nil,
567
+ :port => uri.port,
568
+ :scheme => uri.scheme,
569
+ }
570
+ if uri.password
571
+ @data[:proxy][:password] = uri.password
468
572
  end
469
-
470
- unless no_proxy_env && no_proxy_list.index { |h| /(^|\.)#{h[0]}$/.match(@data[:host]) && (h[1].nil? || h[1].to_i == @data[:port]) }
471
- if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY'))
472
- @data[:proxy] = ENV['https_proxy'] || ENV['HTTPS_PROXY']
473
- elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY'))
474
- @data[:proxy] = ENV['http_proxy'] || ENV['HTTP_PROXY']
475
- end
573
+ if uri.user
574
+ @data[:proxy][:user] = uri.user
476
575
  end
477
-
478
- case @data[:proxy]
479
- when nil
480
- @data.delete(:proxy)
481
- when ''
482
- @data.delete(:proxy)
483
- when Hash
484
- # no processing needed
485
- when String, URI
486
- uri = @data[:proxy].is_a?(String) ? URI.parse(@data[:proxy]) : @data[:proxy]
487
- @data[:proxy] = {
488
- :host => uri.host,
489
- :hostname => uri.hostname,
490
- # path is only sensible for a Unix socket proxy
491
- :path => uri.scheme == UNIX ? uri.path : nil,
492
- :port => uri.port,
493
- :scheme => uri.scheme,
494
- }
495
- if uri.password
496
- @data[:proxy][:password] = uri.password
497
- end
498
- if uri.user
499
- @data[:proxy][:user] = uri.user
500
- end
501
- if @data[:proxy][:scheme] == UNIX
502
- if @data[:proxy][:host]
503
- raise ArgumentError, "The `:host` parameter should not be set for `unix://` proxies.\n" +
504
- "When supplying a `unix://` URI, it should start with `unix:/` or `unix:///`."
505
- end
506
- else
507
- unless uri.host && uri.port && uri.scheme
508
- raise Excon::Errors::ProxyParse, "Proxy is invalid"
509
- end
576
+ if @data[:proxy][:scheme] == UNIX
577
+ if @data[:proxy][:host]
578
+ raise ArgumentError, "The `:host` parameter should not be set for `unix://` proxies.\n" +
579
+ "When supplying a `unix://` URI, it should start with `unix:/` or `unix:///`."
510
580
  end
511
581
  else
512
- raise Excon::Errors::ProxyParse, "Proxy is invalid"
582
+ unless uri.host && uri.port && uri.scheme
583
+ raise Excon::Errors::ProxyParse, "Proxy is invalid"
584
+ end
513
585
  end
586
+ else
587
+ raise Excon::Errors::ProxyParse, "Proxy is invalid"
588
+ end
514
589
 
515
- if @data.has_key?(:proxy) && @data[:scheme] == 'http'
516
- @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
517
- # https credentials happen in handshake
518
- if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
519
- user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
520
- auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
521
- @data[:headers]['Proxy-Authorization'] = 'Basic ' + auth
522
- end
590
+ if @data.has_key?(:proxy) && @data[:scheme] == 'http'
591
+ @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
592
+ # https credentials happen in handshake
593
+ if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
594
+ user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
595
+ auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
596
+ @data[:headers]['Proxy-Authorization'] = 'Basic ' + auth
523
597
  end
524
598
  end
525
599
  end