httpi 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,6 +2,9 @@ require "uri"
2
2
 
3
3
  require "httpi/adapter/base"
4
4
  require "httpi/response"
5
+ require 'net/ntlm'
6
+ require 'kconv'
7
+ require 'socket'
5
8
 
6
9
  module HTTPI
7
10
  module Adapter
@@ -27,10 +30,17 @@ module HTTPI
27
30
  unless REQUEST_METHODS.include? method
28
31
  raise NotSupportedError, "Net::HTTP does not support custom HTTP methods"
29
32
  end
30
-
31
33
  do_request(method) do |http, http_request|
32
34
  http_request.body = @request.body
33
- http.request http_request
35
+ if @request.on_body then
36
+ perform(http, http_request) do |res|
37
+ res.read_body do |seg|
38
+ @request.on_body.call(seg)
39
+ end
40
+ end
41
+ else
42
+ perform(http, http_request)
43
+ end
34
44
  end
35
45
  rescue OpenSSL::SSL::SSLError
36
46
  raise SSLError
@@ -41,19 +51,81 @@ module HTTPI
41
51
 
42
52
  private
43
53
 
54
+ def perform(http, http_request, &block)
55
+ http.request http_request, &block
56
+ end
57
+
44
58
  def create_client
45
59
  proxy_url = @request.proxy || URI("")
46
60
  proxy = Net::HTTP::Proxy(proxy_url.host, proxy_url.port, proxy_url.user, proxy_url.password)
47
61
  proxy.new(@request.url.host, @request.url.port)
48
62
  end
49
63
 
50
- def do_request(type)
64
+ def do_request(type, &requester)
65
+ setup
66
+ response = @client.start do |http|
67
+ negotiate_ntlm_auth(http, &requester) if @request.auth.ntlm?
68
+ requester.call(http, request_client(type))
69
+ end
70
+ respond_with(response)
71
+ end
72
+
73
+ def setup
51
74
  setup_client
52
75
  setup_ssl_auth if @request.auth.ssl?
76
+ end
77
+
78
+ def negotiate_ntlm_auth(http, &requester)
79
+ # first figure out if we should use NTLM or Negotiate
80
+ nego_auth_response = respond_with(requester.call(http, request_client(:head)))
81
+ if nego_auth_response.headers['www-authenticate'].include? 'Negotiate'
82
+ auth_method = 'Negotiate'
83
+ elsif nego_auth_response.headers['www-authenticate'].include? 'NTLM'
84
+ auth_method = 'NTLM'
85
+ else
86
+ auth_method = 'NTLM'
87
+ HTTPI.logger.debug 'Server does not support NTLM/Negotiate. Trying NTLM anyway'
88
+ end
89
+
90
+ # initiate a request is to authenticate (exchange secret and auth) using the method determined above...
91
+ ntlm_message_type1 = Net::NTLM::Message::Type1.new
92
+ %w(workstation domain).each do |a|
93
+ ntlm_message_type1.send("#{a}=",'')
94
+ ntlm_message_type1.enable(a.to_sym)
95
+ end
96
+
97
+ @request.headers["Authorization"] = "#{auth_method} #{ntlm_message_type1.encode64}"
98
+
99
+ auth_response = respond_with(requester.call(http, request_client(:head)))
100
+
101
+ # build an authentication request based on the token provided by the server
102
+ if auth_response.headers["WWW-Authenticate"] =~ /(NTLM|Negotiate) (.+)/
103
+ auth_token = $2
104
+ ntlm_message = Net::NTLM::Message.decode64(auth_token)
105
+
106
+ message_builder = {}
107
+ # copy the username and password from the authorization parameters
108
+ message_builder[:user] = @request.auth.ntlm[0]
109
+ message_builder[:password] = @request.auth.ntlm[1]
110
+
111
+ # we need to provide a domain in the packet if an only if it was provided by the user in the auth request
112
+ if @request.auth.ntlm[2]
113
+ message_builder[:domain] = Net::NTLM::EncodeUtil.encode_utf16le(@request.auth.ntlm[2].upcase)
114
+ else
115
+ message_builder[:domain] = ''
116
+ end
117
+
118
+ # we should also provide the workstation name, currently the rubyntlm provider does not automatically
119
+ # set the workstation name
120
+ message_builder[:workstation] = Net::NTLM::EncodeUtil.encode_utf16le(Socket.gethostname)
121
+
122
+ ntlm_response = ntlm_message.response(message_builder ,
123
+ {:ntlmv2 => true})
124
+ # Finally add header of Authorization
125
+ @request.headers["Authorization"] = "#{auth_method} #{ntlm_response.encode64}"
126
+ end
53
127
 
54
- respond_with(@client.start do |http|
55
- yield http, request_client(type)
56
- end)
128
+ nil
57
129
  end
58
130
 
59
131
  def setup_client
@@ -66,11 +138,13 @@ module HTTPI
66
138
  ssl = @request.auth.ssl
67
139
 
68
140
  unless ssl.verify_mode == :none
69
- @client.key = ssl.cert_key
70
- @client.cert = ssl.cert
71
141
  @client.ca_file = ssl.ca_cert_file if ssl.ca_cert_file
72
142
  end
73
143
 
144
+ # Send client-side certificate regardless of state of SSL verify mode
145
+ @client.key = ssl.cert_key
146
+ @client.cert = ssl.cert
147
+
74
148
  @client.verify_mode = ssl.openssl_verify_mode
75
149
  @client.ssl_version = ssl.ssl_version if ssl.ssl_version
76
150
  end
@@ -87,6 +161,10 @@ module HTTPI
87
161
  request_client = request_class.new @request.url.request_uri, @request.headers
88
162
  request_client.basic_auth *@request.auth.credentials if @request.auth.basic?
89
163
 
164
+ if @request.auth.digest?
165
+ raise NotSupportedError, "Net::HTTP does not support HTTP digest authentication"
166
+ end
167
+
90
168
  request_client
91
169
  end
92
170
 
@@ -95,7 +173,8 @@ module HTTPI
95
173
  headers.each do |key, value|
96
174
  headers[key] = value[0] if value.size <= 1
97
175
  end
98
- Response.new response.code, headers, response.body
176
+ body = (response.body.kind_of?(Net::ReadAdapter) ? "" : response.body)
177
+ Response.new response.code, headers, body
99
178
  end
100
179
 
101
180
  end
@@ -0,0 +1,43 @@
1
+ module HTTPI
2
+ module Adapter
3
+
4
+ # = HTTPI::Adapter::NetHTTPPersistent
5
+ #
6
+ # Adapter for the Net::HTTP::Persistent client.
7
+ # http://docs.seattlerb.org/net-http-persistent/Net/HTTP/Persistent.html
8
+ class NetHTTPPersistent < NetHTTP
9
+
10
+ register :net_http_persistent, :deps => %w(net/http/persistent)
11
+
12
+ private
13
+
14
+ def create_client
15
+ Net::HTTP::Persistent.new thread_key
16
+ end
17
+
18
+ def perform(http, http_request, &on_body)
19
+ http.request @request.url, http_request, &on_body
20
+ end
21
+
22
+ def do_request(type, &requester)
23
+ setup
24
+ response = requester.call @client, request_client(type)
25
+ respond_with(response)
26
+ end
27
+
28
+ def setup_client
29
+ if @request.auth.ntlm?
30
+ raise NotSupportedError, "Net::HTTP-Persistent does not support NTLM authentication"
31
+ end
32
+
33
+ @client.open_timeout = @request.open_timeout if @request.open_timeout
34
+ @client.read_timeout = @request.read_timeout if @request.read_timeout
35
+ end
36
+
37
+ def thread_key
38
+ @request.url.host.split(/\W/).reject{|p|p == ""}.join('-')
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,92 @@
1
+ require 'httpi/adapter/base'
2
+ require 'httpi/response'
3
+
4
+ module HTTPI
5
+ module Adapter
6
+
7
+ # = HTTPI::Adapter::Rack
8
+ #
9
+ # Adapter for Rack::MockRequest.
10
+ # Due to limitations, not all features are supported.
11
+ # https://github.com/rack/rack/blob/master/lib/rack/mock.rb
12
+ #
13
+ # Usage:
14
+ #
15
+ # HTTPI::Adapter::Rack.mount 'application', RackApplication
16
+ # HTTPI.get("http://application/path", :rack)
17
+ class Rack < Base
18
+ register :rack, :deps => %w(rack/mock)
19
+
20
+ attr_reader :client
21
+
22
+ class << self
23
+ attr_accessor :mounted_apps
24
+ end
25
+
26
+ self.mounted_apps = {}
27
+
28
+ # Attaches Rack endpoint at specified host.
29
+ # Endpoint will be acessible at {http://host/ http://host/} url.
30
+ def self.mount(host, application)
31
+ self.mounted_apps[host] = application
32
+ end
33
+
34
+ # Removes Rack endpoint.
35
+ def self.unmount(host)
36
+ self.mounted_apps.delete(host)
37
+ end
38
+
39
+ def initialize(request)
40
+ @app = self.class.mounted_apps[request.url.host]
41
+
42
+
43
+ if @app.nil?
44
+ message = "Application '#{request.url.host}' not mounted: ";
45
+ message += "use `HTTPI::Adapter::Rack.mount('#{request.url.host}', RackApplicationClass)`"
46
+
47
+ raise message
48
+ end
49
+
50
+ @request = request
51
+ @client = ::Rack::MockRequest.new(@app)
52
+ end
53
+
54
+ # Executes arbitrary HTTP requests.
55
+ # You have to mount required Rack application before you can use it.
56
+ #
57
+ # @see .mount
58
+ # @see HTTPI.request
59
+ def request(method)
60
+ unless REQUEST_METHODS.include? method
61
+ raise NotSupportedError, "Rack adapter does not support custom HTTP methods"
62
+ end
63
+
64
+ env = {}
65
+ @request.headers.each do |header, value|
66
+ env["HTTP_#{header.gsub('-', '_').upcase}"] = value
67
+ end
68
+
69
+ if @request.proxy
70
+ raise NotSupportedError, "Rack adapter does not support proxying"
71
+ end
72
+
73
+ if @request.auth.http?
74
+ raise NotSupportedError, "Rack adapter does not support HTTP auth"
75
+ end
76
+
77
+ if @request.auth.ssl?
78
+ raise NotSupportedError, "Rack adapter does not support SSL client auth"
79
+ end
80
+
81
+ if @request.on_body
82
+ raise NotSupportedError, "Rack adapter does not support response streaming"
83
+ end
84
+
85
+ response = @client.request(method.to_s.upcase, @request.url.to_s,
86
+ { :fatal => true, :input => @request.body.to_s }.merge(env))
87
+
88
+ Response.new(response.status, response.headers, response.body)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -10,7 +10,7 @@ module HTTPI
10
10
  class Config
11
11
 
12
12
  # Supported authentication types.
13
- TYPES = [:basic, :digest, :gssnegotiate, :ssl]
13
+ TYPES = [:basic, :digest, :gssnegotiate, :ssl, :ntlm]
14
14
 
15
15
  # Accessor for the HTTP basic auth credentials.
16
16
  def basic(*args)
@@ -53,14 +53,15 @@ module HTTPI
53
53
  type == :basic || type == :digest
54
54
  end
55
55
 
56
- # Only available with the httpi-ntlm gem.
57
56
  def ntlm(*args)
58
- raise "Install the httpi-ntlm gem for experimental NTLM support"
57
+ return @ntlm if args.empty?
58
+
59
+ self.type = :ntlm
60
+ @ntlm = args.flatten.compact
59
61
  end
60
62
 
61
- # Only available with the httpi-ntlm gem.
62
63
  def ntlm?
63
- raise "Install the httpi-ntlm gem for experimental NTLM support"
64
+ type == :ntlm
64
65
  end
65
66
 
66
67
  # Returns the <tt>HTTPI::Auth::SSL</tt> object.
data/lib/httpi/request.rb CHANGED
@@ -99,6 +99,15 @@ module HTTPI
99
99
  @body = params.kind_of?(Hash) ? Rack::Utils.build_query(params) : params
100
100
  end
101
101
 
102
+ # Sets the block to be called while processing the response. The block
103
+ # accepts a single parameter - the chunked response body.
104
+ def on_body(&block)
105
+ if block_given? then
106
+ @on_body = block
107
+ end
108
+ @on_body
109
+ end
110
+
102
111
  # Returns the <tt>HTTPI::Authentication</tt> object.
103
112
  def auth
104
113
  @auth ||= Auth::Config.new
data/lib/httpi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module HTTPI
2
2
 
3
- VERSION = "2.0.2"
3
+ VERSION = "2.1.0"
4
4
 
5
5
  end
@@ -168,6 +168,15 @@ unless RUBY_PLATFORM =~ /java/
168
168
  end
169
169
  end
170
170
 
171
+ describe "NTLM authentication" do
172
+ it "is not supported" do
173
+ request.auth.ntlm("tester", "vReqSoafRe5O")
174
+
175
+ expect { adapter.request(:get) }.
176
+ to raise_error(HTTPI::NotSupportedError, /does not support NTLM authentication/)
177
+ end
178
+ end
179
+
171
180
  describe "http_auth_types" do
172
181
  it "is set to :basic for HTTP basic auth" do
173
182
  request.auth.basic "username", "password"
@@ -217,6 +226,14 @@ unless RUBY_PLATFORM =~ /java/
217
226
  request
218
227
  end
219
228
 
229
+ it "send certificate regardless of state of SSL verify mode" do
230
+ request.auth.ssl.verify_mode = :none
231
+ curb.expects(:cert_key=).with(request.auth.ssl.cert_key_file)
232
+ curb.expects(:cert=).with(request.auth.ssl.cert_file)
233
+
234
+ adapter.request(:get)
235
+ end
236
+
220
237
  it "cert_key, cert and ssl_verify_peer should be set" do
221
238
  curb.expects(:cert_key=).with(request.auth.ssl.cert_key_file)
222
239
  curb.expects(:cert=).with(request.auth.ssl.cert_file)
@@ -21,7 +21,7 @@ begin
21
21
  describe "#request(:get)" do
22
22
  it "returns a valid HTTPI::Response" do
23
23
  em_http.expects(:get).
24
- with(:query => nil, :connect_timeout => nil, :inactivity_timeout => nil, :head => {}, :body => nil).
24
+ with(:query => nil, :head => {}, :body => nil).
25
25
  returns(http_message)
26
26
 
27
27
  adapter.request(:get).should match_response(:body => Fixture.xml)
@@ -31,7 +31,7 @@ begin
31
31
  describe "#request(:post)" do
32
32
  it "returns a valid HTTPI::Response" do
33
33
  em_http.expects(:post).
34
- with(:query => nil, :connect_timeout => nil, :inactivity_timeout => nil, :head => {}, :body => Fixture.xml).
34
+ with(:query => nil, :head => {}, :body => Fixture.xml).
35
35
  returns(http_message)
36
36
 
37
37
  request.body = Fixture.xml
@@ -42,7 +42,7 @@ begin
42
42
  describe "#request(:head)" do
43
43
  it "returns a valid HTTPI::Response" do
44
44
  em_http.expects(:head).
45
- with(:query => nil, :connect_timeout => nil, :inactivity_timeout => nil, :head => {}, :body => nil).
45
+ with(:query => nil, :head => {}, :body => nil).
46
46
  returns(http_message)
47
47
 
48
48
  adapter.request(:head).should match_response(:body => Fixture.xml)
@@ -52,7 +52,7 @@ begin
52
52
  describe "#request(:put)" do
53
53
  it "returns a valid HTTPI::Response" do
54
54
  em_http.expects(:put).
55
- with(:query => nil, :connect_timeout => nil, :inactivity_timeout => nil, :head => {}, :body => Fixture.xml).
55
+ with(:query => nil, :head => {}, :body => Fixture.xml).
56
56
  returns(http_message)
57
57
 
58
58
  request.body = Fixture.xml
@@ -63,7 +63,7 @@ begin
63
63
  describe "#request(:delete)" do
64
64
  it "returns a valid HTTPI::Response" do
65
65
  em_http.expects(:delete).
66
- with(:query => nil, :connect_timeout => nil, :inactivity_timeout => nil, :head => {}, :body => nil).
66
+ with(:query => nil, :head => {}, :body => nil).
67
67
  returns(http_message(""))
68
68
 
69
69
  adapter.request(:delete).should match_response(:body => "")
@@ -73,7 +73,7 @@ begin
73
73
  describe "#request(:custom)" do
74
74
  it "returns a valid HTTPI::Response" do
75
75
  em_http.expects(:custom).
76
- with(:query => nil, :connect_timeout => nil, :inactivity_timeout => nil, :head => {}, :body => nil).
76
+ with(:query => nil, :head => {}, :body => nil).
77
77
  returns(http_message(""))
78
78
 
79
79
  adapter.request(:custom).should match_response(:body => "")
@@ -88,38 +88,48 @@ begin
88
88
  request.proxy.password = "password"
89
89
  end
90
90
 
91
- it "sets host, port, and authorization" do
92
- em_http.expects(:get).once.
93
- with(has_entries(:proxy => { :host => "proxy-host.com", :port => 443, :authorization => %w( username password ) })).
94
- returns(http_message)
91
+ it "sets host, port and authorization" do
92
+ url = 'http://example.com:80'
95
93
 
96
- adapter.request(:get)
94
+ connection_options = {
95
+ :connect_timeout => nil,
96
+ :inactivity_timeout => nil,
97
+ :proxy => {
98
+ :host => 'proxy-host.com',
99
+ :port => 443,
100
+ :authorization => ['username', 'password']
101
+ }
102
+ }
103
+
104
+ EventMachine::HttpRequest.expects(:new).with(url, connection_options)
105
+
106
+ adapter
97
107
  end
98
108
  end
99
109
 
100
110
  describe "connect_timeout" do
101
- it "is not set unless specified" do
102
- em_http.expects(:get).once.with(has_entries(:connect_timeout => nil)).returns(http_message)
103
- adapter.request(:get)
104
- end
105
-
106
- it "is set if specified" do
111
+ it "is passed as a connection option" do
107
112
  request.open_timeout = 30
108
- em_http.expects(:get).once.with(has_entries(:connect_timeout => 30)).returns(http_message)
109
- adapter.request(:get)
113
+
114
+ url = 'http://example.com:80'
115
+ connection_options = { :connect_timeout => 30, :inactivity_timeout => nil }
116
+
117
+ EventMachine::HttpRequest.expects(:new).with(url, connection_options)
118
+
119
+ adapter
110
120
  end
111
121
  end
112
122
 
113
123
  describe "receive_timeout" do
114
- it "is not set unless specified" do
115
- em_http.expects(:get).once.with(has_entries(:inactivity_timeout => nil)).returns(http_message)
116
- adapter.request(:get)
117
- end
124
+ it "is passed as a connection option" do
125
+ request.read_timeout = 60
118
126
 
119
- it "is set if specified" do
120
- request.read_timeout = 30
121
- em_http.expects(:get).once.with(has_entries(:inactivity_timeout => 30)).returns(http_message)
122
- adapter.request(:get)
127
+ url = 'http://example.com:80'
128
+ connection_options = { :connect_timeout => nil, :inactivity_timeout => 60 }
129
+
130
+ EventMachine::HttpRequest.expects(:new).with(url, connection_options)
131
+
132
+ adapter
123
133
  end
124
134
  end
125
135