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
@@ -18,7 +18,8 @@ class HttpConnectionOptions
|
|
18
18
|
end
|
19
19
|
|
20
20
|
uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
|
21
|
-
|
21
|
+
@https = uri.scheme == "https"
|
22
|
+
uri.port ||= (@https ? 443 : 80)
|
22
23
|
|
23
24
|
if proxy = options[:proxy]
|
24
25
|
@host = proxy[:host]
|
@@ -29,5 +30,15 @@ class HttpConnectionOptions
|
|
29
30
|
end
|
30
31
|
end
|
31
32
|
|
32
|
-
def http_proxy
|
33
|
+
def http_proxy?
|
34
|
+
@proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && !@https
|
35
|
+
end
|
36
|
+
|
37
|
+
def connect_proxy?
|
38
|
+
@proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && @https
|
39
|
+
end
|
40
|
+
|
41
|
+
def socks_proxy?
|
42
|
+
@proxy && (@proxy[:type] == :socks5)
|
43
|
+
end
|
33
44
|
end
|
data/lib/em-http/http_header.rb
CHANGED
@@ -25,7 +25,7 @@ module EventMachine
|
|
25
25
|
|
26
26
|
# HTTP response status as an integer
|
27
27
|
def status
|
28
|
-
Integer(http_status) rescue 0
|
28
|
+
@status ||= Integer(http_status) rescue 0
|
29
29
|
end
|
30
30
|
|
31
31
|
# Length of content as an integer, or nil if chunked/unspecified
|
@@ -59,5 +59,25 @@ module EventMachine
|
|
59
59
|
def [](key)
|
60
60
|
super(key) || super(key.upcase.gsub('-','_'))
|
61
61
|
end
|
62
|
+
|
63
|
+
def informational?
|
64
|
+
100 <= status && 200 > status
|
65
|
+
end
|
66
|
+
|
67
|
+
def successful?
|
68
|
+
200 <= status && 300 > status
|
69
|
+
end
|
70
|
+
|
71
|
+
def redirection?
|
72
|
+
300 <= status && 400 > status
|
73
|
+
end
|
74
|
+
|
75
|
+
def client_error?
|
76
|
+
400 <= status && 500 > status
|
77
|
+
end
|
78
|
+
|
79
|
+
def server_error?
|
80
|
+
500 <= status && 600 > status
|
81
|
+
end
|
62
82
|
end
|
63
83
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Middleware
|
3
|
+
require 'digest'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
class DigestAuth
|
7
|
+
include EventMachine::HttpEncoding
|
8
|
+
|
9
|
+
attr_accessor :auth_digest, :is_digest_auth
|
10
|
+
|
11
|
+
def initialize(www_authenticate, opts = {})
|
12
|
+
@nonce_count = -1
|
13
|
+
@opts = opts
|
14
|
+
@digest_params = {
|
15
|
+
algorithm: 'MD5' # MD5 is the default hashing algorithm
|
16
|
+
}
|
17
|
+
if (@is_digest_auth = www_authenticate =~ /^Digest/)
|
18
|
+
get_params(www_authenticate)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def request(client, head, body)
|
23
|
+
# Allow HTTP basic auth fallback
|
24
|
+
if @is_digest_auth
|
25
|
+
head['Authorization'] = build_auth_digest(client.req.method, client.req.uri.path, @opts.merge(@digest_params))
|
26
|
+
else
|
27
|
+
head['Authorization'] = [@opts[:username], @opts[:password]]
|
28
|
+
end
|
29
|
+
[head, body]
|
30
|
+
end
|
31
|
+
|
32
|
+
def response(resp)
|
33
|
+
# If the server responds with the Authentication-Info header, set the nonce to the new value
|
34
|
+
if @is_digest_auth && (authentication_info = resp.response_header['Authentication-Info'])
|
35
|
+
authentication_info =~ /nextnonce="?(.*?)"?(,|\z)/
|
36
|
+
@digest_params[:nonce] = $1
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_auth_digest(method, uri, params = nil)
|
41
|
+
params = @opts.merge(@digest_params) if !params
|
42
|
+
nonce_count = next_nonce
|
43
|
+
|
44
|
+
user = unescape params[:username]
|
45
|
+
password = unescape params[:password]
|
46
|
+
|
47
|
+
splitted_algorithm = params[:algorithm].split('-')
|
48
|
+
sess = "-sess" if splitted_algorithm[1]
|
49
|
+
raw_algorithm = splitted_algorithm[0]
|
50
|
+
if %w(MD5 SHA1 SHA2 SHA256 SHA384 SHA512 RMD160).include? raw_algorithm
|
51
|
+
algorithm = eval("Digest::#{raw_algorithm}")
|
52
|
+
else
|
53
|
+
raise "Unknown algorithm: #{raw_algorithm}"
|
54
|
+
end
|
55
|
+
qop = params[:qop]
|
56
|
+
cnonce = make_cnonce if qop or sess
|
57
|
+
a1 = if sess
|
58
|
+
[
|
59
|
+
algorithm.hexdigest("#{params[:username]}:#{params[:realm]}:#{params[:password]}"),
|
60
|
+
params[:nonce],
|
61
|
+
cnonce,
|
62
|
+
].join ':'
|
63
|
+
else
|
64
|
+
"#{params[:username]}:#{params[:realm]}:#{params[:password]}"
|
65
|
+
end
|
66
|
+
ha1 = algorithm.hexdigest a1
|
67
|
+
ha2 = algorithm.hexdigest "#{method}:#{uri}"
|
68
|
+
|
69
|
+
request_digest = [ha1, params[:nonce]]
|
70
|
+
request_digest.push(('%08x' % @nonce_count), cnonce, qop) if qop
|
71
|
+
request_digest << ha2
|
72
|
+
request_digest = request_digest.join ':'
|
73
|
+
header = [
|
74
|
+
"Digest username=\"#{params[:username]}\"",
|
75
|
+
"realm=\"#{params[:realm]}\"",
|
76
|
+
"algorithm=#{raw_algorithm}#{sess}",
|
77
|
+
"uri=\"#{uri}\"",
|
78
|
+
"nonce=\"#{params[:nonce]}\"",
|
79
|
+
"response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",
|
80
|
+
]
|
81
|
+
if params[:qop]
|
82
|
+
header << "qop=#{qop}"
|
83
|
+
header << "nc=#{'%08x' % @nonce_count}"
|
84
|
+
header << "cnonce=\"#{cnonce}\""
|
85
|
+
end
|
86
|
+
header << "opaque=\"#{params[:opaque]}\"" if params.key? :opaque
|
87
|
+
header.join(', ')
|
88
|
+
end
|
89
|
+
|
90
|
+
# Process the WWW_AUTHENTICATE header to get the authentication parameters
|
91
|
+
def get_params(www_authenticate)
|
92
|
+
www_authenticate.scan(/(\w+)="?(.*?)"?(,|\z)/).each do |match|
|
93
|
+
@digest_params[match[0].to_sym] = match[1]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Generate a client nonce
|
98
|
+
def make_cnonce
|
99
|
+
Digest::MD5.hexdigest [
|
100
|
+
Time.now.to_i,
|
101
|
+
$$,
|
102
|
+
SecureRandom.random_number(2**32),
|
103
|
+
].join ':'
|
104
|
+
end
|
105
|
+
|
106
|
+
# Keep track of the nounce count
|
107
|
+
def next_nonce
|
108
|
+
@nonce_count += 1
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Middleware
|
3
|
+
class OAuth2
|
4
|
+
include EM::HttpEncoding
|
5
|
+
attr_accessor :access_token
|
6
|
+
|
7
|
+
def initialize(opts={})
|
8
|
+
self.access_token = opts[:access_token] or raise "No :access_token provided"
|
9
|
+
end
|
10
|
+
|
11
|
+
def request(client, head, body)
|
12
|
+
uri = client.req.uri.dup
|
13
|
+
update_uri! uri
|
14
|
+
client.req.set_uri uri
|
15
|
+
|
16
|
+
[head, body]
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_uri!(uri)
|
20
|
+
if uri.query.nil?
|
21
|
+
uri.query = encode_param(:access_token, access_token)
|
22
|
+
else
|
23
|
+
uri.query += "&#{encode_param(:access_token, access_token)}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/em-http/request.rb
CHANGED
data/lib/em-http/version.rb
CHANGED
data/spec/client_spec.rb
CHANGED
@@ -453,6 +453,7 @@ describe EventMachine::HttpRequest do
|
|
453
453
|
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/timeout', :inactivity_timeout => 0.1).get
|
454
454
|
|
455
455
|
http.errback {
|
456
|
+
http.error.should == Errno::ETIMEDOUT
|
456
457
|
(Time.now.to_i - t).should <= 1
|
457
458
|
EventMachine.stop
|
458
459
|
}
|
@@ -682,9 +683,6 @@ describe EventMachine::HttpRequest do
|
|
682
683
|
end
|
683
684
|
|
684
685
|
it "should get the body without Content-Length" do
|
685
|
-
pending "blocked on new http_parser.rb release"
|
686
|
-
# https://github.com/igrigorik/em-http-request/issues/168
|
687
|
-
|
688
686
|
EventMachine.run {
|
689
687
|
@s = StubServer.new("HTTP/1.1 200 OK\r\n\r\nFoo")
|
690
688
|
|
@@ -763,15 +761,17 @@ describe EventMachine::HttpRequest do
|
|
763
761
|
|
764
762
|
it "should reconnect if connection was closed between requests" do
|
765
763
|
EventMachine.run {
|
766
|
-
conn = EM::HttpRequest.new('http://127.0.0.1:8090/'
|
767
|
-
req = conn.get
|
764
|
+
conn = EM::HttpRequest.new('http://127.0.0.1:8090/')
|
765
|
+
req = conn.get
|
768
766
|
|
769
767
|
req.callback do
|
770
|
-
|
771
|
-
req = conn.get
|
768
|
+
conn.close('client closing connection')
|
772
769
|
|
770
|
+
EM.next_tick do
|
771
|
+
req = conn.get :path => "/gzip"
|
773
772
|
req.callback do
|
774
773
|
req.response_header.status.should == 200
|
774
|
+
req.response.should match('compressed')
|
775
775
|
EventMachine.stop
|
776
776
|
end
|
777
777
|
end
|
@@ -779,6 +779,23 @@ describe EventMachine::HttpRequest do
|
|
779
779
|
}
|
780
780
|
end
|
781
781
|
|
782
|
+
it "should report error if connection was closed by server on client keepalive requests" do
|
783
|
+
EventMachine.run {
|
784
|
+
conn = EM::HttpRequest.new('http://127.0.0.1:8090/')
|
785
|
+
req = conn.get :keepalive => true
|
786
|
+
|
787
|
+
req.callback do
|
788
|
+
req = conn.get
|
789
|
+
|
790
|
+
req.callback { failed(http) }
|
791
|
+
req.errback do
|
792
|
+
req.error.should match('connection closed by server')
|
793
|
+
EventMachine.stop
|
794
|
+
end
|
795
|
+
end
|
796
|
+
}
|
797
|
+
end
|
798
|
+
|
782
799
|
it 'should handle malformed Content-Type header repetitions' do
|
783
800
|
EventMachine.run {
|
784
801
|
response =<<-HTTP.gsub(/^ +/, '').strip
|
@@ -830,4 +847,42 @@ describe EventMachine::HttpRequest do
|
|
830
847
|
}
|
831
848
|
}
|
832
849
|
end
|
850
|
+
|
851
|
+
context "User-Agent" do
|
852
|
+
it 'should default to "EventMachine HttpClient"' do
|
853
|
+
EventMachine.run {
|
854
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get
|
855
|
+
|
856
|
+
http.errback { failed(http) }
|
857
|
+
http.callback {
|
858
|
+
http.response.should == '"EventMachine HttpClient"'
|
859
|
+
EventMachine.stop
|
860
|
+
}
|
861
|
+
}
|
862
|
+
end
|
863
|
+
|
864
|
+
it 'should keep header if given empty string' do
|
865
|
+
EventMachine.run {
|
866
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get(:head => { 'user-agent'=>'' })
|
867
|
+
|
868
|
+
http.errback { failed(http) }
|
869
|
+
http.callback {
|
870
|
+
http.response.should == '""'
|
871
|
+
EventMachine.stop
|
872
|
+
}
|
873
|
+
}
|
874
|
+
end
|
875
|
+
|
876
|
+
it 'should ommit header if given nil' do
|
877
|
+
EventMachine.run {
|
878
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo-user-agent').get(:head => { 'user-agent'=>nil })
|
879
|
+
|
880
|
+
http.errback { failed(http) }
|
881
|
+
http.callback {
|
882
|
+
http.response.should == 'nil'
|
883
|
+
EventMachine.stop
|
884
|
+
}
|
885
|
+
}
|
886
|
+
end
|
887
|
+
end
|
833
888
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
$: << 'lib' << '../lib'
|
4
|
+
|
5
|
+
require 'em-http/middleware/digest_auth'
|
6
|
+
|
7
|
+
describe 'Digest Auth Authentication header generation' do
|
8
|
+
before :each do
|
9
|
+
@reference_header = 'Digest username="digest_username", realm="DigestAuth_REALM", algorithm=MD5, uri="/", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", response="96829962ffc31fa2852f86dc7f9f609b", opaque="BzdNK3gsJ2ixTrBJ"'
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should generate the correct header' do
|
13
|
+
www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
|
14
|
+
|
15
|
+
params = {
|
16
|
+
username: 'digest_username',
|
17
|
+
password: 'digest_password'
|
18
|
+
}
|
19
|
+
|
20
|
+
middleware = EM::Middleware::DigestAuth.new(www_authenticate, params)
|
21
|
+
middleware.build_auth_digest('GET', '/').should == @reference_header
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should not generate the same header for a different user' do
|
25
|
+
www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg5", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
|
26
|
+
|
27
|
+
params = {
|
28
|
+
username: 'digest_username_2',
|
29
|
+
password: 'digest_password'
|
30
|
+
}
|
31
|
+
|
32
|
+
middleware = EM::Middleware::DigestAuth.new(www_authenticate, params)
|
33
|
+
middleware.build_auth_digest('GET', '/').should_not == @reference_header
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should not generate the same header if the nounce changes' do
|
37
|
+
www_authenticate = 'Digest realm="DigestAuth_REALM", nonce="MDAxMzQzNzQwNjA2OmRjZjAyZDY3YWMyMWVkZGQ4OWE2Nzg3ZTY3YTNlMjg6", opaque="BzdNK3gsJ2ixTrBJ", stale=false, algorithm=MD5'
|
38
|
+
|
39
|
+
params = {
|
40
|
+
username: 'digest_username_2',
|
41
|
+
password: 'digest_password'
|
42
|
+
}
|
43
|
+
|
44
|
+
middleware = EM::Middleware::DigestAuth.new(www_authenticate, params)
|
45
|
+
middleware.build_auth_digest('GET', '/').should_not == @reference_header
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
data/spec/external_spec.rb
CHANGED
@@ -17,7 +17,7 @@ requires_connection do
|
|
17
17
|
|
18
18
|
it "should follow redirect to https and initiate the handshake" do
|
19
19
|
EventMachine.run {
|
20
|
-
http = EventMachine::HttpRequest.new('http://
|
20
|
+
http = EventMachine::HttpRequest.new('http://github.com/').get :redirects => 5
|
21
21
|
|
22
22
|
http.errback { failed(http) }
|
23
23
|
http.callback {
|
@@ -31,7 +31,7 @@ requires_connection do
|
|
31
31
|
EventMachine.run {
|
32
32
|
|
33
33
|
# digg.com uses chunked encoding
|
34
|
-
http = EventMachine::HttpRequest.new('http://
|
34
|
+
http = EventMachine::HttpRequest.new('http://www.httpwatch.com/httpgallery/chunked/').get
|
35
35
|
|
36
36
|
http.errback { failed(http) }
|
37
37
|
http.callback {
|
Binary file
|
data/spec/gzip_spec.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe EventMachine::HttpDecoders::GZip do
|
4
|
+
|
5
|
+
let(:compressed) {
|
6
|
+
compressed = ["1f8b08089668a6500003686900cbc8e402007a7a6fed03000000"].pack("H*")
|
7
|
+
}
|
8
|
+
|
9
|
+
it "should extract the stream of a vanilla gzip" do
|
10
|
+
header = EventMachine::HttpDecoders::GZipHeader.new
|
11
|
+
stream = header.extract_stream(compressed)
|
12
|
+
|
13
|
+
stream.unpack("H*")[0].should eq("cbc8e402007a7a6fed03000000")
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should decompress a vanilla gzip" do
|
17
|
+
decompressed = ""
|
18
|
+
|
19
|
+
gz = EventMachine::HttpDecoders::GZip.new do |data|
|
20
|
+
decompressed << data
|
21
|
+
end
|
22
|
+
|
23
|
+
gz << compressed
|
24
|
+
gz.finalize!
|
25
|
+
|
26
|
+
decompressed.should eq("hi\n")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should decompress a vanilla gzip file byte by byte" do
|
30
|
+
decompressed = ""
|
31
|
+
|
32
|
+
gz = EventMachine::HttpDecoders::GZip.new do |data|
|
33
|
+
decompressed << data
|
34
|
+
end
|
35
|
+
|
36
|
+
compressed.each_char do |byte|
|
37
|
+
gz << byte
|
38
|
+
end
|
39
|
+
|
40
|
+
gz.finalize!
|
41
|
+
|
42
|
+
decompressed.should eq("hi\n")
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should decompress a large file" do
|
46
|
+
decompressed = ""
|
47
|
+
|
48
|
+
gz = EventMachine::HttpDecoders::GZip.new do |data|
|
49
|
+
decompressed << data
|
50
|
+
end
|
51
|
+
|
52
|
+
gz << File.read(File.dirname(__FILE__) + "/fixtures/gzip-sample.gz")
|
53
|
+
|
54
|
+
gz.finalize!
|
55
|
+
|
56
|
+
decompressed.size.should eq(32907)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should fail with a DecoderError if not a gzip file" do
|
60
|
+
not_a_gzip = ["1f8c08089668a650000"].pack("H*")
|
61
|
+
header = EventMachine::HttpDecoders::GZipHeader.new
|
62
|
+
|
63
|
+
lambda {
|
64
|
+
header.extract_stream(not_a_gzip)
|
65
|
+
}.should raise_exception(EventMachine::HttpDecoders::DecoderError)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|