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.

@@ -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
- uri.port = (uri.scheme == "https" ? (uri.port || 443) : (uri.port || 80))
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?; @proxy && [nil, :http].include?(@proxy[:type]); end
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
@@ -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
@@ -3,6 +3,7 @@ module EventMachine
3
3
  @middleware = []
4
4
 
5
5
  def self.new(uri, options={})
6
+ uri = uri.clone
6
7
  connopt = HttpConnectionOptions.new(uri, options)
7
8
 
8
9
  c = HttpConnection.new
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  class HttpRequest
3
- VERSION = "1.0.3"
3
+ VERSION = "1.1.0"
4
4
  end
5
5
  end
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/', :inactivity_timeout => 0.5)
767
- req = conn.get :keepalive => true
764
+ conn = EM::HttpRequest.new('http://127.0.0.1:8090/')
765
+ req = conn.get
768
766
 
769
767
  req.callback do
770
- EM.add_timer(1) do
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
@@ -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://analytics.postrank.com/').get :redirects => 5
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://digg.com/news').get
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