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.

@@ -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