excon 0.62.0 → 0.85.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 +6 -5
  4. data/data/cacert.pem +939 -1720
  5. data/excon.gemspec +17 -2
  6. data/lib/excon.rb +25 -17
  7. data/lib/excon/connection.rb +206 -139
  8. data/lib/excon/constants.rb +38 -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 +51 -33
  25. data/lib/excon/ssl_socket.rb +33 -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 +58 -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
 
@@ -108,7 +115,7 @@ module Excon
108
115
  # we already have data from a middleware, so bail
109
116
  return datum
110
117
  else
111
- socket.data = datum
118
+ socket(datum).data = datum
112
119
  # start with "METHOD /path"
113
120
  request = datum[:method].to_s.upcase + ' '
114
121
  if datum[:proxy] && datum[:scheme] != HTTPS
@@ -137,31 +144,25 @@ module Excon
137
144
  end
138
145
 
139
146
  # add headers to request
140
- datum[:headers].each do |key, values|
141
- [values].flatten.each do |value|
142
- request << key.to_s << ': ' << value.to_s << CR_NL
143
- end
144
- end
147
+ request << Utils.headers_hash_to_s(datum[:headers])
145
148
 
146
149
  # add additional "\r\n" to indicate end of headers
147
150
  request << CR_NL
148
151
 
149
152
  if datum.has_key?(:request_block)
150
- socket.write(request) # write out request + headers
153
+ socket(datum).write(request) # write out request + headers
151
154
  while true # write out body with chunked encoding
152
155
  chunk = datum[:request_block].call
153
- if FORCE_ENC
154
- chunk.force_encoding('BINARY')
155
- end
156
+ chunk = binary_encode(chunk)
156
157
  if chunk.length > 0
157
- 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)
158
159
  else
159
- socket.write(String.new("0#{CR_NL}#{CR_NL}"))
160
+ socket(datum).write(String.new("0#{CR_NL}#{CR_NL}"))
160
161
  break
161
162
  end
162
163
  end
163
164
  elsif body.nil?
164
- socket.write(request) # write out request + headers
165
+ socket(datum).write(request) # write out request + headers
165
166
  else # write out body
166
167
  if body.respond_to?(:binmode) && !body.is_a?(StringIO)
167
168
  body.binmode
@@ -171,27 +172,23 @@ module Excon
171
172
  end
172
173
 
173
174
  # if request + headers is less than chunk size, fill with body
174
- if FORCE_ENC
175
- request.force_encoding('BINARY')
176
- end
175
+ request = binary_encode(request)
177
176
  chunk = body.read([datum[:chunk_size] - request.length, 0].max)
178
177
  if chunk
179
- if FORCE_ENC
180
- chunk.force_encoding('BINARY')
181
- end
182
- socket.write(request << chunk)
178
+ chunk = binary_encode(chunk)
179
+ socket(datum).write(request << chunk)
183
180
  else
184
- socket.write(request) # write out request + headers
181
+ socket(datum).write(request) # write out request + headers
185
182
  end
186
183
 
187
- while chunk = body.read(datum[:chunk_size])
188
- socket.write(chunk)
184
+ while (chunk = body.read(datum[:chunk_size]))
185
+ socket(datum).write(chunk)
189
186
  end
190
187
  end
191
188
  end
192
189
  rescue => error
193
190
  case error
194
- when Excon::Errors::StubNotFound, Excon::Errors::Timeout
191
+ when Excon::Errors::InvalidHeaderKey, Excon::Errors::InvalidHeaderValue, Excon::Errors::StubNotFound, Excon::Errors::Timeout
195
192
  raise(error)
196
193
  else
197
194
  raise_socket_error(error)
@@ -216,18 +213,24 @@ module Excon
216
213
  end
217
214
 
218
215
  # 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'
216
+ # @yield [chunk] @see Response#self.parse
217
+ # @param [Hash<Symbol, >] params One or more optional params, override defaults set in Connection.new
218
+ # @option params [String] :body text to be sent over a socket
219
+ # @option params [Hash<Symbol, String>] :headers The default headers to supply in a request
220
+ # @option params [String] :path appears after 'scheme://host:port/'
221
+ # @option params [Hash] :query appended to the 'scheme://host:port/path/' in the form of '?key=value'
225
222
  def request(params={}, &block)
226
- params = validate_params(:request, params)
227
223
  # @data has defaults, merge in new params to override
228
224
  datum = @data.merge(params)
229
225
  datum[:headers] = @data[:headers].merge(datum[:headers] || {})
230
226
 
227
+ validate_params(:request, params, datum[:middlewares])
228
+ # If the user passed in new middleware, we want to validate that the original connection parameters
229
+ # are still valid with the provided middleware.
230
+ if params[:middlewares]
231
+ validate_params(:connection, @data, datum[:middlewares])
232
+ end
233
+
231
234
  if datum[:user] || datum[:password]
232
235
  user, pass = Utils.unescape_uri(datum[:user].to_s), Utils.unescape_uri(datum[:password].to_s)
233
236
  datum[:headers]['Authorization'] ||= 'Basic ' + ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
@@ -238,7 +241,12 @@ module Excon
238
241
  else
239
242
  datum[:headers]['Host'] ||= datum[:host] + port_string(datum)
240
243
  end
241
- datum[:retries_remaining] ||= datum[:retry_limit]
244
+
245
+ # RFC 7230, section 5.4, states that the Host header SHOULD be the first one # to be present.
246
+ # Some web servers will reject the request if it comes too late, so let's hoist it to the top.
247
+ if (host = datum[:headers].delete('Host'))
248
+ datum[:headers] = { 'Host' => host }.merge(datum[:headers])
249
+ end
242
250
 
243
251
  # if path is empty or doesn't start with '/', insert one
244
252
  unless datum[:path][0, 1] == '/'
@@ -247,11 +255,16 @@ module Excon
247
255
 
248
256
  if block_given?
249
257
  Excon.display_warning('Excon requests with a block are deprecated, pass :response_block instead.')
250
- datum[:response_block] = Proc.new
258
+ datum[:response_block] = block
251
259
  end
252
260
 
253
261
  datum[:connection] = self
254
262
 
263
+ # cleanup data left behind on persistent connection after interrupt
264
+ if datum[:persistent] && !@persistent_socket_reusable
265
+ reset
266
+ end
267
+
255
268
  datum[:stack] = datum[:middlewares].map do |middleware|
256
269
  lambda {|stack| middleware.new(stack)}
257
270
  end.reverse.inject(self) do |middlewares, middleware|
@@ -260,10 +273,12 @@ module Excon
260
273
  datum = datum[:stack].request_call(datum)
261
274
 
262
275
  unless datum[:pipeline]
276
+ @persistent_socket_reusable = false
263
277
  datum = response(datum)
278
+ @persistent_socket_reusable = true
264
279
 
265
280
  if datum[:persistent]
266
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
281
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
267
282
  if datum[:response][:headers][key].casecmp('close') == 0
268
283
  reset
269
284
  end
@@ -278,6 +293,10 @@ module Excon
278
293
  end
279
294
  rescue => error
280
295
  reset
296
+
297
+ # If we didn't get far enough to initialize datum and the middleware stack, just raise
298
+ raise error if !datum
299
+
281
300
  datum[:error] = error
282
301
  if datum[:stack]
283
302
  datum[:stack].error_call(datum)
@@ -287,7 +306,7 @@ module Excon
287
306
  end
288
307
 
289
308
  # 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
309
+ # @param pipeline_params [Array<Hash>] An array of one or more optional params, override defaults set in Connection.new, see #request for details
291
310
  def requests(pipeline_params)
292
311
  pipeline_params.each {|params| params.merge!(:pipeline => true, :persistent => true) }
293
312
  pipeline_params.last.merge!(:persistent => @data[:persistent])
@@ -299,7 +318,7 @@ module Excon
299
318
  end
300
319
 
301
320
  if @data[:persistent]
302
- if key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
321
+ if (key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
303
322
  if responses.last[:headers][key].casecmp('close') == 0
304
323
  reset
305
324
  end
@@ -314,7 +333,7 @@ module Excon
314
333
  # Sends the supplied requests to the destination host using pipelining in
315
334
  # batches of @limit [Numeric] requests. This is your soft file descriptor
316
335
  # 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
336
+ # @param pipeline_params [Array<Hash>] An array of one or more optional params, override defaults set in Connection.new, see #request for details
318
337
  def batch_requests(pipeline_params, limit = nil)
319
338
  limit ||= Process.respond_to?(:getrlimit) ? Process.getrlimit(:NOFILE).first : 256
320
339
  responses = []
@@ -327,9 +346,10 @@ module Excon
327
346
  end
328
347
 
329
348
  def reset
330
- if old_socket = sockets.delete(@socket_key)
349
+ if (old_socket = sockets.delete(@socket_key))
331
350
  old_socket.close rescue nil
332
351
  end
352
+ @persistent_socket_reusable = true
333
353
  end
334
354
 
335
355
  # Generate HTTP request verb methods
@@ -355,15 +375,7 @@ module Excon
355
375
  vars = instance_variables.inject({}) do |accum, var|
356
376
  accum.merge!(var.to_sym => instance_variable_get(var))
357
377
  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
378
+ vars[:'@data'] = Utils.redact(vars[:'@data'])
367
379
  inspection = '#<Excon::Connection:'
368
380
  inspection += (object_id << 1).to_s(16)
369
381
  vars.each do |key, value|
@@ -373,6 +385,10 @@ module Excon
373
385
  inspection
374
386
  end
375
387
 
388
+ def valid_request_keys(middlewares)
389
+ valid_middleware_keys(middlewares) + Excon::VALID_REQUEST_KEYS
390
+ end
391
+
376
392
  private
377
393
 
378
394
  def detect_content_length(body)
@@ -387,60 +403,77 @@ module Excon
387
403
  end
388
404
  end
389
405
 
390
- def validate_params(validation, params)
406
+ def valid_middleware_keys(middlewares)
407
+ middlewares.flat_map do |middleware|
408
+ if middleware.respond_to?(:valid_parameter_keys)
409
+ middleware.valid_parameter_keys
410
+ else
411
+ Excon.display_warning(
412
+ "Excon middleware #{middleware} does not define #valid_parameter_keys"
413
+ )
414
+ []
415
+ end
416
+ end
417
+ end
418
+
419
+ def validate_params(validation, params, middlewares)
391
420
  valid_keys = case validation
392
421
  when :connection
393
- Excon::VALID_CONNECTION_KEYS
422
+ valid_middleware_keys(middlewares) + Excon::VALID_CONNECTION_KEYS
394
423
  when :request
395
- Excon::VALID_REQUEST_KEYS
424
+ valid_request_keys(middlewares)
425
+ else
426
+ raise ArgumentError.new("Invalid validation type '#{validation}'")
396
427
  end
428
+
397
429
  invalid_keys = params.keys - valid_keys
398
430
  unless invalid_keys.empty?
399
431
  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
432
 
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]
433
+ if validation == :request
434
+ deprecated_keys = invalid_keys & Excon::DEPRECATED_VALID_REQUEST_KEYS.keys
435
+ mw_msg = deprecated_keys.map do |k|
436
+ "#{k}: #{Excon::DEPRECATED_VALID_REQUEST_KEYS[k]}"
437
+ end.join(', ')
438
+ Excon.display_warning(
439
+ "The following request keys are only valid with the associated middleware: #{mw_msg}"
440
+ )
441
+ end
408
442
  end
409
-
410
- params
411
443
  end
412
444
 
413
445
  def response(datum={})
414
446
  datum[:stack].response_call(datum)
415
447
  rescue => error
416
448
  case error
417
- when Excon::Errors::HTTPStatusError, Excon::Errors::Timeout
449
+ when Excon::Errors::HTTPStatusError, Excon::Errors::Timeout, Excon::Errors::TooManyRedirects
418
450
  raise(error)
419
451
  else
420
452
  raise_socket_error(error)
421
453
  end
422
454
  end
423
455
 
424
- def socket
425
- unix_proxy = @data[:proxy] ? @data[:proxy][:scheme] == UNIX : false
426
- sockets[@socket_key] ||= if @data[:scheme] == UNIX || unix_proxy
427
- Excon::UnixSocket.new(@data)
428
- elsif @data[:ssl_uri_schemes].include?(@data[:scheme])
429
- Excon::SSLSocket.new(@data)
456
+ def socket(datum = @data)
457
+ unix_proxy = datum[:proxy] ? datum[:proxy][:scheme] == UNIX : false
458
+ sockets[@socket_key] ||= if datum[:scheme] == UNIX || unix_proxy
459
+ Excon::UnixSocket.new(datum)
460
+ elsif datum[:ssl_uri_schemes].include?(datum[:scheme])
461
+ Excon::SSLSocket.new(datum)
430
462
  else
431
- Excon::Socket.new(@data)
463
+ Excon::Socket.new(datum)
432
464
  end
433
465
  end
434
466
 
435
467
  def sockets
436
468
  @_excon_sockets ||= {}
469
+ @_excon_sockets.compare_by_identity
437
470
 
438
471
  if @data[:thread_safe_sockets]
439
472
  # In a multi-threaded world, if the same connection is used by multiple
440
473
  # threads at the same time to connect to the same destination, they may
441
474
  # stomp on each other's sockets. This ensures every thread gets their
442
475
  # own socket cache, within the context of a single connection.
443
- @_excon_sockets[Thread.current.object_id] ||= {}
476
+ @_excon_sockets[Thread.current] ||= {}
444
477
  else
445
478
  @_excon_sockets
446
479
  end
@@ -454,6 +487,47 @@ module Excon
454
487
  end
455
488
  end
456
489
 
490
+ def proxy_match_host_port(host, port)
491
+ host_match = if host.is_a? IPAddr
492
+ begin
493
+ host.include? @data[:host]
494
+ rescue IPAddr::Error
495
+ false
496
+ end
497
+ else
498
+ /(^|\.)#{host}$/.match(@data[:host])
499
+ end
500
+ host_match && (port.nil? || port.to_i == @data[:port])
501
+ end
502
+
503
+ def proxy_from_env
504
+ if (no_proxy_env = ENV['no_proxy'] || ENV['NO_PROXY'])
505
+ no_proxy_list = no_proxy_env.scan(/\s*(?:\[([\dA-Fa-f:\/]+)\]|\*?\.?([^\s,:]+))(?::(\d+))?\s*/i).map { |e|
506
+ if e[0]
507
+ begin
508
+ [IPAddr.new(e[0]), e[2]]
509
+ rescue IPAddr::Error
510
+ nil
511
+ end
512
+ else
513
+ begin
514
+ [IPAddr.new(e[1]), e[2]]
515
+ rescue IPAddr::Error
516
+ [e[1], e[2]]
517
+ end
518
+ end
519
+ }.reject { |e| e.nil? || e[0].nil? }
520
+ end
521
+
522
+ unless no_proxy_env && no_proxy_list.index { |h| proxy_match_host_port(h[0], h[1]) }
523
+ if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY'))
524
+ @data[:proxy] = ENV['https_proxy'] || ENV['HTTPS_PROXY']
525
+ elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY'))
526
+ @data[:proxy] = ENV['http_proxy'] || ENV['HTTP_PROXY']
527
+ end
528
+ end
529
+ end
530
+
457
531
  def setup_proxy
458
532
  if @data[:disable_proxy]
459
533
  if @data[:proxy]
@@ -462,64 +536,57 @@ module Excon
462
536
  return
463
537
  end
464
538
 
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]] }
539
+ return if @data[:scheme] == UNIX
540
+
541
+ proxy_from_env
542
+
543
+ case @data[:proxy]
544
+ when nil
545
+ @data.delete(:proxy)
546
+ when ''
547
+ @data.delete(:proxy)
548
+ when Hash
549
+ # no processing needed
550
+ when String, URI
551
+ uri = @data[:proxy].is_a?(String) ? URI.parse(@data[:proxy]) : @data[:proxy]
552
+ @data[:proxy] = {
553
+ :host => uri.host,
554
+ :hostname => uri.hostname,
555
+ # path is only sensible for a Unix socket proxy
556
+ :path => uri.scheme == UNIX ? uri.path : nil,
557
+ :port => uri.port,
558
+ :scheme => uri.scheme,
559
+ }
560
+ if uri.password
561
+ @data[:proxy][:password] = uri.password
468
562
  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
563
+ if uri.user
564
+ @data[:proxy][:user] = uri.user
476
565
  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
566
+ if @data[:ssl_proxy_headers] && !@data[:ssl_uri_schemes].include?(@data[:scheme])
567
+ raise ArgumentError, "The `:ssl_proxy_headers` parameter should only be used with HTTPS requests."
568
+ end
569
+ if @data[:proxy][:scheme] == UNIX
570
+ if @data[:proxy][:host]
571
+ raise ArgumentError, "The `:host` parameter should not be set for `unix://` proxies.\n" +
572
+ "When supplying a `unix://` URI, it should start with `unix:/` or `unix:///`."
510
573
  end
511
574
  else
512
- raise Excon::Errors::ProxyParse, "Proxy is invalid"
575
+ unless uri.host && uri.port && uri.scheme
576
+ raise Excon::Errors::ProxyParse, "Proxy is invalid"
577
+ end
513
578
  end
579
+ else
580
+ raise Excon::Errors::ProxyParse, "Proxy is invalid"
581
+ end
514
582
 
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
583
+ if @data.has_key?(:proxy) && @data[:scheme] == 'http'
584
+ @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
585
+ # https credentials happen in handshake
586
+ if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
587
+ user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
588
+ auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
589
+ @data[:headers]['Proxy-Authorization'] = 'Basic ' + auth
523
590
  end
524
591
  end
525
592
  end