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 CHANGED
@@ -1,5 +1,5 @@
1
1
  httpclient - HTTP accessing library.
2
- Copyright (C) 2000-2011 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
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/htlm module; rubyntlm gem)
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
@@ -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).content
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).content
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, :body, :header), &block)
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.successful?(res.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
- raise BadResponseError.new("unexpected response: #{res.header.inspect}", res)
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
- elsif method == 'POST'
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
@@ -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 = BasicAuth.new
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 = generate_cnonce()
357
- @nonce_count += 1
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
- message_digest << ('%08x' % @nonce_count)
369
- message_digest << cnonce
370
- message_digest << param['qop']
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
- header << "cnonce=\"#{cnonce}\""
378
- header << "nc=#{'%08x' % @nonce_count}"
379
- header << "qop=#{param['qop']}"
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
  #
@@ -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 = nil)
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 rescue nil
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
- query.collect { |attr, value|
826
- if value.respond_to?(:read)
827
- value = value.read
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
- escape(attr.to_s) << '=' << escape(value.to_s)
830
- }.join('&')
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
@@ -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
- @socket.ssl_connect(@dest.host)
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
@@ -1,3 +1,3 @@
1
1
  class HTTPClient
2
- VERSION = '2.2.5'
2
+ VERSION = '2.2.6'
3
3
  end
@@ -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")
@@ -1,9 +1,12 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  require 'test/unit'
3
- require 'simplecov'
4
- require 'simplecov-rcov'
5
- SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
6
- SimpleCov.start
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'
@@ -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(
@@ -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
- @client.proxy = "http://"
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 session_ser LjEwMy45Ni40Ni0q%2A-fa0537de8cc31 2000000000 .rubyforge.org / 13\n"
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 foo bar 1924873200 rubyforge.org /account 1), str)
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
- "/#{sym}",
1464
- WEBrick::HTTPServlet::ProcHandler.new(method("do_#{sym}").to_proc)
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' # use this for test purpose only.
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
- v.to_ary.each do |ve|
1619
- escaped << CGI.escape(k) + '=' + CGI.escape(ve)
1620
- end
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.5
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-05-06 00:00:00.000000000 Z
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: -3213102379838367907
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: -3213102379838367907
100
+ hash: 1629827102590288352
98
101
  requirements: []
99
102
  rubyforge_project:
100
103
  rubygems_version: 1.8.23