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.

@@ -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
- case datum[:response][:headers]['Content-Encoding']
7
- when 'deflate'
8
- # assume inflate omits header
9
- datum[:response][:body] = Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(datum[:response][:body])
10
- when 'gzip'
11
- datum[:response][:body] = Zlib::GzipReader.new(StringIO.new(datum[:response][:body])).read
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 => (URI.decode(uri.user) if uri.user),
32
- :password => (URI.decode(uri.password) if uri.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
@@ -41,51 +41,61 @@ module Excon
41
41
  :remote_ip => socket.respond_to?(:remote_ip) && socket.remote_ip
42
42
  }
43
43
 
44
- until ((data = socket.readline).chop!).empty?
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
- # check to see if expects was set and matched
57
- expected_status = !datum.has_key?(:expects) || [*datum[:expects]].include?(datum[:response][:status])
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
- # if expects matched and there is a block, use it
60
- if expected_status && datum.has_key?(:response_block)
61
- if transfer_encoding_chunked
62
- # 2 == "\r\n".length
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
- datum[:response_block].call(socket.read(chunk_size + 2).chop!, nil, nil)
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
- socket.read(2) # empty chunk-body
84
- elsif remaining = content_length
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
@@ -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
- check_nonblock_support
95
+ # backwards compatability for things lacking nonblock
96
+ @nonblock = HAVE_NONBLOCK && @nonblock
99
97
  super
100
98
  end
101
99
 
@@ -4,18 +4,34 @@ module Excon
4
4
  private
5
5
 
6
6
  def connect
7
- begin
8
- @socket = ::UNIXSocket.new(@data[:socket])
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 @data[:tcp_nodelay]
15
- @socket.setsockopt(::Socket::IPPROTO_TCP,
16
- ::Socket::TCP_NODELAY,
17
- true)
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 decompression support') do
1
+ Shindo.tests('Excon Decompress Middleware') do
2
2
  env_init
3
3
 
4
- with_rackup('deflater.ru') do
4
+ with_server('good') do
5
+
5
6
  connection = Excon.new(
6
- 'http://127.0.0.1:9292/echo',
7
- :body => 'x' * 100,
8
- :method => :post,
9
- :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]
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('deflate').returns('x' * 100) do
13
- response = connection.request(:headers => { 'Accept-Encoding' => 'deflate' })
14
- response.body
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('gzip').returns('x' * 100) do
18
- response = connection.request(:headers => { 'Accept-Encoding' => 'gzip' })
19
- response.body
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