excon 0.29.0 → 0.30.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of excon might be problematic. Click here for more details.
- data/Gemfile +2 -1
- data/Gemfile.lock +209 -1
- data/changelog.txt +25 -0
- data/excon.gemspec +5 -2
- data/lib/excon.rb +6 -3
- data/lib/excon/connection.rb +23 -9
- data/lib/excon/constants.rb +1 -5
- data/lib/excon/errors.rb +2 -0
- data/lib/excon/middlewares/decompress.rb +23 -6
- data/lib/excon/middlewares/redirect_follower.rb +5 -5
- data/lib/excon/middlewares/response_parser.rb +18 -0
- data/lib/excon/response.rb +58 -31
- data/lib/excon/socket.rb +2 -2
- data/lib/excon/ssl_socket.rb +6 -8
- data/lib/excon/unix_socket.rb +26 -10
- data/lib/excon/utils.rb +16 -0
- data/tests/middlewares/canned_response_tests.rb +27 -0
- data/tests/middlewares/decompress_tests.rb +133 -12
- data/tests/middlewares/redirect_follower_tests.rb +33 -0
- data/tests/requests_tests.rb +28 -0
- data/tests/response_tests.rb +220 -0
- data/tests/servers/good.rb +313 -0
- data/tests/test_helper.rb +14 -0
- metadata +6 -3
@@ -1,14 +1,31 @@
|
|
1
1
|
module Excon
|
2
2
|
module Middleware
|
3
3
|
class Decompress < Excon::Middleware::Base
|
4
|
+
def request_call(datum)
|
5
|
+
unless datum.has_key?(:response_block)
|
6
|
+
key = datum[:headers].keys.detect {|k| k.to_s.casecmp('Accept-Encoding') == 0 } || 'Accept-Encoding'
|
7
|
+
if datum[:headers][key].to_s.empty?
|
8
|
+
datum[:headers][key] = 'deflate, gzip'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
@stack.request_call(datum)
|
12
|
+
end
|
13
|
+
|
4
14
|
def response_call(datum)
|
5
15
|
unless datum.has_key?(:response_block)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
16
|
+
if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Encoding') == 0 }
|
17
|
+
encodings = Utils.split_header_value(datum[:response][:headers][key])
|
18
|
+
if encoding = encodings.last
|
19
|
+
if encoding.casecmp('deflate') == 0
|
20
|
+
# assume inflate omits header
|
21
|
+
datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(datum[:response][:body])
|
22
|
+
encodings.pop
|
23
|
+
elsif encoding.casecmp('gzip') == 0 || encoding.casecmp('x-gzip') == 0
|
24
|
+
datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(datum[:response][:body])).read
|
25
|
+
encodings.pop
|
26
|
+
end
|
27
|
+
datum[:response][:headers][key] = encodings.join(', ')
|
28
|
+
end
|
12
29
|
end
|
13
30
|
end
|
14
31
|
@stack.response_call(datum)
|
@@ -23,13 +23,13 @@ module Excon
|
|
23
23
|
params[:headers].delete('Proxy-Authorization')
|
24
24
|
params[:headers].delete('Host')
|
25
25
|
params.merge!(
|
26
|
-
:scheme => uri.scheme,
|
27
|
-
:host => uri.host,
|
28
|
-
:port => uri.port,
|
26
|
+
:scheme => uri.scheme || datum[:scheme],
|
27
|
+
:host => uri.host || datum[:host],
|
28
|
+
:port => uri.port || datum[:port],
|
29
29
|
:path => uri.path,
|
30
30
|
:query => uri.query,
|
31
|
-
:user => (
|
32
|
-
:password => (
|
31
|
+
:user => (Utils.unescape_uri(uri.user) if uri.user),
|
32
|
+
:password => (Utils.unescape_uri(uri.password) if uri.password)
|
33
33
|
)
|
34
34
|
|
35
35
|
response = Excon::Connection.new(params).request
|
@@ -4,6 +4,24 @@ module Excon
|
|
4
4
|
def response_call(datum)
|
5
5
|
unless datum.has_key?(:response)
|
6
6
|
datum = Excon::Response.parse(datum[:connection].send(:socket), datum)
|
7
|
+
|
8
|
+
# only requests without a :response_block add 'deflate, gzip' to the TE header.
|
9
|
+
unless datum[:response_block]
|
10
|
+
if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Transfer-Encoding') == 0 }
|
11
|
+
encodings = Utils.split_header_value(datum[:response][:headers][key])
|
12
|
+
if encoding = encodings.last
|
13
|
+
if encoding.casecmp('deflate') == 0
|
14
|
+
# assume inflate omits header
|
15
|
+
datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(datum[:response][:body])
|
16
|
+
encodings.pop
|
17
|
+
elsif encoding.casecmp('gzip') == 0 || encoding.casecmp('x-gzip') == 0
|
18
|
+
datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(datum[:response][:body])).read
|
19
|
+
encodings.pop
|
20
|
+
end
|
21
|
+
datum[:response][:headers][key] = encodings.join(', ')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
7
25
|
end
|
8
26
|
@stack.response_call(datum)
|
9
27
|
end
|
data/lib/excon/response.rb
CHANGED
@@ -41,51 +41,61 @@ module Excon
|
|
41
41
|
:remote_ip => socket.respond_to?(:remote_ip) && socket.remote_ip
|
42
42
|
}
|
43
43
|
|
44
|
-
|
45
|
-
key, value = data.split(/:\s*/, 2)
|
46
|
-
datum[:response][:headers][key] = ([*datum[:response][:headers][key]] << value).compact.join(', ')
|
47
|
-
if key.casecmp('Content-Length') == 0
|
48
|
-
content_length = value.to_i
|
49
|
-
elsif (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0)
|
50
|
-
transfer_encoding_chunked = true
|
51
|
-
end
|
52
|
-
end
|
44
|
+
parse_headers(socket, datum)
|
53
45
|
|
54
46
|
unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(datum[:response][:status])
|
55
47
|
|
56
|
-
|
57
|
-
|
48
|
+
if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Transfer-Encoding') == 0 }
|
49
|
+
encodings = Utils.split_header_value(datum[:response][:headers][key])
|
50
|
+
if (encoding = encodings.last) && encoding.casecmp('chunked') == 0
|
51
|
+
transfer_encoding_chunked = true
|
52
|
+
encodings.pop
|
53
|
+
datum[:response][:headers][key] = encodings.join(', ')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
unless transfer_encoding_chunked
|
57
|
+
if key = datum[:response][:headers].keys.detect {|k| k.casecmp('Content-Length') == 0 }
|
58
|
+
content_length = datum[:response][:headers][key].to_i
|
59
|
+
end
|
60
|
+
end
|
58
61
|
|
59
|
-
#
|
60
|
-
if
|
61
|
-
if
|
62
|
-
|
62
|
+
# use :response_block unless :expects would fail
|
63
|
+
if response_block = datum[:response_block]
|
64
|
+
if datum[:middlewares].include?(Excon::Middleware::Expects) && datum[:expects] &&
|
65
|
+
!Array(datum[:expects]).include?(datum[:response][:status])
|
66
|
+
response_block = nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if transfer_encoding_chunked
|
71
|
+
# 2 == "\r\n".length
|
72
|
+
if response_block
|
63
73
|
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
64
|
-
|
65
|
-
end
|
66
|
-
socket.read(2) # empty chunk-body
|
67
|
-
elsif remaining = content_length
|
68
|
-
while remaining > 0
|
69
|
-
datum[:response_block].call(socket.read([datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length)
|
70
|
-
remaining -= datum[:chunk_size]
|
74
|
+
response_block.call(socket.read(chunk_size + 2).chop!, nil, nil)
|
71
75
|
end
|
72
76
|
else
|
73
|
-
while remaining = socket.read(datum[:chunk_size])
|
74
|
-
datum[:response_block].call(remaining, remaining.length, content_length)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
else # no block or unexpected status
|
78
|
-
if transfer_encoding_chunked
|
79
|
-
# 2 == "\r\n".length
|
80
77
|
while (chunk_size = socket.readline.chop!.to_i(16)) > 0
|
81
78
|
datum[:response][:body] << socket.read(chunk_size + 2).chop!
|
82
79
|
end
|
83
|
-
|
84
|
-
|
80
|
+
end
|
81
|
+
parse_headers(socket, datum) # merge trailers into headers
|
82
|
+
elsif remaining = content_length
|
83
|
+
if response_block
|
84
|
+
while remaining > 0
|
85
|
+
response_block.call(socket.read([datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length)
|
86
|
+
remaining -= datum[:chunk_size]
|
87
|
+
end
|
88
|
+
else
|
85
89
|
while remaining > 0
|
86
90
|
datum[:response][:body] << socket.read([datum[:chunk_size], remaining].min)
|
87
91
|
remaining -= datum[:chunk_size]
|
88
92
|
end
|
93
|
+
end
|
94
|
+
else
|
95
|
+
if response_block
|
96
|
+
while chunk = socket.read(datum[:chunk_size])
|
97
|
+
response_block.call(chunk, nil, nil)
|
98
|
+
end
|
89
99
|
else
|
90
100
|
datum[:response][:body] << socket.read
|
91
101
|
end
|
@@ -94,6 +104,23 @@ module Excon
|
|
94
104
|
datum
|
95
105
|
end
|
96
106
|
|
107
|
+
def self.parse_headers(socket, datum)
|
108
|
+
last_key = nil
|
109
|
+
until (data = socket.readline.chop!).empty?
|
110
|
+
if !data.lstrip!.nil?
|
111
|
+
raise Excon::Errors::ResponseParseError, 'malformed header' unless last_key
|
112
|
+
# append to last_key's last value
|
113
|
+
datum[:response][:headers][last_key] << ' ' << data.rstrip
|
114
|
+
else
|
115
|
+
key, value = data.split(':', 2)
|
116
|
+
raise Excon::Errors::ResponseParseError, 'malformed header' unless value
|
117
|
+
# add key/value or append value to existing values
|
118
|
+
datum[:response][:headers][key] = ([datum[:response][:headers][key]] << value.strip).compact.join(', ')
|
119
|
+
last_key = key
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
97
124
|
def initialize(params={})
|
98
125
|
@data = {
|
99
126
|
:body => '',
|
data/lib/excon/socket.rb
CHANGED
@@ -200,11 +200,11 @@ module Excon
|
|
200
200
|
@socket = socket
|
201
201
|
break
|
202
202
|
rescue SystemCallError => exception
|
203
|
-
socket.close
|
203
|
+
socket.close rescue nil
|
204
204
|
next
|
205
205
|
end
|
206
206
|
rescue SystemCallError => exception
|
207
|
-
socket.close if socket
|
207
|
+
socket.close rescue nil if socket
|
208
208
|
next
|
209
209
|
end
|
210
210
|
end
|
data/lib/excon/ssl_socket.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
module Excon
|
2
2
|
class SSLSocket < Socket
|
3
3
|
|
4
|
+
HAVE_NONBLOCK = [:connect_nonblock, :read_nonblock, :write_nonblock].all? {|m|
|
5
|
+
OpenSSL::SSL::SSLSocket.public_method_defined?(m)
|
6
|
+
}
|
7
|
+
|
4
8
|
def initialize(data = {})
|
5
9
|
super
|
6
10
|
|
@@ -87,15 +91,9 @@ module Excon
|
|
87
91
|
|
88
92
|
private
|
89
93
|
|
90
|
-
def check_nonblock_support
|
91
|
-
# backwards compatability for things lacking nonblock
|
92
|
-
if !DEFAULT_NONBLOCK && @nonblock
|
93
|
-
@nonblock = false
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
94
|
def connect
|
98
|
-
|
95
|
+
# backwards compatability for things lacking nonblock
|
96
|
+
@nonblock = HAVE_NONBLOCK && @nonblock
|
99
97
|
super
|
100
98
|
end
|
101
99
|
|
data/lib/excon/unix_socket.rb
CHANGED
@@ -4,18 +4,34 @@ module Excon
|
|
4
4
|
private
|
5
5
|
|
6
6
|
def connect
|
7
|
-
|
8
|
-
|
9
|
-
rescue Errno::ECONNREFUSED
|
10
|
-
@socket.close if @socket
|
11
|
-
raise
|
12
|
-
end
|
7
|
+
@socket = ::Socket.new(::Socket::AF_UNIX, ::Socket::SOCK_STREAM, 0)
|
8
|
+
sockaddr = ::Socket.sockaddr_un(@data[:socket])
|
13
9
|
|
14
|
-
if @
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
if @nonblock
|
11
|
+
begin
|
12
|
+
@socket.connect_nonblock(sockaddr)
|
13
|
+
rescue Errno::EINPROGRESS
|
14
|
+
unless IO.select(nil, [@socket], nil, @data[:connect_timeout])
|
15
|
+
raise(Excon::Errors::Timeout.new("connect timeout reached"))
|
16
|
+
end
|
17
|
+
begin
|
18
|
+
@socket.connect_nonblock(sockaddr)
|
19
|
+
rescue Errno::EISCONN
|
20
|
+
end
|
21
|
+
end
|
22
|
+
else
|
23
|
+
begin
|
24
|
+
Timeout.timeout(@data[:connect_timeout]) do
|
25
|
+
@socket.connect(sockaddr)
|
26
|
+
end
|
27
|
+
rescue Timeout::Error
|
28
|
+
raise Excon::Errors::Timeout.new('connect timeout reached')
|
29
|
+
end
|
18
30
|
end
|
31
|
+
|
32
|
+
rescue => error
|
33
|
+
@socket.close rescue nil if @socket
|
34
|
+
raise error
|
19
35
|
end
|
20
36
|
|
21
37
|
end
|
data/lib/excon/utils.rb
CHANGED
@@ -53,5 +53,21 @@ module Excon
|
|
53
53
|
end
|
54
54
|
str
|
55
55
|
end
|
56
|
+
|
57
|
+
# Splits a header value +str+ according to HTTP specification.
|
58
|
+
def split_header_value(str)
|
59
|
+
return [] if str.nil?
|
60
|
+
WEBrick::HTTPUtils.split_header_value(str.strip)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Unescapes HTTP reserved and unwise characters in +str+
|
64
|
+
def unescape_uri(str)
|
65
|
+
WEBrick::HTTPUtils.unescape(str)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Unescape form encoded values in +str+
|
69
|
+
def unescape_form(str)
|
70
|
+
WEBrick::HTTPUtils.unescape_form(str)
|
71
|
+
end
|
56
72
|
end
|
57
73
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
Shindo.tests("Excon support for middlewares that return canned responses") do
|
2
|
+
the_body = "canned"
|
3
|
+
|
4
|
+
canned_response_middleware = Class.new(Excon::Middleware::Base) do
|
5
|
+
define_method :request_call do |params|
|
6
|
+
params[:response] = {
|
7
|
+
:body => the_body,
|
8
|
+
:headers => {},
|
9
|
+
:status => 200
|
10
|
+
}
|
11
|
+
super(params)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
connection = Excon.new(
|
16
|
+
'http://some-host.com/some-path',
|
17
|
+
:method => :get,
|
18
|
+
:middlewares => [canned_response_middleware] + Excon.defaults[:middlewares],
|
19
|
+
:response_block => Proc.new { } # to force streaming
|
20
|
+
)
|
21
|
+
|
22
|
+
tests('does not mutate the canned response body').returns("canned") do
|
23
|
+
connection.request
|
24
|
+
the_body
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -1,23 +1,144 @@
|
|
1
|
-
Shindo.tests('Excon
|
1
|
+
Shindo.tests('Excon Decompress Middleware') do
|
2
2
|
env_init
|
3
3
|
|
4
|
-
|
4
|
+
with_server('good') do
|
5
|
+
|
5
6
|
connection = Excon.new(
|
6
|
-
'http://127.0.0.1:9292/echo',
|
7
|
-
:
|
8
|
-
:
|
9
|
-
:middlewares
|
7
|
+
'http://127.0.0.1:9292/echo/content-encoded',
|
8
|
+
:method => :post,
|
9
|
+
:body => 'hello world',
|
10
|
+
:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]
|
10
11
|
)
|
11
12
|
|
12
|
-
tests('
|
13
|
-
|
14
|
-
|
13
|
+
tests('gzip') do
|
14
|
+
resp = connection.request(
|
15
|
+
:headers => { 'Accept-Encoding' => 'gzip, deflate;q=0' }
|
16
|
+
)
|
17
|
+
|
18
|
+
tests('response body decompressed').returns('hello world') do
|
19
|
+
resp[:body]
|
20
|
+
end
|
21
|
+
|
22
|
+
tests('server sent content-encoding').returns('gzip') do
|
23
|
+
resp[:headers]['Content-Encoding-Sent']
|
24
|
+
end
|
25
|
+
|
26
|
+
tests('removes processed encoding from header').returns('') do
|
27
|
+
resp[:headers]['Content-Encoding']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
tests('deflate') do
|
32
|
+
resp = connection.request(
|
33
|
+
:headers => { 'Accept-Encoding' => 'gzip;q=0, deflate' }
|
34
|
+
)
|
35
|
+
|
36
|
+
tests('response body decompressed').returns('hello world') do
|
37
|
+
resp[:body]
|
38
|
+
end
|
39
|
+
|
40
|
+
tests('server sent content-encoding').returns('deflate') do
|
41
|
+
resp[:headers]['Content-Encoding-Sent']
|
42
|
+
end
|
43
|
+
|
44
|
+
tests('removes processed encoding from header').returns('') do
|
45
|
+
resp[:headers]['Content-Encoding']
|
46
|
+
end
|
15
47
|
end
|
16
48
|
|
17
|
-
tests('
|
18
|
-
|
19
|
-
|
49
|
+
tests('with pre-encoding') do
|
50
|
+
resp = connection.request(
|
51
|
+
:headers => { 'Accept-Encoding' => 'gzip, deflate;q=0',
|
52
|
+
'Content-Encoding-Pre' => 'other' }
|
53
|
+
)
|
54
|
+
|
55
|
+
tests('server sent content-encoding').returns('other, gzip') do
|
56
|
+
resp[:headers]['Content-Encoding-Sent']
|
57
|
+
end
|
58
|
+
|
59
|
+
tests('processed encoding removed from header').returns('other') do
|
60
|
+
resp[:headers]['Content-Encoding']
|
61
|
+
end
|
62
|
+
|
63
|
+
tests('response body decompressed').returns('hello world') do
|
64
|
+
resp[:body]
|
65
|
+
end
|
66
|
+
|
20
67
|
end
|
68
|
+
|
69
|
+
tests('with post-encoding') do
|
70
|
+
resp = connection.request(
|
71
|
+
:headers => { 'Accept-Encoding' => 'gzip, deflate;q=0',
|
72
|
+
'Content-Encoding-Post' => 'other' }
|
73
|
+
)
|
74
|
+
|
75
|
+
tests('server sent content-encoding').returns('gzip, other') do
|
76
|
+
resp[:headers]['Content-Encoding-Sent']
|
77
|
+
end
|
78
|
+
|
79
|
+
tests('unprocessed since last applied is unknown').returns('gzip, other') do
|
80
|
+
resp[:headers]['Content-Encoding']
|
81
|
+
end
|
82
|
+
|
83
|
+
tests('response body still compressed').returns('hello world') do
|
84
|
+
Zlib::GzipReader.new(StringIO.new(resp[:body])).read
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
tests('with a :response_block') do
|
90
|
+
resp = nil
|
91
|
+
captures = capture_response_block do |block|
|
92
|
+
resp = connection.request(
|
93
|
+
:headers => { 'Accept-Encoding' => 'gzip'},
|
94
|
+
:response_block => block
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
tests('server sent content-encoding').returns('gzip') do
|
99
|
+
resp[:headers]['Content-Encoding-Sent']
|
100
|
+
end
|
101
|
+
|
102
|
+
tests('unprocessed since :response_block was used').returns('gzip') do
|
103
|
+
resp[:headers]['Content-Encoding']
|
104
|
+
end
|
105
|
+
|
106
|
+
tests(':response_block passed unprocessed data').returns('hello world') do
|
107
|
+
body = captures.map {|capture| capture[0] }.join
|
108
|
+
Zlib::GzipReader.new(StringIO.new(body)).read
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
tests('adds Accept-Encoding if needed') do
|
114
|
+
|
115
|
+
tests('without a :response_block').returns('deflate, gzip') do
|
116
|
+
resp = Excon.post(
|
117
|
+
'http://127.0.0.1:9292/echo/request',
|
118
|
+
:body => 'hello world',
|
119
|
+
:middlewares => Excon.defaults[:middlewares] +
|
120
|
+
[Excon::Middleware::Decompress]
|
121
|
+
)
|
122
|
+
request = Marshal.load(resp.body)
|
123
|
+
request[:headers]['Accept-Encoding']
|
124
|
+
end
|
125
|
+
|
126
|
+
tests('with a :response_block').returns(nil) do
|
127
|
+
captures = capture_response_block do |block|
|
128
|
+
resp = Excon.post(
|
129
|
+
'http://127.0.0.1:9292/echo/request',
|
130
|
+
:body => 'hello world',
|
131
|
+
:response_block => block,
|
132
|
+
:middlewares => Excon.defaults[:middlewares] +
|
133
|
+
[Excon::Middleware::Decompress]
|
134
|
+
)
|
135
|
+
end
|
136
|
+
request = Marshal.load(captures.map {|capture| capture[0] }.join)
|
137
|
+
request[:headers]['Accept-Encoding']
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
21
142
|
end
|
22
143
|
|
23
144
|
env_restore
|