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.

@@ -30,3 +30,36 @@ Shindo.tests('Excon redirector support') do
30
30
 
31
31
  env_restore
32
32
  end
33
+
34
+ Shindo.tests('Excon redirect support for relative Location headers') do
35
+ env_init
36
+
37
+ connection = Excon.new(
38
+ 'http://127.0.0.1:9292',
39
+ :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower],
40
+ :mock => true
41
+ )
42
+
43
+ Excon.stub(
44
+ { :path => '/old' },
45
+ {
46
+ :headers => { 'Location' => '/new' },
47
+ :body => 'old',
48
+ :status => 301
49
+ }
50
+ )
51
+
52
+ Excon.stub(
53
+ { :path => '/new' },
54
+ {
55
+ :body => 'new',
56
+ :status => 200
57
+ }
58
+ )
59
+
60
+ tests("request(:method => :get, :path => '/old').body").returns('new') do
61
+ connection.request(:method => :get, :path => '/old').body
62
+ end
63
+
64
+ env_restore
65
+ end
@@ -42,4 +42,32 @@ Shindo.tests('requests should succeed') do
42
42
  end
43
43
  end
44
44
  end
45
+
46
+ with_server('good') do
47
+
48
+ tests('sets transfer-coding and connection options') do
49
+
50
+ tests('without a :response_block') do
51
+ request = Marshal.load(
52
+ Excon.get('http://127.0.0.1:9292/echo/request').body
53
+ )
54
+ returns('trailers, deflate, gzip') { request[:headers]['TE'] }
55
+ returns('TE') { request[:headers]['Connection'] }
56
+ end
57
+
58
+ tests('with a :response_block') do
59
+ captures = capture_response_block do |block|
60
+ Excon.get('http://127.0.0.1:9292/echo/request',
61
+ :response_block => block)
62
+ end
63
+ data = captures.map {|capture| capture[0] }.join
64
+ request = Marshal.load(data)
65
+
66
+ returns('trailers') { request[:headers]['TE'] }
67
+ returns('TE') { request[:headers]['Connection'] }
68
+ end
69
+
70
+ end
71
+
72
+ end
45
73
  end
@@ -0,0 +1,220 @@
1
+ Shindo.tests('Excon Response Parsing') do
2
+ env_init
3
+
4
+ with_server('good') do
5
+
6
+ tests('responses with chunked transfer-encoding') do
7
+
8
+ tests('simple response').returns('hello world') do
9
+ Excon.get('http://127.0.0.1:9292/chunked/simple').body
10
+ end
11
+
12
+ tests('with :response_block') do
13
+
14
+ tests('simple response').
15
+ returns([['hello ', nil, nil], ['world', nil, nil]]) do
16
+ capture_response_block do |block|
17
+ Excon.get('http://127.0.0.1:9292/chunked/simple',
18
+ :response_block => block,
19
+ :chunk_size => 5) # not used
20
+ end
21
+ end
22
+
23
+ tests('with expected response status').
24
+ returns([['hello ', nil, nil], ['world', nil, nil]]) do
25
+ capture_response_block do |block|
26
+ Excon.get('http://127.0.0.1:9292/chunked/simple',
27
+ :response_block => block,
28
+ :expects => 200)
29
+ end
30
+ end
31
+
32
+ tests('with unexpected response status').returns('hello world') do
33
+ begin
34
+ Excon.get('http://127.0.0.1:9292/chunked/simple',
35
+ :response_block => Proc.new { raise 'test failed' },
36
+ :expects => 500)
37
+ rescue Excon::Errors::HTTPStatusError => err
38
+ err.response[:body]
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ tests('merges trailers into headers').
45
+ returns('one, two, three, four, five, six') do
46
+ Excon.get('http://127.0.0.1:9292/chunked/trailers').headers['Test-Header']
47
+ end
48
+
49
+ tests("removes 'chunked' from Transfer-Encoding").returns('') do
50
+ Excon.get('http://127.0.0.1:9292/chunked/simple').headers['Transfer-Encoding']
51
+ end
52
+
53
+ end
54
+
55
+ tests('responses with content-length') do
56
+
57
+ tests('simple response').returns('hello world') do
58
+ Excon.get('http://127.0.0.1:9292/content-length/simple').body
59
+ end
60
+
61
+ tests('with :response_block') do
62
+
63
+ tests('simple response').
64
+ returns([['hello', 6, 11], [' worl', 1, 11], ['d', 0, 11]]) do
65
+ capture_response_block do |block|
66
+ Excon.get('http://127.0.0.1:9292/content-length/simple',
67
+ :response_block => block,
68
+ :chunk_size => 5)
69
+ end
70
+ end
71
+
72
+ tests('with expected response status').
73
+ returns([['hello', 6, 11], [' worl', 1, 11], ['d', 0, 11]]) do
74
+ capture_response_block do |block|
75
+ Excon.get('http://127.0.0.1:9292/content-length/simple',
76
+ :response_block => block,
77
+ :chunk_size => 5,
78
+ :expects => 200)
79
+ end
80
+ end
81
+
82
+ tests('with unexpected response status').returns('hello world') do
83
+ begin
84
+ Excon.get('http://127.0.0.1:9292/content-length/simple',
85
+ :response_block => Proc.new { raise 'test failed' },
86
+ :chunk_size => 5,
87
+ :expects => 500)
88
+ rescue Excon::Errors::HTTPStatusError => err
89
+ err.response[:body]
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ end
96
+
97
+ tests('responses with unknown length') do
98
+
99
+ tests('simple response').returns('hello world') do
100
+ Excon.get('http://127.0.0.1:9292/unknown/simple').body
101
+ end
102
+
103
+ tests('with :response_block') do
104
+
105
+ tests('simple response').
106
+ returns([['hello', nil, nil], [' worl', nil, nil], ['d', nil, nil]]) do
107
+ capture_response_block do |block|
108
+ Excon.get('http://127.0.0.1:9292/unknown/simple',
109
+ :response_block => block,
110
+ :chunk_size => 5)
111
+ end
112
+ end
113
+
114
+ tests('with expected response status').
115
+ returns([['hello', nil, nil], [' worl', nil, nil], ['d', nil, nil]]) do
116
+ capture_response_block do |block|
117
+ Excon.get('http://127.0.0.1:9292/unknown/simple',
118
+ :response_block => block,
119
+ :chunk_size => 5,
120
+ :expects => 200)
121
+ end
122
+ end
123
+
124
+ tests('with unexpected response status').returns('hello world') do
125
+ begin
126
+ Excon.get('http://127.0.0.1:9292/unknown/simple',
127
+ :response_block => Proc.new { raise 'test failed' },
128
+ :chunk_size => 5,
129
+ :expects => 500)
130
+ rescue Excon::Errors::HTTPStatusError => err
131
+ err.response[:body]
132
+ end
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ tests('header continuation') do
140
+
141
+ tests('proper continuation').returns('one, two, three, four, five, six') do
142
+ resp = Excon.get('http://127.0.0.1:9292/unknown/header_continuation')
143
+ resp.headers['Test-Header']
144
+ end
145
+
146
+ tests('malformed header').raises(Excon::Errors::SocketError) do
147
+ Excon.get('http://127.0.0.1:9292/bad/malformed_header')
148
+ end
149
+
150
+ tests('malformed header continuation').raises(Excon::Errors::SocketError) do
151
+ Excon.get('http://127.0.0.1:9292/bad/malformed_header_continuation')
152
+ end
153
+
154
+ end
155
+
156
+ tests('Transfer-Encoding') do
157
+
158
+ tests('used with chunked response') do
159
+ resp = Excon.post(
160
+ 'http://127.0.0.1:9292/echo/transfer-encoded/chunked',
161
+ :body => 'hello world'
162
+ )
163
+
164
+ tests('server sent transfer-encoding').returns('gzip, chunked') do
165
+ resp[:headers]['Transfer-Encoding-Sent']
166
+ end
167
+
168
+ tests('processed encodings removed from header').returns('') do
169
+ resp[:headers]['Transfer-Encoding']
170
+ end
171
+
172
+ tests('response body decompressed').returns('hello world') do
173
+ resp[:body]
174
+ end
175
+ end
176
+
177
+ tests('used with non-chunked response') do
178
+ resp = Excon.post(
179
+ 'http://127.0.0.1:9292/echo/transfer-encoded',
180
+ :body => 'hello world'
181
+ )
182
+
183
+ tests('server sent transfer-encoding').returns('gzip') do
184
+ resp[:headers]['Transfer-Encoding-Sent']
185
+ end
186
+
187
+ tests('processed encoding removed from header').returns('') do
188
+ resp[:headers]['Transfer-Encoding']
189
+ end
190
+
191
+ tests('response body decompressed').returns('hello world') do
192
+ resp[:body]
193
+ end
194
+ end
195
+
196
+ # sends TE header without gzip/deflate accepted (see requests_tests)
197
+ tests('with a :response_block') do
198
+ resp = nil
199
+ captures = capture_response_block do |block|
200
+ resp = Excon.post('http://127.0.0.1:9292/echo/transfer-encoded/chunked',
201
+ :body => 'hello world',
202
+ :response_block => block)
203
+ end
204
+
205
+ tests('server does not compress').returns('chunked') do
206
+ resp[:headers]['Transfer-Encoding-Sent']
207
+ end
208
+
209
+ tests('block receives uncompressed response').returns('hello world') do
210
+ captures.map {|capture| capture[0] }.join
211
+ end
212
+
213
+ end
214
+
215
+ end
216
+
217
+ end
218
+
219
+ env_restore
220
+ end
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'eventmachine'
4
+ require 'stringio'
5
+ require 'uri'
6
+ require 'zlib'
7
+
8
+ module GoodServer
9
+ # This method will be called with each request received.
10
+ #
11
+ # request = {
12
+ # :method => method,
13
+ # :uri => URI.parse(uri),
14
+ # :headers => {},
15
+ # :body => ''
16
+ # }
17
+ #
18
+ # Each connection to this server is persistent unless the client sends
19
+ # "Connection: close" in the request. If a response requires the connection
20
+ # to be closed, it should set `@persistent = false` and send "Connection: close".
21
+ def send_response(request)
22
+ type, path = request[:uri].path.split('/', 3)[1, 2]
23
+ case type
24
+ when 'echo'
25
+ case path
26
+ when 'request'
27
+ data = Marshal.dump(request)
28
+ send_data "HTTP/1.1 200 OK\r\n"
29
+ send_data "Content-Length: #{ data.size }\r\n"
30
+ send_data "\r\n"
31
+ send_data data
32
+
33
+ when /(content|transfer)-encoded\/?(.*)/
34
+ if (encoding_type = $1) == 'content'
35
+ accept_header = 'Accept-Encoding'
36
+ encoding_header = 'Content-Encoding'
37
+ else
38
+ accept_header = 'TE'
39
+ encoding_header = 'Transfer-Encoding'
40
+ end
41
+ chunked = $2 == 'chunked'
42
+
43
+ encodings = parse_encodings(request[:headers][accept_header])
44
+ while encoding = encodings.pop
45
+ break if ['gzip', 'deflate'].include?(encoding)
46
+ end
47
+
48
+ case encoding
49
+ when 'gzip'
50
+ io = (Zlib::GzipWriter.new(StringIO.new) << request[:body]).finish
51
+ io.rewind
52
+ body = io.read
53
+ when 'deflate'
54
+ # drops the zlib header
55
+ deflator = Zlib::Deflate.new(nil, -Zlib::MAX_WBITS)
56
+ body = deflator.deflate(request[:body], Zlib::FINISH)
57
+ deflator.close
58
+ else
59
+ body = request[:body]
60
+ end
61
+
62
+ # simulate server pre/post content encoding
63
+ encodings = [
64
+ request[:headers]["#{ encoding_header }-Pre"],
65
+ encoding,
66
+ request[:headers]["#{ encoding_header }-Post"],
67
+ ]
68
+ if chunked && encoding_type == 'transfer'
69
+ encodings << 'chunked'
70
+ end
71
+ encodings = encodings.compact.join(', ')
72
+
73
+ send_data "HTTP/1.1 200 OK\r\n"
74
+ # let the test know what the server sent
75
+ send_data "#{ encoding_header }-Sent: #{ encodings }\r\n"
76
+ send_data "#{ encoding_header }: #{ encodings }\r\n" unless encodings.empty?
77
+ if chunked
78
+ if encoding_type == 'content'
79
+ send_data "Transfer-Encoding: chunked\r\n"
80
+ end
81
+ send_data "\r\n"
82
+ send_data chunks_for(body)
83
+ send_data "\r\n"
84
+ else
85
+ send_data "Content-Length: #{ body.size }\r\n"
86
+ send_data "\r\n"
87
+ send_data body
88
+ end
89
+ end
90
+
91
+ when 'chunked'
92
+ case path
93
+ when 'simple'
94
+ send_data "HTTP/1.1 200 OK\r\n"
95
+ send_data "Transfer-Encoding: chunked\r\n"
96
+ send_data "\r\n"
97
+ # chunk-extension is currently ignored.
98
+ # this works because "6; chunk-extension".to_i => "6"
99
+ send_data "6; chunk-extension\r\n"
100
+ send_data "hello \r\n"
101
+ send_data "5; chunk-extension\r\n"
102
+ send_data "world\r\n"
103
+ send_data "0; chunk-extension\r\n" # last-chunk
104
+ send_data "\r\n"
105
+
106
+ # merged trailers also support continuations
107
+ when 'trailers'
108
+ send_data "HTTP/1.1 200 OK\r\n"
109
+ send_data "Transfer-Encoding: chunked\r\n"
110
+ send_data "Test-Header: one, two\r\n"
111
+ send_data "\r\n"
112
+ send_data chunks_for('hello world')
113
+ send_data "Test-Header: three, four,\r\n"
114
+ send_data "\tfive, six\r\n"
115
+ send_data "\r\n"
116
+ end
117
+
118
+ when 'content-length'
119
+ case path
120
+ when 'simple'
121
+ send_data "HTTP/1.1 200 OK\r\n"
122
+ send_data "Content-Length: 11\r\n"
123
+ send_data "\r\n"
124
+ send_data "hello world"
125
+ end
126
+
127
+ when 'unknown'
128
+ @persistent = false
129
+ case path
130
+ when 'simple'
131
+ send_data "HTTP/1.1 200 OK\r\n"
132
+ send_data "Connection: close\r\n"
133
+ send_data "\r\n"
134
+ send_data "hello world"
135
+
136
+ when 'header_continuation'
137
+ send_data "HTTP/1.1 200 OK\r\n"
138
+ send_data "Connection: close\r\n"
139
+ send_data "Test-Header: one, two\r\n"
140
+ send_data "Test-Header: three, four,\r\n"
141
+ send_data " five, six\r\n"
142
+ send_data "\r\n"
143
+ send_data "hello world"
144
+ end
145
+
146
+ when 'bad'
147
+ # Excon will close these connections due to the errors.
148
+ case path
149
+ when 'malformed_header'
150
+ send_data "HTTP/1.1 200 OK\r\n"
151
+ send_data "Bad-Header\r\n" # no ':'
152
+ send_data "\r\n"
153
+ send_data "hello world"
154
+
155
+ when 'malformed_header_continuation'
156
+ send_data "HTTP/1.1 200 OK\r\n"
157
+ send_data " Bad-Header: one, two\r\n" # no previous header
158
+ send_data "\r\n"
159
+ send_data "hello world"
160
+ end
161
+ end
162
+
163
+ close_connection(true) unless @persistent
164
+ end
165
+
166
+ def post_init
167
+ @buffer = StringIO.new
168
+ @buffer.set_encoding('BINARY') if @buffer.respond_to?(:set_encoding)
169
+ end
170
+
171
+ # Receives a String of +data+ sent from the client.
172
+ # +data+ may only be a portion of what the client sent.
173
+ # The data is buffered, then processed and removed from the buffer
174
+ # as data becomes available until the @request is complete.
175
+ def receive_data(data)
176
+ @buffer.write(data)
177
+
178
+ parse_headers unless @request
179
+ parse_body if @request
180
+
181
+ if @request_complete
182
+ send_response(@request)
183
+ sync_buffer
184
+ @request = nil
185
+ @request_complete = false
186
+ end
187
+
188
+ @buffer.seek(0, IO::SEEK_END) # wait for more data
189
+ end
190
+
191
+ # Removes the processed portion of the buffer
192
+ # by replacing the buffer with it's contents from the current pos.
193
+ def sync_buffer
194
+ @buffer.string = @buffer.read
195
+ end
196
+
197
+ def parse_headers
198
+ @buffer.rewind
199
+ # wait until buffer contains the end of the headers
200
+ if /\sHTTP\/\d+\.\d+\r\n.*?\r\n\r\n/m =~ @buffer.read
201
+ @buffer.rewind
202
+ # For persistent connections, the buffer could start with the
203
+ # \r\n chunked-message terminator from the previous request.
204
+ # This will discard anything up to the request-line.
205
+ until m = /^(\w+)\s(.*)\sHTTP\/\d+\.\d+$/.match(@buffer.readline.chop!); end
206
+ method, uri = m[1, 2]
207
+
208
+ headers = {}
209
+ last_key = nil
210
+ until (line = @buffer.readline.chop!).empty?
211
+ if !line.lstrip!.nil?
212
+ headers[last_key] << ' ' << line.rstrip
213
+ else
214
+ key, value = line.split(':', 2)
215
+ headers[key] = ([headers[key]] << value.strip).compact.join(', ')
216
+ last_key = key
217
+ end
218
+ end
219
+
220
+ sync_buffer
221
+
222
+ @chunked = headers['Transfer-Encoding'] =~ /chunked/i
223
+ @content_length = headers['Content-Length'].to_i
224
+ @persistent = headers['Connection'] !~ /close/i
225
+ @request = {
226
+ :method => method,
227
+ :uri => URI.parse(uri),
228
+ :headers => headers,
229
+ :body => ''
230
+ }
231
+ end
232
+ end
233
+
234
+ def parse_body
235
+ if @chunked
236
+ @buffer.rewind
237
+ until @request_complete || @buffer.eof?
238
+ unless @chunk_size
239
+ # in case buffer only contains a portion of the chunk-size line
240
+ if (line = @buffer.readline) =~ /\r\n\z/
241
+ @chunk_size = line.to_i(16)
242
+ if @chunk_size > 0
243
+ sync_buffer
244
+ else # last-chunk
245
+ @buffer.read(2) # the final \r\n may or may not be in the buffer
246
+ @chunk_size = nil
247
+ @body_pos = nil
248
+ @request_complete = true
249
+ end
250
+ end
251
+ end
252
+ if @chunk_size
253
+ if @buffer.size >= @chunk_size + 2
254
+ @request[:body] << @buffer.read(@chunk_size + 2).chop!
255
+ @chunk_size = nil
256
+ sync_buffer
257
+ else
258
+ break # wait for more data
259
+ end
260
+ end
261
+ end
262
+ elsif @content_length > 0
263
+ @buffer.rewind
264
+ unless @buffer.eof? # buffer only contained the headers
265
+ @request[:body] << @buffer.read(@content_length - @request[:body].size)
266
+ if @request[:body].size == @content_length
267
+ @request_complete = true
268
+ else
269
+ sync_buffer
270
+ end
271
+ end
272
+ else
273
+ # no body
274
+ @request_complete = true
275
+ end
276
+ end
277
+
278
+ def chunks_for(str)
279
+ chunks = ''
280
+ str.force_encoding('BINARY') if str.respond_to?(:force_encoding)
281
+ chunk_size = str.size / 2
282
+ until (chunk = str.slice!(0, chunk_size)).empty?
283
+ chunks << chunk.size.to_s(16) << "\r\n"
284
+ chunks << chunk << "\r\n"
285
+ end
286
+ chunks << "0\r\n" # last-chunk
287
+ end
288
+
289
+ # only supports a single quality parameter for tokens
290
+ def parse_encodings(encodings)
291
+ return [] if encodings.nil?
292
+ split_header_value(encodings).map do |value|
293
+ token, q_val = /^(.*?)(?:;q=(.*))?$/.match(value.strip)[1, 2]
294
+ if q_val && q_val.to_f == 0
295
+ nil
296
+ else
297
+ [token, (q_val || 1).to_f]
298
+ end
299
+ end.compact.sort_by {|_, q_val| q_val }.map {|token, _| token }
300
+ end
301
+
302
+ # Splits a header value +str+ according to HTTP specification.
303
+ def split_header_value(str)
304
+ return [] if str.nil?
305
+ str.strip.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+)
306
+ (?:,\s*|\Z)'xn).flatten
307
+ end
308
+ end
309
+
310
+ EM.run do
311
+ EM.start_server("127.0.0.1", 9292, GoodServer)
312
+ $stderr.puts "ready"
313
+ end