excon 0.62.0 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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