excon 0.71.1 → 0.89.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/cacert.pem +473 -673
- data/excon.gemspec +4 -2
- data/lib/excon/connection.rb +122 -84
- data/lib/excon/constants.rb +3 -0
- data/lib/excon/error.rb +11 -1
- data/lib/excon/headers.rb +4 -3
- data/lib/excon/instrumentors/logging_instrumentor.rb +3 -3
- data/lib/excon/instrumentors/standard_instrumentor.rb +1 -1
- data/lib/excon/middlewares/capture_cookies.rb +1 -1
- data/lib/excon/middlewares/decompress.rb +11 -4
- data/lib/excon/middlewares/expects.rb +1 -1
- data/lib/excon/middlewares/mock.rb +2 -2
- data/lib/excon/middlewares/redirect_follower.rb +1 -1
- data/lib/excon/response.rb +11 -8
- data/lib/excon/socket.rb +15 -24
- data/lib/excon/ssl_socket.rb +23 -13
- data/lib/excon/test/plugin/server/exec.rb +5 -2
- data/lib/excon/test/plugin/server/puma.rb +4 -1
- data/lib/excon/test/plugin/server/unicorn.rb +5 -0
- data/lib/excon/test/plugin/server/webrick.rb +4 -1
- data/lib/excon/test/server.rb +1 -1
- data/lib/excon/unix_socket.rb +1 -0
- data/lib/excon/utils.rb +20 -2
- data/lib/excon/version.rb +1 -1
- data/lib/excon.rb +5 -5
- metadata +20 -6
data/excon.gemspec
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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 --
|
|
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',
|
data/lib/excon/connection.rb
CHANGED
|
@@ -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
|
|
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]
|
|
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 =
|
|
460
|
-
sockets[@socket_key] ||= if
|
|
461
|
-
Excon::UnixSocket.new(
|
|
462
|
-
elsif
|
|
463
|
-
Excon::SSLSocket.new(
|
|
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(
|
|
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
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
data/lib/excon/constants.rb
CHANGED
|
@@ -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
|
-
|
|
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 = {}
|
|
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.
|
|
43
|
+
logger.info(info) if info
|
|
44
44
|
|
|
45
45
|
yield if block_given?
|
|
46
46
|
end
|
|
@@ -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
|
-
|
|
23
|
-
|
|
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,
|
|
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,
|
|
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
|
data/lib/excon/response.rb
CHANGED
|
@@ -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
|
-
|
|
62
|
+
line = nil
|
|
63
|
+
loop do
|
|
63
64
|
line = socket.readline
|
|
64
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|