httpi 2.0.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +42 -16
- data/Gemfile +12 -6
- data/README.md +19 -9
- data/httpi.gemspec +2 -1
- data/lib/httpi.rb +3 -0
- data/lib/httpi/adapter.rb +1 -1
- data/lib/httpi/adapter/curb.rb +11 -2
- data/lib/httpi/adapter/em_http.rb +21 -9
- data/lib/httpi/adapter/excon.rb +80 -0
- data/lib/httpi/adapter/httpclient.rb +13 -6
- data/lib/httpi/adapter/net_http.rb +88 -9
- data/lib/httpi/adapter/net_http_persistent.rb +43 -0
- data/lib/httpi/adapter/rack.rb +92 -0
- data/lib/httpi/auth/config.rb +6 -5
- data/lib/httpi/request.rb +9 -0
- data/lib/httpi/version.rb +1 -1
- data/spec/httpi/adapter/curb_spec.rb +17 -0
- data/spec/httpi/adapter/em_http_spec.rb +37 -27
- data/spec/httpi/adapter/excon_spec.rb +96 -0
- data/spec/httpi/adapter/httpclient_spec.rb +16 -8
- data/spec/httpi/adapter/net_http_persistent_spec.rb +96 -0
- data/spec/httpi/adapter/net_http_spec.rb +24 -151
- data/spec/httpi/adapter/rack_spec.rb +111 -0
- data/spec/httpi/auth/config_spec.rb +28 -0
- data/spec/httpi/httpi_spec.rb +17 -1
- data/spec/integration/curb_spec.rb +12 -0
- data/spec/integration/em_http_spec.rb +2 -0
- data/spec/integration/httpclient_spec.rb +32 -18
- data/spec/integration/net_http_persistent_spec.rb +139 -0
- data/spec/integration/net_http_spec.rb +59 -14
- data/spec/integration/support/application.rb +28 -0
- data/spec/spec_helper.rb +15 -6
- metadata +34 -32
- data/.rvmrc +0 -1
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/httpi/auth/config.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
@@ -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, :
|
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, :
|
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, :
|
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, :
|
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, :
|
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, :
|
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
|
92
|
-
|
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
|
-
|
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
|
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
|
-
|
109
|
-
|
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
|
115
|
-
|
116
|
-
adapter.request(:get)
|
117
|
-
end
|
124
|
+
it "is passed as a connection option" do
|
125
|
+
request.read_timeout = 60
|
118
126
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
|