httpi 2.0.2 → 2.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.
@@ -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