em-http-request 1.0.3 → 1.1.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/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
|