em-http-request 0.2.7 → 0.2.9
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.
Potentially problematic release.
This version of em-http-request might be problematic. Click here for more details.
- data/README.rdoc +29 -11
- data/Rakefile +5 -4
- data/VERSION +1 -1
- data/em-http-request.gemspec +97 -0
- data/examples/fetch.rb +1 -1
- data/examples/websocket-handler.rb +28 -0
- data/examples/websocket-server.rb +8 -0
- data/ext/buffer/em_buffer.c +74 -65
- data/lib/em-http-request.rb +1 -0
- data/lib/em-http.rb +6 -3
- data/lib/em-http/client.rb +114 -90
- data/lib/em-http/core_ext/bytesize.rb +6 -0
- data/lib/em-http/http_options.rb +32 -0
- data/lib/em-http/mock.rb +38 -15
- data/lib/em-http/request.rb +78 -102
- data/spec/mock_spec.rb +49 -4
- data/spec/multi_spec.rb +2 -2
- data/spec/request_spec.rb +67 -10
- data/spec/stallion.rb +10 -6
- metadata +39 -14
@@ -0,0 +1 @@
|
|
1
|
+
require 'em-http'
|
data/lib/em-http.rb
CHANGED
@@ -3,14 +3,17 @@
|
|
3
3
|
# You can redistribute this under the terms of the Ruby license
|
4
4
|
# See file LICENSE for details
|
5
5
|
#++
|
6
|
-
|
7
|
-
require 'eventmachine'
|
6
|
+
|
7
|
+
require 'eventmachine'
|
8
8
|
|
9
9
|
require File.dirname(__FILE__) + '/http11_client'
|
10
10
|
require File.dirname(__FILE__) + '/em_buffer'
|
11
11
|
|
12
12
|
require File.dirname(__FILE__) + '/em-http/core_ext/hash'
|
13
|
+
require File.dirname(__FILE__) + '/em-http/core_ext/bytesize'
|
14
|
+
|
13
15
|
require File.dirname(__FILE__) + '/em-http/client'
|
14
16
|
require File.dirname(__FILE__) + '/em-http/multi'
|
15
17
|
require File.dirname(__FILE__) + '/em-http/request'
|
16
|
-
require File.dirname(__FILE__) + '/em-http/decoders'
|
18
|
+
require File.dirname(__FILE__) + '/em-http/decoders'
|
19
|
+
require File.dirname(__FILE__) + '/em-http/http_options'
|
data/lib/em-http/client.rb
CHANGED
@@ -20,12 +20,12 @@ module EventMachine
|
|
20
20
|
|
21
21
|
# The status code (as a string!)
|
22
22
|
attr_accessor :http_status
|
23
|
-
|
23
|
+
|
24
24
|
# E-Tag
|
25
25
|
def etag
|
26
26
|
self["ETag"]
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
def last_modified
|
30
30
|
time = self["Last-Modified"]
|
31
31
|
Time.parse(time) if time
|
@@ -39,7 +39,7 @@ module EventMachine
|
|
39
39
|
# Length of content as an integer, or nil if chunked/unspecified
|
40
40
|
def content_length
|
41
41
|
@content_length ||= ((s = self[HttpClient::CONTENT_LENGTH]) &&
|
42
|
-
|
42
|
+
(s =~ /^(\d+)$/)) ? $1.to_i : nil
|
43
43
|
end
|
44
44
|
|
45
45
|
# Cookie header from the server
|
@@ -104,7 +104,11 @@ module EventMachine
|
|
104
104
|
# you include port 80 then further redirects will tack on the :80 which is
|
105
105
|
# annoying.
|
106
106
|
def encode_host
|
107
|
-
|
107
|
+
if @uri.port == 80 || @uri.port == 443
|
108
|
+
return @uri.host
|
109
|
+
else
|
110
|
+
@uri.host + ":#{@uri.port}"
|
111
|
+
end
|
108
112
|
end
|
109
113
|
|
110
114
|
def encode_request(method, path, query, uri_query)
|
@@ -147,7 +151,7 @@ module EventMachine
|
|
147
151
|
FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).chomp].join(" ")]
|
148
152
|
else
|
149
153
|
encode_field(k,v)
|
150
|
-
end
|
154
|
+
end
|
151
155
|
end
|
152
156
|
|
153
157
|
def encode_headers(head)
|
@@ -155,10 +159,10 @@ module EventMachine
|
|
155
159
|
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
156
160
|
key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
|
157
161
|
result << case key
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
+
when 'Authorization', 'Proxy-authorization'
|
163
|
+
encode_auth(key, value)
|
164
|
+
else
|
165
|
+
encode_field(key, value)
|
162
166
|
end
|
163
167
|
end
|
164
168
|
end
|
@@ -186,7 +190,7 @@ module EventMachine
|
|
186
190
|
CRLF="\r\n"
|
187
191
|
|
188
192
|
attr_accessor :method, :options, :uri
|
189
|
-
attr_reader :response, :response_header, :
|
193
|
+
attr_reader :response, :response_header, :error, :redirects, :last_effective_url
|
190
194
|
|
191
195
|
def post_init
|
192
196
|
@parser = HttpClientParser.new
|
@@ -194,34 +198,36 @@ module EventMachine
|
|
194
198
|
@chunk_header = HttpChunkHeader.new
|
195
199
|
@response_header = HttpResponseHeader.new
|
196
200
|
@parser_nbytes = 0
|
201
|
+
@redirects = 0
|
197
202
|
@response = ''
|
198
|
-
@
|
203
|
+
@error = ''
|
204
|
+
@last_effective_url = nil
|
199
205
|
@content_decoder = nil
|
200
206
|
@stream = nil
|
207
|
+
@disconnect = nil
|
201
208
|
@state = :response_header
|
202
209
|
end
|
203
210
|
|
204
211
|
# start HTTP request once we establish connection to host
|
205
|
-
def connection_completed
|
212
|
+
def connection_completed
|
206
213
|
# if connecting to proxy, then first negotiate the connection
|
207
|
-
# to intermediate server and wait for 200 response
|
208
|
-
if @options[:proxy] and @state == :response_header
|
214
|
+
# to intermediate server and wait for 200 response
|
215
|
+
if @options[:proxy] and @state == :response_header
|
209
216
|
@state = :response_proxy
|
210
217
|
send_request_header
|
211
|
-
|
218
|
+
|
212
219
|
# if connecting via proxy, then state will be :proxy_connected,
|
213
220
|
# indicating successful tunnel. from here, initiate normal http
|
214
221
|
# exchange
|
215
222
|
else
|
216
223
|
@state = :response_header
|
217
|
-
|
218
224
|
ssl = @options[:tls] || @options[:ssl] || {}
|
219
225
|
start_tls(ssl) if @uri.scheme == "https" or @uri.port == 443
|
220
226
|
send_request_header
|
221
227
|
send_request_body
|
222
228
|
end
|
223
229
|
end
|
224
|
-
|
230
|
+
|
225
231
|
# request is done, invoke the callback
|
226
232
|
def on_request_complete
|
227
233
|
begin
|
@@ -229,13 +235,14 @@ module EventMachine
|
|
229
235
|
rescue HttpDecoders::DecoderError
|
230
236
|
on_error "Content-decoder error"
|
231
237
|
end
|
232
|
-
|
238
|
+
|
239
|
+
close_connection
|
233
240
|
end
|
234
|
-
|
241
|
+
|
235
242
|
# request failed, invoke errback
|
236
243
|
def on_error(msg, dns_error = false)
|
237
|
-
@
|
238
|
-
|
244
|
+
@error = msg
|
245
|
+
|
239
246
|
# no connection signature on DNS failures
|
240
247
|
# fail the connection directly
|
241
248
|
dns_error == true ? fail(self) : unbind
|
@@ -246,6 +253,11 @@ module EventMachine
|
|
246
253
|
@stream = blk
|
247
254
|
end
|
248
255
|
|
256
|
+
# assign disconnect callback for websocket
|
257
|
+
def disconnect(&blk)
|
258
|
+
@disconnect = blk
|
259
|
+
end
|
260
|
+
|
249
261
|
# raw data push from the client (WebSocket) should
|
250
262
|
# only be invoked after handshake, otherwise it will
|
251
263
|
# inject data into the header exchange
|
@@ -269,23 +281,13 @@ module EventMachine
|
|
269
281
|
end
|
270
282
|
end
|
271
283
|
end
|
272
|
-
|
273
|
-
def normalize_uri
|
274
|
-
@normalized_uri ||= begin
|
275
|
-
uri = @uri.dup
|
276
|
-
encoded_query = encode_query(@uri.path, @options[:query], @uri.query)
|
277
|
-
path, query = encoded_query.split("?", 2)
|
278
|
-
uri.query = query unless encoded_query.empty?
|
279
|
-
uri.path = path
|
280
|
-
uri
|
281
|
-
end
|
282
|
-
end
|
283
284
|
|
284
285
|
def websocket?; @uri.scheme == 'ws'; end
|
285
|
-
|
286
|
+
|
286
287
|
def send_request_header
|
287
288
|
query = @options[:query]
|
288
289
|
head = @options[:head] ? munge_header_keys(@options[:head]) : {}
|
290
|
+
file = @options[:file]
|
289
291
|
body = normalize_body
|
290
292
|
request_header = nil
|
291
293
|
|
@@ -296,13 +298,16 @@ module EventMachine
|
|
296
298
|
head = proxy[:head] ? munge_header_keys(proxy[:head]) : {}
|
297
299
|
head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
|
298
300
|
request_header = HTTP_REQUEST_HEADER % ['CONNECT', "#{@uri.host}:#{@uri.port}"]
|
299
|
-
|
301
|
+
|
300
302
|
elsif websocket?
|
301
303
|
head['upgrade'] = 'WebSocket'
|
302
304
|
head['connection'] = 'Upgrade'
|
303
305
|
head['origin'] = @options[:origin] || @uri.host
|
304
|
-
|
306
|
+
|
305
307
|
else
|
308
|
+
# Set the Content-Length if file is given
|
309
|
+
head['content-length'] = File.size(file) if file
|
310
|
+
|
306
311
|
# Set the Content-Length if body is given
|
307
312
|
head['content-length'] = body.bytesize if body
|
308
313
|
|
@@ -323,6 +328,9 @@ module EventMachine
|
|
323
328
|
# Set the User-Agent if it hasn't been specified
|
324
329
|
head['user-agent'] ||= "EventMachine HttpClient"
|
325
330
|
|
331
|
+
# Record last seen URL
|
332
|
+
@last_effective_url = @uri
|
333
|
+
|
326
334
|
# Build the request headers
|
327
335
|
request_header ||= encode_request(@method, @uri.path, query, @uri.query)
|
328
336
|
request_header << encode_headers(head)
|
@@ -331,9 +339,13 @@ module EventMachine
|
|
331
339
|
end
|
332
340
|
|
333
341
|
def send_request_body
|
334
|
-
|
335
|
-
|
336
|
-
|
342
|
+
if @options[:body]
|
343
|
+
body = normalize_body
|
344
|
+
send_data body
|
345
|
+
return
|
346
|
+
elsif @options[:file]
|
347
|
+
stream_file_data @options[:file], :http_chunks => false
|
348
|
+
end
|
337
349
|
end
|
338
350
|
|
339
351
|
def receive_data(data)
|
@@ -363,12 +375,28 @@ module EventMachine
|
|
363
375
|
end
|
364
376
|
|
365
377
|
def unbind
|
366
|
-
if @
|
367
|
-
|
378
|
+
if @last_effective_url != @uri and @redirects < @options[:redirects]
|
379
|
+
# update uri to redirect location if we're allowed to traverse deeper
|
380
|
+
@uri = @last_effective_url
|
381
|
+
|
382
|
+
# keep track of the depth of requests we made in this session
|
383
|
+
@redirects += 1
|
384
|
+
|
385
|
+
# swap current connection and reassign current handler
|
386
|
+
req = HttpOptions.new(@method, @uri, @options)
|
387
|
+
reconnect(req.host, req.port)
|
388
|
+
|
389
|
+
@response_header = HttpResponseHeader.new
|
390
|
+
@state = :response_header
|
391
|
+
@data.clear
|
368
392
|
else
|
369
|
-
|
393
|
+
if @state == :finished || (@state == :body && @bytes_remaining.nil?)
|
394
|
+
succeed(self)
|
395
|
+
else
|
396
|
+
@disconnect.call(self) if @state == :websocket and @disconnect
|
397
|
+
fail(self)
|
398
|
+
end
|
370
399
|
end
|
371
|
-
close_connection
|
372
400
|
end
|
373
401
|
|
374
402
|
#
|
@@ -377,25 +405,25 @@ module EventMachine
|
|
377
405
|
|
378
406
|
def dispatch
|
379
407
|
while case @state
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
408
|
+
when :response_proxy
|
409
|
+
parse_response_header
|
410
|
+
when :response_header
|
411
|
+
parse_response_header
|
412
|
+
when :chunk_header
|
413
|
+
parse_chunk_header
|
414
|
+
when :chunk_body
|
415
|
+
process_chunk_body
|
416
|
+
when :chunk_footer
|
417
|
+
process_chunk_footer
|
418
|
+
when :response_footer
|
419
|
+
process_response_footer
|
420
|
+
when :body
|
421
|
+
process_body
|
422
|
+
when :websocket
|
423
|
+
process_websocket
|
424
|
+
when :finished, :invalid
|
425
|
+
break
|
426
|
+
else raise RuntimeError, "invalid state: #{@state}"
|
399
427
|
end
|
400
428
|
end
|
401
429
|
end
|
@@ -419,28 +447,6 @@ module EventMachine
|
|
419
447
|
|
420
448
|
true
|
421
449
|
end
|
422
|
-
|
423
|
-
# TODO: refactor with parse_response_header
|
424
|
-
def parse_response_proxy
|
425
|
-
return false unless parse_header(@response_header)
|
426
|
-
|
427
|
-
unless @response_header.http_status and @response_header.http_reason
|
428
|
-
@state = :invalid
|
429
|
-
on_error "no HTTP response"
|
430
|
-
return false
|
431
|
-
end
|
432
|
-
|
433
|
-
# when a successfull tunnel is established, the proxy responds with a
|
434
|
-
# 200 response code. from here, the tunnel is transparent.
|
435
|
-
if @response_header.http_status.to_i == 200
|
436
|
-
@response_header = HttpResponseHeader.new
|
437
|
-
connection_completed
|
438
|
-
else
|
439
|
-
@state = :invalid
|
440
|
-
on_error "proxy not accessible"
|
441
|
-
return false
|
442
|
-
end
|
443
|
-
end
|
444
450
|
|
445
451
|
def parse_response_header
|
446
452
|
return false unless parse_header(@response_header)
|
@@ -451,14 +457,32 @@ module EventMachine
|
|
451
457
|
return false
|
452
458
|
end
|
453
459
|
|
460
|
+
if @state == :response_proxy
|
461
|
+
# when a successfull tunnel is established, the proxy responds with a
|
462
|
+
# 200 response code. from here, the tunnel is transparent.
|
463
|
+
if @response_header.http_status.to_i == 200
|
464
|
+
@response_header = HttpResponseHeader.new
|
465
|
+
connection_completed
|
466
|
+
return true
|
467
|
+
else
|
468
|
+
@state = :invalid
|
469
|
+
on_error "proxy not accessible"
|
470
|
+
return false
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
454
474
|
# correct location header - some servers will incorrectly give a relative URI
|
455
475
|
if @response_header.location
|
456
476
|
begin
|
457
|
-
location = Addressable::URI.parse
|
477
|
+
location = Addressable::URI.parse(@response_header.location)
|
458
478
|
if location.relative?
|
459
|
-
location =
|
460
|
-
@response_header[LOCATION] = location
|
479
|
+
location = @uri.join(location)
|
480
|
+
@response_header[LOCATION] = location.to_s
|
461
481
|
end
|
482
|
+
|
483
|
+
# store last url on any sign of redirect
|
484
|
+
@last_effective_url = location
|
485
|
+
|
462
486
|
rescue
|
463
487
|
on_error "Location header format error"
|
464
488
|
return false
|
@@ -468,7 +492,7 @@ module EventMachine
|
|
468
492
|
# shortcircuit on HEAD requests
|
469
493
|
if @method == "HEAD"
|
470
494
|
@state = :finished
|
471
|
-
|
495
|
+
unbind
|
472
496
|
end
|
473
497
|
|
474
498
|
if websocket?
|
@@ -478,7 +502,7 @@ module EventMachine
|
|
478
502
|
else
|
479
503
|
fail "websocket handshake failed"
|
480
504
|
end
|
481
|
-
|
505
|
+
|
482
506
|
elsif @response_header.chunked_encoding?
|
483
507
|
@state = :chunk_header
|
484
508
|
elsif @response_header.content_length
|
@@ -496,7 +520,7 @@ module EventMachine
|
|
496
520
|
on_error "Content-decoder error"
|
497
521
|
end
|
498
522
|
end
|
499
|
-
|
523
|
+
|
500
524
|
true
|
501
525
|
end
|
502
526
|
|
@@ -610,7 +634,7 @@ module EventMachine
|
|
610
634
|
end
|
611
635
|
|
612
636
|
# store remainder if message boundary has not yet
|
613
|
-
# been
|
637
|
+
# been received
|
614
638
|
@data << buffer if not buffer.empty?
|
615
639
|
|
616
640
|
false
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class HttpOptions
|
2
|
+
attr_reader :uri, :method, :host, :port, :options
|
3
|
+
|
4
|
+
def initialize(method, uri, options)
|
5
|
+
raise ArgumentError, "invalid request path" unless /^\// === uri.path
|
6
|
+
|
7
|
+
@options = options
|
8
|
+
@method = method.to_s.upcase
|
9
|
+
@uri = uri
|
10
|
+
|
11
|
+
if proxy = options[:proxy]
|
12
|
+
@host = proxy[:host]
|
13
|
+
@port = proxy[:port]
|
14
|
+
else
|
15
|
+
@host = uri.host
|
16
|
+
@port = uri.port
|
17
|
+
end
|
18
|
+
|
19
|
+
@options[:timeout] ||= 10 # default connect & inactivity timeouts
|
20
|
+
@options[:redirects] ||= 0 # default number of redirects to follow
|
21
|
+
|
22
|
+
# Make sure the ports are set as Addressable::URI doesn't
|
23
|
+
# set the port if it isn't there
|
24
|
+
if uri.scheme == "https"
|
25
|
+
@uri.port ||= 443
|
26
|
+
@port ||= 443
|
27
|
+
else
|
28
|
+
@uri.port ||= 80
|
29
|
+
@port ||= 80
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|