em-http-request 0.2.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.
Potentially problematic release.
This version of em-http-request might be problematic. Click here for more details.
- data/.gitignore +4 -0
- data/LICENSE +58 -0
- data/README.rdoc +71 -0
- data/Rakefile +103 -0
- data/VERSION +1 -0
- data/examples/fetch.rb +30 -0
- data/examples/fibered-http.rb +39 -0
- data/ext/buffer/em_buffer.c +630 -0
- data/ext/buffer/extconf.rb +53 -0
- data/ext/http11_client/ext_help.h +14 -0
- data/ext/http11_client/extconf.rb +6 -0
- data/ext/http11_client/http11_client.c +302 -0
- data/ext/http11_client/http11_parser.c +418 -0
- data/ext/http11_client/http11_parser.h +48 -0
- data/ext/http11_client/http11_parser.rl +170 -0
- data/lib/em-http.rb +18 -0
- data/lib/em-http/client.rb +504 -0
- data/lib/em-http/core_ext/hash.rb +53 -0
- data/lib/em-http/decoders.rb +122 -0
- data/lib/em-http/multi.rb +51 -0
- data/lib/em-http/request.rb +81 -0
- data/test/helper.rb +5 -0
- data/test/stallion.rb +144 -0
- data/test/stub_server.rb +22 -0
- data/test/test_hash.rb +19 -0
- data/test/test_multi.rb +29 -0
- data/test/test_request.rb +393 -0
- metadata +108 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
class Hash
|
2
|
+
# Stolen partially from Merb : http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html
|
3
|
+
# Convert this hash to a query string:
|
4
|
+
#
|
5
|
+
# { :name => "Bob",
|
6
|
+
# :address => {
|
7
|
+
# :street => '111 Ruby Ave.',
|
8
|
+
# :city => 'Ruby Central',
|
9
|
+
# :phones => ['111-111-1111', '222-222-2222']
|
10
|
+
# }
|
11
|
+
# }.to_params
|
12
|
+
# #=> "name=Bob&address[city]=Ruby Central&address[phones]=111-111-1111222-222-2222&address[street]=111 Ruby Ave."
|
13
|
+
#
|
14
|
+
def to_params
|
15
|
+
params = ''
|
16
|
+
stack = []
|
17
|
+
|
18
|
+
each do |k, v|
|
19
|
+
if v.is_a?(Hash)
|
20
|
+
stack << [k,v]
|
21
|
+
elsif v.is_a?(Array)
|
22
|
+
stack << [k,Hash.from_array(v)]
|
23
|
+
else
|
24
|
+
params << "#{k}=#{v}&"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
stack.each do |parent, hash|
|
29
|
+
hash.each do |k, v|
|
30
|
+
if v.is_a?(Hash)
|
31
|
+
stack << ["#{parent}[#{k}]", v]
|
32
|
+
else
|
33
|
+
params << "#{parent}[#{k}]=#{v}&"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
params.chop! # trailing &
|
39
|
+
params
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Builds a hash from an array with keys as array indices.
|
44
|
+
def self.from_array(array = [])
|
45
|
+
h = Hash.new
|
46
|
+
array.size.times do |t|
|
47
|
+
h[t] = array[t]
|
48
|
+
end
|
49
|
+
h
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Provides a unified callback interface to decompression libraries.
|
6
|
+
module EventMachine::HttpDecoders
|
7
|
+
|
8
|
+
class DecoderError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def accepted_encodings
|
13
|
+
DECODERS.inject([]) { |r,d| r + d.encoding_names }
|
14
|
+
end
|
15
|
+
|
16
|
+
def decoder_for_encoding(encoding)
|
17
|
+
DECODERS.each { |d|
|
18
|
+
return d if d.encoding_names.include? encoding
|
19
|
+
}
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Base
|
25
|
+
def self.encoding_names
|
26
|
+
name = to_s.split('::').last.downcase
|
27
|
+
[name]
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# chunk_callback:: [Block] To handle a decompressed chunk
|
32
|
+
def initialize(&chunk_callback)
|
33
|
+
@chunk_callback = chunk_callback
|
34
|
+
end
|
35
|
+
|
36
|
+
def <<(compressed)
|
37
|
+
return unless compressed && compressed.size > 0
|
38
|
+
|
39
|
+
decompressed = decompress(compressed)
|
40
|
+
receive_decompressed decompressed
|
41
|
+
end
|
42
|
+
|
43
|
+
def finalize!
|
44
|
+
decompressed = finalize
|
45
|
+
receive_decompressed decompressed
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def receive_decompressed(decompressed)
|
51
|
+
if decompressed && decompressed.size > 0
|
52
|
+
@chunk_callback.call(decompressed)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
##
|
59
|
+
# Must return a part of decompressed
|
60
|
+
def decompress(compressed)
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# May return last part
|
66
|
+
def finalize
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Deflate < Base
|
72
|
+
def decompress(compressed)
|
73
|
+
begin
|
74
|
+
@zstream ||= Zlib::Inflate.new(nil)
|
75
|
+
@zstream.inflate(compressed)
|
76
|
+
rescue Zlib::Error
|
77
|
+
raise DecoderError
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def finalize
|
82
|
+
return nil unless @zstream
|
83
|
+
|
84
|
+
begin
|
85
|
+
r = @zstream.inflate(nil)
|
86
|
+
@zstream.close
|
87
|
+
r
|
88
|
+
rescue Zlib::Error
|
89
|
+
raise DecoderError
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Oneshot decompressor, due to lack of a streaming Gzip reader
|
96
|
+
# implementation. We may steal code from Zliby to improve this.
|
97
|
+
#
|
98
|
+
# For now, do not put `gzip' or `compressed' in your accept-encoding
|
99
|
+
# header if you expect much data through the :on_response interface.
|
100
|
+
class GZip < Base
|
101
|
+
def self.encoding_names
|
102
|
+
%w(gzip compressed)
|
103
|
+
end
|
104
|
+
|
105
|
+
def decompress(compressed)
|
106
|
+
@buf ||= ''
|
107
|
+
@buf += compressed
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
def finalize
|
112
|
+
begin
|
113
|
+
Zlib::GzipReader.new(StringIO.new(@buf.to_s)).read
|
114
|
+
rescue Zlib::Error
|
115
|
+
raise DecoderError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
DECODERS = [Deflate, GZip]
|
121
|
+
|
122
|
+
end
|
@@ -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,81 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'addressable/uri'
|
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 = host.kind_of?(Addressable::URI) ? host : Addressable::URI::parse(host)
|
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
|
+
|
49
|
+
def get options = {}; send_request(:get, options); end
|
50
|
+
def head options = {}; send_request(:head, options); end
|
51
|
+
def post options = {}; send_request(:post, options); end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def send_request(method, options)
|
56
|
+
raise ArgumentError, "invalid request path" unless /^\// === @uri.path
|
57
|
+
|
58
|
+
# default connect & inactivity timeouts
|
59
|
+
options[:timeout] = 5 if not options[:timeout]
|
60
|
+
|
61
|
+
# Make sure the port is set as Addressable::URI doesn't set the
|
62
|
+
# port if it isn't there.
|
63
|
+
@uri.port = @uri.port ? @uri.port : 80
|
64
|
+
method = method.to_s.upcase
|
65
|
+
begin
|
66
|
+
EventMachine.connect(@uri.host, @uri.port, EventMachine::HttpClient) { |c|
|
67
|
+
c.uri = @uri
|
68
|
+
c.method = method
|
69
|
+
c.options = options
|
70
|
+
c.comm_inactivity_timeout = options[:timeout]
|
71
|
+
c.pending_connect_timeout = options[:timeout]
|
72
|
+
}
|
73
|
+
rescue RuntimeError => e
|
74
|
+
raise e unless e.message == "no connection"
|
75
|
+
conn = EventMachine::HttpClient.new("")
|
76
|
+
conn.on_error("no connection", true)
|
77
|
+
conn
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/test/helper.rb
ADDED
data/test/stallion.rb
ADDED
@@ -0,0 +1,144 @@
|
|
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.path_info == '/echo_content_length'
|
84
|
+
stable.response.write stable.request.content_length
|
85
|
+
|
86
|
+
elsif stable.request.head?
|
87
|
+
stable.response.status = 200
|
88
|
+
|
89
|
+
elsif stable.request.post?
|
90
|
+
stable.response.write stable.request.body.read
|
91
|
+
|
92
|
+
elsif stable.request.path_info == '/set_cookie'
|
93
|
+
stable.response["Set-Cookie"] = "id=1; expires=Tue, 09-Aug-2011 17:53:39 GMT; path=/;"
|
94
|
+
stable.response.write "cookie set"
|
95
|
+
|
96
|
+
elsif stable.request.path_info == '/echo_cookie'
|
97
|
+
stable.response.write stable.request.env["HTTP_COOKIE"]
|
98
|
+
|
99
|
+
elsif stable.request.path_info == '/timeout'
|
100
|
+
sleep(10)
|
101
|
+
stable.response.write 'timeout'
|
102
|
+
|
103
|
+
elsif stable.request.path_info == '/gzip'
|
104
|
+
io = StringIO.new
|
105
|
+
gzip = Zlib::GzipWriter.new(io)
|
106
|
+
gzip << "compressed"
|
107
|
+
gzip.close
|
108
|
+
|
109
|
+
stable.response.write io.string
|
110
|
+
stable.response["Content-Encoding"] = "gzip"
|
111
|
+
|
112
|
+
elsif stable.request.path_info == '/deflate'
|
113
|
+
stable.response.write Zlib::Deflate.deflate("compressed")
|
114
|
+
stable.response["Content-Encoding"] = "deflate"
|
115
|
+
|
116
|
+
elsif stable.request.env["HTTP_IF_NONE_MATCH"]
|
117
|
+
stable.response.status = 304
|
118
|
+
|
119
|
+
elsif stable.request.env["HTTP_AUTHORIZATION"]
|
120
|
+
auth = "Basic %s" % Base64.encode64(['user', 'pass'].join(':')).chomp
|
121
|
+
|
122
|
+
if auth == stable.request.env["HTTP_AUTHORIZATION"]
|
123
|
+
stable.response.status = 200
|
124
|
+
stable.response.write 'success'
|
125
|
+
else
|
126
|
+
stable.response.status = 401
|
127
|
+
end
|
128
|
+
|
129
|
+
elsif
|
130
|
+
stable.response.write 'Hello, World!'
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
Thread.new do
|
137
|
+
begin
|
138
|
+
Stallion.run :Host => '127.0.0.1', :Port => 8080
|
139
|
+
rescue Exception => e
|
140
|
+
print e
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
sleep(1)
|