excon 0.75.0 → 0.80.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.
data/excon.gemspec CHANGED
@@ -11,7 +11,7 @@ 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 -- {data,lib}/*`.split("\n") + [
14
+ s.files = `git ls-files -- data/* lib/*`.split("\n") + [
15
15
  "CONTRIBUTING.md",
16
16
  "CONTRIBUTORS.md",
17
17
  "LICENSE.md",
@@ -31,6 +31,7 @@ Gem::Specification.new do |s|
31
31
  s.add_development_dependency('sinatra-contrib')
32
32
  s.add_development_dependency('json', '>= 1.8.5')
33
33
  s.add_development_dependency('puma')
34
+ s.add_development_dependency('webrick')
34
35
 
35
36
  s.metadata = {
36
37
  'homepage_uri' => 'https://github.com/excon/excon',
data/lib/excon.rb CHANGED
@@ -146,10 +146,10 @@ module Excon
146
146
  # @param request_params [Hash<Symbol, >] request params to match against, omitted params match all
147
147
  # @param response_params [Hash<Symbol, >] response params to return from matched request or block to call with params
148
148
  def stub(request_params = {}, response_params = nil, &block)
149
- if method = request_params.delete(:method)
149
+ if (method = request_params.delete(:method))
150
150
  request_params[:method] = method.to_s.downcase.to_sym
151
151
  end
152
- if url = request_params.delete(:url)
152
+ if (url = request_params.delete(:url))
153
153
  uri = URI.parse(url)
154
154
  request_params = {
155
155
  :host => uri.host,
@@ -190,7 +190,7 @@ module Excon
190
190
  # @param request_params [Hash<Symbol, >] request params to match against, omitted params match all
191
191
  # @return [Hash<Symbol, >] response params to return from matched request or block to call with params
192
192
  def stub_for(request_params={})
193
- if method = request_params.delete(:method)
193
+ if (method = request_params.delete(:method))
194
194
  request_params[:method] = method.to_s.downcase.to_sym
195
195
  end
196
196
  Excon.stubs.each do |stub, response_params|
@@ -198,7 +198,7 @@ module Excon
198
198
  headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
199
199
  case value = stub[:headers][key]
200
200
  when Regexp
201
- if match = value.match(request_params[:headers][key])
201
+ if (match = value.match(request_params[:headers][key]))
202
202
  captures[:headers][key] = match.captures
203
203
  end
204
204
  match
@@ -209,7 +209,7 @@ module Excon
209
209
  non_headers_match = (stub.keys - [:headers]).all? do |key|
210
210
  case value = stub[key]
211
211
  when Regexp
212
- if match = value.match(request_params[key])
212
+ if (match = value.match(request_params[key]))
213
213
  captures[key] = match.captures
214
214
  end
215
215
  match
@@ -79,7 +79,7 @@ module Excon
79
79
 
80
80
  setup_proxy
81
81
 
82
- if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
82
+ if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
83
83
  @data[:instrumentor] = Excon::StandardInstrumentor
84
84
  end
85
85
 
@@ -191,7 +191,7 @@ module Excon
191
191
  socket.write(request) # write out request + headers
192
192
  end
193
193
 
194
- while chunk = body.read(datum[:chunk_size])
194
+ while (chunk = body.read(datum[:chunk_size]))
195
195
  socket.write(chunk)
196
196
  end
197
197
  end
@@ -252,6 +252,12 @@ module Excon
252
252
  datum[:headers]['Host'] ||= datum[:host] + port_string(datum)
253
253
  end
254
254
 
255
+ # RFC 7230, section 5.4, states that the Host header SHOULD be the first one # to be present.
256
+ # Some web servers will reject the request if it comes too late, so let's hoist it to the top.
257
+ if (host = datum[:headers].delete('Host'))
258
+ datum[:headers] = { 'Host' => host }.merge(datum[:headers])
259
+ end
260
+
255
261
  # if path is empty or doesn't start with '/', insert one
256
262
  unless datum[:path][0, 1] == '/'
257
263
  datum[:path] = datum[:path].dup.insert(0, '/')
@@ -282,7 +288,7 @@ module Excon
282
288
  @persistent_socket_reusable = true
283
289
 
284
290
  if datum[:persistent]
285
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
291
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
286
292
  if datum[:response][:headers][key].casecmp('close') == 0
287
293
  reset
288
294
  end
@@ -322,7 +328,7 @@ module Excon
322
328
  end
323
329
 
324
330
  if @data[:persistent]
325
- if key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
331
+ if (key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
326
332
  if responses.last[:headers][key].casecmp('close') == 0
327
333
  reset
328
334
  end
@@ -350,7 +356,7 @@ module Excon
350
356
  end
351
357
 
352
358
  def reset
353
- if old_socket = sockets.delete(@socket_key)
359
+ if (old_socket = sockets.delete(@socket_key))
354
360
  old_socket.close rescue nil
355
361
  end
356
362
  @persistent_socket_reusable = true
@@ -470,13 +476,14 @@ module Excon
470
476
 
471
477
  def sockets
472
478
  @_excon_sockets ||= {}
479
+ @_excon_sockets.compare_by_identity
473
480
 
474
481
  if @data[:thread_safe_sockets]
475
482
  # In a multi-threaded world, if the same connection is used by multiple
476
483
  # threads at the same time to connect to the same destination, they may
477
484
  # stomp on each other's sockets. This ensures every thread gets their
478
485
  # own socket cache, within the context of a single connection.
479
- @_excon_sockets[Thread.current.object_id] ||= {}
486
+ @_excon_sockets[Thread.current] ||= {}
480
487
  else
481
488
  @_excon_sockets
482
489
  end
@@ -95,6 +95,7 @@ module Excon
95
95
  :ssl_verify_callback,
96
96
  :ssl_verify_peer,
97
97
  :ssl_verify_peer_host,
98
+ :ssl_verify_hostname,
98
99
  :ssl_version,
99
100
  :ssl_min_version,
100
101
  :ssl_max_version,
data/lib/excon/headers.rb CHANGED
@@ -22,6 +22,7 @@ module Excon
22
22
  alias_method :raw_values_at, :values_at
23
23
 
24
24
  def initialize
25
+ super
25
26
  @downcased = {}
26
27
  end
27
28
 
@@ -29,11 +30,11 @@ module Excon
29
30
  @downcased[key.to_s.downcase]
30
31
  end
31
32
 
32
- alias_method :[]=, :store
33
33
  def []=(key, value)
34
34
  raw_writer(key, value)
35
35
  @downcased[key.to_s.downcase] = value
36
36
  end
37
+ alias_method :store, :[]=
37
38
 
38
39
  if SENTINEL.respond_to? :assoc
39
40
  def assoc(obj)
@@ -54,11 +55,11 @@ module Excon
54
55
  end
55
56
  end
56
57
 
57
- alias_method :has_key?, :key?
58
- alias_method :has_key?, :member?
59
58
  def has_key?(key)
60
59
  raw_key?(key) || @downcased.has_key?(key.to_s.downcase)
61
60
  end
61
+ alias_method :key?, :has_key?
62
+ alias_method :member?, :has_key?
62
63
 
63
64
  def merge(other_hash)
64
65
  self.dup.merge!(other_hash)
@@ -3,7 +3,7 @@ require 'logger'
3
3
  module Excon
4
4
  class LoggingInstrumentor
5
5
 
6
- def self.instrument(name, params = {}, &block)
6
+ def self.instrument(name, params = {})
7
7
  params = params.dup
8
8
 
9
9
  logger = params[:logger] || Logger.new($stderr)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Excon
3
3
  class StandardInstrumentor
4
- def self.instrument(name, params = {}, &block)
4
+ def self.instrument(name, params = {})
5
5
  params = params.dup
6
6
 
7
7
  # reduce duplication/noise of output
@@ -8,7 +8,7 @@ module Excon
8
8
  end
9
9
 
10
10
  def get_header(datum, header)
11
- _, header_value = datum[:response][:headers].detect do |key, value|
11
+ _, header_value = datum[:response][:headers].detect do |key, _|
12
12
  key.casecmp(header) == 0
13
13
  end
14
14
  header_value
@@ -15,9 +15,9 @@ module Excon
15
15
  def response_call(datum)
16
16
  body = datum[:response][:body]
17
17
  unless datum.has_key?(:response_block) || body.nil? || body.empty?
18
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 }
18
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 })
19
19
  encodings = Utils.split_header_value(datum[:response][:headers][key])
20
- if encoding = encodings.last
20
+ if (encoding = encodings.last)
21
21
  if encoding.casecmp('deflate') == 0
22
22
  # assume inflate omits header
23
23
  datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(body)
@@ -12,7 +12,7 @@ module Excon
12
12
  if datum.has_key?(:expects) && ![*datum[:expects]].include?(datum[:response][:status])
13
13
  raise(
14
14
  Excon::Errors.status_error(
15
- datum.reject {|key,value| key == :response},
15
+ datum.reject {|key,_| key == :response},
16
16
  Excon::Response.new(datum[:response])
17
17
  )
18
18
  )
@@ -25,7 +25,7 @@ module Excon
25
25
  raise Excon::Errors::InvalidStub.new("Request body should be a string or an IO object. #{datum[:body].class} provided")
26
26
  end
27
27
 
28
- if stub = Excon.stub_for(datum)
28
+ if (stub = Excon.stub_for(datum))
29
29
  datum[:remote_ip] ||= '127.0.0.1'
30
30
  datum[:response] = {
31
31
  :body => '',
@@ -41,7 +41,7 @@ module Excon
41
41
  stub.last
42
42
  end
43
43
 
44
- datum[:response].merge!(stub_datum.reject {|key,value| key == :headers})
44
+ datum[:response].merge!(stub_datum.reject {|key,_| key == :headers})
45
45
  if stub_datum.has_key?(:headers)
46
46
  datum[:response][:headers].merge!(stub_datum[:headers])
47
47
  end
@@ -16,7 +16,7 @@ module Excon
16
16
  end
17
17
 
18
18
  def get_header(datum, header)
19
- _, header_value = datum[:response][:headers].detect do |key, value|
19
+ _, header_value = datum[:response][:headers].detect do |key, _|
20
20
  key.casecmp(header) == 0
21
21
  end
22
22
  header_value
@@ -59,10 +59,13 @@ module Excon
59
59
 
60
60
  def self.parse(socket, datum)
61
61
  # this will discard any trailing lines from the previous response if any.
62
- begin
62
+ line = nil
63
+ loop do
63
64
  line = socket.readline
64
- end until status = line[9, 3].to_i
65
+ break if line[9,3].to_i != 0
66
+ end
65
67
 
68
+ status = line[9, 3].to_i
66
69
  reason_phrase = line[13..-3] # -3 strips the trailing "\r\n"
67
70
 
68
71
  datum[:response] = {
@@ -90,7 +93,7 @@ module Excon
90
93
 
91
94
  unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(datum[:response][:status])
92
95
 
93
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Transfer-Encoding') == 0 }
96
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Transfer-Encoding') == 0 })
94
97
  encodings = Utils.split_header_value(datum[:response][:headers][key])
95
98
  if (encoding = encodings.last) && encoding.casecmp('chunked') == 0
96
99
  transfer_encoding_chunked = true
@@ -103,7 +106,7 @@ module Excon
103
106
  end
104
107
 
105
108
  # use :response_block unless :expects would fail
106
- if response_block = datum[:response_block]
109
+ if (response_block = datum[:response_block])
107
110
  if datum[:middlewares].include?(Excon::Middleware::Expects) && datum[:expects] &&
108
111
  !Array(datum[:expects]).include?(datum[:response][:status])
109
112
  response_block = nil
@@ -140,11 +143,11 @@ module Excon
140
143
  end
141
144
  parse_headers(socket, datum) # merge trailers into headers
142
145
  else
143
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Length') == 0 }
146
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Length') == 0 })
144
147
  content_length = datum[:response][:headers][key].to_i
145
148
  end
146
149
 
147
- if remaining = content_length
150
+ if (remaining = content_length)
148
151
  if response_block
149
152
  while remaining > 0
150
153
  chunk = socket.read([datum[:chunk_size], remaining].min) || raise(EOFError)
@@ -160,11 +163,11 @@ module Excon
160
163
  end
161
164
  else
162
165
  if response_block
163
- while chunk = socket.read(datum[:chunk_size])
166
+ while (chunk = socket.read(datum[:chunk_size]))
164
167
  response_block.call(chunk, nil, nil)
165
168
  end
166
169
  else
167
- while chunk = socket.read(datum[:chunk_size])
170
+ while (chunk = socket.read(datum[:chunk_size]))
168
171
  datum[:response][:body] << chunk
169
172
  end
170
173
  end
data/lib/excon/socket.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
+ require 'resolv'
3
+
2
4
  module Excon
3
5
  class Socket
4
6
  include Utils
@@ -60,18 +62,8 @@ module Excon
60
62
  def readline
61
63
  return legacy_readline if RUBY_VERSION.to_f <= 1.8_7
62
64
  buffer = String.new
63
- begin
64
- buffer << @socket.read_nonblock(1) while buffer[-1] != "\n"
65
- buffer
66
- rescue *READ_RETRY_EXCEPTION_CLASSES
67
- select_with_timeout(@socket, :read) && retry
68
- rescue OpenSSL::SSL::SSLError => error
69
- if error.message == 'read would block'
70
- select_with_timeout(@socket, :read) && retry
71
- else
72
- raise(error)
73
- end
74
- end
65
+ buffer << (read_nonblock(1) || raise(EOFError)) while buffer[-1] != "\n"
66
+ buffer
75
67
  end
76
68
 
77
69
  def legacy_readline
@@ -105,20 +97,17 @@ module Excon
105
97
  def connect
106
98
  @socket = nil
107
99
  exception = nil
100
+ hostname = @data[:hostname]
101
+ port = @port
102
+ family = @data[:family]
108
103
 
109
104
  if @data[:proxy]
110
- family = @data[:proxy][:family] || ::Socket::Constants::AF_UNSPEC
111
- args = [@data[:proxy][:hostname], @data[:proxy][:port], family, ::Socket::Constants::SOCK_STREAM]
112
- else
113
- family = @data[:family] || ::Socket::Constants::AF_UNSPEC
114
- args = [@data[:hostname], @port, family, ::Socket::Constants::SOCK_STREAM]
105
+ hostname = @data[:proxy][:hostname]
106
+ port = @data[:proxy][:port]
107
+ family = @data[:proxy][:family]
115
108
  end
116
- if RUBY_VERSION >= '1.9.2' && defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
117
- args << nil << nil << false # no reverse lookup
118
- end
119
- addrinfo = ::Socket.getaddrinfo(*args)
120
109
 
121
- addrinfo.each do |_, port, _, ip, a_family, s_type|
110
+ Resolv.each_address(hostname) do |ip|
122
111
  # already succeeded on previous addrinfo
123
112
  if @socket
124
113
  break
@@ -130,8 +119,8 @@ module Excon
130
119
  # nonblocking connect
131
120
  begin
132
121
  sockaddr = ::Socket.sockaddr_in(port, ip)
133
-
134
- socket = ::Socket.new(a_family, s_type, 0)
122
+ addrinfo = Addrinfo.getaddrinfo(ip, port, family, :STREAM).first
123
+ socket = ::Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol)
135
124
 
136
125
  if @data[:reuseaddr]
137
126
  socket.setsockopt(::Socket::Constants::SOL_SOCKET, ::Socket::Constants::SO_REUSEADDR, true)
@@ -161,6 +150,8 @@ module Excon
161
150
  end
162
151
  end
163
152
 
153
+ exception ||= Resolv::ResolvError.new("no address for #{hostname}")
154
+
164
155
  # this will be our last encountered exception
165
156
  fail exception unless @socket
166
157
 
@@ -39,13 +39,13 @@ module Excon
39
39
  # turn verification on
40
40
  ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
41
41
 
42
- if ca_file = @data[:ssl_ca_file] || ENV['SSL_CERT_FILE']
42
+ if (ca_file = @data[:ssl_ca_file] || ENV['SSL_CERT_FILE'])
43
43
  ssl_context.ca_file = ca_file
44
44
  end
45
- if ca_path = @data[:ssl_ca_path] || ENV['SSL_CERT_DIR']
45
+ if (ca_path = @data[:ssl_ca_path] || ENV['SSL_CERT_DIR'])
46
46
  ssl_context.ca_path = ca_path
47
47
  end
48
- if cert_store = @data[:ssl_cert_store]
48
+ if (cert_store = @data[:ssl_cert_store])
49
49
  ssl_context.cert_store = cert_store
50
50
  end
51
51
 
@@ -65,7 +65,7 @@ module Excon
65
65
  end
66
66
  end
67
67
 
68
- if verify_callback = @data[:ssl_verify_callback]
68
+ if (verify_callback = @data[:ssl_verify_callback])
69
69
  ssl_context.verify_callback = verify_callback
70
70
  end
71
71
  else
@@ -73,6 +73,9 @@ module Excon
73
73
  ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
74
74
  end
75
75
 
76
+ # Verify certificate hostname if supported (ruby >= 2.4.0)
77
+ ssl_context.verify_hostname = @data[:ssl_verify_hostname] if ssl_context.respond_to?(:verify_hostname=)
78
+
76
79
  if client_cert_data && client_key_data
77
80
  ssl_context.cert = OpenSSL::X509::Certificate.new client_cert_data
78
81
  if OpenSSL::PKey.respond_to? :read
@@ -142,18 +145,16 @@ module Excon
142
145
  if @data[:ssl_verify_peer]
143
146
  @socket.post_connection_check(@data[:ssl_verify_peer_host] || @data[:host])
144
147
  end
145
-
146
- @socket
147
148
  end
148
149
 
149
150
  private
150
151
 
151
152
  def client_cert_data
152
- @client_cert_data ||= if ccd = @data[:client_cert_data]
153
+ @client_cert_data ||= if (ccd = @data[:client_cert_data])
153
154
  ccd
154
- elsif path = @data[:client_cert]
155
+ elsif (path = @data[:client_cert])
155
156
  File.read path
156
- elsif path = @data[:certificate_path]
157
+ elsif (path = @data[:certificate_path])
157
158
  warn ":certificate_path is no longer supported and will be deprecated. Please use :client_cert or :client_cert_data"
158
159
  File.read path
159
160
  end
@@ -166,11 +167,11 @@ module Excon
166
167
  end
167
168
 
168
169
  def client_key_data
169
- @client_key_data ||= if ckd = @data[:client_key_data]
170
+ @client_key_data ||= if (ckd = @data[:client_key_data])
170
171
  ckd
171
- elsif path = @data[:client_key]
172
+ elsif (path = @data[:client_key])
172
173
  File.read path
173
- elsif path = @data[:private_key_path]
174
+ elsif (path = @data[:private_key_path])
174
175
  warn ":private_key_path is no longer supported and will be deprecated. Please use :client_key or :client_key_data"
175
176
  File.read path
176
177
  end