em-http-request 1.0.3 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of em-http-request might be problematic. Click here for more details.
- data/Changelog.md +4 -0
- data/README.md +2 -2
- data/em-http-request.gemspec +4 -4
- data/examples/digest_auth/client.rb +25 -0
- data/examples/digest_auth/server.rb +28 -0
- data/examples/fibered-http.rb +1 -1
- data/lib/em-http/client.rb +13 -6
- data/lib/em-http/decoders.rb +138 -29
- data/lib/em-http/http_client_options.rb +6 -5
- data/lib/em-http/http_connection.rb +9 -4
- data/lib/em-http/http_connection_options.rb +13 -2
- data/lib/em-http/http_header.rb +21 -1
- data/lib/em-http/middleware/digest_auth.rb +112 -0
- data/lib/em-http/middleware/oauth2.rb +28 -0
- data/lib/em-http/request.rb +1 -0
- data/lib/em-http/version.rb +1 -1
- data/spec/client_spec.rb +62 -7
- data/spec/digest_auth_spec.rb +48 -0
- data/spec/external_spec.rb +2 -2
- data/spec/fixtures/gzip-sample.gz +0 -0
- data/spec/gzip_spec.rb +68 -0
- data/spec/helper.rb +1 -0
- data/spec/middleware/oauth2_spec.rb +15 -0
- data/spec/pipelining_spec.rb +2 -2
- data/spec/redirect_spec.rb +66 -12
- data/spec/socksify_proxy_spec.rb +36 -0
- data/spec/stallion.rb +7 -0
- metadata +20 -10
data/Changelog.md
CHANGED
data/README.md
CHANGED
@@ -36,7 +36,7 @@ Several higher-order Ruby projects have incorporated em-http and other Ruby HTTP
|
|
36
36
|
- [EM-Synchrony](https://github.com/igrigorik/em-synchrony) - Collection of convenience classes and primitives to help untangle evented code (Ruby 1.9 + Fibers).
|
37
37
|
- [Rack-Client](https://github.com/halorgium/rack-client) - Use Rack API for server, test, and client side. Supports Rack middleware!
|
38
38
|
- [Example in action](https://gist.github.com/802391)
|
39
|
-
- [Faraday](https://github.com/
|
39
|
+
- [Faraday](https://github.com/lostisland/faraday) - Modular HTTP client library using middleware heavily inspired by Rack.
|
40
40
|
- [Example in action](https://gist.github.com/802395)
|
41
41
|
|
42
42
|
## Testing
|
@@ -59,4 +59,4 @@ Several higher-order Ruby projects have incorporated em-http and other Ruby HTTP
|
|
59
59
|
|
60
60
|
### License
|
61
61
|
|
62
|
-
(MIT License) - Copyright (c) 2011 Ilya Grigorik
|
62
|
+
(MIT License) - Copyright (c) 2011 Ilya Grigorik
|
data/em-http-request.gemspec
CHANGED
@@ -14,10 +14,10 @@ Gem::Specification.new do |s|
|
|
14
14
|
s.description = s.summary
|
15
15
|
s.rubyforge_project = "em-http-request"
|
16
16
|
|
17
|
-
s.add_dependency "eventmachine", ">= 1.0.
|
18
|
-
s.add_dependency "addressable", ">= 2.
|
19
|
-
s.add_dependency "http_parser.rb", ">= 0.
|
20
|
-
s.add_dependency "em-socksify"
|
17
|
+
s.add_dependency "eventmachine", ">= 1.0.3"
|
18
|
+
s.add_dependency "addressable", ">= 2.3.4"
|
19
|
+
s.add_dependency "http_parser.rb", ">= 0.6.0.beta.2"
|
20
|
+
s.add_dependency "em-socksify", ">= 0.3"
|
21
21
|
s.add_dependency "cookiejar"
|
22
22
|
|
23
23
|
s.add_development_dependency "rspec"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
$: << 'lib' << '../../lib'
|
2
|
+
|
3
|
+
require 'em-http'
|
4
|
+
require 'em-http/middleware/digest_auth'
|
5
|
+
|
6
|
+
digest_config = {
|
7
|
+
:username => 'digest_username',
|
8
|
+
:password => 'digest_password'
|
9
|
+
}
|
10
|
+
|
11
|
+
EM.run do
|
12
|
+
|
13
|
+
conn_handshake = EM::HttpRequest.new('http://localhost:3000')
|
14
|
+
http_handshake = conn_handshake.get
|
15
|
+
|
16
|
+
http_handshake.callback do
|
17
|
+
conn = EM::HttpRequest.new('http://localhost:3000')
|
18
|
+
conn.use EM::Middleware::DigestAuth, http_handshake.response_header['WWW_AUTHENTICATE'], digest_config
|
19
|
+
http = conn.get
|
20
|
+
http.callback do
|
21
|
+
puts http.response
|
22
|
+
EM.stop
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
|
3
|
+
include WEBrick
|
4
|
+
|
5
|
+
config = { :Realm => 'DigestAuth_REALM' }
|
6
|
+
|
7
|
+
htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
|
8
|
+
htdigest.set_passwd config[:Realm], 'digest_username', 'digest_password'
|
9
|
+
htdigest.flush
|
10
|
+
|
11
|
+
config[:UserDB] = htdigest
|
12
|
+
|
13
|
+
digest_auth = WEBrick::HTTPAuth::DigestAuth.new config
|
14
|
+
|
15
|
+
class TestServlet < HTTPServlet::AbstractServlet
|
16
|
+
def do_GET(req, res)
|
17
|
+
@options[0][:authenticator].authenticate req, res
|
18
|
+
res.body = "You are authenticated to see the super secret data\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
s = HTTPServer.new(:Port => 3000)
|
23
|
+
s.mount('/', TestServlet, {:authenticator => digest_auth})
|
24
|
+
trap("INT") do
|
25
|
+
File.delete('my_password_file')
|
26
|
+
s.shutdown
|
27
|
+
end
|
28
|
+
s.start
|
data/examples/fibered-http.rb
CHANGED
@@ -9,7 +9,7 @@ require 'fiber'
|
|
9
9
|
|
10
10
|
def async_fetch(url)
|
11
11
|
f = Fiber.current
|
12
|
-
http = EventMachine::HttpRequest.new(url
|
12
|
+
http = EventMachine::HttpRequest.new(url, :connect_timeout => 10, :inactivity_timeout => 20).get
|
13
13
|
|
14
14
|
http.callback { f.resume(http) }
|
15
15
|
http.errback { f.resume(http) }
|
data/lib/em-http/client.rb
CHANGED
@@ -80,7 +80,7 @@ module EventMachine
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def redirect?
|
83
|
-
@response_header.
|
83
|
+
@response_header.redirection? && @req.follow_redirect?
|
84
84
|
end
|
85
85
|
|
86
86
|
def unbind(reason = nil)
|
@@ -101,6 +101,7 @@ module EventMachine
|
|
101
101
|
@cookies.clear
|
102
102
|
@cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies
|
103
103
|
@req.set_uri(@response_header.location)
|
104
|
+
|
104
105
|
@conn.redirect(self)
|
105
106
|
else
|
106
107
|
succeed(self)
|
@@ -114,7 +115,7 @@ module EventMachine
|
|
114
115
|
end
|
115
116
|
|
116
117
|
else
|
117
|
-
on_error(reason)
|
118
|
+
on_error(reason || 'connection closed by server')
|
118
119
|
end
|
119
120
|
end
|
120
121
|
|
@@ -133,7 +134,7 @@ module EventMachine
|
|
133
134
|
|
134
135
|
def build_request
|
135
136
|
head = @req.headers ? munge_header_keys(@req.headers) : {}
|
136
|
-
|
137
|
+
|
137
138
|
if @conn.connopts.http_proxy?
|
138
139
|
proxy = @conn.connopts.proxy
|
139
140
|
head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
|
@@ -154,7 +155,11 @@ module EventMachine
|
|
154
155
|
head['host'] ||= encode_host
|
155
156
|
|
156
157
|
# Set the User-Agent if it hasn't been specified
|
157
|
-
head
|
158
|
+
if !head.key?('user-agent')
|
159
|
+
head['user-agent'] = "EventMachine HttpClient"
|
160
|
+
elsif head['user-agent'].nil?
|
161
|
+
head.delete('user-agent')
|
162
|
+
end
|
158
163
|
|
159
164
|
# Set the auth from the URI if given
|
160
165
|
head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo
|
@@ -180,7 +185,7 @@ module EventMachine
|
|
180
185
|
end
|
181
186
|
|
182
187
|
# Set content-type header if missing and body is a Ruby hash
|
183
|
-
if
|
188
|
+
if !head['content-type'] and @req.body.is_a? Hash
|
184
189
|
head['content-type'] = 'application/x-www-form-urlencoded'
|
185
190
|
end
|
186
191
|
|
@@ -246,15 +251,17 @@ module EventMachine
|
|
246
251
|
if @response_header.location
|
247
252
|
begin
|
248
253
|
location = Addressable::URI.parse(@response_header.location)
|
254
|
+
location.path = "/" if location.path.empty?
|
249
255
|
|
250
256
|
if location.relative?
|
251
257
|
location = @req.uri.join(location)
|
252
|
-
@response_header[LOCATION] = location.to_s
|
253
258
|
else
|
254
259
|
# if redirect is to an absolute url, check for correct URI structure
|
255
260
|
raise if location.host.nil?
|
256
261
|
end
|
257
262
|
|
263
|
+
@response_header[LOCATION] = location.to_s
|
264
|
+
|
258
265
|
rescue
|
259
266
|
on_error "Location header format error"
|
260
267
|
return
|
data/lib/em-http/decoders.rb
CHANGED
@@ -91,52 +91,161 @@ module EventMachine::HttpDecoders
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
-
|
95
|
-
|
96
|
-
|
94
|
+
##
|
95
|
+
# Partial implementation of RFC 1952 to extract the deflate stream from a gzip file
|
96
|
+
class GZipHeader
|
97
|
+
def initialize
|
98
|
+
@state = :begin
|
99
|
+
@data = ""
|
100
|
+
@pos = 0
|
97
101
|
end
|
98
102
|
|
99
|
-
def
|
100
|
-
@
|
101
|
-
|
103
|
+
def finished?
|
104
|
+
@state == :finish
|
105
|
+
end
|
102
106
|
|
103
|
-
|
104
|
-
if @
|
105
|
-
@
|
106
|
-
@
|
107
|
+
def read(n, buffer)
|
108
|
+
if (@pos + n) <= @data.size
|
109
|
+
buffer << @data[@pos..(@pos + n - 1)]
|
110
|
+
@pos += n
|
111
|
+
return true
|
112
|
+
else
|
113
|
+
return false
|
107
114
|
end
|
108
115
|
end
|
109
116
|
|
110
|
-
def
|
111
|
-
|
112
|
-
@
|
113
|
-
@
|
114
|
-
rescue Zlib::Error
|
115
|
-
raise DecoderError
|
117
|
+
def readbyte
|
118
|
+
if (@pos + 1) <= @data.size
|
119
|
+
@pos += 1
|
120
|
+
@data.getbyte(@pos - 1)
|
116
121
|
end
|
117
122
|
end
|
118
123
|
|
119
|
-
|
120
|
-
|
121
|
-
|
124
|
+
def eof?
|
125
|
+
@pos >= @data.size
|
126
|
+
end
|
127
|
+
|
128
|
+
def extract_stream(compressed)
|
129
|
+
@data << compressed
|
130
|
+
pos = @pos
|
131
|
+
|
132
|
+
while !eof? && !finished?
|
133
|
+
buffer = ""
|
134
|
+
|
135
|
+
case @state
|
136
|
+
when :begin
|
137
|
+
break if !read(10, buffer)
|
138
|
+
|
139
|
+
if buffer.getbyte(0) != 0x1f || buffer.getbyte(1) != 0x8b
|
140
|
+
raise DecoderError.new("magic header not found")
|
141
|
+
end
|
142
|
+
|
143
|
+
if buffer.getbyte(2) != 0x08
|
144
|
+
raise DecoderError.new("unknown compression method")
|
145
|
+
end
|
146
|
+
|
147
|
+
@flags = buffer.getbyte(3)
|
148
|
+
if (@flags & 0xe0).nonzero?
|
149
|
+
raise DecoderError.new("unknown header flags set")
|
150
|
+
end
|
151
|
+
|
152
|
+
# We don't care about these values, I'm leaving the code for reference
|
153
|
+
# @time = buffer[4..7].unpack("V")[0] # little-endian uint32
|
154
|
+
# @extra_flags = buffer.getbyte(8)
|
155
|
+
# @os = buffer.getbyte(9)
|
156
|
+
|
157
|
+
@state = :extra_length
|
158
|
+
|
159
|
+
when :extra_length
|
160
|
+
if (@flags & 0x04).nonzero?
|
161
|
+
break if !read(2, buffer)
|
162
|
+
@extra_length = buffer.unpack("v")[0] # little-endian uint16
|
163
|
+
@state = :extra
|
164
|
+
else
|
165
|
+
@state = :extra
|
166
|
+
end
|
167
|
+
|
168
|
+
when :extra
|
169
|
+
if (@flags & 0x04).nonzero?
|
170
|
+
break if read(@extra_length, buffer)
|
171
|
+
@state = :name
|
172
|
+
else
|
173
|
+
@state = :name
|
174
|
+
end
|
175
|
+
|
176
|
+
when :name
|
177
|
+
if (@flags & 0x08).nonzero?
|
178
|
+
while !(buffer = readbyte).nil?
|
179
|
+
if buffer == 0
|
180
|
+
@state = :comment
|
181
|
+
break
|
182
|
+
end
|
183
|
+
end
|
184
|
+
else
|
185
|
+
@state = :comment
|
186
|
+
end
|
187
|
+
|
188
|
+
when :comment
|
189
|
+
if (@flags & 0x10).nonzero?
|
190
|
+
while !(buffer = readbyte).nil?
|
191
|
+
if buffer == 0
|
192
|
+
@state = :hcrc
|
193
|
+
break
|
194
|
+
end
|
195
|
+
end
|
196
|
+
else
|
197
|
+
@state = :hcrc
|
198
|
+
end
|
199
|
+
|
200
|
+
when :hcrc
|
201
|
+
if (@flags & 0x02).nonzero?
|
202
|
+
break if !read(2, buffer)
|
203
|
+
@state = :finish
|
204
|
+
else
|
205
|
+
@state = :finish
|
206
|
+
end
|
207
|
+
end
|
122
208
|
end
|
123
209
|
|
124
|
-
|
125
|
-
@
|
210
|
+
if finished?
|
211
|
+
compressed[(@pos - pos)..-1]
|
212
|
+
else
|
213
|
+
""
|
126
214
|
end
|
215
|
+
end
|
216
|
+
end
|
127
217
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
218
|
+
class GZip < Base
|
219
|
+
def self.encoding_names
|
220
|
+
%w(gzip compressed)
|
221
|
+
end
|
222
|
+
|
223
|
+
def decompress(compressed)
|
224
|
+
@header ||= GZipHeader.new
|
225
|
+
if !@header.finished?
|
226
|
+
compressed = @header.extract_stream(compressed)
|
134
227
|
end
|
135
228
|
|
136
|
-
|
137
|
-
|
229
|
+
@zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
230
|
+
@zstream.inflate(compressed)
|
231
|
+
rescue Zlib::Error
|
232
|
+
raise DecoderError
|
233
|
+
end
|
234
|
+
|
235
|
+
def finalize
|
236
|
+
if @zstream
|
237
|
+
if !@zstream.finished?
|
238
|
+
r = @zstream.finish
|
239
|
+
end
|
240
|
+
@zstream.close
|
241
|
+
r
|
242
|
+
else
|
243
|
+
nil
|
138
244
|
end
|
245
|
+
rescue Zlib::Error
|
246
|
+
raise DecoderError
|
139
247
|
end
|
248
|
+
|
140
249
|
end
|
141
250
|
|
142
251
|
DECODERS = [Deflate, GZip]
|
@@ -11,9 +11,9 @@ class HttpClientOptions
|
|
11
11
|
@followed = options[:followed] ||= 0 # keep track of number of followed requests
|
12
12
|
|
13
13
|
@method = method.to_s.upcase
|
14
|
-
@headers = options[:head]
|
14
|
+
@headers = options[:head] || {}
|
15
15
|
@query = options[:query]
|
16
|
-
|
16
|
+
|
17
17
|
|
18
18
|
@file = options[:file]
|
19
19
|
@body = options[:body]
|
@@ -21,19 +21,20 @@ class HttpClientOptions
|
|
21
21
|
@pass_cookies = options.fetch(:pass_cookies, true) # pass cookies between redirects
|
22
22
|
@decoding = options.fetch(:decoding, true) # auto-decode compressed response
|
23
23
|
|
24
|
-
set_uri(uri)
|
24
|
+
set_uri(uri, options[:path])
|
25
25
|
end
|
26
26
|
|
27
27
|
def follow_redirect?; @followed < @redirects; end
|
28
28
|
def ssl?; @uri.scheme == "https" || @uri.port == 443; end
|
29
29
|
def no_body?; @method == "HEAD"; end
|
30
30
|
|
31
|
-
def set_uri(uri)
|
31
|
+
def set_uri(uri, path = nil)
|
32
32
|
uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
|
33
|
+
uri.path = path if path
|
33
34
|
uri.path = '/' if uri.path.empty?
|
34
|
-
uri.path = @path if @path
|
35
35
|
|
36
36
|
@uri = uri
|
37
|
+
@path = uri.path
|
37
38
|
|
38
39
|
# Make sure the ports are set as Addressable::URI doesn't
|
39
40
|
# set the port if it isn't there
|
@@ -35,6 +35,7 @@ module EventMachine
|
|
35
35
|
class HttpConnection
|
36
36
|
include HTTPMethods
|
37
37
|
include Socksify
|
38
|
+
include Connectify
|
38
39
|
|
39
40
|
attr_reader :deferred
|
40
41
|
attr_accessor :error, :connopts, :uri, :conn
|
@@ -51,7 +52,9 @@ module EventMachine
|
|
51
52
|
|
52
53
|
def activate_connection(client)
|
53
54
|
begin
|
54
|
-
EventMachine.bind_connect(@connopts.bind, @connopts.bind_port,
|
55
|
+
EventMachine.bind_connect(@connopts.bind, @connopts.bind_port,
|
56
|
+
@connopts.host, @connopts.port,
|
57
|
+
HttpStubConnection) do |conn|
|
55
58
|
post_init
|
56
59
|
|
57
60
|
@deferred = false
|
@@ -120,7 +123,7 @@ module EventMachine
|
|
120
123
|
end
|
121
124
|
|
122
125
|
@p.on_message_complete = proc do
|
123
|
-
if
|
126
|
+
if !client.continue?
|
124
127
|
c = @clients.shift
|
125
128
|
c.state = :finished
|
126
129
|
c.on_request_complete
|
@@ -148,8 +151,10 @@ module EventMachine
|
|
148
151
|
def connection_completed
|
149
152
|
@peer = @conn.get_peername
|
150
153
|
|
151
|
-
if @connopts.
|
154
|
+
if @connopts.socks_proxy?
|
152
155
|
socksify(client.req.uri.host, client.req.uri.port, *@connopts.proxy[:authorization]) { start }
|
156
|
+
elsif @connopts.connect_proxy?
|
157
|
+
connectify(client.req.uri.host, client.req.uri.port, *@connopts.proxy[:authorization]) { start }
|
153
158
|
else
|
154
159
|
start
|
155
160
|
end
|
@@ -164,7 +169,7 @@ module EventMachine
|
|
164
169
|
@pending.push client
|
165
170
|
end
|
166
171
|
|
167
|
-
def unbind(reason)
|
172
|
+
def unbind(reason = nil)
|
168
173
|
@clients.map { |c| c.unbind(reason) }
|
169
174
|
|
170
175
|
if r = @pending.shift
|