httpclient 2.2.5 → 2.2.6
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.
- data/README.txt +57 -2
- data/lib/httpclient.rb +24 -14
- data/lib/httpclient/auth.rb +78 -10
- data/lib/httpclient/http.rb +38 -10
- data/lib/httpclient/include_client.rb +83 -0
- data/lib/httpclient/session.rb +21 -1
- data/lib/httpclient/version.rb +1 -1
- data/sample/oauth_salesforce_10.rb +63 -0
- data/test/helper.rb +7 -4
- data/test/test_auth.rb +123 -1
- data/test/test_httpclient.rb +62 -14
- data/test/test_include_client.rb +52 -0
- metadata +7 -4
data/README.txt
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
httpclient - HTTP accessing library.
|
2
|
-
Copyright (C) 2000-
|
2
|
+
Copyright (C) 2000-2012 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
|
3
3
|
|
4
4
|
'httpclient' gives something like the functionality of libwww-perl (LWP) in
|
5
5
|
Ruby. 'httpclient' formerly known as 'http-access2'.
|
@@ -18,7 +18,7 @@ See HTTPClient for documentation.
|
|
18
18
|
* MT-safe
|
19
19
|
* streaming POST (POST with File/IO)
|
20
20
|
* Digest auth
|
21
|
-
* Negotiate/NTLM auth for WWW-Authenticate (requires net/
|
21
|
+
* Negotiate/NTLM auth for WWW-Authenticate (requires net/ntlm module; rubyntlm gem)
|
22
22
|
* NTLM auth for Proxy-Authenticate (requires 'win32/sspi' module; rubysspi gem)
|
23
23
|
* extensible with filter interface
|
24
24
|
* you don't have to care HTTP/1.1 persistent connection
|
@@ -95,6 +95,61 @@ Thanks in advance.
|
|
95
95
|
|
96
96
|
== Changes
|
97
97
|
|
98
|
+
= Changes in 2.2.6 =
|
99
|
+
|
100
|
+
August 14, 2012 - version 2.2.6
|
101
|
+
|
102
|
+
* Bug fixes
|
103
|
+
|
104
|
+
* Make get_content doesn't raise a BadResponseError for perfectly good
|
105
|
+
responses like 304 Not Modified. Thanks to Florian Hars.
|
106
|
+
|
107
|
+
* Add 'Content-Type: application/x-www-form-urlencoded' for the PUT
|
108
|
+
request that has urlencoded entity-body.
|
109
|
+
|
110
|
+
* Features
|
111
|
+
|
112
|
+
* Add HTTPClient::IncludeClient by Jonathan Rochkind, a mix-in for easily
|
113
|
+
adding a thread-safe lazily initialized class-level HTTPClient object
|
114
|
+
to your class.
|
115
|
+
|
116
|
+
* Proxy DigestAuth support. Thanks to Alexander Kotov and Florian Hars.
|
117
|
+
|
118
|
+
* Accept an array of strings (and IO-likes) as a query value
|
119
|
+
e.g. `{ x: 'a', y: [1,2,3] }` is encoded into `"x=a&y=1&y=2&y=3"`.
|
120
|
+
Thanks to Akinori MUSHA.
|
121
|
+
|
122
|
+
* Allow body for DELETE method.
|
123
|
+
|
124
|
+
* Allow :follow_redirect => true for HEAD request.
|
125
|
+
|
126
|
+
* Fill request parameters request_method, request_uri and request_query
|
127
|
+
as part of response Message::Header.
|
128
|
+
|
129
|
+
= Changes in 2.2.5 =
|
130
|
+
|
131
|
+
May 06, 2012 - version 2.2.5
|
132
|
+
|
133
|
+
* Bug fixes
|
134
|
+
|
135
|
+
* Added Magic encoding comment to hexdump.rb to avoid encoding error.
|
136
|
+
* Add workaround for JRuby issue on Windows (JRUBY-6136)
|
137
|
+
On Windows, calling File#size fails with an Unknown error (20047).
|
138
|
+
This workaround uses File#lstat instead.
|
139
|
+
* Require open-uri only on ruby 1.9, since it is not needed on 1.8.
|
140
|
+
|
141
|
+
* Features
|
142
|
+
|
143
|
+
* Allow symbol Header name for HTTP request.
|
144
|
+
* Dump more SSL certificate information under $DEBUG.
|
145
|
+
* Add HTTPClient::SSLConfig#ssl_version property.
|
146
|
+
* Add 'Accept: */*' header to request by default. Rails requies it.
|
147
|
+
It doesn't override given Accept header from API.
|
148
|
+
* Add HTTPClient::SSLConfig#set_default_paths. This method makes
|
149
|
+
HTTPClient instance to use OpenSSL's default trusted CA certificates.
|
150
|
+
* Allow to set Date header manually.
|
151
|
+
ex. clent.get(uri, :header => {'Date' => Time.now.httpdate})
|
152
|
+
|
98
153
|
= Changes in 2.2.4 =
|
99
154
|
|
100
155
|
Dec 08, 2011 - version 2.2.4
|
data/lib/httpclient.rb
CHANGED
@@ -577,7 +577,7 @@ class HTTPClient
|
|
577
577
|
# follow HTTP redirect by yourself if you need.
|
578
578
|
def get_content(uri, *args, &block)
|
579
579
|
query, header = keyword_argument(args, :query, :header)
|
580
|
-
follow_redirect(:get, uri, query, nil, header || {}, &block)
|
580
|
+
success_content(follow_redirect(:get, uri, query, nil, header || {}, &block))
|
581
581
|
end
|
582
582
|
|
583
583
|
# Posts a content.
|
@@ -607,12 +607,14 @@ class HTTPClient
|
|
607
607
|
# post_content follows HTTP redirect status (see HTTP::Status.redirect?)
|
608
608
|
# internally and try to post the content to redirected URL. See
|
609
609
|
# redirect_uri_callback= how HTTP redirection is handled.
|
610
|
+
# Bear in mind that you should not depend on post_content because it sends
|
611
|
+
# the same POST method to the new location which is prohibited in HTTP spec.
|
610
612
|
#
|
611
613
|
# If you need to get full HTTP response including HTTP status and headers,
|
612
614
|
# use post method.
|
613
615
|
def post_content(uri, *args, &block)
|
614
616
|
body, header = keyword_argument(args, :body, :header)
|
615
|
-
follow_redirect(:post, uri, nil, body, header || {}, &block)
|
617
|
+
success_content(follow_redirect(:post, uri, nil, body, header || {}, &block))
|
616
618
|
end
|
617
619
|
|
618
620
|
# A method for redirect uri callback. How to use:
|
@@ -653,7 +655,7 @@ class HTTPClient
|
|
653
655
|
|
654
656
|
# Sends HEAD request to the specified URL. See request for arguments.
|
655
657
|
def head(uri, *args)
|
656
|
-
request(:head, uri, argument_to_hash(args, :query, :header))
|
658
|
+
request(:head, uri, argument_to_hash(args, :query, :header, :follow_redirect))
|
657
659
|
end
|
658
660
|
|
659
661
|
# Sends GET request to the specified URL. See request for arguments.
|
@@ -662,6 +664,8 @@ class HTTPClient
|
|
662
664
|
end
|
663
665
|
|
664
666
|
# Sends POST request to the specified URL. See request for arguments.
|
667
|
+
# You should not depend on :follow_redirect => true for POST method. It
|
668
|
+
# sends the same POST method to the new location which is prohibited in HTTP spec.
|
665
669
|
def post(uri, *args, &block)
|
666
670
|
request(:post, uri, argument_to_hash(args, :body, :header, :follow_redirect), &block)
|
667
671
|
end
|
@@ -673,7 +677,7 @@ class HTTPClient
|
|
673
677
|
|
674
678
|
# Sends DELETE request to the specified URL. See request for arguments.
|
675
679
|
def delete(uri, *args, &block)
|
676
|
-
request(:delete, uri, argument_to_hash(args, :header), &block)
|
680
|
+
request(:delete, uri, argument_to_hash(args, :body, :header), &block)
|
677
681
|
end
|
678
682
|
|
679
683
|
# Sends OPTIONS request to the specified URL. See request for arguments.
|
@@ -693,7 +697,7 @@ class HTTPClient
|
|
693
697
|
|
694
698
|
# Sends TRACE request to the specified URL. See request for arguments.
|
695
699
|
def trace(uri, *args, &block)
|
696
|
-
request('TRACE', uri, argument_to_hash(args, :query, :
|
700
|
+
request('TRACE', uri, argument_to_hash(args, :query, :header), &block)
|
697
701
|
end
|
698
702
|
|
699
703
|
# Sends a request to the specified URL.
|
@@ -939,18 +943,24 @@ private
|
|
939
943
|
while retry_number < @follow_redirect_count
|
940
944
|
body.pos = pos if pos
|
941
945
|
res = do_request(method, uri, query, body, header, &filtered_block)
|
942
|
-
if HTTP::Status.
|
943
|
-
return res
|
944
|
-
elsif HTTP::Status.redirect?(res.status)
|
946
|
+
if HTTP::Status.redirect?(res.status)
|
945
947
|
uri = urify(@redirect_uri_callback.call(uri, res))
|
946
948
|
retry_number += 1
|
947
949
|
else
|
948
|
-
|
950
|
+
return res
|
949
951
|
end
|
950
952
|
end
|
951
953
|
raise BadResponseError.new("retry count exceeded", res)
|
952
954
|
end
|
953
955
|
|
956
|
+
def success_content(res)
|
957
|
+
if HTTP::Status.successful?(res.status)
|
958
|
+
return res.content
|
959
|
+
else
|
960
|
+
raise BadResponseError.new("unexpected response: #{res.header.inspect}", res)
|
961
|
+
end
|
962
|
+
end
|
963
|
+
|
954
964
|
def protect_keep_alive_disconnected
|
955
965
|
begin
|
956
966
|
yield
|
@@ -984,7 +994,7 @@ private
|
|
984
994
|
header = override_header(header, 'Content-Type', content_type)
|
985
995
|
end
|
986
996
|
end
|
987
|
-
|
997
|
+
else
|
988
998
|
if file_in_form_data?(body)
|
989
999
|
boundary = create_boundary
|
990
1000
|
content_type = "multipart/form-data; boundary=#{boundary}"
|
@@ -1051,11 +1061,11 @@ private
|
|
1051
1061
|
end
|
1052
1062
|
if str = @test_loopback_response.shift
|
1053
1063
|
dump_dummy_request_response(req.http_body.dump, str) if @debug_dev
|
1054
|
-
conn.push(HTTP::Message.new_response(str))
|
1064
|
+
conn.push(HTTP::Message.new_response(str, req.header))
|
1055
1065
|
return
|
1056
1066
|
end
|
1057
1067
|
content = block ? nil : ''
|
1058
|
-
res = HTTP::Message.new_response(content)
|
1068
|
+
res = HTTP::Message.new_response(content, req.header)
|
1059
1069
|
@debug_dev << "= Request\n\n" if @debug_dev
|
1060
1070
|
sess = @session_manager.query(req, proxy)
|
1061
1071
|
res.peer_cert = sess.ssl_peer_cert
|
@@ -1087,11 +1097,11 @@ private
|
|
1087
1097
|
end
|
1088
1098
|
if str = @test_loopback_response.shift
|
1089
1099
|
dump_dummy_request_response(req.http_body.dump, str) if @debug_dev
|
1090
|
-
conn.push(HTTP::Message.new_response(StringIO.new(str)))
|
1100
|
+
conn.push(HTTP::Message.new_response(StringIO.new(str), req.header))
|
1091
1101
|
return
|
1092
1102
|
end
|
1093
1103
|
piper, pipew = IO.pipe
|
1094
|
-
res = HTTP::Message.new_response(piper)
|
1104
|
+
res = HTTP::Message.new_response(piper, req.header)
|
1095
1105
|
@debug_dev << "= Request\n\n" if @debug_dev
|
1096
1106
|
sess = @session_manager.query(req, proxy)
|
1097
1107
|
res.peer_cert = sess.ssl_peer_cert
|
data/lib/httpclient/auth.rb
CHANGED
@@ -120,6 +120,10 @@ class HTTPClient
|
|
120
120
|
|
121
121
|
# Filter API implementation. Traps HTTP response and parses
|
122
122
|
# 'WWW-Authenticate' header.
|
123
|
+
#
|
124
|
+
# This remembers the challenges for all authentication methods
|
125
|
+
# available to the client. On the subsequent retry of the request,
|
126
|
+
# filter_request will select the strongest method.
|
123
127
|
def filter_response(req, res)
|
124
128
|
command = nil
|
125
129
|
if res.status == HTTP::Status::UNAUTHORIZED
|
@@ -156,17 +160,19 @@ class HTTPClient
|
|
156
160
|
# SSPINegotiateAuth requires 'win32/sspi' module.
|
157
161
|
class ProxyAuth < AuthFilterBase
|
158
162
|
attr_reader :basic_auth
|
163
|
+
attr_reader :digest_auth
|
159
164
|
attr_reader :negotiate_auth
|
160
165
|
attr_reader :sspi_negotiate_auth
|
161
166
|
|
162
167
|
# Creates new ProxyAuth.
|
163
168
|
def initialize
|
164
|
-
@basic_auth =
|
169
|
+
@basic_auth = ProxyBasicAuth.new
|
165
170
|
@negotiate_auth = NegotiateAuth.new
|
166
171
|
@ntlm_auth = NegotiateAuth.new('NTLM')
|
167
172
|
@sspi_negotiate_auth = SSPINegotiateAuth.new
|
173
|
+
@digest_auth = ProxyDigestAuth.new
|
168
174
|
# sort authenticators by priority
|
169
|
-
@authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @basic_auth]
|
175
|
+
@authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
|
170
176
|
end
|
171
177
|
|
172
178
|
# Resets challenge state. See sub filters for more details.
|
@@ -281,6 +287,25 @@ class HTTPClient
|
|
281
287
|
end
|
282
288
|
end
|
283
289
|
|
290
|
+
class ProxyBasicAuth < BasicAuth
|
291
|
+
|
292
|
+
def set(uri, user, passwd)
|
293
|
+
@set = true
|
294
|
+
@cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
295
|
+
end
|
296
|
+
|
297
|
+
def get(req)
|
298
|
+
target_uri = req.header.request_uri
|
299
|
+
return nil unless @challengeable['challenged']
|
300
|
+
@cred
|
301
|
+
end
|
302
|
+
|
303
|
+
# Challenge handler: remember URL for response.
|
304
|
+
def challenge(uri, param_str = nil)
|
305
|
+
@challengeable['challenged'] = true
|
306
|
+
true
|
307
|
+
end
|
308
|
+
end
|
284
309
|
|
285
310
|
# Authentication filter for handling DigestAuth negotiation.
|
286
311
|
# Used in WWWAuth.
|
@@ -352,9 +377,12 @@ class HTTPClient
|
|
352
377
|
path = req.header.create_query_uri
|
353
378
|
a_1 = "#{user}:#{param['realm']}:#{passwd}"
|
354
379
|
a_2 = "#{method}:#{path}"
|
380
|
+
qop = param['qop']
|
355
381
|
nonce = param['nonce']
|
356
|
-
cnonce =
|
357
|
-
|
382
|
+
cnonce = nil
|
383
|
+
if qop || param['algorithm'] =~ /MD5-sess/
|
384
|
+
cnonce = generate_cnonce()
|
385
|
+
end
|
358
386
|
a_1_md5sum = Digest::MD5.hexdigest(a_1)
|
359
387
|
if param['algorithm'] =~ /MD5-sess/
|
360
388
|
a_1_md5sum = Digest::MD5.hexdigest("#{a_1_md5sum}:#{nonce}:#{cnonce}")
|
@@ -365,18 +393,25 @@ class HTTPClient
|
|
365
393
|
message_digest = []
|
366
394
|
message_digest << a_1_md5sum
|
367
395
|
message_digest << nonce
|
368
|
-
|
369
|
-
|
370
|
-
|
396
|
+
if qop
|
397
|
+
@nonce_count += 1
|
398
|
+
message_digest << ('%08x' % @nonce_count)
|
399
|
+
message_digest << cnonce
|
400
|
+
message_digest << param['qop']
|
401
|
+
end
|
371
402
|
message_digest << Digest::MD5.hexdigest(a_2)
|
372
403
|
header = []
|
373
404
|
header << "username=\"#{user}\""
|
374
405
|
header << "realm=\"#{param['realm']}\""
|
375
406
|
header << "nonce=\"#{nonce}\""
|
376
407
|
header << "uri=\"#{path}\""
|
377
|
-
|
378
|
-
|
379
|
-
|
408
|
+
if cnonce
|
409
|
+
header << "cnonce=\"#{cnonce}\""
|
410
|
+
end
|
411
|
+
if qop
|
412
|
+
header << "nc=#{'%08x' % @nonce_count}"
|
413
|
+
header << "qop=#{param['qop']}"
|
414
|
+
end
|
380
415
|
header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
|
381
416
|
header << "algorithm=#{algorithm}"
|
382
417
|
header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
|
@@ -404,6 +439,39 @@ class HTTPClient
|
|
404
439
|
end
|
405
440
|
|
406
441
|
|
442
|
+
# Authentication filter for handling DigestAuth negotiation.
|
443
|
+
# Ignores uri argument. Used in ProxyAuth.
|
444
|
+
class ProxyDigestAuth < DigestAuth
|
445
|
+
|
446
|
+
# overrides DigestAuth#set. sets default user name and password. uri is not used.
|
447
|
+
def set(uri, user, passwd)
|
448
|
+
@set = true
|
449
|
+
@auth = [user, passwd]
|
450
|
+
end
|
451
|
+
|
452
|
+
# overrides DigestAuth#get. Uses default user name and password
|
453
|
+
# regardless of target uri if the proxy has required authentication
|
454
|
+
# before
|
455
|
+
def get(req)
|
456
|
+
target_uri = req.header.request_uri
|
457
|
+
param = @challenge
|
458
|
+
return nil unless param
|
459
|
+
user, passwd = @auth
|
460
|
+
return nil unless user
|
461
|
+
calc_cred(req, user, passwd, param)
|
462
|
+
end
|
463
|
+
|
464
|
+
def reset_challenge
|
465
|
+
@challenge = nil
|
466
|
+
end
|
467
|
+
|
468
|
+
def challenge(uri, param_str)
|
469
|
+
@challenge = parse_challenge_param(param_str)
|
470
|
+
true
|
471
|
+
end
|
472
|
+
|
473
|
+
end
|
474
|
+
|
407
475
|
# Authentication filter for handling Negotiate/NTLM negotiation.
|
408
476
|
# Used in WWWAuth and ProxyAuth.
|
409
477
|
#
|
data/lib/httpclient/http.rb
CHANGED
@@ -199,9 +199,14 @@ module HTTP
|
|
199
199
|
end
|
200
200
|
|
201
201
|
# Initialize this instance as a response.
|
202
|
-
def init_response(status_code)
|
202
|
+
def init_response(status_code, req)
|
203
203
|
@is_request = false
|
204
204
|
self.status_code = status_code
|
205
|
+
if req
|
206
|
+
@request_method = req.request_method
|
207
|
+
@request_uri = req.request_uri
|
208
|
+
@request_query = req.request_query
|
209
|
+
end
|
205
210
|
end
|
206
211
|
|
207
212
|
# Sets status code and reason phrase.
|
@@ -456,7 +461,7 @@ module HTTP
|
|
456
461
|
end
|
457
462
|
|
458
463
|
# Initialize this instance as a response.
|
459
|
-
def init_response(body
|
464
|
+
def init_response(body)
|
460
465
|
@body = body
|
461
466
|
if @body.respond_to?(:bytesize)
|
462
467
|
@size = @body.bytesize
|
@@ -549,7 +554,7 @@ module HTTP
|
|
549
554
|
|
550
555
|
def remember_pos(io)
|
551
556
|
# IO may not support it (ex. IO.pipe)
|
552
|
-
@positions[io] = io.pos
|
557
|
+
@positions[io] = io.pos if io.respond_to?(:pos)
|
553
558
|
end
|
554
559
|
|
555
560
|
def reset_pos(io)
|
@@ -721,9 +726,9 @@ module HTTP
|
|
721
726
|
|
722
727
|
# Creates a Message instance of response.
|
723
728
|
# body:: a String or an IO of response message body.
|
724
|
-
def new_response(body)
|
729
|
+
def new_response(body, req = nil)
|
725
730
|
m = new
|
726
|
-
m.http_header.init_response(Status::OK)
|
731
|
+
m.http_header.init_response(Status::OK, req)
|
727
732
|
m.http_body = Body.new
|
728
733
|
m.http_body.init_response(body)
|
729
734
|
m.http_header.body_size = m.http_body.size || 0
|
@@ -821,13 +826,36 @@ module HTTP
|
|
821
826
|
end
|
822
827
|
end
|
823
828
|
|
829
|
+
def Array.try_convert(value)
|
830
|
+
return value if value.instance_of?(Array)
|
831
|
+
return nil if !value.respond_to?(:to_ary)
|
832
|
+
converted = value.to_ary
|
833
|
+
return converted if converted.instance_of?(Array)
|
834
|
+
|
835
|
+
cname = value.class.name
|
836
|
+
raise TypeError, "can't convert %s to %s (%s#%s gives %s)" %
|
837
|
+
[cname, Array.name, cname, :to_ary, converted.class.name]
|
838
|
+
end unless Array.respond_to?(:try_convert)
|
839
|
+
|
824
840
|
def escape_query(query) # :nodoc:
|
825
|
-
|
826
|
-
|
827
|
-
|
841
|
+
pairs = []
|
842
|
+
query.each { |attr, value|
|
843
|
+
left = escape(attr.to_s) << '='
|
844
|
+
if values = Array.try_convert(value)
|
845
|
+
values.each { |value|
|
846
|
+
if value.respond_to?(:read)
|
847
|
+
value = value.read
|
848
|
+
end
|
849
|
+
pairs.push(left + escape(value.to_s))
|
850
|
+
}
|
851
|
+
else
|
852
|
+
if value.respond_to?(:read)
|
853
|
+
value = value.read
|
854
|
+
end
|
855
|
+
pairs.push(left << escape(value.to_s))
|
828
856
|
end
|
829
|
-
|
830
|
-
|
857
|
+
}
|
858
|
+
pairs.join('&')
|
831
859
|
end
|
832
860
|
|
833
861
|
# from CGI.escape
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# It is useful to re-use a HTTPClient instance for multiple requests, to
|
2
|
+
# re-use HTTP 1.1 persistent connections.
|
3
|
+
#
|
4
|
+
# To do that, you sometimes want to store an HTTPClient instance in a global/
|
5
|
+
# class variable location, so it can be accessed and re-used.
|
6
|
+
#
|
7
|
+
# This mix-in makes it easy to create class-level access to one or more
|
8
|
+
# HTTPClient instances. The HTTPClient instances are lazily initialized
|
9
|
+
# on first use (to, for instance, avoid interfering with WebMock/VCR),
|
10
|
+
# and are initialized in a thread-safe manner. Note that a
|
11
|
+
# HTTPClient, once initialized, is safe for use in multiple threads.
|
12
|
+
#
|
13
|
+
# Note that you `extend` HTTPClient::IncludeClient, not `include.
|
14
|
+
#
|
15
|
+
# require 'httpclient/include_client'
|
16
|
+
# class Widget
|
17
|
+
# extend HTTPClient::IncludeClient
|
18
|
+
#
|
19
|
+
# include_http_client
|
20
|
+
# # and/or, specify more stuff
|
21
|
+
# include_http_client('http://myproxy:8080', :method_name => :my_client) do |client|
|
22
|
+
# # any init you want
|
23
|
+
# client.set_cookie_store nil
|
24
|
+
# client.
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# That creates two HTTPClient instances available at the class level.
|
29
|
+
# The first will be available from Widget.http_client (default method
|
30
|
+
# name for `include_http_client`), with default initialization.
|
31
|
+
#
|
32
|
+
# The second will be available at Widget.my_client, with the init arguments
|
33
|
+
# provided, further initialized by the block provided.
|
34
|
+
#
|
35
|
+
# In addition to a class-level method, for convenience instance-level methods
|
36
|
+
# are also provided. Widget.http_client is identical to Widget.new.http_client
|
37
|
+
#
|
38
|
+
#
|
39
|
+
class HTTPClient
|
40
|
+
module IncludeClient
|
41
|
+
|
42
|
+
|
43
|
+
def include_http_client(*args, &block)
|
44
|
+
# We're going to dynamically define a class
|
45
|
+
# to hold our state, namespaced, as well as possibly dynamic
|
46
|
+
# name of cover method.
|
47
|
+
method_name = (args.last.delete(:method_name) if args.last.kind_of? Hash) || :http_client
|
48
|
+
args.pop if args.last == {} # if last arg was named methods now empty, remove it.
|
49
|
+
|
50
|
+
# By the amazingness of closures, we can create these things
|
51
|
+
# in local vars here and use em in our method, we don't even
|
52
|
+
# need iVars for state.
|
53
|
+
client_instance = nil
|
54
|
+
client_mutex = Mutex.new
|
55
|
+
client_args = args
|
56
|
+
client_block = block
|
57
|
+
|
58
|
+
# to define a _class method_ on the specific class that's currently
|
59
|
+
# `self`, we have to use this bit of metaprogramming, sorry.
|
60
|
+
(class << self; self ; end).instance_eval do
|
61
|
+
define_method(method_name) do
|
62
|
+
# implementation copied from ruby stdlib singleton
|
63
|
+
# to create this global obj thread-safely.
|
64
|
+
return client_instance if client_instance
|
65
|
+
client_mutex.synchronize do
|
66
|
+
return client_instance if client_instance
|
67
|
+
# init HTTPClient with specified args/block
|
68
|
+
client_instance = HTTPClient.new(*client_args)
|
69
|
+
client_block.call(client_instance) if client_block
|
70
|
+
end
|
71
|
+
return client_instance
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# And for convenience, an _instance method_ on the class that just
|
76
|
+
# delegates to the class method.
|
77
|
+
define_method(method_name) do
|
78
|
+
self.class.send(method_name)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/httpclient/session.rb
CHANGED
@@ -316,6 +316,18 @@ class HTTPClient
|
|
316
316
|
end
|
317
317
|
end
|
318
318
|
|
319
|
+
def ssl_version
|
320
|
+
@ssl_socket.ssl_version if @ssl_socket.respond_to?(:ssl_version)
|
321
|
+
end
|
322
|
+
|
323
|
+
def ssl_cipher
|
324
|
+
@ssl_socket.cipher
|
325
|
+
end
|
326
|
+
|
327
|
+
def ssl_state
|
328
|
+
@ssl_socket.state
|
329
|
+
end
|
330
|
+
|
319
331
|
def peer_cert
|
320
332
|
@ssl_socket.peer_cert
|
321
333
|
end
|
@@ -741,7 +753,15 @@ class HTTPClient
|
|
741
753
|
else
|
742
754
|
@socket = create_ssl_socket(@socket)
|
743
755
|
connect_ssl_proxy(@socket, URI.parse(@dest.to_s)) if @proxy
|
744
|
-
|
756
|
+
begin
|
757
|
+
@socket.ssl_connect(@dest.host)
|
758
|
+
ensure
|
759
|
+
if $DEBUG
|
760
|
+
warn("Protocol version: #{@socket.ssl_version}")
|
761
|
+
warn("Cipher: #{@socket.ssl_cipher.inspect}")
|
762
|
+
warn("State: #{@socket.ssl_state}")
|
763
|
+
end
|
764
|
+
end
|
745
765
|
@socket.post_connection_check(@dest)
|
746
766
|
@ssl_peer_cert = @socket.peer_cert
|
747
767
|
end
|
data/lib/httpclient/version.rb
CHANGED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'oauthclient'
|
2
|
+
|
3
|
+
# Get your own consumer token from http://twitter.com/apps
|
4
|
+
consumer_key = '3MVG9y6x0357HledNGHa9tJrrlOmpCSo5alTv4W4AG1M0f9a8cGBIwo5wN2bQ7hjAEsjD7SBWf3H2Oycc9Qql'
|
5
|
+
consumer_secret = '1404017425765973464'
|
6
|
+
|
7
|
+
callback = ARGV.shift # can be nil for OAuth 1.0. (not 1.0a)
|
8
|
+
request_token_url = 'https://login.salesforce.com/_nc_external/system/security/oauth/RequestTokenHandler'
|
9
|
+
oob_authorize_url = 'https://login.salesforce.com/setup/secur/RemoteAccessAuthorizationPage.apexp'
|
10
|
+
access_token_url = 'https://login.salesforce.com/_nc_external/system/security/oauth/AccessTokenHandler'
|
11
|
+
|
12
|
+
STDOUT.sync = true
|
13
|
+
|
14
|
+
# create OAuth client.
|
15
|
+
client = OAuthClient.new
|
16
|
+
client.oauth_config.consumer_key = consumer_key
|
17
|
+
client.oauth_config.consumer_secret = consumer_secret
|
18
|
+
client.oauth_config.signature_method = 'HMAC-SHA1'
|
19
|
+
client.oauth_config.http_method = :get # Twitter does not allow :post
|
20
|
+
client.debug_dev = STDERR if $DEBUG
|
21
|
+
|
22
|
+
client.ssl_config.ssl_version = "TLSv1_1"
|
23
|
+
|
24
|
+
# Get request token.
|
25
|
+
res = client.get_request_token(request_token_url, callback)
|
26
|
+
p res.status
|
27
|
+
p res.oauth_params
|
28
|
+
p res.content
|
29
|
+
p client.oauth_config
|
30
|
+
token = res.oauth_params['oauth_token']
|
31
|
+
secret = res.oauth_params['oauth_token_secret']
|
32
|
+
raise if token.nil? or secret.nil?
|
33
|
+
|
34
|
+
# You need to confirm authorization out of band.
|
35
|
+
puts
|
36
|
+
puts "Go here and do confirm: #{oob_authorize_url}?oauth_token=#{token}&oauth_consumer_key=#{consumer_key}"
|
37
|
+
puts "Type oauth_verifier/PIN (if given) and hit [enter] to go"
|
38
|
+
verifier = gets.chomp
|
39
|
+
verifier = nil if verifier.empty?
|
40
|
+
|
41
|
+
# Get access token.
|
42
|
+
# FYI: You may need to re-construct OAuthClient instance here.
|
43
|
+
# In normal web app flow, getting access token and getting request token
|
44
|
+
# must be done in different HTTP requests.
|
45
|
+
# client = OAuthClient.new
|
46
|
+
# client.oauth_config.consumer_key = consumer_key
|
47
|
+
# client.oauth_config.consumer_secret = consumer_secret
|
48
|
+
# client.oauth_config.signature_method = 'HMAC-SHA1'
|
49
|
+
# client.oauth_config.http_method = :get # Twitter does not allow :post
|
50
|
+
res = client.get_access_token(access_token_url, token, secret, verifier)
|
51
|
+
p res.status
|
52
|
+
p res.oauth_params
|
53
|
+
p res.content
|
54
|
+
p client.oauth_config
|
55
|
+
id = res.oauth_params['user_id']
|
56
|
+
|
57
|
+
puts
|
58
|
+
puts "Access token usage example"
|
59
|
+
puts "Hit [enter] to go"
|
60
|
+
gets
|
61
|
+
|
62
|
+
# Access to a protected resource. (DM)
|
63
|
+
puts client.get("http://twitter.com/direct_messages.json")
|
data/test/helper.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
require 'test/unit'
|
3
|
-
|
4
|
-
require 'simplecov
|
5
|
-
|
6
|
-
SimpleCov.
|
3
|
+
begin
|
4
|
+
require 'simplecov'
|
5
|
+
require 'simplecov-rcov'
|
6
|
+
SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
|
7
|
+
SimpleCov.start
|
8
|
+
rescue LoadError
|
9
|
+
end
|
7
10
|
|
8
11
|
require 'httpclient'
|
9
12
|
require 'webrick'
|
data/test/test_auth.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require File.expand_path('helper', File.dirname(__FILE__))
|
2
|
-
|
2
|
+
require 'digest/md5'
|
3
3
|
|
4
4
|
class TestAuth < Test::Unit::TestCase
|
5
5
|
include Helper
|
@@ -56,6 +56,23 @@ class TestAuth < Test::Unit::TestCase
|
|
56
56
|
:UserDB => htdigest_userdb
|
57
57
|
)
|
58
58
|
@server_thread = start_server_thread(@server)
|
59
|
+
|
60
|
+
@proxy_digest_auth = WEBrick::HTTPAuth::ProxyDigestAuth.new(
|
61
|
+
:Logger => @proxylogger,
|
62
|
+
:Algorithm => 'MD5',
|
63
|
+
:Realm => 'auth',
|
64
|
+
:UserDB => htdigest_userdb
|
65
|
+
)
|
66
|
+
|
67
|
+
@proxyserver = WEBrick::HTTPProxyServer.new(
|
68
|
+
:ProxyAuthProc => @proxy_digest_auth.method(:authenticate).to_proc,
|
69
|
+
:BindAddress => "localhost",
|
70
|
+
:Logger => @proxylogger,
|
71
|
+
:Port => 0,
|
72
|
+
:AccessLog => []
|
73
|
+
)
|
74
|
+
@proxyport = @proxyserver.config[:Port]
|
75
|
+
@proxyserver_thread = start_server_thread(@proxyserver)
|
59
76
|
end
|
60
77
|
|
61
78
|
def do_basic_auth(req, res)
|
@@ -105,12 +122,32 @@ class TestAuth < Test::Unit::TestCase
|
|
105
122
|
end
|
106
123
|
end
|
107
124
|
|
125
|
+
def test_basic_auth_reuses_credentials
|
126
|
+
c = HTTPClient.new
|
127
|
+
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
|
128
|
+
assert_equal('basic_auth OK', c.get_content("http://localhost:#{serverport}/basic_auth/"))
|
129
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
130
|
+
c.debug_dev = str = ''
|
131
|
+
c.get_content("http://localhost:#{serverport}/basic_auth/sub/dir/")
|
132
|
+
assert_match /Authorization: Basic YWRtaW46YWRtaW4=/, str
|
133
|
+
end
|
134
|
+
|
108
135
|
def test_digest_auth
|
109
136
|
c = HTTPClient.new
|
110
137
|
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
|
111
138
|
assert_equal('digest_auth OK', c.get_content("http://localhost:#{serverport}/digest_auth"))
|
112
139
|
end
|
113
140
|
|
141
|
+
def test_digest_auth_reuses_credentials
|
142
|
+
c = HTTPClient.new
|
143
|
+
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
|
144
|
+
assert_equal('digest_auth OK', c.get_content("http://localhost:#{serverport}/digest_auth/"))
|
145
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
146
|
+
c.debug_dev = str = ''
|
147
|
+
c.get_content("http://localhost:#{serverport}/digest_auth/sub/dir/")
|
148
|
+
assert_match /Authorization: Digest/, str
|
149
|
+
end
|
150
|
+
|
114
151
|
def test_digest_auth_with_block
|
115
152
|
c = HTTPClient.new
|
116
153
|
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
|
@@ -147,6 +184,16 @@ class TestAuth < Test::Unit::TestCase
|
|
147
184
|
assert_equal('digest_auth OKbar=baz', c.get_content("http://localhost:#{serverport}/digest_auth/foo?bar=baz"))
|
148
185
|
end
|
149
186
|
|
187
|
+
def test_perfer_digest
|
188
|
+
c = HTTPClient.new
|
189
|
+
c.set_auth('http://example.com/', 'admin', 'admin')
|
190
|
+
c.test_loopback_http_response << "HTTP/1.0 401 Unauthorized\nWWW-Authenticate: Basic realm=\"foo\"\nWWW-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nContent-Length: 2\n\nNG"
|
191
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
192
|
+
c.debug_dev = str = ''
|
193
|
+
c.get_content('http://example.com/')
|
194
|
+
assert_match(/^Authorization: Digest/, str)
|
195
|
+
end
|
196
|
+
|
150
197
|
def test_digest_sess_auth
|
151
198
|
c = HTTPClient.new
|
152
199
|
c.set_auth("http://localhost:#{serverport}/", 'admin', 'admin')
|
@@ -163,6 +210,81 @@ class TestAuth < Test::Unit::TestCase
|
|
163
210
|
assert_match(/Proxy-Authorization: Basic YWRtaW46YWRtaW4=/, str)
|
164
211
|
end
|
165
212
|
|
213
|
+
def test_proxy_auth_reuses_credentials
|
214
|
+
c = HTTPClient.new
|
215
|
+
c.set_proxy_auth('admin', 'admin')
|
216
|
+
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Basic realm=\"foo\"\nContent-Length: 2\n\nNG"
|
217
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
218
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
219
|
+
c.get_content('http://www1.example.com/')
|
220
|
+
c.debug_dev = str = ''
|
221
|
+
c.get_content('http://www2.example.com/')
|
222
|
+
assert_match(/Proxy-Authorization: Basic YWRtaW46YWRtaW4=/, str)
|
223
|
+
end
|
224
|
+
|
225
|
+
def test_digest_proxy_auth_loop
|
226
|
+
c = HTTPClient.new
|
227
|
+
c.set_proxy_auth('admin', 'admin')
|
228
|
+
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nContent-Length: 2\n\nNG"
|
229
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
230
|
+
md5 = Digest::MD5.new
|
231
|
+
ha1 = md5.hexdigest("admin:foo:admin")
|
232
|
+
ha2 = md5.hexdigest("GET:/")
|
233
|
+
response = md5.hexdigest("#{ha1}:nonce:#{ha2}")
|
234
|
+
c.debug_dev = str = ''
|
235
|
+
c.get_content('http://example.com/')
|
236
|
+
assert_match(/Proxy-Authorization: Digest/, str)
|
237
|
+
assert_match(%r"response=\"#{response}\"", str)
|
238
|
+
end
|
239
|
+
|
240
|
+
def test_digest_proxy_auth
|
241
|
+
c=HTTPClient.new("http://localhost:#{proxyport}/")
|
242
|
+
c.set_proxy_auth('admin', 'admin')
|
243
|
+
c.set_auth("http://127.0.0.1:#{serverport}/", 'admin', 'admin')
|
244
|
+
assert_equal('basic_auth OK', c.get_content("http://127.0.0.1:#{serverport}/basic_auth"))
|
245
|
+
end
|
246
|
+
|
247
|
+
def test_digest_proxy_invalid_auth
|
248
|
+
c=HTTPClient.new("http://localhost:#{proxyport}/")
|
249
|
+
c.set_proxy_auth('admin', 'wrong')
|
250
|
+
c.set_auth("http://127.0.0.1:#{serverport}/", 'admin', 'admin')
|
251
|
+
assert_raises(HTTPClient::BadResponseError) do
|
252
|
+
c.get_content("http://127.0.0.1:#{serverport}/basic_auth")
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def test_prefer_digest_to_basic_proxy_auth
|
257
|
+
c = HTTPClient.new
|
258
|
+
c.set_proxy_auth('admin', 'admin')
|
259
|
+
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nProxy-Authenticate: Basic realm=\"bar\"\nContent-Length: 2\n\nNG"
|
260
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
261
|
+
md5 = Digest::MD5.new
|
262
|
+
ha1 = md5.hexdigest("admin:foo:admin")
|
263
|
+
ha2 = md5.hexdigest("GET:/")
|
264
|
+
response = md5.hexdigest("#{ha1}:nonce:#{ha2}")
|
265
|
+
c.debug_dev = str = ''
|
266
|
+
c.get_content('http://example.com/')
|
267
|
+
assert_match(/Proxy-Authorization: Digest/, str)
|
268
|
+
assert_match(%r"response=\"#{response}\"", str)
|
269
|
+
end
|
270
|
+
|
271
|
+
def test_digest_proxy_auth_reuses_credentials
|
272
|
+
c = HTTPClient.new
|
273
|
+
c.set_proxy_auth('admin', 'admin')
|
274
|
+
c.test_loopback_http_response << "HTTP/1.0 407 Unauthorized\nProxy-Authenticate: Digest realm=\"foo\", nonce=\"nonce\", stale=false\nContent-Length: 2\n\nNG"
|
275
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
276
|
+
c.test_loopback_http_response << "HTTP/1.0 200 OK\nContent-Length: 2\n\nOK"
|
277
|
+
md5 = Digest::MD5.new
|
278
|
+
ha1 = md5.hexdigest("admin:foo:admin")
|
279
|
+
ha2 = md5.hexdigest("GET:/")
|
280
|
+
response = md5.hexdigest("#{ha1}:nonce:#{ha2}")
|
281
|
+
c.get_content('http://www1.example.com/')
|
282
|
+
c.debug_dev = str = ''
|
283
|
+
c.get_content('http://www2.example.com/')
|
284
|
+
assert_match(/Proxy-Authorization: Digest/, str)
|
285
|
+
assert_match(%r"response=\"#{response}\"", str)
|
286
|
+
end
|
287
|
+
|
166
288
|
def test_oauth
|
167
289
|
c = HTTPClient.new
|
168
290
|
config = HTTPClient::OAuth::Config.new(
|
data/test/test_httpclient.rb
CHANGED
@@ -134,6 +134,14 @@ class TestHTTPClient < Test::Unit::TestCase
|
|
134
134
|
assert_equal("Host: foo", lines[4]) # use given param
|
135
135
|
end
|
136
136
|
|
137
|
+
def test_redirect_returns_not_modified
|
138
|
+
assert_nothing_raised do
|
139
|
+
timeout(2) do
|
140
|
+
@client.get(serverurl + 'status', {:status => 306}, {:follow_redirect => true})
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
137
145
|
def test_protocol_version_http11
|
138
146
|
assert_equal(nil, @client.protocol_version)
|
139
147
|
str = ""
|
@@ -167,7 +175,7 @@ class TestHTTPClient < Test::Unit::TestCase
|
|
167
175
|
setup_proxyserver
|
168
176
|
escape_noproxy do
|
169
177
|
assert_raises(URI::InvalidURIError) do
|
170
|
-
|
178
|
+
@client.proxy = "http://"
|
171
179
|
end
|
172
180
|
@client.proxy = ""
|
173
181
|
assert_nil(@client.proxy)
|
@@ -387,6 +395,17 @@ EOS
|
|
387
395
|
assert_equal('message body 1', res.content)
|
388
396
|
end
|
389
397
|
|
398
|
+
def test_request_uri_in_response
|
399
|
+
@client.test_loopback_http_response << "HTTP/1.0 200 OK\ncontent-length: 100\n\nmessage body"
|
400
|
+
assert_equal(URI('http://google.com/'), @client.get('http://google.com/').header.request_uri)
|
401
|
+
end
|
402
|
+
|
403
|
+
def test_request_uri_in_response_when_redirect
|
404
|
+
expected = URI(serverurl + 'hello')
|
405
|
+
assert_equal(expected, @client.get(serverurl + 'redirect1', :follow_redirect => true).header.request_uri)
|
406
|
+
assert_equal(expected, @client.get(serverurl + 'redirect2', :follow_redirect => true).header.request_uri)
|
407
|
+
end
|
408
|
+
|
390
409
|
def test_redirect_non_https
|
391
410
|
url = serverurl + 'redirect1'
|
392
411
|
https_url = URI.parse(url)
|
@@ -584,6 +603,13 @@ EOS
|
|
584
603
|
assert_nil(res.contenttype)
|
585
604
|
end
|
586
605
|
|
606
|
+
def test_head_follow_redirect
|
607
|
+
expected = URI(serverurl + 'hello')
|
608
|
+
assert_equal(expected, @client.head(serverurl + 'hello', :follow_redirect => true).header.request_uri)
|
609
|
+
assert_equal(expected, @client.head(serverurl + 'redirect1', :follow_redirect => true).header.request_uri)
|
610
|
+
assert_equal(expected, @client.head(serverurl + 'redirect2', :follow_redirect => true).header.request_uri)
|
611
|
+
end
|
612
|
+
|
587
613
|
def test_get_follow_redirect
|
588
614
|
assert_equal('hello', @client.get(serverurl + 'hello', :follow_redirect => true).body)
|
589
615
|
assert_equal('hello', @client.get(serverurl + 'redirect1', :follow_redirect => true).body)
|
@@ -777,8 +803,10 @@ EOS
|
|
777
803
|
def test_put
|
778
804
|
assert_equal("put", @client.put(serverurl + 'servlet').content)
|
779
805
|
param = {'1'=>'2', '3'=>'4'}
|
806
|
+
@client.debug_dev = str = ''
|
780
807
|
res = @client.put(serverurl + 'servlet', param)
|
781
808
|
assert_equal(param, params(res.header["x-query"][0]))
|
809
|
+
assert_equal('Content-Type: application/x-www-form-urlencoded', str.split(/\r?\n/)[5])
|
782
810
|
end
|
783
811
|
|
784
812
|
def test_put_bytesize
|
@@ -799,6 +827,14 @@ EOS
|
|
799
827
|
assert_equal("delete", @client.delete(serverurl + 'servlet').content)
|
800
828
|
end
|
801
829
|
|
830
|
+
# Not prohibited by spec, but normally it's ignored
|
831
|
+
def test_delete_with_body
|
832
|
+
param = {'1'=>'2', '3'=>'4'}
|
833
|
+
@client.debug_dev = str = ''
|
834
|
+
assert_equal("delete", @client.delete(serverurl + 'servlet', param).content)
|
835
|
+
assert_equal({'1' => ['2'], '3' => ['4']}, HTTP::Message.parse(str.split(/\r?\n\r?\n/)[2]))
|
836
|
+
end
|
837
|
+
|
802
838
|
def test_delete_async
|
803
839
|
conn = @client.delete_async(serverurl + 'servlet')
|
804
840
|
Thread.pass while !conn.finished?
|
@@ -882,6 +918,18 @@ EOS
|
|
882
918
|
assert_equal({}, check_query_get(''))
|
883
919
|
assert_equal({'1'=>'2'}, check_query_get({1=>StringIO.new('2')}))
|
884
920
|
assert_equal({'1'=>'2', '3'=>'4'}, check_query_get(StringIO.new('3=4&1=2')))
|
921
|
+
|
922
|
+
hash = check_query_get({"a"=>["A","a"], "B"=>"b"})
|
923
|
+
assert_equal({'a'=>'A', 'B'=>'b'}, hash)
|
924
|
+
assert_equal(['A','a'], hash['a'].to_ary)
|
925
|
+
|
926
|
+
hash = check_query_get({"a"=>WEBrick::HTTPUtils::FormData.new("A","a"), "B"=>"b"})
|
927
|
+
assert_equal({'a'=>'A', 'B'=>'b'}, hash)
|
928
|
+
assert_equal(['A','a'], hash['a'].to_ary)
|
929
|
+
|
930
|
+
hash = check_query_get({"a"=>[StringIO.new("A"),StringIO.new("a")], "B"=>StringIO.new("b")})
|
931
|
+
assert_equal({'a'=>'A', 'B'=>'b'}, hash)
|
932
|
+
assert_equal(['A','a'], hash['a'].to_ary)
|
885
933
|
end
|
886
934
|
|
887
935
|
def test_post_body
|
@@ -1002,7 +1050,7 @@ EOS
|
|
1002
1050
|
def test_cookies
|
1003
1051
|
cookiefile = File.join(File.dirname(File.expand_path(__FILE__)), 'test_cookies_file')
|
1004
1052
|
File.open(cookiefile, "wb") do |f|
|
1005
|
-
f << "http://rubyforge.org/account/login.php
|
1053
|
+
f << "http://rubyforge.org/account/login.php\tsession_ser\tLjEwMy45Ni40Ni0q%2A-fa0537de8cc31\t2000000000\t.rubyforge.org\t/\t13\n"
|
1006
1054
|
end
|
1007
1055
|
@client.set_cookie_store(cookiefile)
|
1008
1056
|
cookie = @client.cookie_manager.cookies.first
|
@@ -1014,7 +1062,7 @@ EOS
|
|
1014
1062
|
@client.get_content('http://rubyforge.org/account/login.php')
|
1015
1063
|
@client.save_cookie_store
|
1016
1064
|
str = File.read(cookiefile)
|
1017
|
-
assert_match(%r(http://rubyforge.org/account/login.php
|
1065
|
+
assert_match(%r(http://rubyforge.org/account/login.php\tfoo\tbar\t1924873200\trubyforge.org\t/account\t1), str)
|
1018
1066
|
File.unlink(cookiefile)
|
1019
1067
|
end
|
1020
1068
|
|
@@ -1460,8 +1508,8 @@ private
|
|
1460
1508
|
@serverport = @server.config[:Port]
|
1461
1509
|
[:hello, :sleep, :servlet_redirect, :redirect1, :redirect2, :redirect3, :redirect_self, :relative_redirect, :chunked, :largebody, :status, :compressed, :charset].each do |sym|
|
1462
1510
|
@server.mount(
|
1463
|
-
|
1464
|
-
|
1511
|
+
"/#{sym}",
|
1512
|
+
WEBrick::HTTPServlet::ProcHandler.new(method("do_#{sym}").to_proc)
|
1465
1513
|
)
|
1466
1514
|
end
|
1467
1515
|
@server.mount('/servlet', TestServlet.new(@server))
|
@@ -1489,11 +1537,11 @@ private
|
|
1489
1537
|
end
|
1490
1538
|
|
1491
1539
|
def do_servlet_redirect(req, res)
|
1492
|
-
res.set_redirect(WEBrick::HTTPStatus::Found, serverurl + "servlet")
|
1540
|
+
res.set_redirect(WEBrick::HTTPStatus::Found, serverurl + "servlet")
|
1493
1541
|
end
|
1494
1542
|
|
1495
1543
|
def do_redirect1(req, res)
|
1496
|
-
res.set_redirect(WEBrick::HTTPStatus::MovedPermanently, serverurl + "hello")
|
1544
|
+
res.set_redirect(WEBrick::HTTPStatus::MovedPermanently, serverurl + "hello")
|
1497
1545
|
end
|
1498
1546
|
|
1499
1547
|
def do_redirect2(req, res)
|
@@ -1501,15 +1549,15 @@ private
|
|
1501
1549
|
end
|
1502
1550
|
|
1503
1551
|
def do_redirect3(req, res)
|
1504
|
-
res.set_redirect(WEBrick::HTTPStatus::Found, serverurl + "hello")
|
1552
|
+
res.set_redirect(WEBrick::HTTPStatus::Found, serverurl + "hello")
|
1505
1553
|
end
|
1506
1554
|
|
1507
1555
|
def do_redirect_self(req, res)
|
1508
|
-
res.set_redirect(WEBrick::HTTPStatus::Found, serverurl + "redirect_self")
|
1556
|
+
res.set_redirect(WEBrick::HTTPStatus::Found, serverurl + "redirect_self")
|
1509
1557
|
end
|
1510
1558
|
|
1511
1559
|
def do_relative_redirect(req, res)
|
1512
|
-
res.set_redirect(WEBrick::HTTPStatus::Found, "hello")
|
1560
|
+
res.set_redirect(WEBrick::HTTPStatus::Found, "hello")
|
1513
1561
|
end
|
1514
1562
|
|
1515
1563
|
def do_chunked(req, res)
|
@@ -1555,7 +1603,7 @@ private
|
|
1555
1603
|
end
|
1556
1604
|
|
1557
1605
|
def do_HEAD(req, res)
|
1558
|
-
res["x-head"] = 'head'
|
1606
|
+
res["x-head"] = 'head' # use this for test purpose only.
|
1559
1607
|
res["x-query"] = query_response(req)
|
1560
1608
|
end
|
1561
1609
|
|
@@ -1615,9 +1663,9 @@ private
|
|
1615
1663
|
def query_escape(query)
|
1616
1664
|
escaped = []
|
1617
1665
|
query.sort_by { |k, v| k }.collect do |k, v|
|
1618
|
-
|
1619
|
-
|
1620
|
-
|
1666
|
+
v.to_ary.each do |ve|
|
1667
|
+
escaped << CGI.escape(k) + '=' + CGI.escape(ve)
|
1668
|
+
end
|
1621
1669
|
end
|
1622
1670
|
escaped.join('&')
|
1623
1671
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('helper', File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'httpclient/include_client'
|
5
|
+
class TestIncludeClient < Test::Unit::TestCase
|
6
|
+
class Widget
|
7
|
+
extend HTTPClient::IncludeClient
|
8
|
+
|
9
|
+
include_http_client("http://example.com") do |client|
|
10
|
+
client.cookie_manager = nil
|
11
|
+
client.agent_name = "iMonkey 4k"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class OtherWidget
|
16
|
+
extend HTTPClient::IncludeClient
|
17
|
+
|
18
|
+
include_http_client
|
19
|
+
include_http_client(:method_name => :other_http_client)
|
20
|
+
end
|
21
|
+
|
22
|
+
class UnrelatedBlankClass ; end
|
23
|
+
|
24
|
+
def test_client_class_level_singleton
|
25
|
+
assert_equal Widget.http_client.object_id, Widget.http_client.object_id
|
26
|
+
|
27
|
+
assert_equal Widget.http_client.object_id, Widget.new.http_client.object_id
|
28
|
+
|
29
|
+
assert_not_equal Widget.http_client.object_id, OtherWidget.http_client.object_id
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_configured
|
33
|
+
assert_equal Widget.http_client.agent_name, "iMonkey 4k"
|
34
|
+
assert_nil Widget.http_client.cookie_manager
|
35
|
+
assert_equal Widget.http_client.proxy.to_s, "http://example.com"
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_two_includes
|
39
|
+
assert_not_equal OtherWidget.http_client.object_id, OtherWidget.other_http_client.object_id
|
40
|
+
|
41
|
+
assert_equal OtherWidget.other_http_client.object_id, OtherWidget.new.other_http_client.object_id
|
42
|
+
end
|
43
|
+
|
44
|
+
# meta-programming gone wrong sometimes accidentally
|
45
|
+
# adds the class method to _everyone_, a mistake we've made before.
|
46
|
+
def test_not_infected_class_hieararchy
|
47
|
+
assert ! Class.respond_to?(:http_client)
|
48
|
+
assert ! UnrelatedBlankClass.respond_to?(:http_client)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: httpclient
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.2.
|
4
|
+
version: 2.2.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-08-14 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description:
|
15
15
|
email: nahi@ruby-lang.org
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- lib/httpclient/timeout.rb
|
25
25
|
- lib/httpclient/version.rb
|
26
26
|
- lib/httpclient/connection.rb
|
27
|
+
- lib/httpclient/include_client.rb
|
27
28
|
- lib/httpclient/session.rb
|
28
29
|
- lib/httpclient/cacert.p7s
|
29
30
|
- lib/httpclient/cookie.rb
|
@@ -46,6 +47,7 @@ files:
|
|
46
47
|
- sample/ssl/htdocs/index.html
|
47
48
|
- sample/ssl/0cert.pem
|
48
49
|
- sample/ssl/webrick_httpsd.rb
|
50
|
+
- sample/oauth_salesforce_10.rb
|
49
51
|
- sample/thread.rb
|
50
52
|
- sample/oauth_friendfeed.rb
|
51
53
|
- sample/oauth_twitter.rb
|
@@ -63,6 +65,7 @@ files:
|
|
63
65
|
- test/htpasswd
|
64
66
|
- test/test_auth.rb
|
65
67
|
- test/client.key
|
68
|
+
- test/test_include_client.rb
|
66
69
|
- test/helper.rb
|
67
70
|
- test/ca.cert
|
68
71
|
- test/sslsvr.rb
|
@@ -85,7 +88,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
88
|
version: '0'
|
86
89
|
segments:
|
87
90
|
- 0
|
88
|
-
hash:
|
91
|
+
hash: 1629827102590288352
|
89
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
93
|
none: false
|
91
94
|
requirements:
|
@@ -94,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
97
|
version: '0'
|
95
98
|
segments:
|
96
99
|
- 0
|
97
|
-
hash:
|
100
|
+
hash: 1629827102590288352
|
98
101
|
requirements: []
|
99
102
|
rubyforge_project:
|
100
103
|
rubygems_version: 1.8.23
|