astro-em-http-request 0.1.3.20090419

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ require 'zlib'
2
+
3
+ ##
4
+ # Provides a unified callback interface to decompression libraries.
5
+ module EventMachine::HttpDecoders
6
+
7
+ class DecoderError < StandardError
8
+ end
9
+
10
+ class << self
11
+ def accepted_encodings
12
+ DECODERS.inject([]) { |r,d| r + d.encoding_names }
13
+ end
14
+
15
+ def decoder_for_encoding(encoding)
16
+ DECODERS.each { |d|
17
+ return d if d.encoding_names.include? encoding
18
+ }
19
+ nil
20
+ end
21
+ end
22
+
23
+ class Base
24
+ def self.encoding_names
25
+ name = to_s.split('::').last.downcase
26
+ [name]
27
+ end
28
+
29
+ ##
30
+ # chunk_callback:: [Block] To handle a decompressed chunk
31
+ def initialize(&chunk_callback)
32
+ @chunk_callback = chunk_callback
33
+ end
34
+
35
+ def <<(compressed)
36
+ return unless compressed && compressed.size > 0
37
+
38
+ decompressed = decompress(compressed)
39
+ receive_decompressed decompressed
40
+ end
41
+
42
+ def finalize!
43
+ decompressed = finalize
44
+ receive_decompressed decompressed
45
+ end
46
+
47
+ private
48
+
49
+ def receive_decompressed(decompressed)
50
+ if decompressed && decompressed.size > 0
51
+ @chunk_callback.call(decompressed)
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ ##
58
+ # Must return a part of decompressed
59
+ def decompress(compressed)
60
+ nil
61
+ end
62
+
63
+ ##
64
+ # May return last part
65
+ def finalize
66
+ nil
67
+ end
68
+ end
69
+
70
+ class Deflate < Base
71
+ def decompress(compressed)
72
+ begin
73
+ @zstream ||= Zlib::Inflate.new(nil)
74
+ @zstream.inflate(compressed)
75
+ rescue Zlib::Error
76
+ raise DecoderError
77
+ end
78
+ end
79
+
80
+ def finalize
81
+ return nil unless @zstream
82
+
83
+ begin
84
+ r = @zstream.inflate(nil)
85
+ @zstream.close
86
+ r
87
+ rescue Zlib::Error
88
+ raise DecoderError
89
+ end
90
+ end
91
+ end
92
+
93
+ ##
94
+ # Oneshot decompressor, due to lack of a streaming Gzip reader
95
+ # implementation. We may steal code from Zliby to improve this.
96
+ #
97
+ # For now, do not put `gzip' or `compressed' in your accept-encoding
98
+ # header if you expect much data through the :on_response interface.
99
+ class GZip < Base
100
+ def self.encoding_names
101
+ %w(gzip compressed)
102
+ end
103
+
104
+ def decompress(compressed)
105
+ @buf ||= ''
106
+ @buf += compressed
107
+ nil
108
+ end
109
+
110
+ def finalize
111
+ Zlib::GzipReader.new(StringIO.new(@buf)).read
112
+ end
113
+ end
114
+
115
+ DECODERS = [Deflate, GZip]
116
+
117
+ end
118
+
119
+
@@ -0,0 +1,51 @@
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
+ conn.callback { @responses[:succeeded].push(conn); check_progress }
38
+ conn.errback { @responses[:failed].push(conn); check_progress }
39
+
40
+ @requests.push(conn)
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
@@ -0,0 +1,77 @@
1
+ require 'uri'
2
+ require 'base64'
3
+
4
+ module EventMachine
5
+
6
+ # EventMachine based HTTP request class with support for streaming consumption
7
+ # of the response. Response is parsed with a Ragel-generated whitelist parser
8
+ # which supports chunked HTTP encoding.
9
+ #
10
+ # == Example
11
+ #
12
+ #
13
+ # EventMachine.run {
14
+ # http = EventMachine::HttpRequest.new('http://127.0.0.1/').get :query => {'keyname' => 'value'}
15
+ #
16
+ # http.callback {
17
+ # p http.response_header.status
18
+ # p http.response_header
19
+ # p http.response
20
+ #
21
+ # EventMachine.stop
22
+ # }
23
+ # }
24
+ #
25
+
26
+ class HttpRequest
27
+
28
+ def initialize(host, headers = {})
29
+ @headers = headers
30
+ @uri = URI::parse(host) unless host.kind_of? URI
31
+ end
32
+
33
+ # Send an HTTP request and consume the response. Supported options:
34
+ #
35
+ # head: {Key: Value}
36
+ # Specify an HTTP header, e.g. {'Connection': 'close'}
37
+ #
38
+ # query: {Key: Value}
39
+ # Specify query string parameters (auto-escaped)
40
+ #
41
+ # body: String
42
+ # Specify the request body (you must encode it for now)
43
+ #
44
+ # on_response: Proc
45
+ # Called for each response body chunk (you may assume HTTP 200
46
+ # OK then)
47
+ #
48
+ # host: String
49
+ # Manually specify TCP connect host address, independent of
50
+ # Host: header
51
+
52
+ def get options = {}; send_request(:get, options); end
53
+ def post options = {}; send_request(:post, options); end
54
+
55
+ protected
56
+
57
+ def send_request(method, options)
58
+ raise ArgumentError, "invalid request path" unless /^\// === @uri.path
59
+
60
+ method = method.to_s.upcase
61
+ begin
62
+ host = options[:host] || @uri.host
63
+ EventMachine.connect(host, @uri.port, EventMachine::HttpClient) { |c|
64
+ c.uri = @uri
65
+ c.method = method
66
+ c.options = options
67
+ c.comm_inactivity_timeout = options[:timeout] || 5
68
+ }
69
+ rescue RuntimeError => e
70
+ raise e unless e.message == "no connection"
71
+ conn = EventMachine::HttpClient.new("")
72
+ conn.on_error("no connection")
73
+ conn
74
+ end
75
+ end
76
+ end
77
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'pp'
4
+
5
+ require 'lib/em-http'
data/test/stallion.rb ADDED
@@ -0,0 +1,127 @@
1
+ # #--
2
+ # Includes portion originally Copyright (C)2008 Michael Fellinger
3
+ # license See file LICENSE for details
4
+ # #--
5
+
6
+ require 'rack'
7
+
8
+ module Stallion
9
+ class Mount
10
+ def initialize(name, *methods, &block)
11
+ @name, @methods, @block = name, methods, block
12
+ end
13
+
14
+ def ride
15
+ @block.call
16
+ end
17
+
18
+ def match?(request)
19
+ method = request['REQUEST_METHOD']
20
+ right_method = @methods.empty? or @methods.include?(method)
21
+ end
22
+ end
23
+
24
+ class Stable
25
+ attr_reader :request, :response
26
+
27
+ def initialize
28
+ @boxes = {}
29
+ end
30
+
31
+ def in(path, *methods, &block)
32
+ mount = Mount.new(path, *methods, &block)
33
+ @boxes[[path, methods]] = mount
34
+ mount
35
+ end
36
+
37
+ def call(request, response)
38
+ @request, @response = request, response
39
+ @boxes.each do |(path, methods), mount|
40
+ if mount.match?(request)
41
+ mount.ride
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ STABLES = {}
48
+
49
+ def self.saddle(name = nil)
50
+ STABLES[name] = stable = Stable.new
51
+ yield stable
52
+ end
53
+
54
+ def self.run(options = {})
55
+ options = {:Host => "127.0.0.1", :Port => 8080}.merge(options)
56
+ Rack::Handler::Mongrel.run(Rack::Lint.new(self), options)
57
+ end
58
+
59
+ def self.call(env)
60
+ request = Rack::Request.new(env)
61
+ response = Rack::Response.new
62
+
63
+ STABLES.each do |name, stable|
64
+ stable.call(request, response)
65
+ end
66
+
67
+ response.finish
68
+ end
69
+ end
70
+
71
+ Stallion.saddle :spec do |stable|
72
+ stable.in '/' do
73
+
74
+ if stable.request.path_info == '/fail'
75
+ stable.response.status = 404
76
+
77
+ elsif stable.request.query_string == 'q=test'
78
+ stable.response.write 'test'
79
+
80
+ elsif stable.request.path_info == '/echo_query'
81
+ stable.response.write stable.request.query_string
82
+
83
+ elsif stable.request.post?
84
+ stable.response.write 'test'
85
+
86
+ elsif stable.request.path_info == '/timeout'
87
+ sleep(10)
88
+ stable.response.write 'timeout'
89
+
90
+ elsif stable.request.path_info == '/gzip'
91
+ io = StringIO.new
92
+ gzip = Zlib::GzipWriter.new(io)
93
+ gzip << "compressed"
94
+ gzip.close
95
+
96
+ stable.response.write io.string
97
+ stable.response["Content-Encoding"] = "gzip"
98
+
99
+ elsif stable.request.path_info == '/deflate'
100
+ stable.response.write Zlib::Deflate.deflate("compressed")
101
+ stable.response["Content-Encoding"] = "deflate"
102
+
103
+ elsif stable.request.env["HTTP_IF_NONE_MATCH"]
104
+ stable.response.status = 304
105
+
106
+ elsif stable.request.env["HTTP_AUTHORIZATION"]
107
+ auth = "Basic %s" % Base64.encode64(['user', 'pass'].join(':')).chomp
108
+
109
+ if auth == stable.request.env["HTTP_AUTHORIZATION"]
110
+ stable.response.status = 200
111
+ stable.response.write 'success'
112
+ else
113
+ stable.response.status = 401
114
+ end
115
+
116
+ elsif
117
+ stable.response.write 'Hello, World!'
118
+ end
119
+
120
+ end
121
+ end
122
+
123
+ Thread.new do
124
+ Stallion.run :Host => '127.0.0.1', :Port => 8080
125
+ end
126
+
127
+ sleep(2)
@@ -0,0 +1,34 @@
1
+ require 'test/helper'
2
+ require 'test/stallion'
3
+
4
+ describe EventMachine::MultiRequest do
5
+
6
+ def failed
7
+ EventMachine.stop
8
+ fail
9
+ end
10
+
11
+ it "should submit multiple requests in parallel and return once all of them are complete" do
12
+ EventMachine.run {
13
+
14
+ # create an instance of multi-request handler, and the requests themselves
15
+ multi = EventMachine::MultiRequest.new
16
+
17
+ # add multiple requests to the multi-handler
18
+ multi.add(EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get(:query => {:q => 'test'}))
19
+ multi.add(EventMachine::HttpRequest.new('http://169.169.169.169/').get)
20
+
21
+ multi.callback {
22
+ # verify successfull request
23
+ multi.responses[:succeeded].size.should == 1
24
+ multi.responses[:succeeded].first.response.should match(/test/)
25
+
26
+ # verify invalid requests
27
+ multi.responses[:failed].size.should == 1
28
+ multi.responses[:failed].first.response_header.status.should == 0
29
+
30
+ EventMachine.stop
31
+ }
32
+ }
33
+ end
34
+ end
@@ -0,0 +1,264 @@
1
+ require 'test/helper'
2
+ require 'test/stallion'
3
+
4
+ describe EventMachine::HttpRequest do
5
+
6
+ def failed
7
+ EventMachine.stop
8
+ fail
9
+ end
10
+
11
+ it "should fail GET on DNS timeout" do
12
+ EventMachine.run {
13
+ http = EventMachine::HttpRequest.new('http://127.1.1.1/').get
14
+ http.callback { failed }
15
+ http.errback {
16
+ http.response_header.status.should == 0
17
+ EventMachine.stop
18
+ }
19
+ }
20
+ end
21
+
22
+ it "should fail GET on invalid host" do
23
+ EventMachine.run {
24
+ http = EventMachine::HttpRequest.new('http://google1.com/').get
25
+ http.callback { failed }
26
+ http.errback {
27
+ http.response_header.status.should == 0
28
+ http.errors.should match(/no connection/)
29
+ EventMachine.stop
30
+ }
31
+ }
32
+ end
33
+
34
+ it "should fail GET on missing path" do
35
+ EventMachine.run {
36
+ lambda {
37
+ EventMachine::HttpRequest.new('http://www.google.com').get
38
+ }.should raise_error(ArgumentError)
39
+
40
+ EventMachine.stop
41
+ }
42
+ end
43
+
44
+ it "should perform successfull GET" do
45
+ EventMachine.run {
46
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get
47
+
48
+ http.errback { failed }
49
+ http.callback {
50
+ http.response_header.status.should == 200
51
+ http.response.should match(/Hello/)
52
+ EventMachine.stop
53
+ }
54
+ }
55
+ end
56
+
57
+ it "should return 404 on invalid path" do
58
+ EventMachine.run {
59
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/fail').get
60
+
61
+ http.errback { failed }
62
+ http.callback {
63
+ http.response_header.status.should == 404
64
+ EventMachine.stop
65
+ }
66
+ }
67
+ end
68
+
69
+ it "should build query parameters from Hash" do
70
+ EventMachine.run {
71
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => {:q => 'test'}
72
+
73
+ http.errback { failed }
74
+ http.callback {
75
+ http.response_header.status.should == 200
76
+ http.response.should match(/test/)
77
+ EventMachine.stop
78
+ }
79
+ }
80
+ end
81
+
82
+ it "should pass query parameters string" do
83
+ EventMachine.run {
84
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => "q=test"
85
+
86
+ http.errback { failed }
87
+ http.callback {
88
+ http.response_header.status.should == 200
89
+ http.response.should match(/test/)
90
+ EventMachine.stop
91
+ }
92
+ }
93
+ end
94
+
95
+ it "should encode an array of query parameters" do
96
+ EventMachine.run {
97
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/echo_query').get :query => {:hash => ['value1', 'value2']}
98
+
99
+ http.errback { failed }
100
+ http.callback {
101
+ http.response_header.status.should == 200
102
+ http.response.should match(/hash\[\]=value1&hash\[\]=value2/)
103
+ EventMachine.stop
104
+ }
105
+ }
106
+ end
107
+
108
+ it "should perform successfull POST" do
109
+ EventMachine.run {
110
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').post :body => "data"
111
+
112
+ http.errback { failed }
113
+ http.callback {
114
+ http.response_header.status.should == 200
115
+ http.response.should match(/test/)
116
+ EventMachine.stop
117
+ }
118
+ }
119
+ end
120
+
121
+ it "should perform successfull GET with custom header" do
122
+ EventMachine.run {
123
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'if-none-match' => 'evar!'}
124
+
125
+ http.errback { failed }
126
+ http.callback {
127
+ http.response_header.status.should == 304
128
+ EventMachine.stop
129
+ }
130
+ }
131
+ end
132
+
133
+ it "should perform a streaming GET" do
134
+ EventMachine.run {
135
+
136
+ # digg.com uses chunked encoding
137
+ http = EventMachine::HttpRequest.new('http://digg.com/').get
138
+
139
+ http.errback { failed }
140
+ http.callback {
141
+ http.response_header.status.should == 200
142
+ EventMachine.stop
143
+ }
144
+ }
145
+ end
146
+
147
+ it "should perform basic auth" do
148
+ EventMachine.run {
149
+
150
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'authorization' => ['user', 'pass']}
151
+
152
+ http.errback { failed }
153
+ http.callback {
154
+ http.response_header.status.should == 200
155
+ EventMachine.stop
156
+ }
157
+ }
158
+ end
159
+
160
+ it "should work with keep-alive servers" do
161
+ EventMachine.run {
162
+
163
+ http = EventMachine::HttpRequest.new('http://mexicodiario.com/touch.public.json.php').get
164
+
165
+ http.errback { failed }
166
+ http.callback {
167
+ http.response_header.status.should == 200
168
+ EventMachine.stop
169
+ }
170
+ }
171
+ end
172
+
173
+ it "should detect deflate encoding" do
174
+ EventMachine.run {
175
+
176
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/deflate').get :head => {"accept-encoding" => "deflate"}
177
+
178
+ http.errback { failed }
179
+ http.callback {
180
+ http.response_header.status.should == 200
181
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
182
+ http.response.should == "compressed"
183
+
184
+ EventMachine.stop
185
+ }
186
+ }
187
+ end
188
+
189
+ it "should detect gzip encoding" do
190
+ EventMachine.run {
191
+
192
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/gzip').get :head => {"accept-encoding" => "gzip, compressed"}
193
+
194
+ http.errback { failed }
195
+ http.callback {
196
+ http.response_header.status.should == 200
197
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
198
+ http.response.should == "compressed"
199
+
200
+ EventMachine.stop
201
+ }
202
+ }
203
+ end
204
+
205
+ it "should timeout after 10 seconds" do
206
+ EventMachine.run {
207
+ t = Time.now.to_i
208
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/timeout').get :timeout => 2
209
+
210
+ http.errback {
211
+ (Time.now.to_i - t).should == 2
212
+ EventMachine.stop
213
+ }
214
+ http.callback { failed }
215
+ }
216
+ end
217
+
218
+ it "should optionally pass the response body progressively" do
219
+ EventMachine.run {
220
+ body = ''
221
+ on_body = lambda { |chunk| body += chunk }
222
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :on_response => on_body
223
+
224
+ http.errback { failed }
225
+ http.callback {
226
+ http.response_header.status.should == 200
227
+ http.response.should == ''
228
+ body.should match(/Hello/)
229
+ EventMachine.stop
230
+ }
231
+ }
232
+ end
233
+
234
+ it "should optionally pass the deflate-encoded response body progressively" do
235
+ EventMachine.run {
236
+ body = ''
237
+ on_body = lambda { |chunk| body += chunk }
238
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/deflate').get :head => {"accept-encoding" => "deflate, compressed"},
239
+ :on_response => on_body
240
+
241
+ http.errback { failed }
242
+ http.callback {
243
+ http.response_header.status.should == 200
244
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
245
+ http.response.should == ''
246
+ body.should == "compressed"
247
+ EventMachine.stop
248
+ }
249
+ }
250
+ end
251
+
252
+ it "should respect manually-passed host address" do
253
+ EventMachine.run {
254
+ http = EventMachine::HttpRequest.new('http://127.1.1.1:8080/').get :host => '127.0.0.1'
255
+
256
+ http.errback { failed }
257
+ http.callback {
258
+ http.response_header.status.should == 200
259
+ http.response.should match(/Hello/)
260
+ EventMachine.stop
261
+ }
262
+ }
263
+ end
264
+ end