em-http-request 1.0.0.beta.3 → 1.0.0.beta.4

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.

@@ -1,50 +1,35 @@
1
- # Courtesy of Darcy Laycock:
2
- # http://gist.github.com/265261
3
- #
4
-
5
- require 'rubygems'
1
+ $: << 'lib' << '../lib'
6
2
 
7
3
  require 'em-http'
8
- require 'oauth'
9
-
10
- # At a minimum, require 'oauth/request_proxy/em_http_request'
11
- # for this example, we'll use Net::HTTP like support.
12
- require 'oauth/client/em_http'
4
+ require 'em-http/middleware/oauth'
5
+ require 'em-http/middleware/json_response'
13
6
 
14
- # You need two things: an oauth consumer and an access token.
15
- # You need to generate an access token, I suggest looking elsewhere how to do that or wait for a full tutorial.
16
- # For a consumer key / consumer secret, signup for an app at:
17
- # http://twitter.com/apps/new
7
+ require 'pp'
18
8
 
19
- # Edit in your details.
20
- CONSUMER_KEY = ""
21
- CONSUMER_SECRET = ""
22
- ACCESS_TOKEN = ""
23
- ACCESS_TOKEN_SECRET = ""
24
-
25
- def twitter_oauth_consumer
26
- @twitter_oauth_consumer ||= OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, :site => "http://twitter.com")
27
- end
28
-
29
- def twitter_oauth_access_token
30
- @twitter_oauth_access_token ||= OAuth::AccessToken.new(twitter_oauth_consumer, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)
31
- end
9
+ OAuthConfig = {
10
+ :consumer_key => '',
11
+ :consumer_secret => '',
12
+ :access_token => '',
13
+ :access_token_secret => ''
14
+ }
32
15
 
33
16
  EM.run do
17
+ # automatically parse the JSON response into a Ruby object
18
+ EventMachine::HttpRequest.use EventMachine::Middleware::JSONResponse
34
19
 
35
- request = EventMachine::HttpRequest.new('http://twitter.com/statuses/update.json')
36
- http = request.post(:body => {'status' => 'Hello Twitter from em-http-request with OAuth'}, :head => {"Content-Type" => "application/x-www-form-urlencoded"}) do |client|
37
- twitter_oauth_consumer.sign!(client, twitter_oauth_access_token)
38
- end
20
+ # sign the request with OAuth credentials
21
+ conn = EventMachine::HttpRequest.new('http://api.twitter.com/1/statuses/home_timeline.json')
22
+ conn.use EventMachine::Middleware::OAuth, OAuthConfig
39
23
 
24
+ http = conn.get
40
25
  http.callback do
41
- puts "Response: #{http.response} (Code: #{http.response_header.status})"
42
- EM.stop_event_loop
26
+ pp http.response
27
+ EM.stop
43
28
  end
44
29
 
45
30
  http.errback do
46
- puts "Failed to post"
47
- EM.stop_event_loop
31
+ puts "Failed retrieving user stream."
32
+ pp http.response
33
+ EM.stop
48
34
  end
49
-
50
- end
35
+ end
data/lib/em-http.rb CHANGED
@@ -10,8 +10,9 @@ require 'em-http/core_ext/bytesize'
10
10
  require 'em-http/http_connection'
11
11
  require 'em-http/http_header'
12
12
  require 'em-http/http_encoding'
13
- require 'em-http/http_options'
13
+ require 'em-http/http_client_options'
14
+ require 'em-http/http_connection_options'
14
15
  require 'em-http/client'
15
16
  require 'em-http/multi'
16
17
  require 'em-http/request'
17
- require 'em-http/decoders'
18
+ require 'em-http/decoders'
@@ -1,4 +1,5 @@
1
1
  module EventMachine
2
+
2
3
  class HttpClient
3
4
  include Deferrable
4
5
  include HttpEncoding
@@ -17,17 +18,15 @@ module EventMachine
17
18
  CRLF="\r\n"
18
19
 
19
20
  attr_accessor :state, :response
20
- attr_reader :response_header, :error, :content_charset, :req
21
+ attr_reader :response_header, :error, :content_charset, :req, :cookies
21
22
 
22
- def initialize(conn, req, options)
23
+ def initialize(conn, options)
23
24
  @conn = conn
24
-
25
- @req = req
26
- @method = req.method
27
- @options = options
25
+ @req = options
28
26
 
29
27
  @stream = nil
30
28
  @headers = nil
29
+ @cookies = []
31
30
 
32
31
  reset!
33
32
  end
@@ -43,14 +42,15 @@ module EventMachine
43
42
  end
44
43
 
45
44
  def last_effective_url; @req.uri; end
46
- def redirects; @req.options[:followed]; end
45
+ def redirects; @req.followed; end
46
+ def peer; @conn.peer; end
47
47
 
48
48
  def connection_completed
49
49
  @state = :response_header
50
50
 
51
- head, body = build_request, @options[:body]
51
+ head, body = build_request, @req.body
52
52
  @conn.middleware.each do |m|
53
- head, body = m.request(head, body) if m.respond_to?(:request)
53
+ head, body = m.request(self, head, body) if m.respond_to?(:request)
54
54
  end
55
55
 
56
56
  send_request(head, body)
@@ -66,6 +66,10 @@ module EventMachine
66
66
  unbind
67
67
  end
68
68
 
69
+ def continue?
70
+ @response_header.status == 100 && (@req.method == 'POST' || @req.method == 'PUT')
71
+ end
72
+
69
73
  def finished?
70
74
  @state == :finished || (@state == :body && @response_header.content_length.nil?)
71
75
  end
@@ -77,8 +81,26 @@ module EventMachine
77
81
  def unbind
78
82
  if finished?
79
83
  if redirect?
80
- @req.options[:followed] += 1
81
- @conn.redirect(self, @response_header.location)
84
+
85
+ begin
86
+ @conn.middleware.each do |m|
87
+ m.response(self) if m.respond_to?(:response)
88
+ end
89
+
90
+ # one of the injected middlewares could have changed
91
+ # our redirect settings, check if we still want to
92
+ # follow the location header
93
+ if redirect?
94
+ @req.followed += 1
95
+ @req.set_uri(@response_header.location)
96
+ @conn.redirect(self)
97
+ else
98
+ succeed(self)
99
+ end
100
+
101
+ rescue Exception => e
102
+ on_error(e.message)
103
+ end
82
104
  else
83
105
  succeed(self)
84
106
  end
@@ -101,32 +123,22 @@ module EventMachine
101
123
  body.is_a?(Hash) ? form_encode_body(body) : body
102
124
  end
103
125
 
104
- def proxy?; !@options[:proxy].nil?; end
105
- def http_proxy?; proxy? && [nil, :http].include?(@options[:proxy][:type]); end
106
-
107
- def ssl?; @req.uri.scheme == "https" || @req.uri.port == 443; end
108
-
109
- def continue?
110
- @response_header.status == 100 && (@method == 'POST' || @method == 'PUT')
111
- end
112
-
113
126
  def build_request
114
- head = @options[:head] ? munge_header_keys(@options[:head]) : {}
115
- proxy = @options[:proxy]
127
+ head = @req.headers ? munge_header_keys(@req.headers) : {}
128
+ proxy = @req.proxy
116
129
 
117
- if http_proxy?
118
- # initialize headers for the http proxy
119
- head = proxy[:head] ? munge_header_keys(proxy[:head]) : {}
120
- head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
130
+ if @req.http_proxy?
131
+ head['proxy-authorization'] = @req.proxy[:authorization] if @req.proxy[:authorization]
121
132
  end
122
133
 
123
134
  # Set the cookie header if provided
124
- if cookie = head.delete('cookie')
125
- head['cookie'] = encode_cookie(cookie)
135
+ if cookie = head['cookie']
136
+ @cookies << encode_cookie(cookie) if cookie
126
137
  end
138
+ head['cookie'] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?
127
139
 
128
140
  # Set connection close unless keepalive
129
- unless @options[:keepalive]
141
+ if !@req.keepalive
130
142
  head['connection'] = 'close'
131
143
  end
132
144
 
@@ -141,8 +153,8 @@ module EventMachine
141
153
 
142
154
  def send_request(head, body)
143
155
  body = normalize_body(body)
144
- file = @options[:file]
145
- query = @options[:query]
156
+ file = @req.file
157
+ query = @req.query
146
158
 
147
159
  # Set the Content-Length if file is given
148
160
  head['content-length'] = File.size(file) if file
@@ -151,19 +163,19 @@ module EventMachine
151
163
  head['content-length'] = body.bytesize if body
152
164
 
153
165
  # Set content-type header if missing and body is a Ruby hash
154
- if not head['content-type'] and @options[:body].is_a? Hash
166
+ if not head['content-type'] and @req.body.is_a? Hash
155
167
  head['content-type'] = 'application/x-www-form-urlencoded'
156
168
  end
157
169
 
158
- request_header ||= encode_request(@method, @req.uri, query, @conn.opts.proxy)
170
+ request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts.proxy)
159
171
  request_header << encode_headers(head)
160
172
  request_header << CRLF
161
173
  @conn.send_data request_header
162
174
 
163
175
  if body
164
176
  @conn.send_data body
165
- elsif @options[:file]
166
- @conn.stream_file_data @options[:file], :http_chunks => false
177
+ elsif @req.file
178
+ @conn.stream_file_data @req.file, :http_chunks => false
167
179
  end
168
180
  end
169
181
 
@@ -207,6 +219,9 @@ module EventMachine
207
219
  return
208
220
  end
209
221
 
222
+ # add set-cookie's to cookie list
223
+ @cookies << @response_header.cookie if @response_header.cookie && @req.pass_cookies
224
+
210
225
  # correct location header - some servers will incorrectly give a relative URI
211
226
  if @response_header.location
212
227
  begin
@@ -229,7 +244,7 @@ module EventMachine
229
244
  # Fire callbacks immediately after recieving header requests
230
245
  # if the request method is HEAD. In case of a redirect, terminate
231
246
  # current connection and reinitialize the process.
232
- if @method == "HEAD"
247
+ if @req.method == "HEAD"
233
248
  @state = :finished
234
249
  return
235
250
  end
@@ -242,7 +257,7 @@ module EventMachine
242
257
  @state = :body
243
258
  end
244
259
 
245
- if decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
260
+ if @req.decoding && decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
246
261
  begin
247
262
  @content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
248
263
  rescue HttpDecoders::DecoderError
@@ -71,7 +71,7 @@ module EventMachine::HttpDecoders
71
71
  class Deflate < Base
72
72
  def decompress(compressed)
73
73
  begin
74
- @zstream ||= Zlib::Inflate.new(nil)
74
+ @zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS)
75
75
  @zstream.inflate(compressed)
76
76
  rescue Zlib::Error
77
77
  raise DecoderError
@@ -0,0 +1,56 @@
1
+ class HttpClientOptions
2
+ attr_reader :uri, :method, :host, :port, :proxy
3
+ attr_reader :headers, :file, :body, :query, :path
4
+ attr_reader :keepalive, :pass_cookies, :decoding
5
+
6
+ attr_accessor :followed, :redirects
7
+
8
+ def initialize(uri, options, method)
9
+ @keepalive = options[:keepalive] || false # default to single request per connection
10
+ @redirects = options[:redirects] ||= 0 # default number of redirects to follow
11
+ @followed = options[:followed] ||= 0 # keep track of number of followed requests
12
+
13
+ @method = method.to_s.upcase
14
+ @headers = options[:head] || {}
15
+ @proxy = options[:proxy] || {}
16
+ @query = options[:query]
17
+ @path = options[:path]
18
+
19
+ @file = options[:file]
20
+ @body = options[:body]
21
+
22
+ @pass_cookies = options.fetch(:pass_cookies, true) # pass cookies between redirects
23
+ @decoding = options.fetch(:decoding, true) # auto-decode compressed response
24
+
25
+ set_uri(uri)
26
+ end
27
+
28
+ def follow_redirect?; @followed < @redirects; end
29
+ def http_proxy?; @proxy && [nil, :http].include?(@proxy[:type]); end
30
+ def ssl?; @uri.scheme == "https" || @uri.port == 443; end
31
+
32
+ def set_uri(uri)
33
+ uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
34
+ uri.path = '/' if uri.path.empty?
35
+ uri.path = @path if @path
36
+
37
+ @uri = uri
38
+
39
+ # Make sure the ports are set as Addressable::URI doesn't
40
+ # set the port if it isn't there
41
+ if @uri.scheme == "https"
42
+ @uri.port ||= 443
43
+ else
44
+ @uri.port ||= 80
45
+ end
46
+
47
+ if !@proxy.empty?
48
+ @host = @proxy[:host]
49
+ @port = @proxy[:port]
50
+ else
51
+ @host = @uri.host
52
+ @port = @uri.port
53
+ end
54
+
55
+ end
56
+ end
@@ -8,40 +8,90 @@ module EventMachine
8
8
  def post options = {}, &blk; setup_request(:post, options, &blk); end
9
9
  end
10
10
 
11
- class FailedConnection
12
- include HTTPMethods
11
+ class HttpStubConnection < Connection
13
12
  include Deferrable
13
+ attr_reader :parent
14
14
 
15
- attr_accessor :error, :opts
15
+ def parent=(p)
16
+ @parent = p
17
+ @parent.conn = self
18
+ end
16
19
 
17
- def initialize(req)
18
- @opts = req
20
+ def receive_data(data)
21
+ @parent.receive_data data
19
22
  end
20
23
 
21
- def setup_request(method, options)
22
- c = HttpClient.new(self, HttpOptions.new(@opts.uri, options, method), options)
23
- c.close(@error)
24
- c
24
+ def connection_completed
25
+ @parent.connection_completed
26
+ end
27
+
28
+ def unbind
29
+ @parent.unbind
25
30
  end
26
31
  end
27
32
 
28
- class HttpConnection < Connection
33
+ class HttpConnection
29
34
  include HTTPMethods
30
- include Deferrable
31
35
  include Socksify
32
36
 
33
- attr_accessor :error, :opts
37
+ attr_reader :deferred
38
+ attr_accessor :error, :connopts, :uri, :conn
34
39
 
35
- def setup_request(method, options = {})
36
- c = HttpClient.new(self, HttpOptions.new(@opts.uri, options, method), options)
37
- callback { c.connection_completed }
40
+ def initialize
41
+ @deferred = true
42
+ @middleware = []
43
+ end
44
+
45
+ def conn=(c)
46
+ @conn = c
47
+ @deferred = false
48
+ end
49
+
50
+ def activate_connection(client)
51
+ begin
52
+ EventMachine.connect(@connopts.host, @connopts.port, HttpStubConnection) do |conn|
53
+ post_init
54
+
55
+ @deferred = false
56
+ @conn = conn
57
+
58
+ conn.parent = self
59
+ conn.pending_connect_timeout = @connopts.connect_timeout
60
+ conn.comm_inactivity_timeout = @connopts.inactivity_timeout
61
+ end
62
+
63
+ finalize_request(client)
64
+ rescue EventMachine::ConnectionError => e
65
+ #
66
+ # Currently, this can only fire on initial connection setup
67
+ # since #connect is a synchronous method. Hence, rescue the
68
+ # exception, and return a failed deferred which will immediately
69
+ # fail any client request.
70
+ #
71
+ # Once there is async-DNS, then we'll iterate over the outstanding
72
+ # client requests and fail them in order.
73
+ #
74
+ # Net outcome: failed connection will invoke the same ConnectionError
75
+ # message on the connection deferred, and on the client deferred.
76
+ #
77
+ client.close(e.message)
78
+ end
79
+ end
80
+
81
+ def setup_request(method, options = {}, c = nil)
82
+ c ||= HttpClient.new(self, HttpClientOptions.new(@uri, options, method))
83
+ @deferred ? activate_connection(c) : finalize_request(c)
84
+ c
85
+ end
86
+
87
+ def finalize_request(c)
88
+ @conn.callback { c.connection_completed }
38
89
 
39
90
  middleware.each do |m|
40
91
  c.callback &m.method(:response) if m.respond_to?(:response)
41
92
  end
42
93
 
43
94
  @clients.push c
44
- c
45
95
  end
46
96
 
47
97
  def middleware
@@ -52,19 +102,17 @@ module EventMachine
52
102
  @clients = []
53
103
  @pending = []
54
104
 
55
- @middleware = []
56
-
57
105
  @p = Http::Parser.new
58
106
  @p.on_headers_complete = proc do |h|
59
- @clients.first.parse_response_header(h, @p.http_version, @p.status_code)
107
+ client.parse_response_header(h, @p.http_version, @p.status_code)
60
108
  end
61
109
 
62
110
  @p.on_body = proc do |b|
63
- @clients.first.on_body_data(b)
111
+ client.on_body_data(b)
64
112
  end
65
113
 
66
114
  @p.on_message_complete = proc do
67
- if not @clients.first.continue?
115
+ if not client.continue?
68
116
  c = @clients.shift
69
117
  c.state = :finished
70
118
  c.on_request_complete
@@ -72,8 +120,12 @@ module EventMachine
72
120
  end
73
121
  end
74
122
 
75
- def use(klass)
76
- @middleware << klass
123
+ def use(klass, *args, &block)
124
+ @middleware << klass.new(*args, &block)
125
+ end
126
+
127
+ def peer
128
+ Socket.unpack_sockaddr_in(@peer)[1] rescue nil
77
129
  end
78
130
 
79
131
  def receive_data(data)
@@ -81,35 +133,31 @@ module EventMachine
81
133
  @p << data
82
134
  rescue HTTP::Parser::Error => e
83
135
  c = @clients.shift
84
- c.on_error(e.message)
136
+ c.nil? ? unbind : c.on_error(e.message)
85
137
  end
86
138
  end
87
139
 
88
140
  def connection_completed
89
- if @opts.proxy && @opts.proxy[:type] == :socks5
90
- socksify(client.req.uri.host, client.req.uri.port, *@opts.proxy[:authorization]) { start }
141
+ @peer = @conn.get_peername
91
142
 
143
+ if @connopts.proxy && @connopts.proxy[:type] == :socks5
144
+ socksify(client.req.uri.host, client.req.uri.port, *@connopts.proxy[:authorization]) { start }
92
145
  else
93
146
  start
94
147
  end
95
148
  end
96
149
 
97
150
  def start
98
- ssl = @opts.options[:tls] || @opts.options[:ssl] || {}
99
- start_tls(ssl) if client && client.ssl?
100
-
101
- succeed
151
+ @conn.start_tls(@connopts.tls) if client && client.req.ssl?
152
+ @conn.succeed
102
153
  end
103
154
 
104
- def redirect(client, location)
105
- client.req.set_uri(location)
155
+ def redirect(client)
106
156
  @pending.push client
107
- rescue Exception => e
108
- client.on_error(e.message)
109
157
  end
110
158
 
111
159
  def unbind
112
- @clients.map {|c| c.unbind }
160
+ @clients.map { |c| c.unbind }
113
161
 
114
162
  if r = @pending.shift
115
163
  @clients.push r
@@ -118,19 +166,27 @@ module EventMachine
118
166
  @p.reset!
119
167
 
120
168
  begin
121
- set_deferred_status :unknown
122
- reconnect(r.req.host, r.req.port)
123
- callback { r.connection_completed }
169
+ @conn.set_deferred_status :unknown
170
+ @conn.reconnect(r.req.host, r.req.port)
171
+ @conn.callback { r.connection_completed }
124
172
  rescue EventMachine::ConnectionError => e
125
173
  @clients.pop.close(e.message)
126
174
  end
127
175
  end
128
176
  end
129
177
 
178
+ def send_data(data)
179
+ @conn.send_data data
180
+ end
181
+
182
+ def stream_file_data(filename, args = {})
183
+ @conn.stream_file_data filename, args
184
+ end
185
+
130
186
  private
131
187
 
132
- def client
133
- @clients.first
134
- end
188
+ def client
189
+ @clients.first
190
+ end
135
191
  end
136
192
  end