excon 0.49.0 → 0.88.0

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