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