vmc 0.0.4 → 0.0.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.
@@ -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.5'
203
+ RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
204
+ /: (\S+) (\S+)/ =~ %q$Id: httpclient.rb 280 2009-06-02 15:44:28Z 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 = '', 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 = '', 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
+ if block
662
+ filtered_block = proc { |res, str|
663
+ block.call(str)
664
+ }
665
+ end
666
+ do_request(method, uri, query, body, extheader, &filtered_block)
667
+ end
668
+
669
+ # Sends HEAD request in async style. See request_async for arguments.
670
+ # It immediately returns a HTTPClient::Connection instance as a result.
671
+ def head_async(uri, query = nil, extheader = {})
672
+ request_async(:head, uri, query, nil, extheader)
673
+ end
674
+
675
+ # Sends GET request in async style. See request_async for arguments.
676
+ # It immediately returns a HTTPClient::Connection instance as a result.
677
+ def get_async(uri, query = nil, extheader = {})
678
+ request_async(:get, uri, query, nil, extheader)
679
+ end
680
+
681
+ # Sends POST request in async style. See request_async for arguments.
682
+ # It immediately returns a HTTPClient::Connection instance as a result.
683
+ def post_async(uri, body = nil, extheader = {})
684
+ request_async(:post, uri, nil, body, extheader)
685
+ end
686
+
687
+ # Sends PUT request in async style. See request_async for arguments.
688
+ # It immediately returns a HTTPClient::Connection instance as a result.
689
+ def put_async(uri, body = nil, extheader = {})
690
+ request_async(:put, uri, nil, body, extheader)
691
+ end
692
+
693
+ # Sends DELETE request in async style. See request_async for arguments.
694
+ # It immediately returns a HTTPClient::Connection instance as a result.
695
+ def delete_async(uri, extheader = {})
696
+ request_async(:delete, uri, nil, nil, extheader)
697
+ end
698
+
699
+ # Sends OPTIONS request in async style. See request_async for arguments.
700
+ # It immediately returns a HTTPClient::Connection instance as a result.
701
+ def options_async(uri, extheader = {})
702
+ request_async(:options, uri, nil, nil, extheader)
703
+ end
704
+
705
+ # Sends PROPFIND request in async style. See request_async for arguments.
706
+ # It immediately returns a HTTPClient::Connection instance as a result.
707
+ def propfind_async(uri, extheader = PROPFIND_DEFAULT_EXTHEADER)
708
+ request_async(:propfind, uri, nil, nil, extheader)
709
+ end
710
+
711
+ # Sends PROPPATCH request in async style. See request_async for arguments.
712
+ # It immediately returns a HTTPClient::Connection instance as a result.
713
+ def proppatch_async(uri, body = nil, extheader = {})
714
+ request_async(:proppatch, uri, nil, body, extheader)
715
+ end
716
+
717
+ # Sends TRACE request in async style. See request_async for arguments.
718
+ # It immediately returns a HTTPClient::Connection instance as a result.
719
+ def trace_async(uri, query = nil, body = nil, extheader = {})
720
+ request_async(:trace, uri, query, body, extheader)
721
+ end
722
+
723
+ # Sends a request in async style. request method creates new Thread for
724
+ # HTTP connection and returns a HTTPClient::Connection instance immediately.
725
+ #
726
+ # Arguments definition is the same as request.
727
+ def request_async(method, uri, query = nil, body = nil, extheader = {})
728
+ uri = urify(uri)
729
+ do_request_async(method, uri, query, body, extheader)
730
+ end
731
+
732
+ # Resets internal session for the given URL. Keep-alive connection for the
733
+ # site (host-port pair) is disconnected if exists.
734
+ def reset(uri)
735
+ uri = urify(uri)
736
+ @session_manager.reset(uri)
737
+ end
738
+
739
+ # Resets all of internal sessions. Keep-alive connections are disconnected.
740
+ def reset_all
741
+ @session_manager.reset_all
742
+ end
743
+
744
+ private
745
+
746
+ class RetryableResponse < StandardError # :nodoc:
747
+ end
748
+
749
+ class KeepAliveDisconnected < StandardError # :nodoc:
750
+ end
751
+
752
+ def do_request(method, uri, query, body, extheader, &block)
753
+ conn = Connection.new
754
+ res = nil
755
+ if HTTP::Message.file?(body)
756
+ pos = body.pos rescue nil
757
+ end
758
+ retry_count = @session_manager.protocol_retry_count
759
+ proxy = no_proxy?(uri) ? nil : @proxy
760
+ while retry_count > 0
761
+ body.pos = pos if pos
762
+ req = create_request(method, uri, query, body, extheader)
763
+ begin
764
+ protect_keep_alive_disconnected do
765
+ do_get_block(req, proxy, conn, &block)
766
+ end
767
+ res = conn.pop
768
+ break
769
+ rescue RetryableResponse
770
+ res = conn.pop
771
+ retry_count -= 1
772
+ end
773
+ end
774
+ res
775
+ end
776
+
777
+ def do_request_async(method, uri, query, body, extheader)
778
+ conn = Connection.new
779
+ t = Thread.new(conn) { |tconn|
780
+ if HTTP::Message.file?(body)
781
+ pos = body.pos rescue nil
782
+ end
783
+ retry_count = @session_manager.protocol_retry_count
784
+ proxy = no_proxy?(uri) ? nil : @proxy
785
+ while retry_count > 0
786
+ body.pos = pos if pos
787
+ req = create_request(method, uri, query, body, extheader)
788
+ begin
789
+ protect_keep_alive_disconnected do
790
+ do_get_stream(req, proxy, tconn)
791
+ end
792
+ break
793
+ rescue RetryableResponse
794
+ retry_count -= 1
795
+ end
796
+ end
797
+ }
798
+ conn.async_thread = t
799
+ conn
800
+ end
801
+
802
+ def load_environment
803
+ # http_proxy
804
+ if getenv('REQUEST_METHOD')
805
+ # HTTP_PROXY conflicts with the environment variable usage in CGI where
806
+ # HTTP_* is used for HTTP header information. Unlike open-uri, we
807
+ # simply ignore http_proxy in CGI env and use cgi_http_proxy instead.
808
+ self.proxy = getenv('cgi_http_proxy')
809
+ else
810
+ self.proxy = getenv('http_proxy')
811
+ end
812
+ # no_proxy
813
+ self.no_proxy = getenv('no_proxy')
814
+ end
815
+
816
+ def getenv(name)
817
+ ENV[name.downcase] || ENV[name.upcase]
818
+ end
819
+
820
+ def follow_redirect(method, uri, query, body, extheader, &block)
821
+ uri = urify(uri)
822
+ if block
823
+ filtered_block = proc { |r, str|
824
+ block.call(str) if HTTP::Status.successful?(r.status)
825
+ }
826
+ end
827
+ if HTTP::Message.file?(body)
828
+ pos = body.pos rescue nil
829
+ end
830
+ retry_number = 0
831
+ while retry_number < @follow_redirect_count
832
+ body.pos = pos if pos
833
+ res = do_request(method, uri, query, body, extheader, &filtered_block)
834
+ if HTTP::Status.successful?(res.status)
835
+ return res
836
+ elsif HTTP::Status.redirect?(res.status)
837
+ uri = urify(@redirect_uri_callback.call(uri, res))
838
+ retry_number += 1
839
+ else
840
+ raise BadResponseError.new("unexpected response: #{res.header.inspect}", res)
841
+ end
842
+ end
843
+ raise BadResponseError.new("retry count exceeded", res)
844
+ end
845
+
846
+ def protect_keep_alive_disconnected
847
+ begin
848
+ yield
849
+ rescue KeepAliveDisconnected
850
+ yield
851
+ end
852
+ end
853
+
854
+ def create_request(method, uri, query, body, extheader)
855
+ method = method.to_s.upcase
856
+ if extheader.is_a?(Hash)
857
+ extheader = extheader.to_a
858
+ else
859
+ extheader = extheader.dup
860
+ end
861
+ boundary = nil
862
+ if body
863
+ dummy, content_type = extheader.find { |key, value|
864
+ key.downcase == 'content-type'
865
+ }
866
+ if content_type
867
+ if /\Amultipart/ =~ content_type
868
+ if content_type =~ /boundary=(.+)\z/
869
+ boundary = $1
870
+ else
871
+ boundary = create_boundary
872
+ content_type = "#{content_type}; boundary=#{boundary}"
873
+ extheader = override_header(extheader, 'Content-Type', content_type)
874
+ end
875
+ end
876
+ elsif method == 'POST'
877
+ if file_in_form_data?(body)
878
+ boundary = create_boundary
879
+ content_type = "multipart/form-data; boundary=#{boundary}"
880
+ else
881
+ content_type = 'application/x-www-form-urlencoded'
882
+ end
883
+ extheader << ['Content-Type', content_type]
884
+ end
885
+ end
886
+ req = HTTP::Message.new_request(method, uri, query, body, boundary)
887
+ extheader.each do |key, value|
888
+ req.header.add(key, value)
889
+ end
890
+ if @cookie_manager && cookie = @cookie_manager.find(uri)
891
+ req.header.add('Cookie', cookie)
892
+ end
893
+ req
894
+ end
895
+
896
+ def create_boundary
897
+ Digest::SHA1.hexdigest(Time.now.to_s)
898
+ end
899
+
900
+ def file_in_form_data?(body)
901
+ HTTP::Message.multiparam_query?(body) &&
902
+ body.any? { |k, v| HTTP::Message.file?(v) }
903
+ end
904
+
905
+ def override_header(extheader, key, value)
906
+ result = []
907
+ extheader.each do |k, v|
908
+ if k.downcase == key.downcase
909
+ result << [key, value]
910
+ else
911
+ result << [k, v]
912
+ end
913
+ end
914
+ result
915
+ end
916
+
917
+ NO_PROXY_HOSTS = ['localhost']
918
+
919
+ def no_proxy?(uri)
920
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
921
+ return true
922
+ end
923
+ unless @no_proxy
924
+ return false
925
+ end
926
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
927
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
928
+ (!port || uri.port == port.to_i)
929
+ return true
930
+ end
931
+ end
932
+ false
933
+ end
934
+
935
+ def https?(uri)
936
+ uri.scheme.downcase == 'https'
937
+ end
938
+
939
+ # !! CAUTION !!
940
+ # Method 'do_get*' runs under MT conditon. Be careful to change.
941
+ def do_get_block(req, proxy, conn, &block)
942
+ @request_filter.each do |filter|
943
+ filter.filter_request(req)
944
+ end
945
+ if str = @test_loopback_response.shift
946
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
947
+ conn.push(HTTP::Message.new_response(str))
948
+ return
949
+ end
950
+ content = block ? nil : ''
951
+ res = HTTP::Message.new_response(content)
952
+ @debug_dev << "= Request\n\n" if @debug_dev
953
+ sess = @session_manager.query(req, proxy)
954
+ res.peer_cert = sess.ssl_peer_cert
955
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
956
+ do_get_header(req, res, sess)
957
+ conn.push(res)
958
+ sess.get_body do |part|
959
+ if block
960
+ block.call(res, part)
961
+ else
962
+ content << part
963
+ end
964
+ end
965
+ @session_manager.keep(sess) unless sess.closed?
966
+ commands = @request_filter.collect { |filter|
967
+ filter.filter_response(req, res)
968
+ }
969
+ if commands.find { |command| command == :retry }
970
+ raise RetryableResponse.new
971
+ end
972
+ end
973
+
974
+ def do_get_stream(req, proxy, conn)
975
+ @request_filter.each do |filter|
976
+ filter.filter_request(req)
977
+ end
978
+ if str = @test_loopback_response.shift
979
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
980
+ conn.push(HTTP::Message.new_response(StringIO.new(str)))
981
+ return
982
+ end
983
+ piper, pipew = IO.pipe
984
+ res = HTTP::Message.new_response(piper)
985
+ @debug_dev << "= Request\n\n" if @debug_dev
986
+ sess = @session_manager.query(req, proxy)
987
+ res.peer_cert = sess.ssl_peer_cert
988
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
989
+ do_get_header(req, res, sess)
990
+ conn.push(res)
991
+ sess.get_body do |part|
992
+ pipew.syswrite(part)
993
+ end
994
+ pipew.close
995
+ @session_manager.keep(sess) unless sess.closed?
996
+ commands = @request_filter.collect { |filter|
997
+ filter.filter_response(req, res)
998
+ }
999
+ # ignore commands (not retryable in async mode)
1000
+ end
1001
+
1002
+ def do_get_header(req, res, sess)
1003
+ res.version, res.status, res.reason, headers = sess.get_header
1004
+ headers.each do |key, value|
1005
+ res.header.add(key, value)
1006
+ end
1007
+ if @cookie_manager
1008
+ res.header['set-cookie'].each do |cookie|
1009
+ @cookie_manager.parse(cookie, req.header.request_uri)
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