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.

@@ -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'
@@ -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
- (s =~ /^(\d+)$/)) ? $1.to_i : nil
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
- @uri.host + (@uri.port != 80 ? ":#{@uri.port}" : "")
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
- when 'Authorization', 'Proxy-authorization'
159
- encode_auth(key, value)
160
- else
161
- encode_field(key, value)
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, :errors
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
- @errors = ''
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
- unbind
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
- @errors = msg
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
- return unless @options[:body]
335
- body = normalize_body
336
- send_data body
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 @state == :finished || (@state == :body && @bytes_remaining.nil?)
367
- succeed(self)
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
- fail(self)
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
- when :response_proxy
381
- parse_response_proxy
382
- when :response_header
383
- parse_response_header
384
- when :chunk_header
385
- parse_chunk_header
386
- when :chunk_body
387
- process_chunk_body
388
- when :chunk_footer
389
- process_chunk_footer
390
- when :response_footer
391
- process_response_footer
392
- when :body
393
- process_body
394
- when :websocket
395
- process_websocket
396
- when :finished, :invalid
397
- break
398
- else raise RuntimeError, "invalid state: #{@state}"
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 @response_header.location
477
+ location = Addressable::URI.parse(@response_header.location)
458
478
  if location.relative?
459
- location = (@uri.join location).to_s
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
- on_request_complete
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 recieved
637
+ # been received
614
638
  @data << buffer if not buffer.empty?
615
639
 
616
640
  false
@@ -0,0 +1,6 @@
1
+ # bytesize was introduced in 1.8.7+
2
+ if RUBY_VERSION <= "1.8.6"
3
+ class String
4
+ def bytesize; self.size; end
5
+ end
6
+ end
@@ -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