httpclient 2.1.4 → 2.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -199,9 +199,9 @@ require 'httpclient/cookie'
199
199
  # ruby -rhttpclient -e 'p HTTPClient.head(ARGV.shift).header["last-modified"]' http://dev.ctor.org/
200
200
  #
201
201
  class HTTPClient
202
- VERSION = '2.1.4'
202
+ VERSION = '2.1.5'
203
203
  RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
204
- /: (\S+) (\S+)/ =~ %q$Id: httpclient.rb 269 2009-02-13 13:21:15Z nahi $
204
+ /: (\S+) (\S+)/ =~ %q$Id: httpclient.rb 280 2009-06-02 15:44:28Z nahi $
205
205
  LIB_NAME = "(#{$1}/#{$2}, #{RUBY_VERSION_STRING})"
206
206
 
207
207
  include Util
@@ -250,8 +250,13 @@ class HTTPClient
250
250
  class << self
251
251
  %w(get_content post_content head get post put delete options propfind proppatch trace).each do |name|
252
252
  eval <<-EOD
253
- def #{name}(*arg)
254
- new.#{name}(*arg)
253
+ def #{name}(*arg, &block)
254
+ clnt = new
255
+ begin
256
+ clnt.#{name}(*arg, &block)
257
+ ensure
258
+ clnt.reset_all
259
+ end
255
260
  end
256
261
  EOD
257
262
  end
@@ -587,12 +592,12 @@ class HTTPClient
587
592
  end
588
593
 
589
594
  # Sends POST request to the specified URL. See request for arguments.
590
- def post(uri, body = nil, extheader = {}, &block)
595
+ def post(uri, body = '', extheader = {}, &block)
591
596
  request(:post, uri, nil, body, extheader, &block)
592
597
  end
593
598
 
594
599
  # Sends PUT request to the specified URL. See request for arguments.
595
- def put(uri, body = nil, extheader = {}, &block)
600
+ def put(uri, body = '', extheader = {}, &block)
596
601
  request(:put, uri, nil, body, extheader, &block)
597
602
  end
598
603
 
@@ -653,14 +658,12 @@ class HTTPClient
653
658
  # cgi.rb does not support it.
654
659
  def request(method, uri, query = nil, body = nil, extheader = {}, &block)
655
660
  uri = urify(uri)
656
- proxy = no_proxy?(uri) ? nil : @proxy
657
- req = create_request(method, uri, query, body, extheader)
658
661
  if block
659
662
  filtered_block = proc { |res, str|
660
663
  block.call(str)
661
664
  }
662
665
  end
663
- do_request(req, proxy, &filtered_block)
666
+ do_request(method, uri, query, body, extheader, &filtered_block)
664
667
  end
665
668
 
666
669
  # Sends HEAD request in async style. See request_async for arguments.
@@ -723,9 +726,7 @@ class HTTPClient
723
726
  # Arguments definition is the same as request.
724
727
  def request_async(method, uri, query = nil, body = nil, extheader = {})
725
728
  uri = urify(uri)
726
- proxy = no_proxy?(uri) ? nil : @proxy
727
- req = create_request(method, uri, query, body, extheader)
728
- do_request_async(req, proxy)
729
+ do_request_async(method, uri, query, body, extheader)
729
730
  end
730
731
 
731
732
  # Resets internal session for the given URL. Keep-alive connection for the
@@ -748,11 +749,17 @@ private
748
749
  class KeepAliveDisconnected < StandardError # :nodoc:
749
750
  end
750
751
 
751
- def do_request(req, proxy, &block)
752
+ def do_request(method, uri, query, body, extheader, &block)
752
753
  conn = Connection.new
753
754
  res = nil
755
+ if HTTP::Message.file?(body)
756
+ pos = body.pos rescue nil
757
+ end
754
758
  retry_count = @session_manager.protocol_retry_count
759
+ proxy = no_proxy?(uri) ? nil : @proxy
755
760
  while retry_count > 0
761
+ body.pos = pos if pos
762
+ req = create_request(method, uri, query, body, extheader)
756
763
  begin
757
764
  protect_keep_alive_disconnected do
758
765
  do_get_block(req, proxy, conn, &block)
@@ -767,11 +774,17 @@ private
767
774
  res
768
775
  end
769
776
 
770
- def do_request_async(req, proxy)
777
+ def do_request_async(method, uri, query, body, extheader)
771
778
  conn = Connection.new
772
779
  t = Thread.new(conn) { |tconn|
780
+ if HTTP::Message.file?(body)
781
+ pos = body.pos rescue nil
782
+ end
773
783
  retry_count = @session_manager.protocol_retry_count
784
+ proxy = no_proxy?(uri) ? nil : @proxy
774
785
  while retry_count > 0
786
+ body.pos = pos if pos
787
+ req = create_request(method, uri, query, body, extheader)
775
788
  begin
776
789
  protect_keep_alive_disconnected do
777
790
  do_get_stream(req, proxy, tconn)
@@ -817,13 +830,11 @@ private
817
830
  retry_number = 0
818
831
  while retry_number < @follow_redirect_count
819
832
  body.pos = pos if pos
820
- req = create_request(method, uri, query, body, extheader)
821
- proxy = no_proxy?(req.header.request_uri) ? nil : @proxy
822
- res = do_request(req, proxy, &filtered_block)
833
+ res = do_request(method, uri, query, body, extheader, &filtered_block)
823
834
  if HTTP::Status.successful?(res.status)
824
835
  return res
825
836
  elsif HTTP::Status.redirect?(res.status)
826
- uri = urify(@redirect_uri_callback.call(req.header.request_uri, res))
837
+ uri = urify(@redirect_uri_callback.call(uri, res))
827
838
  retry_number += 1
828
839
  else
829
840
  raise BadResponseError.new("unexpected response: #{res.header.inspect}", res)
@@ -0,0 +1,1020 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
+ #
4
+ # This program is copyrighted free software by NAKAMURA, Hiroshi. You can
5
+ # redistribute it and/or modify it under the same terms of Ruby's license;
6
+ # either the dual license version in 2003, or any later version.
7
+
8
+
9
+ require 'uri'
10
+ require 'stringio'
11
+ require 'digest/sha1'
12
+
13
+ # Extra library
14
+ require 'httpclient/util'
15
+ require 'httpclient/ssl_config'
16
+ require 'httpclient/connection'
17
+ require 'httpclient/session'
18
+ require 'httpclient/http'
19
+ require 'httpclient/auth'
20
+ require 'httpclient/cookie'
21
+
22
+
23
+ # The HTTPClient class provides several methods for accessing Web resources
24
+ # via HTTP.
25
+ #
26
+ # HTTPClient instance is designed to be MT-safe. You can call a HTTPClient
27
+ # instance from several threads without synchronization after setting up an
28
+ # instance.
29
+ #
30
+ # clnt = HTTPClient.new
31
+ # clnt.set_cookie_store('/home/nahi/cookie.dat')
32
+ # urls.each do |url|
33
+ # Thread.new(url) do |u|
34
+ # p clnt.head(u).status
35
+ # end
36
+ # end
37
+ #
38
+ # == How to use
39
+ #
40
+ # At first, how to create your client. See initialize for more detail.
41
+ #
42
+ # 1. Create simple client.
43
+ #
44
+ # clnt = HTTPClient.new
45
+ #
46
+ # 2. Accessing resources through HTTP proxy. You can use environment
47
+ # variable 'http_proxy' or 'HTTP_PROXY' instead.
48
+ #
49
+ # clnt = HTTPClient.new('http://myproxy:8080')
50
+ #
51
+ # === How to retrieve web resources
52
+ #
53
+ # See get_content.
54
+ #
55
+ # 1. Get content of specified URL. It returns a String of whole result.
56
+ #
57
+ # puts clnt.get_content('http://dev.ctor.org/')
58
+ #
59
+ # 2. Get content as chunks of String. It yields chunks of String.
60
+ #
61
+ # clnt.get_content('http://dev.ctor.org/') do |chunk|
62
+ # puts chunk
63
+ # end
64
+ #
65
+ # === Invoking other HTTP methods
66
+ #
67
+ # See head, get, post, put, delete, options, propfind, proppatch and trace.
68
+ # It returns a HTTP::Message instance as a response.
69
+ #
70
+ # 1. Do HEAD request.
71
+ #
72
+ # res = clnt.head(uri)
73
+ # p res.header['Last-Modified'][0]
74
+ #
75
+ # 2. Do GET request with query.
76
+ #
77
+ # query = { 'keyword' => 'ruby', 'lang' => 'en' }
78
+ # res = clnt.get(uri, query)
79
+ # p res.status
80
+ # p res.contenttype
81
+ # p res.header['X-Custom']
82
+ # puts res.content
83
+ #
84
+ # === How to POST
85
+ #
86
+ # See post.
87
+ #
88
+ # 1. Do POST a form data.
89
+ #
90
+ # body = { 'keyword' => 'ruby', 'lang' => 'en' }
91
+ # res = clnt.post(uri, body)
92
+ #
93
+ # 2. Do multipart file upload with POST. No need to set extra header by
94
+ # yourself from httpclient/2.1.4.
95
+ #
96
+ # File.open('/tmp/post_data') do |file|
97
+ # body = { 'upload' => file, 'user' => 'nahi' }
98
+ # res = clnt.post(uri, body)
99
+ # end
100
+ #
101
+ # === Accessing via SSL
102
+ #
103
+ # Ruby needs to be compiled with OpenSSL.
104
+ #
105
+ # 1. Get content of specified URL via SSL.
106
+ # Just pass an URL which starts with 'https://'.
107
+ #
108
+ # https_url = 'https://www.rsa.com'
109
+ # clnt.get_content(https_url)
110
+ #
111
+ # 2. Getting peer certificate from response.
112
+ #
113
+ # res = clnt.get(https_url)
114
+ # p res.peer_cert #=> returns OpenSSL::X509::Certificate
115
+ #
116
+ # 3. Configuring OpenSSL options. See HTTPClient::SSLConfig for more details.
117
+ #
118
+ # user_cert_file = 'cert.pem'
119
+ # user_key_file = 'privkey.pem'
120
+ # clnt.ssl_config.set_client_cert_file(user_cert_file, user_key_file)
121
+ # clnt.get_content(https_url)
122
+ #
123
+ # === Handling Cookies
124
+ #
125
+ # 1. Using volatile Cookies. Nothing to do. HTTPClient handles Cookies.
126
+ #
127
+ # clnt = HTTPClient.new
128
+ # clnt.get_content(url1) # receives Cookies.
129
+ # clnt.get_content(url2) # sends Cookies if needed.
130
+ #
131
+ # 2. Saving non volatile Cookies to a specified file. Need to set a file at
132
+ # first and invoke save method at last.
133
+ #
134
+ # clnt = HTTPClient.new
135
+ # clnt.set_cookie_store('/home/nahi/cookie.dat')
136
+ # clnt.get_content(url)
137
+ # ...
138
+ # clnt.save_cookie_store
139
+ #
140
+ # 3. Disabling Cookies.
141
+ #
142
+ # clnt = HTTPClient.new
143
+ # clnt.cookie_manager = nil
144
+ #
145
+ # === Configuring authentication credentials
146
+ #
147
+ # 1. Authentication with Web server. Supports BasicAuth, DigestAuth, and
148
+ # Negotiate/NTLM (requires ruby/ntlm module).
149
+ #
150
+ # clnt = HTTPClient.new
151
+ # domain = 'http://dev.ctor.org/http-access2/'
152
+ # user = 'user'
153
+ # password = 'user'
154
+ # clnt.set_auth(domain, user, password)
155
+ # p clnt.get_content('http://dev.ctor.org/http-access2/login').status
156
+ #
157
+ # 2. Authentication with Proxy server. Supports BasicAuth and NTLM
158
+ # (requires win32/sspi)
159
+ #
160
+ # clnt = HTTPClient.new(proxy)
161
+ # user = 'proxy'
162
+ # password = 'proxy'
163
+ # clnt.set_proxy_auth(user, password)
164
+ # p clnt.get_content(url)
165
+ #
166
+ # === Invoking HTTP methods with custom header
167
+ #
168
+ # Pass a Hash or an Array for extheader argument.
169
+ #
170
+ # extheader = { 'Accept' => '*/*' }
171
+ # clnt.get_content(uri, query, extheader)
172
+ #
173
+ # extheader = [['Accept', 'image/jpeg'], ['Accept', 'image/png']]
174
+ # clnt.get_content(uri, query, extheader)
175
+ #
176
+ # === Invoking HTTP methods asynchronously
177
+ #
178
+ # See head_async, get_async, post_async, put_async, delete_async,
179
+ # options_async, propfind_async, proppatch_async, and trace_async.
180
+ # It immediately returns a HTTPClient::Connection instance as a returning value.
181
+ #
182
+ # connection = clnt.post_async(url, body)
183
+ # print 'posting.'
184
+ # while true
185
+ # break if connection.finished?
186
+ # print '.'
187
+ # sleep 1
188
+ # end
189
+ # puts '.'
190
+ # res = connection.pop
191
+ # p res.status
192
+ # p res.content.read # res.content is an IO for the res of async method.
193
+ #
194
+ # === Shortcut methods
195
+ #
196
+ # You can invoke get_content, get, etc. without creating HTTPClient instance.
197
+ #
198
+ # ruby -rhttpclient -e 'puts HTTPClient.get_content(ARGV.shift)' http://dev.ctor.org/
199
+ # ruby -rhttpclient -e 'p HTTPClient.head(ARGV.shift).header["last-modified"]' http://dev.ctor.org/
200
+ #
201
+ class HTTPClient
202
+ VERSION = '2.1.4'
203
+ RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
204
+ /: (\S+) (\S+)/ =~ %q$Id: httpclient.rb 274 2009-05-13 13:54:08Z nahi $
205
+ LIB_NAME = "(#{$1}/#{$2}, #{RUBY_VERSION_STRING})"
206
+
207
+ include Util
208
+
209
+ # Raised for indicating running environment configuration error for example
210
+ # accessing via SSL under the ruby which is not compiled with OpenSSL.
211
+ class ConfigurationError < StandardError
212
+ end
213
+
214
+ # Raised for indicating HTTP response error.
215
+ class BadResponseError < RuntimeError
216
+ # HTTP::Message:: a response
217
+ attr_reader :res
218
+
219
+ def initialize(msg, res = nil) # :nodoc:
220
+ super(msg)
221
+ @res = res
222
+ end
223
+ end
224
+
225
+ # Raised for indicating a timeout error.
226
+ class TimeoutError < RuntimeError
227
+ end
228
+
229
+ # Raised for indicating a connection timeout error.
230
+ # You can configure connection timeout via HTTPClient#connect_timeout=.
231
+ class ConnectTimeoutError < TimeoutError
232
+ end
233
+
234
+ # Raised for indicating a request sending timeout error.
235
+ # You can configure request sending timeout via HTTPClient#send_timeout=.
236
+ class SendTimeoutError < TimeoutError
237
+ end
238
+
239
+ # Raised for indicating a response receiving timeout error.
240
+ # You can configure response receiving timeout via
241
+ # HTTPClient#receive_timeout=.
242
+ class ReceiveTimeoutError < TimeoutError
243
+ end
244
+
245
+ # Deprecated. just for backward compatibility
246
+ class Session
247
+ BadResponse = ::HTTPClient::BadResponseError
248
+ end
249
+
250
+ class << self
251
+ %w(get_content post_content head get post put delete options propfind proppatch trace).each do |name|
252
+ eval <<-EOD
253
+ def #{name}(*arg, &block)
254
+ clnt = new
255
+ begin
256
+ clnt.#{name}(*arg, &block)
257
+ ensure
258
+ clnt.reset_all
259
+ end
260
+ end
261
+ EOD
262
+ end
263
+
264
+ private
265
+
266
+ def attr_proxy(symbol, assignable = false)
267
+ name = symbol.to_s
268
+ define_method(name) {
269
+ @session_manager.__send__(name)
270
+ }
271
+ if assignable
272
+ aname = name + '='
273
+ define_method(aname) { |rhs|
274
+ reset_all
275
+ @session_manager.__send__(aname, rhs)
276
+ }
277
+ end
278
+ end
279
+ end
280
+
281
+ # HTTPClient::SSLConfig:: SSL configurator.
282
+ attr_reader :ssl_config
283
+ # WebAgent::CookieManager:: Cookies configurator.
284
+ attr_accessor :cookie_manager
285
+ # An array of response HTTP message body String which is used for loop-back
286
+ # test. See test/* to see how to use it. If you want to do loop-back test
287
+ # of HTTP header, use test_loopback_http_response instead.
288
+ attr_reader :test_loopback_response
289
+ # An array of request filter which can trap HTTP request/response.
290
+ # See HTTPClient::WWWAuth to see how to use it.
291
+ attr_reader :request_filter
292
+ # HTTPClient::ProxyAuth:: Proxy authentication handler.
293
+ attr_reader :proxy_auth
294
+ # HTTPClient::WWWAuth:: WWW authentication handler.
295
+ attr_reader :www_auth
296
+ # How many times get_content and post_content follows HTTP redirect.
297
+ # 10 by default.
298
+ attr_accessor :follow_redirect_count
299
+
300
+ # Set HTTP version as a String:: 'HTTP/1.0' or 'HTTP/1.1'
301
+ attr_proxy(:protocol_version, true)
302
+ # Connect timeout in sec.
303
+ attr_proxy(:connect_timeout, true)
304
+ # Request sending timeout in sec.
305
+ attr_proxy(:send_timeout, true)
306
+ # Response receiving timeout in sec.
307
+ attr_proxy(:receive_timeout, true)
308
+ # Negotiation retry count for authentication. 5 by default.
309
+ attr_proxy(:protocol_retry_count, true)
310
+ # if your ruby is older than 2005-09-06, do not set socket_sync = false to
311
+ # avoid an SSL socket blocking bug in openssl/buffering.rb.
312
+ attr_proxy(:socket_sync, true)
313
+ # User-Agent header in HTTP request.
314
+ attr_proxy(:agent_name, true)
315
+ # From header in HTTP request.
316
+ attr_proxy(:from, true)
317
+ # An array of response HTTP String (not a HTTP message body) which is used
318
+ # for loopback test. See test/* to see how to use it.
319
+ attr_proxy(:test_loopback_http_response)
320
+
321
+ # Default extheader for PROPFIND request.
322
+ PROPFIND_DEFAULT_EXTHEADER = { 'Depth' => '0' }
323
+
324
+ # Creates a HTTPClient instance which manages sessions, cookies, etc.
325
+ #
326
+ # HTTPClient.new takes 3 optional arguments for proxy url string,
327
+ # User-Agent String and From header String. User-Agent and From are embedded
328
+ # in HTTP request Header if given. No User-Agent and From header added
329
+ # without setting it explicitly.
330
+ #
331
+ # proxy = 'http://myproxy:8080'
332
+ # agent_name = 'MyAgent/0.1'
333
+ # from = 'from@example.com'
334
+ # HTTPClient.new(proxy, agent_name, from)
335
+ #
336
+ # You can use a keyword argument style Hash. Keys are :proxy, :agent_name
337
+ # and :from.
338
+ #
339
+ # HTTPClient.new(:agent_name = 'MyAgent/0.1')
340
+ def initialize(*args)
341
+ proxy, agent_name, from = keyword_argument(args, :proxy, :agent_name, :from)
342
+ @proxy = nil # assigned later.
343
+ @no_proxy = nil
344
+ @www_auth = WWWAuth.new
345
+ @proxy_auth = ProxyAuth.new
346
+ @request_filter = [@proxy_auth, @www_auth]
347
+ @debug_dev = nil
348
+ @redirect_uri_callback = method(:default_redirect_uri_callback)
349
+ @test_loopback_response = []
350
+ @session_manager = SessionManager.new(self)
351
+ @session_manager.agent_name = agent_name
352
+ @session_manager.from = from
353
+ @session_manager.ssl_config = @ssl_config = SSLConfig.new(self)
354
+ @cookie_manager = WebAgent::CookieManager.new
355
+ @follow_redirect_count = 10
356
+ load_environment
357
+ self.proxy = proxy if proxy
358
+ end
359
+
360
+ # Returns debug device if exists. See debug_dev=.
361
+ def debug_dev
362
+ @debug_dev
363
+ end
364
+
365
+ # Sets debug device. Once debug device is set, all HTTP requests and
366
+ # responses are dumped to given device. dev must respond to << for dump.
367
+ #
368
+ # Calling this method resets all existing sessions.
369
+ def debug_dev=(dev)
370
+ @debug_dev = dev
371
+ reset_all
372
+ @session_manager.debug_dev = dev
373
+ end
374
+
375
+ # Returns URI object of HTTP proxy if exists.
376
+ def proxy
377
+ @proxy
378
+ end
379
+
380
+ # Sets HTTP proxy used for HTTP connection. Given proxy can be an URI,
381
+ # a String or nil. You can set user/password for proxy authentication like
382
+ # HTTPClient#proxy = 'http://user:passwd@myproxy:8080'
383
+ #
384
+ # You can use environment variable 'http_proxy' or 'HTTP_PROXY' for it.
385
+ # You need to use 'cgi_http_proxy' or 'CGI_HTTP_PROXY' instead if you run
386
+ # HTTPClient from CGI environment from security reason. (HTTPClient checks
387
+ # 'REQUEST_METHOD' environment variable whether it's CGI or not)
388
+ #
389
+ # Calling this method resets all existing sessions.
390
+ def proxy=(proxy)
391
+ if proxy.nil?
392
+ @proxy = nil
393
+ @proxy_auth.reset_challenge
394
+ else
395
+ @proxy = urify(proxy)
396
+ if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
397
+ @proxy.host == nil or @proxy.port == nil
398
+ raise ArgumentError.new("unsupported proxy #{proxy}")
399
+ end
400
+ @proxy_auth.reset_challenge
401
+ if @proxy.user || @proxy.password
402
+ @proxy_auth.set_auth(@proxy.user, @proxy.password)
403
+ end
404
+ end
405
+ reset_all
406
+ @session_manager.proxy = @proxy
407
+ @proxy
408
+ end
409
+
410
+ # Returns NO_PROXY setting String if given.
411
+ def no_proxy
412
+ @no_proxy
413
+ end
414
+
415
+ # Sets NO_PROXY setting String. no_proxy must be a comma separated String.
416
+ # Each entry must be 'host' or 'host:port' such as;
417
+ # HTTPClient#no_proxy = 'example.com,example.co.jp:443'
418
+ #
419
+ # 'localhost' is treated as a no_proxy site regardless of explicitly listed.
420
+ # HTTPClient checks given URI objects before accessing it.
421
+ # 'host' is tail string match. No IP-addr conversion.
422
+ #
423
+ # You can use environment variable 'no_proxy' or 'NO_PROXY' for it.
424
+ #
425
+ # Calling this method resets all existing sessions.
426
+ def no_proxy=(no_proxy)
427
+ @no_proxy = no_proxy
428
+ reset_all
429
+ end
430
+
431
+ # Sets credential for Web server authentication.
432
+ # domain:: a String or an URI to specify where HTTPClient should use this
433
+ # credential. If you set uri to nil, HTTPClient uses this credential
434
+ # wherever a server requires it.
435
+ # user:: username String.
436
+ # passwd:: password String.
437
+ #
438
+ # You can set multiple credentials for each uri.
439
+ #
440
+ # clnt.set_auth('http://www.example.com/foo/', 'foo_user', 'passwd')
441
+ # clnt.set_auth('http://www.example.com/bar/', 'bar_user', 'passwd')
442
+ #
443
+ # Calling this method resets all existing sessions.
444
+ def set_auth(domain, user, passwd)
445
+ uri = urify(domain)
446
+ @www_auth.set_auth(uri, user, passwd)
447
+ reset_all
448
+ end
449
+
450
+ # Deprecated. Use set_auth instead.
451
+ def set_basic_auth(domain, user, passwd)
452
+ uri = urify(domain)
453
+ @www_auth.basic_auth.set(uri, user, passwd)
454
+ reset_all
455
+ end
456
+
457
+ # Sets credential for Proxy authentication.
458
+ # user:: username String.
459
+ # passwd:: password String.
460
+ #
461
+ # Calling this method resets all existing sessions.
462
+ def set_proxy_auth(user, passwd)
463
+ @proxy_auth.set_auth(user, passwd)
464
+ reset_all
465
+ end
466
+
467
+ # Sets the filename where non-volatile Cookies be saved by calling
468
+ # save_cookie_store.
469
+ # This method tries to load and managing Cookies from the specified file.
470
+ #
471
+ # Calling this method resets all existing sessions.
472
+ def set_cookie_store(filename)
473
+ @cookie_manager.cookies_file = filename
474
+ @cookie_manager.load_cookies if filename
475
+ reset_all
476
+ end
477
+
478
+ # Try to save Cookies to the file specified in set_cookie_store. Unexpected
479
+ # error will be raised if you don't call set_cookie_store first.
480
+ # (interface mismatch between WebAgent::CookieManager implementation)
481
+ def save_cookie_store
482
+ @cookie_manager.save_cookies
483
+ end
484
+
485
+ # Sets callback proc when HTTP redirect status is returned for get_content
486
+ # and post_content. default_redirect_uri_callback is used by default.
487
+ #
488
+ # If you need strict implementation which does not allow relative URI
489
+ # redirection, set strict_redirect_uri_callback instead.
490
+ #
491
+ # clnt.redirect_uri_callback = clnt.method(:strict_redirect_uri_callback)
492
+ #
493
+ def redirect_uri_callback=(redirect_uri_callback)
494
+ @redirect_uri_callback = redirect_uri_callback
495
+ end
496
+
497
+ # Retrieves a web resource.
498
+ #
499
+ # uri:: a String or an URI object which represents an URL of web resource.
500
+ # query:: a Hash or an Array of query part of URL.
501
+ # e.g. { "a" => "b" } => 'http://host/part?a=b'.
502
+ # Give an array to pass multiple value like
503
+ # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'.
504
+ # extheader:: a Hash or an Array of extra headers. e.g.
505
+ # { 'Accept' => '*/*' } or
506
+ # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
507
+ # &block:: Give a block to get chunked message-body of response like
508
+ # get_content(uri) { |chunked_body| ... }.
509
+ # Size of each chunk may not be the same.
510
+ #
511
+ # get_content follows HTTP redirect status (see HTTP::Status.redirect?)
512
+ # internally and try to retrieve content from redirected URL. See
513
+ # redirect_uri_callback= how HTTP redirection is handled.
514
+ #
515
+ # If you need to get full HTTP response including HTTP status and headers,
516
+ # use get method. get returns HTTP::Message as a response and you need to
517
+ # follow HTTP redirect by yourself if you need.
518
+ def get_content(uri, query = nil, extheader = {}, &block)
519
+ follow_redirect(:get, uri, query, nil, extheader, &block).content
520
+ end
521
+
522
+ # Posts a content.
523
+ #
524
+ # uri:: a String or an URI object which represents an URL of web resource.
525
+ # body:: a Hash or an Array of body part.
526
+ # e.g. { "a" => "b" } => 'a=b'.
527
+ # Give an array to pass multiple value like
528
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
529
+ # When you pass a File as a value, it will be posted as a
530
+ # multipart/form-data. e.g. { 'upload' => file }
531
+ # extheader:: a Hash or an Array of extra headers. e.g.
532
+ # { 'Accept' => '*/*' } or
533
+ # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
534
+ # &block:: Give a block to get chunked message-body of response like
535
+ # post_content(uri) { |chunked_body| ... }.
536
+ # Size of each chunk may not be the same.
537
+ #
538
+ # post_content follows HTTP redirect status (see HTTP::Status.redirect?)
539
+ # internally and try to post the content to redirected URL. See
540
+ # redirect_uri_callback= how HTTP redirection is handled.
541
+ #
542
+ # If you need to get full HTTP response including HTTP status and headers,
543
+ # use post method.
544
+ def post_content(uri, body = nil, extheader = {}, &block)
545
+ follow_redirect(:post, uri, nil, body, extheader, &block).content
546
+ end
547
+
548
+ # A method for redirect uri callback. How to use:
549
+ # clnt.redirect_uri_callback = clnt.method(:strict_redirect_uri_callback)
550
+ # This callback does not allow relative redirect such as
551
+ # Location: ../foo/
552
+ # in HTTP header. (raises BadResponseError instead)
553
+ def strict_redirect_uri_callback(uri, res)
554
+ newuri = URI.parse(res.header['location'][0])
555
+ if https?(uri) && !https?(newuri)
556
+ raise BadResponseError.new("redirecting to non-https resource")
557
+ end
558
+ unless newuri.is_a?(URI::HTTP)
559
+ raise BadResponseError.new("unexpected location: #{newuri}", res)
560
+ end
561
+ puts "redirect to: #{newuri}" if $DEBUG
562
+ newuri
563
+ end
564
+
565
+ # A default method for redirect uri callback. This method is used by
566
+ # HTTPClient instance by default.
567
+ # This callback allows relative redirect such as
568
+ # Location: ../foo/
569
+ # in HTTP header.
570
+ def default_redirect_uri_callback(uri, res)
571
+ newuri = URI.parse(res.header['location'][0])
572
+ if https?(uri) && !https?(newuri)
573
+ raise BadResponseError.new("redirecting to non-https resource")
574
+ end
575
+ unless newuri.is_a?(URI::HTTP)
576
+ newuri = uri + newuri
577
+ STDERR.puts("could be a relative URI in location header which is not recommended")
578
+ STDERR.puts("'The field value consists of a single absolute URI' in HTTP spec")
579
+ end
580
+ puts "redirect to: #{newuri}" if $DEBUG
581
+ newuri
582
+ end
583
+
584
+ # Sends HEAD request to the specified URL. See request for arguments.
585
+ def head(uri, query = nil, extheader = {})
586
+ request(:head, uri, query, nil, extheader)
587
+ end
588
+
589
+ # Sends GET request to the specified URL. See request for arguments.
590
+ def get(uri, query = nil, extheader = {}, &block)
591
+ request(:get, uri, query, nil, extheader, &block)
592
+ end
593
+
594
+ # Sends POST request to the specified URL. See request for arguments.
595
+ def post(uri, body = nil, extheader = {}, &block)
596
+ request(:post, uri, nil, body, extheader, &block)
597
+ end
598
+
599
+ # Sends PUT request to the specified URL. See request for arguments.
600
+ def put(uri, body = nil, extheader = {}, &block)
601
+ request(:put, uri, nil, body, extheader, &block)
602
+ end
603
+
604
+ # Sends DELETE request to the specified URL. See request for arguments.
605
+ def delete(uri, extheader = {}, &block)
606
+ request(:delete, uri, nil, nil, extheader, &block)
607
+ end
608
+
609
+ # Sends OPTIONS request to the specified URL. See request for arguments.
610
+ def options(uri, extheader = {}, &block)
611
+ request(:options, uri, nil, nil, extheader, &block)
612
+ end
613
+
614
+ # Sends PROPFIND request to the specified URL. See request for arguments.
615
+ def propfind(uri, extheader = PROPFIND_DEFAULT_EXTHEADER, &block)
616
+ request(:propfind, uri, nil, nil, extheader, &block)
617
+ end
618
+
619
+ # Sends PROPPATCH request to the specified URL. See request for arguments.
620
+ def proppatch(uri, body = nil, extheader = {}, &block)
621
+ request(:proppatch, uri, nil, body, extheader, &block)
622
+ end
623
+
624
+ # Sends TRACE request to the specified URL. See request for arguments.
625
+ def trace(uri, query = nil, body = nil, extheader = {}, &block)
626
+ request('TRACE', uri, query, body, extheader, &block)
627
+ end
628
+
629
+ # Sends a request to the specified URL.
630
+ #
631
+ # method:: HTTP method to be sent. method.to_s.upcase is used.
632
+ # uri:: a String or an URI object which represents an URL of web resource.
633
+ # query:: a Hash or an Array of query part of URL.
634
+ # e.g. { "a" => "b" } => 'http://host/part?a=b'
635
+ # Give an array to pass multiple value like
636
+ # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
637
+ # body:: a Hash or an Array of body part.
638
+ # e.g. { "a" => "b" } => 'a=b'.
639
+ # Give an array to pass multiple value like
640
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
641
+ # When the given method is 'POST' and the given body contains a file
642
+ # as a value, it will be posted as a multipart/form-data.
643
+ # e.g. { 'upload' => file }
644
+ # See HTTP::Message.file? for actual condition of 'a file'.
645
+ # extheader:: a Hash or an Array of extra headers. e.g.
646
+ # { 'Accept' => '*/*' } or
647
+ # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
648
+ # &block:: Give a block to get chunked message-body of response like
649
+ # get(uri) { |chunked_body| ... }.
650
+ # Size of each chunk may not be the same.
651
+ #
652
+ # You can also pass a String as a body. HTTPClient just sends a String as
653
+ # a HTTP request message body.
654
+ #
655
+ # When you pass an IO as a body, HTTPClient sends it as a HTTP request with
656
+ # chunked encoding (Transfer-Encoding: chunked in HTTP header). Bear in mind
657
+ # that some server application does not support chunked request. At least
658
+ # cgi.rb does not support it.
659
+ def request(method, uri, query = nil, body = nil, extheader = {}, &block)
660
+ uri = urify(uri)
661
+ proxy = no_proxy?(uri) ? nil : @proxy
662
+ req = create_request(method, uri, query, body, extheader)
663
+ if block
664
+ filtered_block = proc { |res, str|
665
+ block.call(str)
666
+ }
667
+ end
668
+ do_request(req, proxy, &filtered_block)
669
+ end
670
+
671
+ # Sends HEAD request in async style. See request_async for arguments.
672
+ # It immediately returns a HTTPClient::Connection instance as a result.
673
+ def head_async(uri, query = nil, extheader = {})
674
+ request_async(:head, uri, query, nil, extheader)
675
+ end
676
+
677
+ # Sends GET request in async style. See request_async for arguments.
678
+ # It immediately returns a HTTPClient::Connection instance as a result.
679
+ def get_async(uri, query = nil, extheader = {})
680
+ request_async(:get, uri, query, nil, extheader)
681
+ end
682
+
683
+ # Sends POST request in async style. See request_async for arguments.
684
+ # It immediately returns a HTTPClient::Connection instance as a result.
685
+ def post_async(uri, body = nil, extheader = {})
686
+ request_async(:post, uri, nil, body, extheader)
687
+ end
688
+
689
+ # Sends PUT request in async style. See request_async for arguments.
690
+ # It immediately returns a HTTPClient::Connection instance as a result.
691
+ def put_async(uri, body = nil, extheader = {})
692
+ request_async(:put, uri, nil, body, extheader)
693
+ end
694
+
695
+ # Sends DELETE request in async style. See request_async for arguments.
696
+ # It immediately returns a HTTPClient::Connection instance as a result.
697
+ def delete_async(uri, extheader = {})
698
+ request_async(:delete, uri, nil, nil, extheader)
699
+ end
700
+
701
+ # Sends OPTIONS request in async style. See request_async for arguments.
702
+ # It immediately returns a HTTPClient::Connection instance as a result.
703
+ def options_async(uri, extheader = {})
704
+ request_async(:options, uri, nil, nil, extheader)
705
+ end
706
+
707
+ # Sends PROPFIND request in async style. See request_async for arguments.
708
+ # It immediately returns a HTTPClient::Connection instance as a result.
709
+ def propfind_async(uri, extheader = PROPFIND_DEFAULT_EXTHEADER)
710
+ request_async(:propfind, uri, nil, nil, extheader)
711
+ end
712
+
713
+ # Sends PROPPATCH request in async style. See request_async for arguments.
714
+ # It immediately returns a HTTPClient::Connection instance as a result.
715
+ def proppatch_async(uri, body = nil, extheader = {})
716
+ request_async(:proppatch, uri, nil, body, extheader)
717
+ end
718
+
719
+ # Sends TRACE request in async style. See request_async for arguments.
720
+ # It immediately returns a HTTPClient::Connection instance as a result.
721
+ def trace_async(uri, query = nil, body = nil, extheader = {})
722
+ request_async(:trace, uri, query, body, extheader)
723
+ end
724
+
725
+ # Sends a request in async style. request method creates new Thread for
726
+ # HTTP connection and returns a HTTPClient::Connection instance immediately.
727
+ #
728
+ # Arguments definition is the same as request.
729
+ def request_async(method, uri, query = nil, body = nil, extheader = {})
730
+ uri = urify(uri)
731
+ proxy = no_proxy?(uri) ? nil : @proxy
732
+ req = create_request(method, uri, query, body, extheader)
733
+ do_request_async(req, proxy)
734
+ end
735
+
736
+ # Resets internal session for the given URL. Keep-alive connection for the
737
+ # site (host-port pair) is disconnected if exists.
738
+ def reset(uri)
739
+ uri = urify(uri)
740
+ @session_manager.reset(uri)
741
+ end
742
+
743
+ # Resets all of internal sessions. Keep-alive connections are disconnected.
744
+ def reset_all
745
+ @session_manager.reset_all
746
+ end
747
+
748
+ private
749
+
750
+ class RetryableResponse < StandardError # :nodoc:
751
+ end
752
+
753
+ class KeepAliveDisconnected < StandardError # :nodoc:
754
+ end
755
+
756
+ def do_request(req, proxy, &block)
757
+ conn = Connection.new
758
+ res = nil
759
+ retry_count = @session_manager.protocol_retry_count
760
+ while retry_count > 0
761
+ begin
762
+ protect_keep_alive_disconnected do
763
+ do_get_block(req, proxy, conn, &block)
764
+ end
765
+ res = conn.pop
766
+ break
767
+ rescue RetryableResponse
768
+ res = conn.pop
769
+ retry_count -= 1
770
+ end
771
+ end
772
+ res
773
+ end
774
+
775
+ def do_request_async(req, proxy)
776
+ conn = Connection.new
777
+ t = Thread.new(conn) { |tconn|
778
+ retry_count = @session_manager.protocol_retry_count
779
+ while retry_count > 0
780
+ begin
781
+ protect_keep_alive_disconnected do
782
+ do_get_stream(req, proxy, tconn)
783
+ end
784
+ break
785
+ rescue RetryableResponse
786
+ retry_count -= 1
787
+ end
788
+ end
789
+ }
790
+ conn.async_thread = t
791
+ conn
792
+ end
793
+
794
+ def load_environment
795
+ # http_proxy
796
+ if getenv('REQUEST_METHOD')
797
+ # HTTP_PROXY conflicts with the environment variable usage in CGI where
798
+ # HTTP_* is used for HTTP header information. Unlike open-uri, we
799
+ # simply ignore http_proxy in CGI env and use cgi_http_proxy instead.
800
+ self.proxy = getenv('cgi_http_proxy')
801
+ else
802
+ self.proxy = getenv('http_proxy')
803
+ end
804
+ # no_proxy
805
+ self.no_proxy = getenv('no_proxy')
806
+ end
807
+
808
+ def getenv(name)
809
+ ENV[name.downcase] || ENV[name.upcase]
810
+ end
811
+
812
+ def follow_redirect(method, uri, query, body, extheader, &block)
813
+ uri = urify(uri)
814
+ if block
815
+ filtered_block = proc { |r, str|
816
+ block.call(str) if HTTP::Status.successful?(r.status)
817
+ }
818
+ end
819
+ if HTTP::Message.file?(body)
820
+ pos = body.pos rescue nil
821
+ end
822
+ retry_number = 0
823
+ while retry_number < @follow_redirect_count
824
+ body.pos = pos if pos
825
+ req = create_request(method, uri, query, body, extheader)
826
+ proxy = no_proxy?(req.header.request_uri) ? nil : @proxy
827
+ res = do_request(req, proxy, &filtered_block)
828
+ if HTTP::Status.successful?(res.status)
829
+ return res
830
+ elsif HTTP::Status.redirect?(res.status)
831
+ uri = urify(@redirect_uri_callback.call(req.header.request_uri, res))
832
+ retry_number += 1
833
+ else
834
+ raise BadResponseError.new("unexpected response: #{res.header.inspect}", res)
835
+ end
836
+ end
837
+ raise BadResponseError.new("retry count exceeded", res)
838
+ end
839
+
840
+ def protect_keep_alive_disconnected
841
+ begin
842
+ yield
843
+ rescue KeepAliveDisconnected
844
+ yield
845
+ end
846
+ end
847
+
848
+ def create_request(method, uri, query, body, extheader)
849
+ method = method.to_s.upcase
850
+ if extheader.is_a?(Hash)
851
+ extheader = extheader.to_a
852
+ else
853
+ extheader = extheader.dup
854
+ end
855
+ boundary = nil
856
+ if body
857
+ dummy, content_type = extheader.find { |key, value|
858
+ key.downcase == 'content-type'
859
+ }
860
+ if content_type
861
+ if /\Amultipart/ =~ content_type
862
+ if content_type =~ /boundary=(.+)\z/
863
+ boundary = $1
864
+ else
865
+ boundary = create_boundary
866
+ content_type = "#{content_type}; boundary=#{boundary}"
867
+ extheader = override_header(extheader, 'Content-Type', content_type)
868
+ end
869
+ end
870
+ elsif method == 'POST'
871
+ if file_in_form_data?(body)
872
+ boundary = create_boundary
873
+ content_type = "multipart/form-data; boundary=#{boundary}"
874
+ else
875
+ content_type = 'application/x-www-form-urlencoded'
876
+ end
877
+ extheader << ['Content-Type', content_type]
878
+ end
879
+ end
880
+ req = HTTP::Message.new_request(method, uri, query, body, boundary)
881
+ extheader.each do |key, value|
882
+ req.header.add(key, value)
883
+ end
884
+ if @cookie_manager && cookie = @cookie_manager.find(uri)
885
+ req.header.add('Cookie', cookie)
886
+ end
887
+ req
888
+ end
889
+
890
+ def create_boundary
891
+ Digest::SHA1.hexdigest(Time.now.to_s)
892
+ end
893
+
894
+ def file_in_form_data?(body)
895
+ HTTP::Message.multiparam_query?(body) &&
896
+ body.any? { |k, v| HTTP::Message.file?(v) }
897
+ end
898
+
899
+ def override_header(extheader, key, value)
900
+ result = []
901
+ extheader.each do |k, v|
902
+ if k.downcase == key.downcase
903
+ result << [key, value]
904
+ else
905
+ result << [k, v]
906
+ end
907
+ end
908
+ result
909
+ end
910
+
911
+ NO_PROXY_HOSTS = ['localhost']
912
+
913
+ def no_proxy?(uri)
914
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
915
+ return true
916
+ end
917
+ unless @no_proxy
918
+ return false
919
+ end
920
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
921
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
922
+ (!port || uri.port == port.to_i)
923
+ return true
924
+ end
925
+ end
926
+ false
927
+ end
928
+
929
+ def https?(uri)
930
+ uri.scheme.downcase == 'https'
931
+ end
932
+
933
+ # !! CAUTION !!
934
+ # Method 'do_get*' runs under MT conditon. Be careful to change.
935
+ def do_get_block(req, proxy, conn, &block)
936
+ @request_filter.each do |filter|
937
+ filter.filter_request(req)
938
+ end
939
+ if str = @test_loopback_response.shift
940
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
941
+ conn.push(HTTP::Message.new_response(str))
942
+ return
943
+ end
944
+ content = block ? nil : ''
945
+ res = HTTP::Message.new_response(content)
946
+ @debug_dev << "= Request\n\n" if @debug_dev
947
+ sess = @session_manager.query(req, proxy)
948
+ res.peer_cert = sess.ssl_peer_cert
949
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
950
+ do_get_header(req, res, sess)
951
+ conn.push(res)
952
+ sess.get_body do |part|
953
+ if block
954
+ block.call(res, part)
955
+ else
956
+ content << part
957
+ end
958
+ end
959
+ @session_manager.keep(sess) unless sess.closed?
960
+ commands = @request_filter.collect { |filter|
961
+ filter.filter_response(req, res)
962
+ }
963
+ if commands.find { |command| command == :retry }
964
+ raise RetryableResponse.new
965
+ end
966
+ end
967
+
968
+ def do_get_stream(req, proxy, conn)
969
+ @request_filter.each do |filter|
970
+ filter.filter_request(req)
971
+ end
972
+ if str = @test_loopback_response.shift
973
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
974
+ conn.push(HTTP::Message.new_response(StringIO.new(str)))
975
+ return
976
+ end
977
+ piper, pipew = IO.pipe
978
+ res = HTTP::Message.new_response(piper)
979
+ @debug_dev << "= Request\n\n" if @debug_dev
980
+ sess = @session_manager.query(req, proxy)
981
+ res.peer_cert = sess.ssl_peer_cert
982
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
983
+ do_get_header(req, res, sess)
984
+ conn.push(res)
985
+ sess.get_body do |part|
986
+ pipew.syswrite(part)
987
+ end
988
+ pipew.close
989
+ @session_manager.keep(sess) unless sess.closed?
990
+ commands = @request_filter.collect { |filter|
991
+ filter.filter_response(req, res)
992
+ }
993
+ # ignore commands (not retryable in async mode)
994
+ end
995
+
996
+ def do_get_header(req, res, sess)
997
+ res.version, res.status, res.reason, headers = sess.get_header
998
+ headers.each do |key, value|
999
+ res.header.add(key, value)
1000
+ end
1001
+ if @cookie_manager
1002
+ res.header['set-cookie'].each do |cookie|
1003
+ @cookie_manager.parse(cookie, req.header.request_uri)
1004
+ end
1005
+
1006
+ # Set the cookie header in the current request so cookies are present in the event we have to retry the
1007
+ # request, such as when responding to an authorization challenge.
1008
+ if (cookie = @cookie_manager.find(req.header.request_uri)) && req.header['Cookie'].empty?
1009
+ req.header.add('Cookie', cookie)
1010
+ end
1011
+ end
1012
+ end
1013
+
1014
+ def dump_dummy_request_response(req, res)
1015
+ @debug_dev << "= Dummy Request\n\n"
1016
+ @debug_dev << req
1017
+ @debug_dev << "\n\n= Dummy Response\n\n"
1018
+ @debug_dev << res
1019
+ end
1020
+ end