em-http-request 0.2.9 → 0.3.0

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.
@@ -0,0 +1,135 @@
1
+ module EventMachine
2
+ module HttpEncoding
3
+ HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
4
+ FIELD_ENCODING = "%s: %s\r\n"
5
+
6
+ # Escapes a URI.
7
+ def escape(s)
8
+ EscapeUtils.escape_url(s.to_s)
9
+ end
10
+
11
+ # Unescapes a URI escaped string.
12
+ def unescape(s)
13
+ EscapeUtils.unescape_url(s.to_s)
14
+ end
15
+
16
+ if ''.respond_to?(:bytesize)
17
+ def bytesize(string)
18
+ string.bytesize
19
+ end
20
+ else
21
+ def bytesize(string)
22
+ string.size
23
+ end
24
+ end
25
+
26
+ # Map all header keys to a downcased string version
27
+ def munge_header_keys(head)
28
+ head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
29
+ end
30
+
31
+ # HTTP is kind of retarded that you have to specify a Host header, but if
32
+ # you include port 80 then further redirects will tack on the :80 which is
33
+ # annoying.
34
+ def encode_host
35
+ if @uri.port == 80 || @uri.port == 443
36
+ return @uri.host
37
+ else
38
+ @uri.host + ":#{@uri.port}"
39
+ end
40
+ end
41
+
42
+ def encode_request(method, uri, query, proxy)
43
+ query = encode_query(uri, query)
44
+
45
+ # Non CONNECT proxies require that you provide the full request
46
+ # uri in request header, as opposed to a relative path.
47
+ query = uri.join(query) if proxy && proxy[:type] != :socks && !proxy[:use_connect]
48
+
49
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
50
+ end
51
+
52
+ def encode_query(uri, query)
53
+ encoded_query = if query.kind_of?(Hash)
54
+ query.map { |k, v| encode_param(k, v) }.join('&')
55
+ else
56
+ query.to_s
57
+ end
58
+
59
+ if !uri.query.to_s.empty?
60
+ encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&")
61
+ end
62
+ encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}"
63
+ end
64
+
65
+ # URL encodes query parameters:
66
+ # single k=v, or a URL encoded array, if v is an array of values
67
+ def encode_param(k, v)
68
+ if v.is_a?(Array)
69
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
70
+ else
71
+ escape(k) + "=" + escape(v)
72
+ end
73
+ end
74
+
75
+ def form_encode_body(obj)
76
+ pairs = []
77
+ recursive = Proc.new do |h, prefix|
78
+ h.each do |k,v|
79
+ key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
80
+
81
+ if v.is_a? Array
82
+ nh = Hash.new
83
+ v.size.times { |t| nh[t] = v[t] }
84
+ recursive.call(nh, key)
85
+
86
+ elsif v.is_a? Hash
87
+ recursive.call(v, key)
88
+ else
89
+ pairs << "#{key}=#{escape(v)}"
90
+ end
91
+ end
92
+ end
93
+
94
+ recursive.call(obj, '')
95
+ return pairs.join('&')
96
+ end
97
+
98
+ # Encode a field in an HTTP header
99
+ def encode_field(k, v)
100
+ FIELD_ENCODING % [k, v]
101
+ end
102
+
103
+ # Encode basic auth in an HTTP header
104
+ # In: Array ([user, pass]) - for basic auth
105
+ # String - custom auth string (OAuth, etc)
106
+ def encode_auth(k,v)
107
+ if v.is_a? Array
108
+ FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).chomp].join(" ")]
109
+ else
110
+ encode_field(k,v)
111
+ end
112
+ end
113
+
114
+ def encode_headers(head)
115
+ head.inject('') do |result, (key, value)|
116
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
117
+ key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
118
+ result << case key
119
+ when 'Authorization', 'Proxy-Authorization'
120
+ encode_auth(key, value)
121
+ else
122
+ encode_field(key, value)
123
+ end
124
+ end
125
+ end
126
+
127
+ def encode_cookie(cookie)
128
+ if cookie.is_a? Hash
129
+ cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" }
130
+ else
131
+ cookie
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,71 @@
1
+ module EventMachine
2
+ # A simple hash is returned for each request made by HttpClient with the
3
+ # headers that were given by the server for that request.
4
+ class HttpResponseHeader < Hash
5
+ # The reason returned in the http response ("OK","File not found",etc.)
6
+ attr_accessor :http_reason
7
+
8
+ # The HTTP version returned.
9
+ attr_accessor :http_version
10
+
11
+ # The status code (as a string!)
12
+ attr_accessor :http_status
13
+
14
+ # E-Tag
15
+ def etag
16
+ self[HttpClient::ETAG]
17
+ end
18
+
19
+ def last_modified
20
+ self[HttpClient::LAST_MODIFIED]
21
+ end
22
+
23
+ # HTTP response status as an integer
24
+ def status
25
+ Integer(http_status) rescue 0
26
+ end
27
+
28
+ # Length of content as an integer, or nil if chunked/unspecified
29
+ def content_length
30
+ @content_length ||= ((s = self[HttpClient::CONTENT_LENGTH]) &&
31
+ (s =~ /^(\d+)$/)) ? $1.to_i : nil
32
+ end
33
+
34
+ # Cookie header from the server
35
+ def cookie
36
+ self[HttpClient::SET_COOKIE]
37
+ end
38
+
39
+ # Is the transfer encoding chunked?
40
+ def chunked_encoding?
41
+ /chunked/i === self[HttpClient::TRANSFER_ENCODING]
42
+ end
43
+
44
+ def keep_alive?
45
+ /keep-alive/i === self[HttpClient::KEEP_ALIVE]
46
+ end
47
+
48
+ def compressed?
49
+ /gzip|compressed|deflate/i === self[HttpClient::CONTENT_ENCODING]
50
+ end
51
+
52
+ def location
53
+ self[HttpClient::LOCATION]
54
+ end
55
+ end
56
+
57
+ class HttpChunkHeader < Hash
58
+ # When parsing chunked encodings this is set
59
+ attr_accessor :http_chunk_size
60
+
61
+ def initialize
62
+ super
63
+ @http_chunk_size = '0'
64
+ end
65
+
66
+ # Size of the chunk as an integer
67
+ def chunk_size
68
+ @http_chunk_size.to_i(base=16)
69
+ end
70
+ end
71
+ end
@@ -2,7 +2,7 @@ class HttpOptions
2
2
  attr_reader :uri, :method, :host, :port, :options
3
3
 
4
4
  def initialize(method, uri, options)
5
- raise ArgumentError, "invalid request path" unless /^\// === uri.path
5
+ uri.path = '/' if uri.path.empty?
6
6
 
7
7
  @options = options
8
8
  @method = method.to_s.upcase
@@ -12,12 +12,15 @@ class HttpOptions
12
12
  @host = proxy[:host]
13
13
  @port = proxy[:port]
14
14
  else
15
- @host = uri.host
15
+ # optional host for cases where you may have
16
+ # pre-resolved the host, or you need an override
17
+ @host = options.delete(:host) || uri.host
16
18
  @port = uri.port
17
19
  end
18
20
 
19
- @options[:timeout] ||= 10 # default connect & inactivity timeouts
20
- @options[:redirects] ||= 0 # default number of redirects to follow
21
+ @options[:timeout] ||= 10 # default connect & inactivity timeouts
22
+ @options[:redirects] ||= 0 # default number of redirects to follow
23
+ @options[:keepalive] ||= false # default to single request per connection
21
24
 
22
25
  # Make sure the ports are set as Addressable::URI doesn't
23
26
  # set the port if it isn't there
data/lib/em-http/mock.rb CHANGED
@@ -1,91 +1,131 @@
1
1
  module EventMachine
2
+ OriginalHttpRequest = HttpRequest unless const_defined?(:OriginalHttpRequest)
3
+
2
4
  class MockHttpRequest < EventMachine::HttpRequest
3
-
5
+
4
6
  include HttpEncoding
5
-
6
- class FakeHttpClient < EventMachine::HttpClient
7
7
 
8
+ class RegisteredRequest < Struct.new(:uri, :method, :headers)
9
+ def self.build(uri, method, headers)
10
+ new(uri, method.to_s.upcase, headers || {})
11
+ end
12
+ end
13
+
14
+ class FakeHttpClient < EventMachine::HttpClient
15
+ attr_writer :response
16
+ attr_reader :data
8
17
  def setup(response, uri)
9
18
  @uri = uri
10
19
  if response == :fail
11
20
  fail(self)
12
21
  else
13
- receive_data(response)
14
- succeed(self)
22
+ if response.respond_to?(:call)
23
+ response.call(self)
24
+ @state = :body
25
+ else
26
+ receive_data(response)
27
+ end
28
+ @state == :body ? succeed(self) : fail(self)
15
29
  end
16
30
  end
17
-
31
+
18
32
  def unbind
19
33
  end
20
-
21
34
  end
22
-
23
- @@registry = nil
24
- @@registry_count = nil
25
-
35
+
36
+ @@registry = Hash.new
37
+ @@registry_count = Hash.new{|h,k| h[k] = 0}
38
+
39
+ def self.use
40
+ activate!
41
+ yield
42
+ ensure
43
+ deactivate!
44
+ end
45
+
46
+ def self.activate!
47
+ EventMachine.send(:remove_const, :HttpRequest)
48
+ EventMachine.send(:const_set, :HttpRequest, MockHttpRequest)
49
+ end
50
+
51
+ def self.deactivate!
52
+ EventMachine.send(:remove_const, :HttpRequest)
53
+ EventMachine.send(:const_set, :HttpRequest, OriginalHttpRequest)
54
+ end
55
+
26
56
  def self.reset_counts!
27
- @@registry_count = Hash.new do |registry,query|
28
- registry[query] = Hash.new{|h,k| h[k] = Hash.new(0)}
29
- end
57
+ @@registry_count.clear
30
58
  end
31
-
59
+
32
60
  def self.reset_registry!
33
- @@registry = Hash.new do |registry,query|
34
- registry[query] = Hash.new{|h,k| h[k] = {}}
35
- end
61
+ @@registry.clear
36
62
  end
37
-
38
- reset_counts!
39
- reset_registry!
40
-
63
+
41
64
  @@pass_through_requests = true
42
65
 
43
66
  def self.pass_through_requests=(pass_through_requests)
44
67
  @@pass_through_requests = pass_through_requests
45
68
  end
46
-
69
+
47
70
  def self.pass_through_requests
48
71
  @@pass_through_requests
49
72
  end
50
-
51
- def self.register(uri, method, headers, data)
52
- method = method.to_s.upcase
53
- headers = headers.to_s
54
- @@registry[uri][method][headers] = data
73
+
74
+ def self.parse_register_args(args, &proc)
75
+ args << proc{|client| proc.call(client); ''} if proc
76
+ headers, data = case args.size
77
+ when 3
78
+ args[2].is_a?(Hash) ?
79
+ [args[2][:headers], args[2][:data]] :
80
+ [{}, args[2]]
81
+ when 4
82
+ [args[2], args[3]]
83
+ else
84
+ raise
85
+ end
86
+
87
+ url = args[0]
88
+ method = args[1]
89
+ [headers, url, method, data]
55
90
  end
56
-
57
- def self.register_file(uri, method, headers, file)
58
- register(uri, method, headers, File.read(file))
91
+
92
+ def self.register(*args, &proc)
93
+ headers, url, method, data = parse_register_args(args, &proc)
94
+ @@registry[RegisteredRequest.build(url, method, headers)] = data
59
95
  end
60
-
61
- def self.count(uri, method, headers)
62
- method = method.to_s.upcase
63
- headers = headers.to_s
64
- @@registry_count[uri][method][headers] rescue 0
96
+
97
+ def self.register_file(*args)
98
+ headers, url, method, data = parse_register_args(args)
99
+ @@registry[RegisteredRequest.build(url, method, headers)] = File.read(data)
100
+ end
101
+
102
+ def self.count(url, method, headers = {})
103
+ @@registry_count[RegisteredRequest.build(url, method, headers)]
65
104
  end
66
-
67
- def self.registered?(query, method, headers)
68
- @@registry[query] and @@registry[query][method] and @@registry[query][method][headers]
105
+
106
+ def self.registered?(url, method, headers = {})
107
+ @@registry.key?(RegisteredRequest.build(url, method, headers))
69
108
  end
70
-
71
- def self.registered_content(query, method, headers)
72
- @@registry[query][method][headers]
109
+
110
+ def self.registered_content(url, method, headers = {})
111
+ @@registry[RegisteredRequest.build(url, method, headers)]
73
112
  end
74
-
75
- def self.increment_access(query, method, headers)
76
- @@registry_count[query][method][headers] += 1
113
+
114
+ def self.increment_access(url, method, headers = {})
115
+ @@registry_count[RegisteredRequest.build(url, method, headers)] += 1
77
116
  end
78
-
117
+
79
118
  alias_method :real_send_request, :send_request
80
-
119
+
81
120
  protected
82
121
  def send_request(&blk)
83
- query = "#{@req.uri.scheme}://#{@req.uri.host}:#{@req.uri.port}#{encode_query(@req.uri.path, @req.options[:query], @req.uri.query)}"
84
- headers = @req.options[:head].to_s
122
+ query = "#{@req.uri.scheme}://#{@req.uri.host}:#{@req.uri.port}#{encode_query(@req.uri, @req.options[:query])}"
123
+ headers = @req.options[:head]
85
124
  if self.class.registered?(query, @req.method, headers)
86
125
  self.class.increment_access(query, @req.method, headers)
87
126
  client = FakeHttpClient.new(nil)
88
- client.setup(self.class.registered_content(query, @req.method, headers), @req.uri)
127
+ content = self.class.registered_content(query, @req.method, headers)
128
+ client.setup(content, @req.uri)
89
129
  client
90
130
  elsif @@pass_through_requests
91
131
  real_send_request
data/lib/em-http/multi.rb CHANGED
@@ -1,51 +1,55 @@
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(EventMachine::HttpRequest.new('http://www.google.com/').get)
15
- # multi.add(EventMachine::HttpRequest.new('http://www.yahoo.com/').get)
16
- #
17
- # multi.callback {
18
- # p multi.responses[:succeeded]
19
- # p multi.responses[:failed]
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 = {:succeeded => [], :failed => []}
34
- end
35
-
36
- def add(conn)
37
- @requests.push(conn)
38
-
39
- conn.callback { @responses[:succeeded].push(conn); check_progress }
40
- conn.errback { @responses[:failed].push(conn); check_progress }
41
- end
42
-
43
- protected
44
-
45
- # invoke callback if all requests have completed
46
- def check_progress
47
- succeed if (@responses[:succeeded].size + @responses[:failed].size) == @requests.size
48
- end
49
-
50
- end
51
- end
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(EventMachine::HttpRequest.new('http://www.google.com/').get)
15
+ # multi.add(EventMachine::HttpRequest.new('http://www.yahoo.com/').get)
16
+ #
17
+ # multi.callback {
18
+ # p multi.responses[:succeeded]
19
+ # p multi.responses[:failed]
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(conns=[], &block)
32
+ @requests = []
33
+ @responses = {:succeeded => [], :failed => []}
34
+
35
+ conns.each {|conn| add(conn)}
36
+ callback(&block) if block_given?
37
+ end
38
+
39
+ def add(conn)
40
+ @requests.push(conn)
41
+
42
+ conn.callback { @responses[:succeeded].push(conn); check_progress }
43
+ conn.errback { @responses[:failed].push(conn); check_progress }
44
+ end
45
+
46
+ protected
47
+
48
+ # invoke callback if all requests have completed
49
+ def check_progress
50
+ succeed(self) if (@responses[:succeeded].size +
51
+ @responses[:failed].size) == @requests.size
52
+ end
53
+
54
+ end
55
+ end
@@ -1,6 +1,3 @@
1
- require 'base64'
2
- require 'addressable/uri'
3
-
4
1
  module EventMachine
5
2
 
6
3
  # EventMachine based HTTP request class with support for streaming consumption
@@ -27,7 +24,7 @@ module EventMachine
27
24
  attr_reader :options, :method
28
25
 
29
26
  def initialize(host)
30
- @uri = host.kind_of?(Addressable::URI) ? host : Addressable::URI::parse(host)
27
+ @uri = host.kind_of?(Addressable::URI) ? host : Addressable::URI::parse(host.to_s)
31
28
  end
32
29
 
33
30
  # Send an HTTP request and consume the response. Supported options:
@@ -72,6 +69,7 @@ module EventMachine
72
69
  rescue EventMachine::ConnectionError => e
73
70
  conn = EventMachine::HttpClient.new("")
74
71
  conn.on_error(e.message, true)
72
+ conn.uri = @req.uri
75
73
  conn
76
74
  end
77
75
  end
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ class HttpRequest
3
+ VERSION = "0.3.0"
4
+ end
5
+ end
data/lib/em-http.rb CHANGED
@@ -1,19 +1,19 @@
1
- #--
2
- # Copyright (C)2008 Ilya Grigorik
3
- # You can redistribute this under the terms of the Ruby license
4
- # See file LICENSE for details
5
- #++
6
-
7
- require 'eventmachine'
8
-
9
- require File.dirname(__FILE__) + '/http11_client'
10
- require File.dirname(__FILE__) + '/em_buffer'
11
-
12
- require File.dirname(__FILE__) + '/em-http/core_ext/hash'
13
- require File.dirname(__FILE__) + '/em-http/core_ext/bytesize'
14
-
15
- require File.dirname(__FILE__) + '/em-http/client'
16
- require File.dirname(__FILE__) + '/em-http/multi'
17
- require File.dirname(__FILE__) + '/em-http/request'
18
- require File.dirname(__FILE__) + '/em-http/decoders'
19
- require File.dirname(__FILE__) + '/em-http/http_options'
1
+ require 'eventmachine'
2
+ require 'escape_utils'
3
+ require 'addressable/uri'
4
+
5
+ require 'base64'
6
+ require 'socket'
7
+
8
+ require 'http11_client'
9
+ require 'em_buffer'
10
+
11
+ require 'em-http/core_ext/bytesize'
12
+ require 'em-http/http_header'
13
+ require 'em-http/http_encoding'
14
+ require 'em-http/http_options'
15
+ require 'em-http/client'
16
+ require 'em-http/multi'
17
+ require 'em-http/request'
18
+ require 'em-http/decoders'
19
+ require 'em-http/mock'
@@ -0,0 +1,40 @@
1
+ require 'helper'
2
+
3
+ describe EventMachine::HttpEncoding do
4
+ include EventMachine::HttpEncoding
5
+
6
+ it "should transform a basic hash into HTTP POST Params" do
7
+ form_encode_body({:a => "alpha", :b => "beta"}).should == "a=alpha&b=beta"
8
+ end
9
+
10
+ it "should transform a more complex hash into HTTP POST Params" do
11
+ form_encode_body({:a => "a", :b => ["c", "d", "e"]}).should == "a=a&b[0]=c&b[1]=d&b[2]=e"
12
+ end
13
+
14
+ it "should transform a very complex hash into HTTP POST Params" do
15
+ params = form_encode_body({:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]})
16
+ params.should == "a=a&b[0][c]=c&b[0][d]=d&b[1][e]=e&b[1][f]=f"
17
+ end
18
+
19
+ it "should escape values" do
20
+ params = form_encode_body({:stuff => 'string&string'})
21
+ params.should == "stuff=string%26string"
22
+ end
23
+
24
+ it "should escape keys" do
25
+ params = form_encode_body({'bad&str'=> {'key&key' => [:a, :b]}})
26
+ params.should == 'bad%26str[key%26key][0]=a&bad%26str[key%26key][1]=b'
27
+ end
28
+
29
+ it "should escape keys and values" do
30
+ params = form_encode_body({'bad&str'=> {'key&key' => ['bad+&stuff', '[test]']}})
31
+ params.should == "bad%26str[key%26key][0]=bad%2B%26stuff&bad%26str[key%26key][1]=%5Btest%5D"
32
+ end
33
+
34
+ it "should be fast on long string escapes" do
35
+ s = Time.now
36
+ 5000.times { |n| form_encode_body({:a => "{a:'b', d:'f', g:['a','b']}"*50}) }
37
+ (Time.now - s).should satisfy { |t| t < 1.5 }
38
+ end
39
+
40
+ end