httpclient 2.4.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -837
- data/lib/httpclient.rb +4 -1
- data/lib/httpclient/auth.rb +14 -50
- data/lib/httpclient/http.rb +4 -4
- data/lib/httpclient/session.rb +3 -3
- data/lib/httpclient/ssl_config.rb +6 -1
- data/lib/httpclient/util.rb +12 -0
- data/lib/httpclient/version.rb +1 -1
- data/sample/github.rb +11 -0
- data/test/test_auth.rb +13 -0
- data/test/test_http-access2.rb +2 -1
- data/test/test_httpclient.rb +1 -1
- data/test/test_ssl.rb +47 -1
- metadata +5 -4
data/lib/httpclient.rb
CHANGED
@@ -870,8 +870,11 @@ private
|
|
870
870
|
|
871
871
|
class KeepAliveDisconnected < StandardError # :nodoc:
|
872
872
|
attr_reader :sess
|
873
|
-
|
873
|
+
attr_reader :cause
|
874
|
+
def initialize(sess = nil, cause = nil)
|
875
|
+
super("#{self.class.name}: #{cause ? cause.message : nil}")
|
874
876
|
@sess = sess
|
877
|
+
@cause = cause
|
875
878
|
end
|
876
879
|
end
|
877
880
|
|
data/lib/httpclient/auth.rb
CHANGED
@@ -13,27 +13,9 @@ require 'mutex_m'
|
|
13
13
|
|
14
14
|
class HTTPClient
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
rescue LoadError
|
20
|
-
NTLMEnabled = false
|
21
|
-
end
|
22
|
-
|
23
|
-
begin
|
24
|
-
require 'win32/sspi'
|
25
|
-
SSPIEnabled = true
|
26
|
-
rescue LoadError
|
27
|
-
SSPIEnabled = false
|
28
|
-
end
|
29
|
-
|
30
|
-
begin
|
31
|
-
require 'gssapi'
|
32
|
-
GSSAPIEnabled = true
|
33
|
-
rescue LoadError
|
34
|
-
GSSAPIEnabled = false
|
35
|
-
end
|
36
|
-
|
16
|
+
NTLMEnabled = false
|
17
|
+
SSPIEnabled = false
|
18
|
+
GSSAPIEnabled = false
|
37
19
|
|
38
20
|
# Common abstract class for authentication filter.
|
39
21
|
#
|
@@ -239,7 +221,6 @@ class HTTPClient
|
|
239
221
|
def initialize
|
240
222
|
super
|
241
223
|
@cred = nil
|
242
|
-
@set = false
|
243
224
|
@auth = {}
|
244
225
|
@challenge = {}
|
245
226
|
@scheme = "Basic"
|
@@ -263,15 +244,12 @@ class HTTPClient
|
|
263
244
|
uri = Util.uri_dirname(uri)
|
264
245
|
@auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
265
246
|
end
|
266
|
-
@set = true
|
267
247
|
end
|
268
248
|
end
|
269
249
|
|
270
250
|
# have we marked this as set - ie that it's valid to use in this context?
|
271
251
|
def set?
|
272
|
-
|
273
|
-
@set == true
|
274
|
-
}
|
252
|
+
@cred || @auth.any?
|
275
253
|
end
|
276
254
|
|
277
255
|
# Response handler: returns credential.
|
@@ -305,12 +283,10 @@ class HTTPClient
|
|
305
283
|
def set(uri, user, passwd)
|
306
284
|
synchronize do
|
307
285
|
@cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
308
|
-
@set = true
|
309
286
|
end
|
310
287
|
end
|
311
288
|
|
312
289
|
def get(req)
|
313
|
-
target_uri = req.header.request_uri
|
314
290
|
synchronize {
|
315
291
|
return nil unless @challenge['challenged']
|
316
292
|
@cred
|
@@ -339,7 +315,6 @@ class HTTPClient
|
|
339
315
|
super
|
340
316
|
@auth = {}
|
341
317
|
@challenge = {}
|
342
|
-
@set = false
|
343
318
|
@nonce_count = 0
|
344
319
|
@scheme = "Digest"
|
345
320
|
end
|
@@ -360,15 +335,12 @@ class HTTPClient
|
|
360
335
|
uri = Util.uri_dirname(uri)
|
361
336
|
@auth[uri] = [user, passwd]
|
362
337
|
end
|
363
|
-
@set = true
|
364
338
|
end
|
365
339
|
end
|
366
340
|
|
367
341
|
# have we marked this as set - ie that it's valid to use in this context?
|
368
342
|
def set?
|
369
|
-
|
370
|
-
@set == true
|
371
|
-
}
|
343
|
+
@auth.any?
|
372
344
|
end
|
373
345
|
|
374
346
|
# Response handler: returns credential.
|
@@ -478,7 +450,6 @@ class HTTPClient
|
|
478
450
|
# overrides DigestAuth#set. sets default user name and password. uri is not used.
|
479
451
|
def set(uri, user, passwd)
|
480
452
|
synchronize do
|
481
|
-
@set = true
|
482
453
|
@auth = [user, passwd]
|
483
454
|
end
|
484
455
|
end
|
@@ -487,7 +458,6 @@ class HTTPClient
|
|
487
458
|
# regardless of target uri if the proxy has required authentication
|
488
459
|
# before
|
489
460
|
def get(req)
|
490
|
-
target_uri = req.header.request_uri
|
491
461
|
synchronize {
|
492
462
|
param = @challenge
|
493
463
|
return nil unless param
|
@@ -531,7 +501,6 @@ class HTTPClient
|
|
531
501
|
@auth_default = nil
|
532
502
|
@challenge = {}
|
533
503
|
@scheme = scheme
|
534
|
-
@set = false
|
535
504
|
@ntlm_opt = {
|
536
505
|
:ntlmv2 => true
|
537
506
|
}
|
@@ -555,21 +524,17 @@ class HTTPClient
|
|
555
524
|
else
|
556
525
|
@auth_default = [user, passwd]
|
557
526
|
end
|
558
|
-
@set = true
|
559
527
|
end
|
560
528
|
end
|
561
529
|
|
562
530
|
# have we marked this as set - ie that it's valid to use in this context?
|
563
531
|
def set?
|
564
|
-
|
565
|
-
@set == true
|
566
|
-
}
|
532
|
+
@auth_default || @auth.any?
|
567
533
|
end
|
568
534
|
|
569
535
|
# Response handler: returns credential.
|
570
536
|
# See ruby/ntlm for negotiation state transition.
|
571
537
|
def get(req)
|
572
|
-
return nil unless NTLMEnabled
|
573
538
|
target_uri = req.header.request_uri
|
574
539
|
synchronize {
|
575
540
|
domain_uri, param = @challenge.find { |uri, v|
|
@@ -583,6 +548,7 @@ class HTTPClient
|
|
583
548
|
user, passwd = @auth_default
|
584
549
|
end
|
585
550
|
return nil unless user
|
551
|
+
Util.try_require('net/ntlm') || return
|
586
552
|
domain = nil
|
587
553
|
domain, user = user.split("\\") if user.index("\\")
|
588
554
|
state = param[:state]
|
@@ -606,7 +572,6 @@ class HTTPClient
|
|
606
572
|
|
607
573
|
# Challenge handler: remember URL and challenge token for response.
|
608
574
|
def challenge(uri, param_str)
|
609
|
-
return false unless NTLMEnabled
|
610
575
|
synchronize {
|
611
576
|
if param_str.nil? or @challenge[uri].nil?
|
612
577
|
c = @challenge[uri] = {}
|
@@ -655,27 +620,27 @@ class HTTPClient
|
|
655
620
|
# not supported
|
656
621
|
end
|
657
622
|
|
658
|
-
#
|
623
|
+
# Check always (not effective but it works)
|
659
624
|
def set?
|
660
|
-
|
625
|
+
!@challenge.empty?
|
661
626
|
end
|
662
627
|
|
663
628
|
# Response handler: returns credential.
|
664
629
|
# See win32/sspi for negotiation state transition.
|
665
630
|
def get(req)
|
666
|
-
return nil unless SSPIEnabled || GSSAPIEnabled
|
667
631
|
target_uri = req.header.request_uri
|
668
632
|
synchronize {
|
669
633
|
domain_uri, param = @challenge.find { |uri, v|
|
670
634
|
Util.uri_part_of(target_uri, uri)
|
671
635
|
}
|
672
636
|
return nil unless param
|
637
|
+
Util.try_require('win32/sspi') || Util.try_require('gssapi') || return
|
673
638
|
state = param[:state]
|
674
639
|
authenticator = param[:authenticator]
|
675
640
|
authphrase = param[:authphrase]
|
676
641
|
case state
|
677
642
|
when :init
|
678
|
-
if
|
643
|
+
if defined?(Win32::SSPI)
|
679
644
|
authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
|
680
645
|
return authenticator.get_initial_token(@scheme)
|
681
646
|
else # use GSSAPI
|
@@ -685,7 +650,7 @@ class HTTPClient
|
|
685
650
|
end
|
686
651
|
when :response
|
687
652
|
@challenge.delete(domain_uri)
|
688
|
-
if
|
653
|
+
if defined?(Win32::SSPI)
|
689
654
|
return authenticator.complete_authentication(authphrase)
|
690
655
|
else # use GSSAPI
|
691
656
|
return authenticator.init_context(authphrase.unpack('m').pop)
|
@@ -697,7 +662,6 @@ class HTTPClient
|
|
697
662
|
|
698
663
|
# Challenge handler: remember URL and challenge token for response.
|
699
664
|
def challenge(uri, param_str)
|
700
|
-
return false unless SSPIEnabled || GSSAPIEnabled
|
701
665
|
synchronize {
|
702
666
|
if param_str.nil? or @challenge[uri].nil?
|
703
667
|
c = @challenge[uri] = {}
|
@@ -825,9 +789,9 @@ class HTTPClient
|
|
825
789
|
# not supported
|
826
790
|
end
|
827
791
|
|
828
|
-
#
|
792
|
+
# Check always (not effective but it works)
|
829
793
|
def set?
|
830
|
-
|
794
|
+
!@challenge.empty?
|
831
795
|
end
|
832
796
|
|
833
797
|
# Set authentication credential.
|
data/lib/httpclient/http.rb
CHANGED
@@ -844,11 +844,11 @@ module HTTP
|
|
844
844
|
query.each { |attr, value|
|
845
845
|
left = escape(attr.to_s) << '='
|
846
846
|
if values = Array.try_convert(value)
|
847
|
-
values.each { |
|
848
|
-
if
|
849
|
-
|
847
|
+
values.each { |v|
|
848
|
+
if v.respond_to?(:read)
|
849
|
+
v = v.read
|
850
850
|
end
|
851
|
-
pairs.push(left + escape(
|
851
|
+
pairs.push(left + escape(v.to_s))
|
852
852
|
}
|
853
853
|
else
|
854
854
|
if value.respond_to?(:read)
|
data/lib/httpclient/session.rb
CHANGED
@@ -619,14 +619,14 @@ class HTTPClient
|
|
619
619
|
rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, IOError
|
620
620
|
# JRuby can raise IOError instead of ECONNRESET for now
|
621
621
|
close
|
622
|
-
raise KeepAliveDisconnected.new(self)
|
622
|
+
raise KeepAliveDisconnected.new(self, $!)
|
623
623
|
rescue HTTPClient::TimeoutError
|
624
624
|
close
|
625
625
|
raise
|
626
626
|
rescue
|
627
627
|
close
|
628
628
|
if SSLEnabled and $!.is_a?(OpenSSL::SSL::SSLError)
|
629
|
-
raise KeepAliveDisconnected.new(self)
|
629
|
+
raise KeepAliveDisconnected.new(self, $!)
|
630
630
|
else
|
631
631
|
raise
|
632
632
|
end
|
@@ -886,7 +886,7 @@ class HTTPClient
|
|
886
886
|
rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, IOError
|
887
887
|
# JRuby can raise IOError instead of ECONNRESET for now
|
888
888
|
close
|
889
|
-
raise KeepAliveDisconnected.new(self)
|
889
|
+
raise KeepAliveDisconnected.new(self, $!)
|
890
890
|
end
|
891
891
|
if StatusParseRegexp !~ initial_line
|
892
892
|
@version = '0.9'
|
@@ -90,7 +90,12 @@ class HTTPClient
|
|
90
90
|
@dest = nil
|
91
91
|
@timeout = nil
|
92
92
|
@ssl_version = :auto
|
93
|
-
|
93
|
+
# Follow ruby-ossl's definition
|
94
|
+
@options = OpenSSL::SSL::OP_ALL
|
95
|
+
@options &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS)
|
96
|
+
@options |= OpenSSL::SSL::OP_NO_COMPRESSION if defined?(OpenSSL::SSL::OP_NO_COMPRESSION)
|
97
|
+
@options |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2)
|
98
|
+
@options |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3)
|
94
99
|
# OpenSSL 0.9.8 default: "ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH"
|
95
100
|
@ciphers = "ALL:!aNULL:!eNULL:!SSLv2" # OpenSSL >1.0.0 default
|
96
101
|
@cacerts_loaded = false
|
data/lib/httpclient/util.rb
CHANGED
@@ -164,6 +164,18 @@ class HTTPClient
|
|
164
164
|
end
|
165
165
|
module_function :hash_find_value
|
166
166
|
|
167
|
+
# Try to require a feature and returns true/false if loaded
|
168
|
+
#
|
169
|
+
# It returns 'true' for the second require in contrast of the standard
|
170
|
+
# require returns false if the feature is already loaded.
|
171
|
+
def try_require(feature)
|
172
|
+
require feature
|
173
|
+
true
|
174
|
+
rescue LoadError
|
175
|
+
false
|
176
|
+
end
|
177
|
+
module_function :try_require
|
178
|
+
|
167
179
|
# Checks if the given URI is https.
|
168
180
|
def https?(uri)
|
169
181
|
uri.scheme && uri.scheme.downcase == 'https'
|
data/lib/httpclient/version.rb
CHANGED
data/sample/github.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'httpclient'
|
2
|
+
|
3
|
+
# Obtain Personal access token from https://github.com/settings/applications
|
4
|
+
personal_access_token = '03d1c4bc880e51c06215cc77eb075a7856e6a9c7'
|
5
|
+
|
6
|
+
h = HTTPClient.new
|
7
|
+
h.set_auth(nil, personal_access_token, 'x-oauth-basic')
|
8
|
+
h.www_auth.basic_auth.challenge('https://api.github.com/')
|
9
|
+
h.debug_dev = STDERR
|
10
|
+
|
11
|
+
puts h.get_content('https://api.github.com/user/repos?type=private')
|
data/test/test_auth.rb
CHANGED
@@ -150,6 +150,19 @@ class TestAuth < Test::Unit::TestCase
|
|
150
150
|
end
|
151
151
|
end
|
152
152
|
|
153
|
+
def test_BASIC_auth_nil_uri
|
154
|
+
c = HTTPClient.new
|
155
|
+
webrick_backup = @basic_auth.instance_eval { @auth_scheme }
|
156
|
+
begin
|
157
|
+
@basic_auth.instance_eval { @auth_scheme = "BASIC" }
|
158
|
+
c.www_auth.basic_auth.instance_eval { @scheme = "BASIC" }
|
159
|
+
c.set_auth(nil, 'admin', 'admin')
|
160
|
+
assert_equal('basic_auth OK', c.get_content("http://localhost:#{serverport}/basic_auth"))
|
161
|
+
ensure
|
162
|
+
@basic_auth.instance_eval { @auth_scheme = webrick_backup }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
153
166
|
# To work this test consistently on CRuby you can to add 'Thread.pass' in
|
154
167
|
# @challenge iteration at BasicAuth#get like;
|
155
168
|
#
|
data/test/test_http-access2.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
1
2
|
require 'http-access2'
|
2
3
|
require File.expand_path('helper', File.dirname(__FILE__))
|
3
4
|
|
@@ -116,7 +117,7 @@ class TestClient < Test::Unit::TestCase
|
|
116
117
|
setup_proxyserver
|
117
118
|
escape_noproxy do
|
118
119
|
begin
|
119
|
-
@client.proxy = "http
|
120
|
+
@client.proxy = "http://あ"
|
120
121
|
rescue
|
121
122
|
assert_match(/InvalidURIError/, $!.class.to_s)
|
122
123
|
end
|
data/test/test_httpclient.rb
CHANGED
data/test/test_ssl.rb
CHANGED
@@ -33,7 +33,10 @@ class TestSSL < Test::Unit::TestCase
|
|
33
33
|
assert_equal(OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT, cfg.verify_mode)
|
34
34
|
assert_nil(cfg.verify_callback)
|
35
35
|
assert_nil(cfg.timeout)
|
36
|
-
|
36
|
+
expected_options = OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3
|
37
|
+
expected_options &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS)
|
38
|
+
expected_options |= OpenSSL::SSL::OP_NO_COMPRESSION if defined?(OpenSSL::SSL::OP_NO_COMPRESSION)
|
39
|
+
assert_equal(expected_options, cfg.options)
|
37
40
|
assert_equal("ALL:!aNULL:!eNULL:!SSLv2", cfg.ciphers)
|
38
41
|
assert_instance_of(OpenSSL::X509::Store, cfg.cert_store)
|
39
42
|
end
|
@@ -166,6 +169,24 @@ end
|
|
166
169
|
end
|
167
170
|
end
|
168
171
|
|
172
|
+
def test_no_sslv3
|
173
|
+
teardown_server
|
174
|
+
setup_server_with_ssl_version(:SSLv3)
|
175
|
+
assert_raise(OpenSSL::SSL::SSLError) do
|
176
|
+
@client.ssl_config.verify_mode = nil
|
177
|
+
@client.get("https://localhost:#{serverport}/hello")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_allow_tlsv1
|
182
|
+
teardown_server
|
183
|
+
setup_server#_with_ssl_version(:TLSv1)
|
184
|
+
assert_nothing_raised do
|
185
|
+
@client.ssl_config.verify_mode = nil
|
186
|
+
@client.get("https://localhost:#{serverport}/hello")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
169
190
|
private
|
170
191
|
|
171
192
|
def cert(filename)
|
@@ -207,6 +228,31 @@ private
|
|
207
228
|
@server_thread = start_server_thread(@server)
|
208
229
|
end
|
209
230
|
|
231
|
+
def setup_server_with_ssl_version(ssl_version)
|
232
|
+
logger = Logger.new(STDERR)
|
233
|
+
#logger.level = Logger::Severity::FATAL # avoid logging SSLError (ERROR level)
|
234
|
+
@server = WEBrick::HTTPServer.new(
|
235
|
+
:BindAddress => "localhost",
|
236
|
+
:Logger => logger,
|
237
|
+
:Port => 0,
|
238
|
+
:AccessLog => [],
|
239
|
+
:DocumentRoot => DIR,
|
240
|
+
:SSLEnable => true,
|
241
|
+
:SSLCACertificateFile => File.join(DIR, 'ca.cert'),
|
242
|
+
:SSLCertificate => cert('server.cert'),
|
243
|
+
:SSLPrivateKey => key('server.key')
|
244
|
+
)
|
245
|
+
@server.ssl_context.ssl_version = ssl_version
|
246
|
+
@serverport = @server.config[:Port]
|
247
|
+
[:hello].each do |sym|
|
248
|
+
@server.mount(
|
249
|
+
"/#{sym}",
|
250
|
+
WEBrick::HTTPServlet::ProcHandler.new(method("do_#{sym}").to_proc)
|
251
|
+
)
|
252
|
+
end
|
253
|
+
@server_thread = start_server_thread(@server)
|
254
|
+
end
|
255
|
+
|
210
256
|
def do_hello(req, res)
|
211
257
|
res['content-type'] = 'text/html'
|
212
258
|
res.body = "hello"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: httpclient
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hiroshi Nakamura
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: nahi@ruby-lang.org
|
@@ -41,6 +41,7 @@ files:
|
|
41
41
|
- sample/auth.rb
|
42
42
|
- sample/cookie.rb
|
43
43
|
- sample/dav.rb
|
44
|
+
- sample/github.rb
|
44
45
|
- sample/howto.rb
|
45
46
|
- sample/oauth_buzz.rb
|
46
47
|
- sample/oauth_friendfeed.rb
|
@@ -84,7 +85,7 @@ files:
|
|
84
85
|
- test/test_httpclient.rb
|
85
86
|
- test/test_include_client.rb
|
86
87
|
- test/test_ssl.rb
|
87
|
-
homepage:
|
88
|
+
homepage: https://github.com/nahi/httpclient
|
88
89
|
licenses:
|
89
90
|
- ruby
|
90
91
|
metadata: {}
|
@@ -104,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
105
|
version: '0'
|
105
106
|
requirements: []
|
106
107
|
rubyforge_project:
|
107
|
-
rubygems_version: 2.
|
108
|
+
rubygems_version: 2.4.1
|
108
109
|
signing_key:
|
109
110
|
specification_version: 4
|
110
111
|
summary: gives something like the functionality of libwww-perl (LWP) in Ruby
|