httpclient 2.2.5 → 2.2.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|