em-http-request-samesite 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +0 -0
  5. data/.travis.yml +7 -0
  6. data/Changelog.md +68 -0
  7. data/Gemfile +14 -0
  8. data/README.md +63 -0
  9. data/Rakefile +10 -0
  10. data/benchmarks/clients.rb +170 -0
  11. data/benchmarks/em-excon.rb +87 -0
  12. data/benchmarks/em-profile.gif +0 -0
  13. data/benchmarks/em-profile.txt +65 -0
  14. data/benchmarks/server.rb +48 -0
  15. data/em-http-request.gemspec +32 -0
  16. data/examples/.gitignore +1 -0
  17. data/examples/digest_auth/client.rb +25 -0
  18. data/examples/digest_auth/server.rb +28 -0
  19. data/examples/fetch.rb +30 -0
  20. data/examples/fibered-http.rb +51 -0
  21. data/examples/multi.rb +25 -0
  22. data/examples/oauth-tweet.rb +35 -0
  23. data/examples/socks5.rb +23 -0
  24. data/lib/em-http-request.rb +1 -0
  25. data/lib/em-http.rb +20 -0
  26. data/lib/em-http/client.rb +341 -0
  27. data/lib/em-http/core_ext/bytesize.rb +6 -0
  28. data/lib/em-http/decoders.rb +252 -0
  29. data/lib/em-http/http_client_options.rb +49 -0
  30. data/lib/em-http/http_connection.rb +321 -0
  31. data/lib/em-http/http_connection_options.rb +70 -0
  32. data/lib/em-http/http_encoding.rb +149 -0
  33. data/lib/em-http/http_header.rb +83 -0
  34. data/lib/em-http/http_status_codes.rb +57 -0
  35. data/lib/em-http/middleware/digest_auth.rb +112 -0
  36. data/lib/em-http/middleware/json_response.rb +15 -0
  37. data/lib/em-http/middleware/oauth.rb +40 -0
  38. data/lib/em-http/middleware/oauth2.rb +28 -0
  39. data/lib/em-http/multi.rb +57 -0
  40. data/lib/em-http/request.rb +23 -0
  41. data/lib/em-http/version.rb +5 -0
  42. data/lib/em/io_streamer.rb +49 -0
  43. data/spec/client_fiber_spec.rb +23 -0
  44. data/spec/client_spec.rb +1000 -0
  45. data/spec/digest_auth_spec.rb +48 -0
  46. data/spec/dns_spec.rb +41 -0
  47. data/spec/encoding_spec.rb +49 -0
  48. data/spec/external_spec.rb +150 -0
  49. data/spec/fixtures/google.ca +16 -0
  50. data/spec/fixtures/gzip-sample.gz +0 -0
  51. data/spec/gzip_spec.rb +91 -0
  52. data/spec/helper.rb +31 -0
  53. data/spec/http_proxy_spec.rb +268 -0
  54. data/spec/middleware/oauth2_spec.rb +15 -0
  55. data/spec/middleware_spec.rb +143 -0
  56. data/spec/multi_spec.rb +104 -0
  57. data/spec/pipelining_spec.rb +66 -0
  58. data/spec/redirect_spec.rb +430 -0
  59. data/spec/socksify_proxy_spec.rb +60 -0
  60. data/spec/spec_helper.rb +25 -0
  61. data/spec/ssl_spec.rb +71 -0
  62. data/spec/stallion.rb +334 -0
  63. data/spec/stub_server.rb +45 -0
  64. metadata +265 -0
@@ -0,0 +1,15 @@
1
+ require 'multi_json'
2
+
3
+ module EventMachine
4
+ module Middleware
5
+ class JSONResponse
6
+ def response(resp)
7
+ begin
8
+ body = MultiJson.load(resp.response)
9
+ resp.response = body
10
+ rescue => e
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,40 @@
1
+ require 'simple_oauth'
2
+
3
+ module EventMachine
4
+ module Middleware
5
+
6
+ class OAuth
7
+ include HttpEncoding
8
+
9
+ def initialize(opts = {})
10
+ @opts = opts.dup
11
+ # Allow both `oauth` gem and `simple_oauth` gem opts formats
12
+ @opts[:token] ||= @opts.delete(:access_token)
13
+ @opts[:token_secret] ||= @opts.delete(:access_token_secret)
14
+ end
15
+
16
+ def request(client, head, body)
17
+ request = client.req
18
+ uri = request.uri.join(encode_query(request.uri, request.query))
19
+ params = {}
20
+
21
+ # from https://github.com/oauth/oauth-ruby/blob/master/lib/oauth/request_proxy/em_http_request.rb
22
+ if ["POST", "PUT"].include?(request.method)
23
+ head["content-type"] ||= "application/x-www-form-urlencoded" if body.is_a? Hash
24
+ form_encoded = head["content-type"].to_s.downcase.start_with?("application/x-www-form-urlencoded")
25
+
26
+ if form_encoded
27
+ CGI.parse(client.normalize_body(body)).each do |k,v|
28
+ # Since `CGI.parse` always returns values as an array
29
+ params[k] = v.size == 1 ? v.first : v
30
+ end
31
+ end
32
+ end
33
+
34
+ head["Authorization"] = SimpleOAuth::Header.new(request.method, uri, params, @opts)
35
+
36
+ [head,body]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ module EventMachine
2
+ module Middleware
3
+ class OAuth2
4
+ include EM::HttpEncoding
5
+ attr_accessor :access_token
6
+
7
+ def initialize(opts={})
8
+ self.access_token = opts[:access_token] or raise "No :access_token provided"
9
+ end
10
+
11
+ def request(client, head, body)
12
+ uri = client.req.uri.dup
13
+ update_uri! uri
14
+ client.req.set_uri uri
15
+
16
+ [head, body]
17
+ end
18
+
19
+ def update_uri!(uri)
20
+ if uri.query.nil?
21
+ uri.query = encode_param(:access_token, access_token)
22
+ else
23
+ uri.query += "&#{encode_param(:access_token, access_token)}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ module EventMachine
2
+
3
+ # EventMachine based Multi request client, based on a streaming HTTPRequest class,
4
+ # which allows you to open multiple parallel connections and return only when all
5
+ # of them finish. (i.e. ideal for parallelizing workloads)
6
+ #
7
+ # == Example
8
+ #
9
+ # EventMachine.run {
10
+ #
11
+ # multi = EventMachine::MultiRequest.new
12
+ #
13
+ # # add multiple requests to the multi-handler
14
+ # multi.add(:a, EventMachine::HttpRequest.new('http://www.google.com/').get)
15
+ # multi.add(:b, EventMachine::HttpRequest.new('http://www.yahoo.com/').get)
16
+ #
17
+ # multi.callback {
18
+ # p multi.responses[:callback]
19
+ # p multi.responses[:errback]
20
+ #
21
+ # EventMachine.stop
22
+ # }
23
+ # }
24
+ #
25
+
26
+ class MultiRequest
27
+ include EventMachine::Deferrable
28
+
29
+ attr_reader :requests, :responses
30
+
31
+ def initialize
32
+ @requests = {}
33
+ @responses = {:callback => {}, :errback => {}}
34
+ end
35
+
36
+ def add(name, conn)
37
+ raise 'Duplicate Multi key' if @requests.key? name
38
+
39
+ @requests[name] = conn
40
+
41
+ conn.callback { @responses[:callback][name] = conn; check_progress }
42
+ conn.errback { @responses[:errback][name] = conn; check_progress }
43
+ end
44
+
45
+ def finished?
46
+ (@responses[:callback].size + @responses[:errback].size) == @requests.size
47
+ end
48
+
49
+ protected
50
+
51
+ # invoke callback if all requests have completed
52
+ def check_progress
53
+ succeed(self) if finished?
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,23 @@
1
+ module EventMachine
2
+ class HttpRequest
3
+ @middleware = []
4
+
5
+ def self.new(uri, options={})
6
+ uri = uri.clone
7
+ connopt = HttpConnectionOptions.new(uri, options)
8
+
9
+ c = HttpConnection.new
10
+ c.connopts = connopt
11
+ c.uri = uri
12
+ c
13
+ end
14
+
15
+ def self.use(klass, *args, &block)
16
+ @middleware << klass.new(*args, &block)
17
+ end
18
+
19
+ def self.middleware
20
+ @middleware
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ class HttpRequest
3
+ VERSION = "1.1.7"
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ require 'em/streamer'
2
+
3
+ # similar to EventMachine::FileStreamer, but for any IO object
4
+ module EventMachine
5
+ class IOStreamer
6
+ include Deferrable
7
+ CHUNK_SIZE = 16384
8
+
9
+ # @param [EventMachine::Connection] connection
10
+ # @param [IO] io Data source
11
+ # @param [Integer] Data size
12
+ #
13
+ # @option opts [Boolean] :http_chunks (false) Use HTTP 1.1 style chunked-encoding semantics.
14
+ def initialize(connection, io, opts = {})
15
+ @connection = connection
16
+ @io = io
17
+ @http_chunks = opts[:http_chunks]
18
+
19
+ @buff = String.new
20
+ @io.binmode if @io.respond_to?(:binmode)
21
+ stream_one_chunk
22
+ end
23
+
24
+ private
25
+
26
+ # Used internally to stream one chunk at a time over multiple reactor ticks
27
+ # @private
28
+ def stream_one_chunk
29
+ loop do
30
+ if @io.eof?
31
+ @connection.send_data "0\r\n\r\n" if @http_chunks
32
+ succeed
33
+ break
34
+ end
35
+
36
+ if @connection.respond_to?(:get_outbound_data_size) && (@connection.get_outbound_data_size > FileStreamer::BackpressureLevel)
37
+ EventMachine::next_tick { stream_one_chunk }
38
+ break
39
+ end
40
+
41
+ if @io.read(CHUNK_SIZE, @buff)
42
+ @connection.send_data("#{@buff.length.to_s(16)}\r\n") if @http_chunks
43
+ @connection.send_data(@buff)
44
+ @connection.send_data("\r\n") if @http_chunks
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,23 @@
1
+ require 'helper'
2
+ require 'fiber'
3
+
4
+ describe EventMachine::HttpRequest do
5
+ context "with fibers" do
6
+
7
+ it "should be transparent to connection errors" do
8
+ EventMachine.run do
9
+ Fiber.new do
10
+ f = Fiber.current
11
+ fired = false
12
+ http = EventMachine::HttpRequest.new('http://non-existing.domain/', :connection_timeout => 0.1).get
13
+ http.callback { failed(http) }
14
+ http.errback { f.resume :errback }
15
+
16
+ Fiber.yield.should == :errback
17
+ EM.stop
18
+ end.resume
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,1000 @@
1
+ require 'helper'
2
+
3
+ describe EventMachine::HttpRequest do
4
+
5
+ def failed(http=nil)
6
+ EventMachine.stop
7
+ http ? fail(http.error) : fail
8
+ end
9
+
10
+ it "should perform successful GET" do
11
+ EventMachine.run {
12
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
13
+
14
+ http.errback { failed(http) }
15
+ http.callback {
16
+ http.response_header.status.should == 200
17
+ http.response.should match(/Hello/)
18
+ EventMachine.stop
19
+ }
20
+ }
21
+ end
22
+
23
+ it "should perform successful GET with a URI passed as argument" do
24
+ EventMachine.run {
25
+ uri = URI.parse('http://127.0.0.1:8090/')
26
+ http = EventMachine::HttpRequest.new(uri).get
27
+
28
+ http.errback { failed(http) }
29
+ http.callback {
30
+ http.response_header.status.should == 200
31
+ http.response.should match(/Hello/)
32
+ EventMachine.stop
33
+ }
34
+ }
35
+ end
36
+
37
+ it "should succeed GET on missing path" do
38
+ EventMachine.run {
39
+ lambda {
40
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090').get
41
+ http.callback {
42
+ http.response.should match(/Hello/)
43
+ EventMachine.stop
44
+ }
45
+ }.should_not raise_error
46
+
47
+ }
48
+ end
49
+
50
+ it "should raise error on invalid URL" do
51
+ EventMachine.run {
52
+ lambda {
53
+ EventMachine::HttpRequest.new('random?text').get
54
+ }.should raise_error(Addressable::URI::InvalidURIError)
55
+
56
+ EM.stop
57
+ }
58
+ end
59
+
60
+ it "should perform successful HEAD with a URI passed as argument" do
61
+ EventMachine.run {
62
+ uri = URI.parse('http://127.0.0.1:8090/')
63
+ http = EventMachine::HttpRequest.new(uri).head
64
+
65
+ http.errback { failed(http) }
66
+ http.callback {
67
+ http.response_header.status.should == 200
68
+ http.response.should == ""
69
+ EventMachine.stop
70
+ }
71
+ }
72
+ end
73
+
74
+ it "should perform successful DELETE with a URI passed as argument" do
75
+ EventMachine.run {
76
+ uri = URI.parse('http://127.0.0.1:8090/')
77
+ http = EventMachine::HttpRequest.new(uri).delete
78
+
79
+ http.errback { failed(http) }
80
+ http.callback {
81
+ http.response_header.status.should == 200
82
+ http.response.should == ""
83
+ EventMachine.stop
84
+ }
85
+ }
86
+ end
87
+
88
+ it "should return 404 on invalid path" do
89
+ EventMachine.run {
90
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/fail').get
91
+
92
+ http.errback { failed(http) }
93
+ http.callback {
94
+ http.response_header.status.should == 404
95
+ EventMachine.stop
96
+ }
97
+ }
98
+ end
99
+
100
+ it "should return HTTP reason" do
101
+ EventMachine.run {
102
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/fail').get
103
+
104
+ http.errback { failed(http) }
105
+ http.callback {
106
+ http.response_header.status.should == 404
107
+ http.response_header.http_reason.should == 'Not Found'
108
+ EventMachine.stop
109
+ }
110
+ }
111
+ end
112
+
113
+ it "should return HTTP reason 'unknown' on a non-standard status code" do
114
+ EventMachine.run {
115
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/fail_with_nonstandard_response').get
116
+
117
+ http.errback { failed(http) }
118
+ http.callback {
119
+ http.response_header.status.should == 420
120
+ http.response_header.http_reason.should == 'unknown'
121
+ EventMachine.stop
122
+ }
123
+ }
124
+ end
125
+
126
+ it "should build query parameters from Hash" do
127
+ EventMachine.run {
128
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :query => {:q => 'test'}
129
+
130
+ http.errback { failed(http) }
131
+ http.callback {
132
+ http.response_header.status.should == 200
133
+ http.response.should match(/test/)
134
+ EventMachine.stop
135
+ }
136
+ }
137
+ end
138
+
139
+ it "should pass query parameters string" do
140
+ EventMachine.run {
141
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :query => "q=test"
142
+
143
+ http.errback { failed(http) }
144
+ http.callback {
145
+ http.response_header.status.should == 200
146
+ http.response.should match(/test/)
147
+ EventMachine.stop
148
+ }
149
+ }
150
+ end
151
+
152
+ it "should encode an array of query parameters" do
153
+ EventMachine.run {
154
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_query').get :query => {:hash =>['value1','value2']}
155
+
156
+ http.errback { failed(http) }
157
+ http.callback {
158
+ http.response_header.status.should == 200
159
+ http.response.should match(/hash\[\]=value1&hash\[\]=value2/)
160
+ EventMachine.stop
161
+ }
162
+ }
163
+ end
164
+
165
+ it "should perform successful PUT" do
166
+ EventMachine.run {
167
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').put :body => "data"
168
+
169
+ http.errback { failed(http) }
170
+ http.callback {
171
+ http.response_header.status.should == 200
172
+ http.response.should match(/data/)
173
+ EventMachine.stop
174
+ }
175
+ }
176
+ end
177
+
178
+ it "should perform successful POST" do
179
+ EventMachine.run {
180
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => "data"
181
+
182
+ http.errback { failed(http) }
183
+ http.callback {
184
+ http.response_header.status.should == 200
185
+ http.response.should match(/data/)
186
+ EventMachine.stop
187
+ }
188
+ }
189
+ end
190
+
191
+ it "should perform successful PATCH" do
192
+ EventMachine.run {
193
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').patch :body => "data"
194
+
195
+ http.errback { failed(http) }
196
+ http.callback {
197
+ http.response_header.status.should == 200
198
+ http.response.should match(/data/)
199
+ EventMachine.stop
200
+ }
201
+ }
202
+ end
203
+
204
+ it "should escape body on POST" do
205
+ EventMachine.run {
206
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => {:stuff => 'string&string'}
207
+
208
+ http.errback { failed(http) }
209
+ http.callback {
210
+ http.response_header.status.should == 200
211
+ http.response.should == "stuff=string%26string"
212
+ EventMachine.stop
213
+ }
214
+ }
215
+ end
216
+
217
+ it "should perform successful POST with Ruby Hash/Array as params" do
218
+ EventMachine.run {
219
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => {"key1" => 1, "key2" => [2,3]}
220
+
221
+ http.errback { failed(http) }
222
+ http.callback {
223
+ http.response_header.status.should == 200
224
+
225
+ http.response.should match(/key1=1&key2\[0\]=2&key2\[1\]=3/)
226
+ EventMachine.stop
227
+ }
228
+ }
229
+ end
230
+
231
+ it "should set content-length to 0 on posts with empty bodies" do
232
+ EventMachine.run {
233
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_length_from_header').post
234
+
235
+ http.errback { failed(http) }
236
+ http.callback {
237
+ http.response_header.status.should == 200
238
+
239
+ http.response.strip.split(':')[1].should == '0'
240
+ EventMachine.stop
241
+ }
242
+ }
243
+ end
244
+
245
+ it "should perform successful POST with Ruby Hash/Array as params and with the correct content length" do
246
+ EventMachine.run {
247
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_length').post :body => {"key1" => "data1"}
248
+
249
+ http.errback { failed(http) }
250
+ http.callback {
251
+ http.response_header.status.should == 200
252
+
253
+ http.response.to_i.should == 10
254
+ EventMachine.stop
255
+ }
256
+ }
257
+ end
258
+
259
+ xit "should support expect-continue header" do
260
+ EventMachine.run {
261
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090').post :body => "data", :head => { 'expect' => '100-continue' }
262
+
263
+ http.errback { failed(http) }
264
+ http.callback {
265
+ http.response_header.status.should == 200
266
+ http.response.should == "data"
267
+ EventMachine.stop
268
+ }
269
+ }
270
+ end
271
+
272
+ it "should perform successful GET with custom header" do
273
+ EventMachine.run {
274
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :head => {'if-none-match' => 'evar!'}
275
+
276
+ http.errback { p http; failed(http) }
277
+ http.callback {
278
+ http.response_header.status.should == 304
279
+ EventMachine.stop
280
+ }
281
+ }
282
+ end
283
+
284
+ it "should perform basic auth" do
285
+ EventMachine.run {
286
+
287
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/authtest').get :head => {'authorization' => ['user', 'pass']}
288
+
289
+ http.errback { failed(http) }
290
+ http.callback {
291
+ http.response_header.status.should == 200
292
+ EventMachine.stop
293
+ }
294
+ }
295
+ end
296
+
297
+ it "should perform basic auth via the URL" do
298
+ EventMachine.run {
299
+
300
+ http = EventMachine::HttpRequest.new('http://user:pass@127.0.0.1:8090/authtest').get
301
+
302
+ http.errback { failed(http) }
303
+ http.callback {
304
+ http.response_header.status.should == 200
305
+ EventMachine.stop
306
+ }
307
+ }
308
+ end
309
+
310
+ it "should return peer's IP address" do
311
+ EventMachine.run {
312
+
313
+ conn = EventMachine::HttpRequest.new('http://127.0.0.1:8090/')
314
+ conn.peer.should be_nil
315
+
316
+ http = conn.get
317
+ http.peer.should be_nil
318
+
319
+ http.errback { failed(http) }
320
+ http.callback {
321
+ conn.peer.should == '127.0.0.1'
322
+ http.peer.should == '127.0.0.1'
323
+
324
+ EventMachine.stop
325
+ }
326
+ }
327
+ end
328
+
329
+ it "should remove all newlines from long basic auth header" do
330
+ EventMachine.run {
331
+ auth = {'authorization' => ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz']}
332
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/auth').get :head => auth
333
+ http.errback { failed(http) }
334
+ http.callback {
335
+ http.response_header.status.should == 200
336
+ http.response.should == "Basic YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhOnp6enp6enp6enp6enp6enp6enp6enp6enp6enp6eg=="
337
+ EventMachine.stop
338
+ }
339
+ }
340
+ end
341
+
342
+ it "should send proper OAuth auth header" do
343
+ EventMachine.run {
344
+ oauth_header = 'OAuth oauth_nonce="oqwgSYFUD87MHmJJDv7bQqOF2EPnVus7Wkqj5duNByU", b=c, d=e'
345
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/auth').get :head => {
346
+ 'authorization' => oauth_header
347
+ }
348
+
349
+ http.errback { failed(http) }
350
+ http.callback {
351
+ http.response_header.status.should == 200
352
+ http.response.should == oauth_header
353
+ EventMachine.stop
354
+ }
355
+ }
356
+ end
357
+
358
+ it "should return ETag and Last-Modified headers" do
359
+ EventMachine.run {
360
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_query').get
361
+
362
+ http.errback { failed(http) }
363
+ http.callback {
364
+ http.response_header.status.should == 200
365
+ http.response_header.etag.should match('abcdefg')
366
+ http.response_header.last_modified.should match('Fri, 13 Aug 2010 17:31:21 GMT')
367
+ EventMachine.stop
368
+ }
369
+ }
370
+ end
371
+
372
+ it "should return raw headers in a hash" do
373
+ EventMachine.run {
374
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_headers').get
375
+
376
+ http.errback { failed(http) }
377
+ http.callback {
378
+ http.response_header.status.should == 200
379
+ http.response_header.raw['Set-Cookie'].should match('test=yes')
380
+ http.response_header.raw['X-Forward-Host'].should match('proxy.local')
381
+ EventMachine.stop
382
+ }
383
+ }
384
+ end
385
+
386
+ it "should detect deflate encoding" do
387
+ EventMachine.run {
388
+
389
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/deflate').get :head => {"accept-encoding" => "deflate"}
390
+
391
+ http.errback { failed(http) }
392
+ http.callback {
393
+ http.response_header.status.should == 200
394
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
395
+ http.response.should == "compressed"
396
+
397
+ EventMachine.stop
398
+ }
399
+ }
400
+ end
401
+
402
+ it "should auto-detect and decode gzip encoding" do
403
+ EventMachine.run {
404
+
405
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/gzip').get :head => {"accept-encoding" => "gzip, compressed"}
406
+
407
+ http.errback { failed(http) }
408
+ http.callback {
409
+ http.response_header.status.should == 200
410
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
411
+ http.response.should == "compressed"
412
+
413
+ EventMachine.stop
414
+ }
415
+ }
416
+ end
417
+
418
+ it "should stream gzip responses" do
419
+ expected_response = Zlib::GzipReader.open(File.dirname(__FILE__) + "/fixtures/gzip-sample.gz") { |f| f.read }
420
+ actual_response = ''
421
+
422
+ EventMachine.run {
423
+
424
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/gzip-large').get :head => {"accept-encoding" => "gzip, compressed"}
425
+
426
+ http.errback { failed(http) }
427
+ http.callback {
428
+ http.response_header.status.should == 200
429
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
430
+ http.response.should == ''
431
+
432
+ actual_response.should == expected_response
433
+
434
+ EventMachine.stop
435
+ }
436
+ http.stream do |chunk|
437
+ actual_response << chunk
438
+ end
439
+ }
440
+ end
441
+
442
+ it "should not decode the response when configured so" do
443
+ EventMachine.run {
444
+
445
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/gzip').get :head => {
446
+ "accept-encoding" => "gzip, compressed"
447
+ }, :decoding => false
448
+
449
+ http.errback { failed(http) }
450
+ http.callback {
451
+ http.response_header.status.should == 200
452
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
453
+
454
+ raw = http.response
455
+ Zlib::GzipReader.new(StringIO.new(raw)).read.should == "compressed"
456
+
457
+ EventMachine.stop
458
+ }
459
+ }
460
+ end
461
+
462
+ it "should default to requesting compressed response" do
463
+ EventMachine.run {
464
+
465
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_accept_encoding').get
466
+
467
+ http.errback { failed(http) }
468
+ http.callback {
469
+ http.response_header.status.should == 200
470
+ http.response.should == "gzip, compressed"
471
+
472
+ EventMachine.stop
473
+ }
474
+ }
475
+ end
476
+
477
+ it "should default to requesting compressed response" do
478
+ EventMachine.run {
479
+
480
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_accept_encoding').get :compressed => false
481
+
482
+ http.errback { failed(http) }
483
+ http.callback {
484
+ http.response_header.status.should == 200
485
+ http.response.should == ""
486
+
487
+ EventMachine.stop
488
+ }
489
+ }
490
+ end
491
+
492
+ it "should timeout after 0.1 seconds of inactivity" do
493
+ EventMachine.run {
494
+ t = Time.now.to_i
495
+ EventMachine.heartbeat_interval = 0.1
496
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/timeout', :inactivity_timeout => 0.1).get
497
+
498
+ http.errback {
499
+ http.error.should == Errno::ETIMEDOUT
500
+ (Time.now.to_i - t).should <= 1
501
+ EventMachine.stop
502
+ }
503
+ http.callback { failed(http) }
504
+ }
505
+ end
506
+
507
+ it "should complete a Location: with a relative path" do
508
+ EventMachine.run {
509
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/relative-location').get
510
+
511
+ http.errback { failed(http) }
512
+ http.callback {
513
+ http.response_header['LOCATION'].should == 'http://127.0.0.1:8090/forwarded'
514
+ EventMachine.stop
515
+ }
516
+ }
517
+ end
518
+
519
+ context "body content-type encoding" do
520
+ it "should not set content type on string in body" do
521
+ EventMachine.run {
522
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post :body => "data"
523
+
524
+ http.errback { failed(http) }
525
+ http.callback {
526
+ http.response_header.status.should == 200
527
+ http.response.should be_empty
528
+ EventMachine.stop
529
+ }
530
+ }
531
+ end
532
+
533
+ it "should set content-type automatically when passed a ruby hash/array for body" do
534
+ EventMachine.run {
535
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post :body => {:a => :b}
536
+
537
+ http.errback { failed(http) }
538
+ http.callback {
539
+ http.response_header.status.should == 200
540
+ http.response.should match("application/x-www-form-urlencoded")
541
+ EventMachine.stop
542
+ }
543
+ }
544
+ end
545
+
546
+ it "should not override content-type when passing in ruby hash/array for body" do
547
+ EventMachine.run {
548
+ ct = 'text; charset=utf-8'
549
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({
550
+ :body => {:a => :b}, :head => {'content-type' => ct}})
551
+
552
+ http.errback { failed(http) }
553
+ http.callback {
554
+ http.response_header.status.should == 200
555
+ http.content_charset.should == Encoding.find('utf-8') if defined? Encoding
556
+ http.response_header["CONTENT_TYPE"].should == ct
557
+ EventMachine.stop
558
+ }
559
+ }
560
+ end
561
+
562
+ it "should default to external encoding on invalid encoding" do
563
+ EventMachine.run {
564
+ ct = 'text/html; charset=utf-8lias'
565
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({
566
+ :body => {:a => :b}, :head => {'content-type' => ct}})
567
+
568
+ http.errback { failed(http) }
569
+ http.callback {
570
+ http.response_header.status.should == 200
571
+ http.content_charset.should == Encoding.find('utf-8') if defined? Encoding
572
+ http.response_header["CONTENT_TYPE"].should == ct
573
+ EventMachine.stop
574
+ }
575
+ }
576
+ end
577
+
578
+ it "should processed escaped content-type" do
579
+ EventMachine.run {
580
+ ct = "text/html; charset=\"ISO-8859-4\""
581
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({
582
+ :body => {:a => :b}, :head => {'content-type' => ct}})
583
+
584
+ http.errback { failed(http) }
585
+ http.callback {
586
+ http.response_header.status.should == 200
587
+ http.content_charset.should == Encoding.find('ISO-8859-4') if defined? Encoding
588
+ http.response_header["CONTENT_TYPE"].should == ct
589
+ EventMachine.stop
590
+ }
591
+ }
592
+ end
593
+ end
594
+
595
+ context "optional header callback" do
596
+ it "should optionally pass the response headers" do
597
+ EventMachine.run {
598
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
599
+
600
+ http.errback { failed(http) }
601
+ http.headers { |hash|
602
+ hash.should be_an_kind_of Hash
603
+ hash.should include 'CONNECTION'
604
+ hash.should include 'CONTENT_LENGTH'
605
+ }
606
+
607
+ http.callback {
608
+ http.response_header.status.should == 200
609
+ http.response.should match(/Hello/)
610
+ EventMachine.stop
611
+ }
612
+ }
613
+ end
614
+
615
+ it "should allow to terminate current connection from header callback" do
616
+ EventMachine.run {
617
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
618
+
619
+ http.callback { failed(http) }
620
+ http.headers { |hash|
621
+ hash.should be_an_kind_of Hash
622
+ hash.should include 'CONNECTION'
623
+ hash.should include 'CONTENT_LENGTH'
624
+
625
+ http.close('header callback terminated connection')
626
+ }
627
+
628
+ http.errback { |e|
629
+ http.response_header.status.should == 200
630
+ http.error.should == 'header callback terminated connection'
631
+ http.response.should == ''
632
+ EventMachine.stop
633
+ }
634
+ }
635
+ end
636
+ end
637
+
638
+ it "should optionally pass the response body progressively" do
639
+ EventMachine.run {
640
+ body = ''
641
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
642
+
643
+ http.errback { failed(http) }
644
+ http.stream { |chunk| body += chunk }
645
+
646
+ http.callback {
647
+ http.response_header.status.should == 200
648
+ http.response.should == ''
649
+ body.should match(/Hello/)
650
+ EventMachine.stop
651
+ }
652
+ }
653
+ end
654
+
655
+ it "should optionally pass the deflate-encoded response body progressively" do
656
+ EventMachine.run {
657
+ body = ''
658
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/deflate').get :head => {
659
+ "accept-encoding" => "deflate, compressed"
660
+ }
661
+
662
+ http.errback { failed(http) }
663
+ http.stream { |chunk| body += chunk }
664
+
665
+ http.callback {
666
+ http.response_header.status.should == 200
667
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
668
+ http.response.should == ''
669
+ body.should == "compressed"
670
+ EventMachine.stop
671
+ }
672
+ }
673
+ end
674
+
675
+ it "should accept & return cookie header to user" do
676
+ EventMachine.run {
677
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/set_cookie').get
678
+
679
+ http.errback { failed(http) }
680
+ http.callback {
681
+ http.response_header.status.should == 200
682
+ http.response_header.cookie.should == "id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;"
683
+ EventMachine.stop
684
+ }
685
+ }
686
+ end
687
+
688
+ it "should return array of cookies on multiple Set-Cookie headers" do
689
+ EventMachine.run {
690
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/set_multiple_cookies').get
691
+
692
+ http.errback { failed(http) }
693
+ http.callback {
694
+ http.response_header.status.should == 200
695
+ http.response_header.cookie.size.should == 2
696
+ http.response_header.cookie.first.should == "id=1; expires=Sat, 09 Aug 2031 17:53:39 GMT; path=/;"
697
+ http.response_header.cookie.last.should == "id=2;"
698
+
699
+ EventMachine.stop
700
+ }
701
+ }
702
+ end
703
+
704
+ it "should pass cookie header to server from string" do
705
+ EventMachine.run {
706
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_cookie').get :head => {'cookie' => 'id=2;'}
707
+
708
+ http.errback { failed(http) }
709
+ http.callback {
710
+ http.response.should == "id=2;"
711
+ EventMachine.stop
712
+ }
713
+ }
714
+ end
715
+
716
+ it "should pass cookie header to server from Hash" do
717
+ EventMachine.run {
718
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_cookie').get :head => {'cookie' => {'id' => 2}}
719
+
720
+ http.errback { failed(http) }
721
+ http.callback {
722
+ http.response.should == "id=2;"
723
+ EventMachine.stop
724
+ }
725
+ }
726
+ end
727
+
728
+ it "should get the body without Content-Length" do
729
+ EventMachine.run {
730
+ @s = StubServer.new("HTTP/1.1 200 OK\r\n\r\nFoo")
731
+
732
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
733
+ http.errback { failed(http) }
734
+ http.callback {
735
+ http.response.should match(/Foo/)
736
+ http.response_header['CONTENT_LENGTH'].should be_nil
737
+
738
+ @s.stop
739
+ EventMachine.stop
740
+ }
741
+ }
742
+ end
743
+
744
+ context "when talking to a stub HTTP/1.0 server" do
745
+ it "should get the body without Content-Length" do
746
+
747
+ EventMachine.run {
748
+ @s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo")
749
+
750
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
751
+ http.errback { failed(http) }
752
+ http.callback {
753
+ http.response.should match(/Foo/)
754
+ http.response_header['CONTENT_LENGTH'].should be_nil
755
+
756
+ @s.stop
757
+ EventMachine.stop
758
+ }
759
+ }
760
+ end
761
+
762
+ it "should work with \\n instead of \\r\\n" do
763
+ EventMachine.run {
764
+ @s = StubServer.new("HTTP/1.0 200 OK\nContent-Type: text/plain\nContent-Length: 3\nConnection: close\n\nFoo")
765
+
766
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
767
+ http.errback { failed(http) }
768
+ http.callback {
769
+ http.response_header.status.should == 200
770
+ http.response_header['CONTENT_TYPE'].should == 'text/plain'
771
+ http.response.should match(/Foo/)
772
+
773
+ @s.stop
774
+ EventMachine.stop
775
+ }
776
+ }
777
+ end
778
+
779
+ it "should handle invalid HTTP response" do
780
+ EventMachine.run {
781
+ @s = StubServer.new("<html></html>")
782
+
783
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
784
+ http.callback { failed(http) }
785
+ http.errback {
786
+ http.error.should_not be_nil
787
+ EM.stop
788
+ }
789
+ }
790
+ end
791
+ end
792
+
793
+ it "should stream a file off disk" do
794
+ EventMachine.run {
795
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :file => 'spec/fixtures/google.ca'
796
+
797
+ http.errback { failed(http) }
798
+ http.callback {
799
+ http.response.should match('google')
800
+ EventMachine.stop
801
+ }
802
+ }
803
+ end
804
+
805
+ it "streams POST request from disk via Pathname" do
806
+ EventMachine.run {
807
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => Pathname.new('spec/fixtures/google.ca')
808
+ http.errback { failed(http) }
809
+ http.callback {
810
+ http.response.should match('google')
811
+ EventMachine.stop
812
+ }
813
+ }
814
+ end
815
+
816
+ it "streams POST request from IO object" do
817
+ EventMachine.run {
818
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => StringIO.new(File.read('spec/fixtures/google.ca'))
819
+ http.errback { failed(http) }
820
+ http.callback {
821
+ http.response.should match('google')
822
+ EventMachine.stop
823
+ }
824
+ }
825
+ end
826
+
827
+ it "should reconnect if connection was closed between requests" do
828
+ EventMachine.run {
829
+ conn = EM::HttpRequest.new('http://127.0.0.1:8090/')
830
+ req = conn.get
831
+
832
+ req.callback do
833
+ conn.close('client closing connection')
834
+
835
+ EM.next_tick do
836
+ req = conn.get :path => "/gzip"
837
+ req.callback do
838
+ req.response_header.status.should == 200
839
+ req.response.should match('compressed')
840
+ EventMachine.stop
841
+ end
842
+ end
843
+ end
844
+ }
845
+ end
846
+
847
+ it "should report error if connection was closed by server on client keepalive requests" do
848
+ EventMachine.run {
849
+ conn = EM::HttpRequest.new('http://127.0.0.1:8090/')
850
+ req = conn.get :keepalive => true
851
+
852
+ req.callback do
853
+ req = conn.get
854
+
855
+ req.callback { failed(http) }
856
+ req.errback do
857
+ req.error.should match('connection closed by server')
858
+ EventMachine.stop
859
+ end
860
+ end
861
+ }
862
+ end
863
+
864
+ it 'should handle malformed Content-Type header repetitions' do
865
+ EventMachine.run {
866
+ response =<<-HTTP.gsub(/^ +/, '').strip
867
+ HTTP/1.0 200 OK
868
+ Content-Type: text/plain; charset=iso-8859-1
869
+ Content-Type: text/plain; charset=utf-8
870
+ Content-Length: 5
871
+ Connection: close
872
+
873
+ Hello
874
+ HTTP
875
+
876
+ @s = StubServer.new(response)
877
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
878
+ http.errback { failed(http) }
879
+ http.callback {
880
+ http.content_charset.should == Encoding::ISO_8859_1 if defined? Encoding
881
+ EventMachine.stop
882
+ }
883
+ }
884
+ end
885
+
886
+ it "should allow indifferent access to headers" do
887
+ EventMachine.run {
888
+ response =<<-HTTP.gsub(/^ +/, '').strip
889
+ HTTP/1.0 200 OK
890
+ Content-Type: text/plain; charset=utf-8
891
+ X-Custom-Header: foo
892
+ Content-Length: 5
893
+ Connection: close
894
+
895
+ Hello
896
+ HTTP
897
+
898
+ @s = StubServer.new(response)
899
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
900
+ http.errback { failed(http) }
901
+ http.callback {
902
+ http.response_header["Content-Type"].should == "text/plain; charset=utf-8"
903
+ http.response_header["CONTENT_TYPE"].should == "text/plain; charset=utf-8"
904
+
905
+ http.response_header["Content-Length"].should == "5"
906
+ http.response_header["CONTENT_LENGTH"].should == "5"
907
+
908
+ http.response_header["X-Custom-Header"].should == "foo"
909
+ http.response_header["X_CUSTOM_HEADER"].should == "foo"
910
+
911
+ EventMachine.stop
912
+ }
913
+ }
914
+ end
915
+
916
+ it "should close connection on invalid HTTP response" do
917
+ EventMachine.run {
918
+ response =<<-HTTP.gsub(/^ +/, '').strip
919
+ HTTP/1.1 403 Forbidden
920
+ Content-Type: text/plain
921
+ Content-Length: 13
922
+
923
+ Access Denied
924
+
925
+ HTTP/1.1 403 Forbidden
926
+ Content-Type: text/plain
927
+ Content-Length: 13
928
+
929
+ Access Denied
930
+ HTTP
931
+
932
+ @s = StubServer.new(response)
933
+ lambda {
934
+ conn = EventMachine::HttpRequest.new('http://127.0.0.1:8081/')
935
+ req = conn.get
936
+ req.errback { failed(http) }
937
+ req.callback { EM.stop }
938
+ }.should_not raise_error
939
+
940
+ }
941
+ end
942
+
943
+ context "User-Agent" do
944
+ it 'should default to "EventMachine HttpClient"' do
945
+ EventMachine.run {
946
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get
947
+
948
+ http.errback { failed(http) }
949
+ http.callback {
950
+ http.response.should == '"EventMachine HttpClient"'
951
+ EventMachine.stop
952
+ }
953
+ }
954
+ end
955
+
956
+ it 'should keep header if given empty string' do
957
+ EventMachine.run {
958
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get(:head => { 'user-agent'=>'' })
959
+
960
+ http.errback { failed(http) }
961
+ http.callback {
962
+ http.response.should == '""'
963
+ EventMachine.stop
964
+ }
965
+ }
966
+ end
967
+
968
+ it 'should ommit header if given nil' do
969
+ EventMachine.run {
970
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get(:head => { 'user-agent'=>nil })
971
+
972
+ http.errback { failed(http) }
973
+ http.callback {
974
+ http.response.should == 'nil'
975
+ EventMachine.stop
976
+ }
977
+ }
978
+ end
979
+ end
980
+
981
+ context "IPv6" do
982
+ it "should perform successful GET" do
983
+ EventMachine.run {
984
+ @s = StubServer.new({
985
+ response: "HTTP/1.1 200 OK\r\n\r\nHello IPv6",
986
+ port: 8091,
987
+ host: '::1',
988
+ })
989
+ http = EventMachine::HttpRequest.new('http://[::1]:8091/').get
990
+
991
+ http.errback { failed(http) }
992
+ http.callback {
993
+ http.response_header.status.should == 200
994
+ http.response.should match(/Hello IPv6/)
995
+ EventMachine.stop
996
+ }
997
+ }
998
+ end
999
+ end
1000
+ end