excon 0.71.1 → 0.89.0

Sign up to get free protection for your applications and to get access to all the features.
data/excon.gemspec CHANGED
@@ -1,4 +1,5 @@
1
- require File.join(File.dirname(__FILE__), 'lib', 'excon', 'version')
1
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
2
+ require 'excon/version'
2
3
 
3
4
  Gem::Specification.new do |s|
4
5
  s.name = 'excon'
@@ -11,7 +12,7 @@ Gem::Specification.new do |s|
11
12
  s.license = 'MIT'
12
13
  s.rdoc_options = ["--charset=UTF-8"]
13
14
  s.extra_rdoc_files = %w[README.md CONTRIBUTORS.md CONTRIBUTING.md]
14
- s.files = `git ls-files -- {data,lib}/*`.split("\n") + [
15
+ s.files = `git ls-files -- data/* lib/*`.split("\n") + [
15
16
  "CONTRIBUTING.md",
16
17
  "CONTRIBUTORS.md",
17
18
  "LICENSE.md",
@@ -31,6 +32,7 @@ Gem::Specification.new do |s|
31
32
  s.add_development_dependency('sinatra-contrib')
32
33
  s.add_development_dependency('json', '>= 1.8.5')
33
34
  s.add_development_dependency('puma')
35
+ s.add_development_dependency('webrick')
34
36
 
35
37
  s.metadata = {
36
38
  'homepage_uri' => 'https://github.com/excon/excon',
@@ -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
@@ -77,7 +79,7 @@ module Excon
77
79
 
78
80
  setup_proxy
79
81
 
80
- if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
82
+ if ENV.has_key?('EXCON_STANDARD_INSTRUMENTOR')
81
83
  @data[:instrumentor] = Excon::StandardInstrumentor
82
84
  end
83
85
 
@@ -113,7 +115,7 @@ module Excon
113
115
  # we already have data from a middleware, so bail
114
116
  return datum
115
117
  else
116
- socket.data = datum
118
+ socket(datum).data = datum
117
119
  # start with "METHOD /path"
118
120
  request = datum[:method].to_s.upcase + ' '
119
121
  if datum[:proxy] && datum[:scheme] != HTTPS
@@ -142,35 +144,25 @@ module Excon
142
144
  end
143
145
 
144
146
  # add headers to request
145
- datum[:headers].each do |key, values|
146
- if key.to_s.match(/[\r\n]/)
147
- raise Excon::Errors::InvalidHeaderKey.new(key.to_s.inspect + ' contains forbidden "\r" or "\n"')
148
- end
149
- [values].flatten.each do |value|
150
- if value.to_s.match(/[\r\n]/)
151
- raise Excon::Errors::InvalidHeaderValue.new(value.to_s.inspect + ' contains forbidden "\r" or "\n"')
152
- end
153
- request << key.to_s << ': ' << value.to_s << CR_NL
154
- end
155
- end
147
+ request << Utils.headers_hash_to_s(datum[:headers])
156
148
 
157
149
  # add additional "\r\n" to indicate end of headers
158
150
  request << CR_NL
159
151
 
160
152
  if datum.has_key?(:request_block)
161
- socket.write(request) # write out request + headers
153
+ socket(datum).write(request) # write out request + headers
162
154
  while true # write out body with chunked encoding
163
155
  chunk = datum[:request_block].call
164
156
  chunk = binary_encode(chunk)
165
157
  if chunk.length > 0
166
- 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)
167
159
  else
168
- socket.write(String.new("0#{CR_NL}#{CR_NL}"))
160
+ socket(datum).write(String.new("0#{CR_NL}#{CR_NL}"))
169
161
  break
170
162
  end
171
163
  end
172
164
  elsif body.nil?
173
- socket.write(request) # write out request + headers
165
+ socket(datum).write(request) # write out request + headers
174
166
  else # write out body
175
167
  if body.respond_to?(:binmode) && !body.is_a?(StringIO)
176
168
  body.binmode
@@ -184,13 +176,13 @@ module Excon
184
176
  chunk = body.read([datum[:chunk_size] - request.length, 0].max)
185
177
  if chunk
186
178
  chunk = binary_encode(chunk)
187
- socket.write(request << chunk)
179
+ socket(datum).write(request << chunk)
188
180
  else
189
- socket.write(request) # write out request + headers
181
+ socket(datum).write(request) # write out request + headers
190
182
  end
191
183
 
192
- while chunk = body.read(datum[:chunk_size])
193
- socket.write(chunk)
184
+ while (chunk = body.read(datum[:chunk_size]))
185
+ socket(datum).write(chunk)
194
186
  end
195
187
  end
196
188
  end
@@ -198,6 +190,11 @@ module Excon
198
190
  case error
199
191
  when Excon::Errors::InvalidHeaderKey, Excon::Errors::InvalidHeaderValue, Excon::Errors::StubNotFound, Excon::Errors::Timeout
200
192
  raise(error)
193
+ when Errno::EPIPE
194
+ # Read whatever remains in the pipe to aid in debugging
195
+ response = socket.read
196
+ error = Excon::Error.new(response + error.message)
197
+ raise_socket_error(error)
201
198
  else
202
199
  raise_socket_error(error)
203
200
  end
@@ -250,6 +247,12 @@ module Excon
250
247
  datum[:headers]['Host'] ||= datum[:host] + port_string(datum)
251
248
  end
252
249
 
250
+ # RFC 7230, section 5.4, states that the Host header SHOULD be the first one # to be present.
251
+ # Some web servers will reject the request if it comes too late, so let's hoist it to the top.
252
+ if (host = datum[:headers].delete('Host'))
253
+ datum[:headers] = { 'Host' => host }.merge(datum[:headers])
254
+ end
255
+
253
256
  # if path is empty or doesn't start with '/', insert one
254
257
  unless datum[:path][0, 1] == '/'
255
258
  datum[:path] = datum[:path].dup.insert(0, '/')
@@ -280,7 +283,7 @@ module Excon
280
283
  @persistent_socket_reusable = true
281
284
 
282
285
  if datum[:persistent]
283
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
286
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
284
287
  if datum[:response][:headers][key].casecmp('close') == 0
285
288
  reset
286
289
  end
@@ -320,7 +323,7 @@ module Excon
320
323
  end
321
324
 
322
325
  if @data[:persistent]
323
- if key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 }
326
+ if (key = responses.last[:headers].keys.detect {|k| k.casecmp('Connection') == 0 })
324
327
  if responses.last[:headers][key].casecmp('close') == 0
325
328
  reset
326
329
  end
@@ -348,7 +351,7 @@ module Excon
348
351
  end
349
352
 
350
353
  def reset
351
- if old_socket = sockets.delete(@socket_key)
354
+ if (old_socket = sockets.delete(@socket_key))
352
355
  old_socket.close rescue nil
353
356
  end
354
357
  @persistent_socket_reusable = true
@@ -455,26 +458,27 @@ module Excon
455
458
  end
456
459
  end
457
460
 
458
- def socket
459
- unix_proxy = @data[:proxy] ? @data[:proxy][:scheme] == UNIX : false
460
- sockets[@socket_key] ||= if @data[:scheme] == UNIX || unix_proxy
461
- Excon::UnixSocket.new(@data)
462
- elsif @data[:ssl_uri_schemes].include?(@data[:scheme])
463
- Excon::SSLSocket.new(@data)
461
+ def socket(datum = @data)
462
+ unix_proxy = datum[:proxy] ? datum[:proxy][:scheme] == UNIX : false
463
+ sockets[@socket_key] ||= if datum[:scheme] == UNIX || unix_proxy
464
+ Excon::UnixSocket.new(datum)
465
+ elsif datum[:ssl_uri_schemes].include?(datum[:scheme])
466
+ Excon::SSLSocket.new(datum)
464
467
  else
465
- Excon::Socket.new(@data)
468
+ Excon::Socket.new(datum)
466
469
  end
467
470
  end
468
471
 
469
472
  def sockets
470
473
  @_excon_sockets ||= {}
474
+ @_excon_sockets.compare_by_identity
471
475
 
472
476
  if @data[:thread_safe_sockets]
473
477
  # In a multi-threaded world, if the same connection is used by multiple
474
478
  # threads at the same time to connect to the same destination, they may
475
479
  # stomp on each other's sockets. This ensures every thread gets their
476
480
  # own socket cache, within the context of a single connection.
477
- @_excon_sockets[Thread.current.object_id] ||= {}
481
+ @_excon_sockets[Thread.current] ||= {}
478
482
  else
479
483
  @_excon_sockets
480
484
  end
@@ -488,6 +492,47 @@ module Excon
488
492
  end
489
493
  end
490
494
 
495
+ def proxy_match_host_port(host, port)
496
+ host_match = if host.is_a? IPAddr
497
+ begin
498
+ host.include? @data[:host]
499
+ rescue IPAddr::Error
500
+ false
501
+ end
502
+ else
503
+ /(^|\.)#{host}$/.match(@data[:host])
504
+ end
505
+ host_match && (port.nil? || port.to_i == @data[:port])
506
+ end
507
+
508
+ def proxy_from_env
509
+ if (no_proxy_env = ENV['no_proxy'] || ENV['NO_PROXY'])
510
+ no_proxy_list = no_proxy_env.scan(/\s*(?:\[([\dA-Fa-f:\/]+)\]|\*?\.?([^\s,:]+))(?::(\d+))?\s*/i).map { |e|
511
+ if e[0]
512
+ begin
513
+ [IPAddr.new(e[0]), e[2]]
514
+ rescue IPAddr::Error
515
+ nil
516
+ end
517
+ else
518
+ begin
519
+ [IPAddr.new(e[1]), e[2]]
520
+ rescue IPAddr::Error
521
+ [e[1], e[2]]
522
+ end
523
+ end
524
+ }.reject { |e| e.nil? || e[0].nil? }
525
+ end
526
+
527
+ unless no_proxy_env && no_proxy_list.index { |h| proxy_match_host_port(h[0], h[1]) }
528
+ if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY'))
529
+ @data[:proxy] = ENV['https_proxy'] || ENV['HTTPS_PROXY']
530
+ elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY'))
531
+ @data[:proxy] = ENV['http_proxy'] || ENV['HTTP_PROXY']
532
+ end
533
+ end
534
+ end
535
+
491
536
  def setup_proxy
492
537
  if @data[:disable_proxy]
493
538
  if @data[:proxy]
@@ -496,64 +541,57 @@ module Excon
496
541
  return
497
542
  end
498
543
 
499
- unless @data[:scheme] == UNIX
500
- if no_proxy_env = ENV["no_proxy"] || ENV["NO_PROXY"]
501
- no_proxy_list = no_proxy_env.scan(/\*?\.?([^\s,:]+)(?::(\d+))?/i).map { |s| [s[0], s[1]] }
544
+ return if @data[:scheme] == UNIX
545
+
546
+ proxy_from_env
547
+
548
+ case @data[:proxy]
549
+ when nil
550
+ @data.delete(:proxy)
551
+ when ''
552
+ @data.delete(:proxy)
553
+ when Hash
554
+ # no processing needed
555
+ when String, URI
556
+ uri = @data[:proxy].is_a?(String) ? URI.parse(@data[:proxy]) : @data[:proxy]
557
+ @data[:proxy] = {
558
+ :host => uri.host,
559
+ :hostname => uri.hostname,
560
+ # path is only sensible for a Unix socket proxy
561
+ :path => uri.scheme == UNIX ? uri.path : nil,
562
+ :port => uri.port,
563
+ :scheme => uri.scheme,
564
+ }
565
+ if uri.password
566
+ @data[:proxy][:password] = uri.password
502
567
  end
503
-
504
- unless no_proxy_env && no_proxy_list.index { |h| /(^|\.)#{h[0]}$/.match(@data[:host]) && (h[1].nil? || h[1].to_i == @data[:port]) }
505
- if @data[:scheme] == HTTPS && (ENV.has_key?('https_proxy') || ENV.has_key?('HTTPS_PROXY'))
506
- @data[:proxy] = ENV['https_proxy'] || ENV['HTTPS_PROXY']
507
- elsif (ENV.has_key?('http_proxy') || ENV.has_key?('HTTP_PROXY'))
508
- @data[:proxy] = ENV['http_proxy'] || ENV['HTTP_PROXY']
509
- end
568
+ if uri.user
569
+ @data[:proxy][:user] = uri.user
510
570
  end
511
-
512
- case @data[:proxy]
513
- when nil
514
- @data.delete(:proxy)
515
- when ''
516
- @data.delete(:proxy)
517
- when Hash
518
- # no processing needed
519
- when String, URI
520
- uri = @data[:proxy].is_a?(String) ? URI.parse(@data[:proxy]) : @data[:proxy]
521
- @data[:proxy] = {
522
- :host => uri.host,
523
- :hostname => uri.hostname,
524
- # path is only sensible for a Unix socket proxy
525
- :path => uri.scheme == UNIX ? uri.path : nil,
526
- :port => uri.port,
527
- :scheme => uri.scheme,
528
- }
529
- if uri.password
530
- @data[:proxy][:password] = uri.password
531
- end
532
- if uri.user
533
- @data[:proxy][:user] = uri.user
534
- end
535
- if @data[:proxy][:scheme] == UNIX
536
- if @data[:proxy][:host]
537
- raise ArgumentError, "The `:host` parameter should not be set for `unix://` proxies.\n" +
538
- "When supplying a `unix://` URI, it should start with `unix:/` or `unix:///`."
539
- end
540
- else
541
- unless uri.host && uri.port && uri.scheme
542
- raise Excon::Errors::ProxyParse, "Proxy is invalid"
543
- end
571
+ if @data[:ssl_proxy_headers] && !@data[:ssl_uri_schemes].include?(@data[:scheme])
572
+ raise ArgumentError, "The `:ssl_proxy_headers` parameter should only be used with HTTPS requests."
573
+ end
574
+ if @data[:proxy][:scheme] == UNIX
575
+ if @data[:proxy][:host]
576
+ raise ArgumentError, "The `:host` parameter should not be set for `unix://` proxies.\n" +
577
+ "When supplying a `unix://` URI, it should start with `unix:/` or `unix:///`."
544
578
  end
545
579
  else
546
- raise Excon::Errors::ProxyParse, "Proxy is invalid"
580
+ unless uri.host && uri.port && uri.scheme
581
+ raise Excon::Errors::ProxyParse, "Proxy is invalid"
582
+ end
547
583
  end
584
+ else
585
+ raise Excon::Errors::ProxyParse, "Proxy is invalid"
586
+ end
548
587
 
549
- if @data.has_key?(:proxy) && @data[:scheme] == 'http'
550
- @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
551
- # https credentials happen in handshake
552
- if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
553
- user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
554
- auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
555
- @data[:headers]['Proxy-Authorization'] = 'Basic ' + auth
556
- end
588
+ if @data.has_key?(:proxy) && @data[:scheme] == 'http'
589
+ @data[:headers]['Proxy-Connection'] ||= 'Keep-Alive'
590
+ # https credentials happen in handshake
591
+ if @data[:proxy].has_key?(:user) || @data[:proxy].has_key?(:password)
592
+ user, pass = Utils.unescape_form(@data[:proxy][:user].to_s), Utils.unescape_form(@data[:proxy][:password].to_s)
593
+ auth = ["#{user}:#{pass}"].pack('m').delete(Excon::CR_NL)
594
+ @data[:headers]['Proxy-Authorization'] = 'Basic ' + auth
557
595
  end
558
596
  end
559
597
  end
@@ -95,9 +95,12 @@ 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,
102
+ :ssl_security_level,
103
+ :ssl_proxy_headers,
101
104
  :ssl_uri_schemes,
102
105
  :tcp_nodelay,
103
106
  :thread_safe_sockets,
data/lib/excon/error.rb CHANGED
@@ -50,7 +50,17 @@ or:
50
50
  class InvalidHeaderValue < Error; end
51
51
  class Timeout < Error; end
52
52
  class ResponseParse < Error; end
53
- class ProxyConnectionError < Error; end
53
+
54
+ class ProxyConnectionError < Error
55
+ attr_reader :request, :response
56
+
57
+ def initialize(msg, request = nil, response = nil)
58
+ super(msg)
59
+ @request = request
60
+ @response = response
61
+ end
62
+ end
63
+
54
64
  class ProxyParse < Error; end
55
65
  class TooManyRedirects < Error; end
56
66
 
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)
@@ -28,7 +28,7 @@ module Excon
28
28
  info << "?"
29
29
 
30
30
  if params[:query].is_a?(Hash)
31
- info << params.to_a.map{ |key,value| "#{key}=#{value}" }.join('&')
31
+ info << params[:query].to_a.map { |key,value| "#{key}=#{value}" }.join('&')
32
32
  else
33
33
  info << params[:query]
34
34
  end
@@ -40,7 +40,7 @@ module Excon
40
40
  end
41
41
  end
42
42
 
43
- logger.log(Logger::INFO, info) if info
43
+ logger.info(info) if info
44
44
 
45
45
  yield if block_given?
46
46
  end
@@ -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
@@ -2,6 +2,10 @@
2
2
  module Excon
3
3
  module Middleware
4
4
  class Decompress < Excon::Middleware::Base
5
+
6
+ INFLATE_ZLIB_OR_GZIP = 47 # Zlib::MAX_WBITS + 32
7
+ INFLATE_RAW = -15 # Zlib::MAX_WBITS * -1
8
+
5
9
  def request_call(datum)
6
10
  unless datum.has_key?(:response_block)
7
11
  key = datum[:headers].keys.detect {|k| k.to_s.casecmp('Accept-Encoding') == 0 } || 'Accept-Encoding'
@@ -15,12 +19,15 @@ module Excon
15
19
  def response_call(datum)
16
20
  body = datum[:response][:body]
17
21
  unless datum.has_key?(:response_block) || body.nil? || body.empty?
18
- if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 }
22
+ if (key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 })
19
23
  encodings = Utils.split_header_value(datum[:response][:headers][key])
20
- if encoding = encodings.last
24
+ if (encoding = encodings.last)
21
25
  if encoding.casecmp('deflate') == 0
22
- # assume inflate omits header
23
- datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(body)
26
+ datum[:response][:body] = begin
27
+ Zlib::Inflate.new(INFLATE_ZLIB_OR_GZIP).inflate(body)
28
+ rescue Zlib::DataError # fallback to raw on error
29
+ Zlib::Inflate.new(INFLATE_RAW).inflate(body)
30
+ end
24
31
  encodings.pop
25
32
  elsif encoding.casecmp('gzip') == 0 || encoding.casecmp('x-gzip') == 0
26
33
  datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(body)).read
@@ -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