httpclient 2.1.4 → 2.1.5

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.
@@ -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