em-http-request-samesite 1.1.7

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.
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