~ubuntu-branches/ubuntu/trusty/ruby1.9/trusty

« back to all changes in this revision

Viewing changes to lib/rubygems/open-uri.rb

  • Committer: Bazaar Package Importer
  • Author(s): Stephan Hermann
  • Date: 2008-01-24 11:42:29 UTC
  • mfrom: (1.1.9 upstream)
  • Revision ID: james.westby@ubuntu.com-20080124114229-jw2f87rdxlq6gp11
Tags: 1.9.0.0-2ubuntu1
* Merge from debian unstable, remaining changes:
  - Robustify check for target_os, fixing build failure on lpia.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
require 'uri'
 
2
require 'stringio'
 
3
require 'time'
 
4
 
 
5
# :stopdoc:
 
6
module Kernel
 
7
  private
 
8
  alias rubygems_open_uri_original_open open # :nodoc:
 
9
 
 
10
  # makes possible to open various resources including URIs.
 
11
  # If the first argument respond to `open' method,
 
12
  # the method is called with the rest arguments.
 
13
  #
 
14
  # If the first argument is a string which begins with xxx://,
 
15
  # it is parsed by URI.parse.  If the parsed object respond to `open' method,
 
16
  # the method is called with the rest arguments.
 
17
  #
 
18
  # Otherwise original open is called.
 
19
  #
 
20
  # Since open-uri.rb provides URI::HTTP#open, URI::HTTPS#open and
 
21
  # URI::FTP#open,
 
22
  # Kernel[#.]open can accepts such URIs and strings which begins with
 
23
  # http://, https:// and ftp://.
 
24
  # In these case, the opened file object is extended by OpenURI::Meta.
 
25
  def open(name, *rest, &block) # :doc:
 
26
    if name.respond_to?(:open)
 
27
      name.open(*rest, &block)
 
28
    elsif name.respond_to?(:to_str) &&
 
29
          %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
 
30
          (uri = URI.parse(name)).respond_to?(:open)
 
31
      uri.open(*rest, &block)
 
32
    else
 
33
      rubygems_open_uri_original_open(name, *rest, &block)
 
34
    end
 
35
  end
 
36
  module_function :open
 
37
end
 
38
 
 
39
# OpenURI is an easy-to-use wrapper for net/http, net/https and net/ftp.
 
40
#
 
41
#== Example
 
42
#
 
43
# It is possible to open http/https/ftp URL as usual like opening a file:
 
44
#
 
45
#   open("http://www.ruby-lang.org/") {|f|
 
46
#     f.each_line {|line| p line}
 
47
#   }
 
48
#
 
49
# The opened file has several methods for meta information as follows since
 
50
# it is extended by OpenURI::Meta.
 
51
#
 
52
#   open("http://www.ruby-lang.org/en") {|f|
 
53
#     f.each_line {|line| p line}
 
54
#     p f.base_uri         # <URI::HTTP:0x40e6ef2 URL:http://www.ruby-lang.org/en/>
 
55
#     p f.content_type     # "text/html"
 
56
#     p f.charset          # "iso-8859-1"
 
57
#     p f.content_encoding # []
 
58
#     p f.last_modified    # Thu Dec 05 02:45:02 UTC 2002
 
59
#   }
 
60
#
 
61
# Additional header fields can be specified by an optional hash argument.
 
62
#
 
63
#   open("http://www.ruby-lang.org/en/",
 
64
#     "User-Agent" => "Ruby/#{RUBY_VERSION}",
 
65
#     "From" => "foo@bar.invalid",
 
66
#     "Referer" => "http://www.ruby-lang.org/") {|f|
 
67
#     # ...
 
68
#   }
 
69
#
 
70
# The environment variables such as http_proxy, https_proxy and ftp_proxy
 
71
# are in effect by default.  :proxy => nil disables proxy.
 
72
#
 
73
#   open("http://www.ruby-lang.org/en/raa.html", :proxy => nil) {|f|
 
74
#     # ...
 
75
#   }
 
76
#
 
77
# URI objects can be opened in a similar way.
 
78
#
 
79
#   uri = URI.parse("http://www.ruby-lang.org/en/")
 
80
#   uri.open {|f|
 
81
#     # ...
 
82
#   }
 
83
#
 
84
# URI objects can be read directly. The returned string is also extended by
 
85
# OpenURI::Meta.
 
86
#
 
87
#   str = uri.read
 
88
#   p str.base_uri
 
89
#
 
90
# Author:: Tanaka Akira <akr@m17n.org>
 
91
 
 
92
module OpenURI
 
93
  Options = {
 
94
    :proxy => true,
 
95
    :proxy_http_basic_authentication => true,
 
96
    :progress_proc => true,
 
97
    :content_length_proc => true,
 
98
    :http_basic_authentication => true,
 
99
    :read_timeout => true,
 
100
    :ssl_ca_cert => nil,
 
101
    :ssl_verify_mode => nil,
 
102
  }
 
103
 
 
104
  def OpenURI.check_options(options) # :nodoc:
 
105
    options.each {|k, v|
 
106
      next unless Symbol === k
 
107
      unless Options.include? k
 
108
        raise ArgumentError, "unrecognized option: #{k}"
 
109
      end
 
110
    }
 
111
  end
 
112
 
 
113
  def OpenURI.scan_open_optional_arguments(*rest) # :nodoc:
 
114
    if !rest.empty? && (String === rest.first || Integer === rest.first)
 
115
      mode = rest.shift
 
116
      if !rest.empty? && Integer === rest.first
 
117
        perm = rest.shift
 
118
      end
 
119
    end
 
120
    return mode, perm, rest
 
121
  end
 
122
 
 
123
  def OpenURI.open_uri(name, *rest) # :nodoc:
 
124
    uri = URI::Generic === name ? name : URI.parse(name)
 
125
    mode, perm, rest = OpenURI.scan_open_optional_arguments(*rest)
 
126
    options = rest.shift if !rest.empty? && Hash === rest.first
 
127
    raise ArgumentError.new("extra arguments") if !rest.empty?
 
128
    options ||= {}
 
129
    OpenURI.check_options(options)
 
130
 
 
131
    unless mode == nil ||
 
132
           mode == 'r' || mode == 'rb' ||
 
133
           mode == File::RDONLY
 
134
      raise ArgumentError.new("invalid access mode #{mode} (#{uri.class} resource is read only.)")
 
135
    end
 
136
 
 
137
    io = open_loop(uri, options)
 
138
    if block_given?
 
139
      begin
 
140
        yield io
 
141
      ensure
 
142
        io.close
 
143
      end
 
144
    else
 
145
      io
 
146
    end
 
147
  end
 
148
 
 
149
  def OpenURI.open_loop(uri, options) # :nodoc:
 
150
    proxy_opts = []
 
151
    proxy_opts << :proxy_http_basic_authentication if options.include? :proxy_http_basic_authentication
 
152
    proxy_opts << :proxy if options.include? :proxy
 
153
    proxy_opts.compact!
 
154
    if 1 < proxy_opts.length
 
155
      raise ArgumentError, "multiple proxy options specified"
 
156
    end
 
157
    case proxy_opts.first
 
158
    when :proxy_http_basic_authentication
 
159
      opt_proxy, proxy_user, proxy_pass = options.fetch(:proxy_http_basic_authentication)
 
160
      proxy_user = proxy_user.to_str
 
161
      proxy_pass = proxy_pass.to_str
 
162
      if opt_proxy == true
 
163
        raise ArgumentError.new("Invalid authenticated proxy option: #{options[:proxy_http_basic_authentication].inspect}")
 
164
      end
 
165
    when :proxy
 
166
      opt_proxy = options.fetch(:proxy)
 
167
      proxy_user = nil
 
168
      proxy_pass = nil
 
169
    when nil
 
170
      opt_proxy = true
 
171
      proxy_user = nil
 
172
      proxy_pass = nil
 
173
    end
 
174
    case opt_proxy
 
175
    when true
 
176
      find_proxy = lambda {|u| pxy = u.find_proxy; pxy ? [pxy, nil, nil] : nil}
 
177
    when nil, false
 
178
      find_proxy = lambda {|u| nil}
 
179
    when String
 
180
      opt_proxy = URI.parse(opt_proxy)
 
181
      find_proxy = lambda {|u| [opt_proxy, proxy_user, proxy_pass]}
 
182
    when URI::Generic
 
183
      find_proxy = lambda {|u| [opt_proxy, proxy_user, proxy_pass]}
 
184
    else
 
185
      raise ArgumentError.new("Invalid proxy option: #{opt_proxy}")
 
186
    end
 
187
 
 
188
    uri_set = {}
 
189
    buf = nil
 
190
    while true
 
191
      redirect = catch(:open_uri_redirect) {
 
192
        buf = Buffer.new
 
193
        uri.buffer_open(buf, find_proxy.call(uri), options)
 
194
        nil
 
195
      }
 
196
      if redirect
 
197
        if redirect.relative?
 
198
          # Although it violates RFC2616, Location: field may have relative
 
199
          # URI.  It is converted to absolute URI using uri as a base URI.
 
200
          redirect = uri + redirect
 
201
        end
 
202
        unless OpenURI.redirectable?(uri, redirect)
 
203
          raise "redirection forbidden: #{uri} -> #{redirect}"
 
204
        end
 
205
        if options.include? :http_basic_authentication
 
206
          # send authentication only for the URI directly specified.
 
207
          options = options.dup
 
208
          options.delete :http_basic_authentication
 
209
        end
 
210
        uri = redirect
 
211
        raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s
 
212
        uri_set[uri.to_s] = true
 
213
      else
 
214
        break
 
215
      end
 
216
    end
 
217
    io = buf.io
 
218
    io.base_uri = uri
 
219
    io
 
220
  end
 
221
 
 
222
  def OpenURI.redirectable?(uri1, uri2) # :nodoc:
 
223
    # This test is intended to forbid a redirection from http://... to
 
224
    # file:///etc/passwd.
 
225
    # However this is ad hoc.  It should be extensible/configurable.
 
226
    uri1.scheme.downcase == uri2.scheme.downcase ||
 
227
    (/\A(?:http|ftp)\z/i =~ uri1.scheme && /\A(?:http|ftp)\z/i =~ uri2.scheme)
 
228
  end
 
229
 
 
230
  def OpenURI.open_http(buf, target, proxy, options) # :nodoc:
 
231
    if proxy
 
232
      proxy_uri, proxy_user, proxy_pass = proxy
 
233
      raise "Non-HTTP proxy URI: #{proxy_uri}" if proxy_uri.class != URI::HTTP
 
234
    end
 
235
 
 
236
    if target.userinfo && "1.9.0" <= RUBY_VERSION
 
237
      # don't raise for 1.8 because compatibility.
 
238
      raise ArgumentError, "userinfo not supported.  [RFC3986]"
 
239
    end
 
240
 
 
241
    header = {}
 
242
    options.each {|k, v| header[k] = v if String === k }
 
243
 
 
244
    require 'net/http'
 
245
    klass = Net::HTTP
 
246
    if URI::HTTP === target
 
247
      # HTTP or HTTPS
 
248
      if proxy
 
249
        if proxy_user && proxy_pass
 
250
          klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_user, proxy_pass)
 
251
        else
 
252
          klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port)
 
253
        end
 
254
      end
 
255
      target_host = target.host
 
256
      target_port = target.port
 
257
      request_uri = target.request_uri
 
258
    else
 
259
      # FTP over HTTP proxy
 
260
      target_host = proxy_uri.host
 
261
      target_port = proxy_uri.port
 
262
      request_uri = target.to_s
 
263
      if proxy_user && proxy_pass
 
264
        header["Proxy-Authorization"] = 'Basic ' + ["#{proxy_user}:#{proxy_pass}"].pack('m').delete("\r\n")
 
265
      end
 
266
    end
 
267
 
 
268
    http = klass.new(target_host, target_port)
 
269
    if target.class == URI::HTTPS
 
270
      require 'net/https'
 
271
      http.use_ssl = true
 
272
      http.verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
 
273
      store = OpenSSL::X509::Store.new
 
274
      if options[:ssl_ca_cert]
 
275
        if File.directory? options[:ssl_ca_cert]
 
276
          store.add_path options[:ssl_ca_cert]
 
277
        else
 
278
          store.add_file options[:ssl_ca_cert]
 
279
        end
 
280
      else
 
281
        store.set_default_paths
 
282
      end
 
283
      store.set_default_paths
 
284
      http.cert_store = store
 
285
    end
 
286
    if options.include? :read_timeout
 
287
      http.read_timeout = options[:read_timeout]
 
288
    end
 
289
 
 
290
    resp = nil
 
291
    http.start {
 
292
      if target.class == URI::HTTPS
 
293
        # xxx: information hiding violation
 
294
        sock = http.instance_variable_get(:@socket)
 
295
        if sock.respond_to?(:io)
 
296
          sock = sock.io # 1.9
 
297
        else
 
298
          sock = sock.instance_variable_get(:@socket) # 1.8
 
299
        end
 
300
        sock.post_connection_check(target_host)
 
301
      end
 
302
      req = Net::HTTP::Get.new(request_uri, header)
 
303
      if options.include? :http_basic_authentication
 
304
        user, pass = options[:http_basic_authentication]
 
305
        req.basic_auth user, pass
 
306
      end
 
307
      http.request(req) {|response|
 
308
        resp = response
 
309
        if options[:content_length_proc] && Net::HTTPSuccess === resp
 
310
          if resp.key?('Content-Length')
 
311
            options[:content_length_proc].call(resp['Content-Length'].to_i)
 
312
          else
 
313
            options[:content_length_proc].call(nil)
 
314
          end
 
315
        end
 
316
        resp.read_body {|str|
 
317
          buf << str
 
318
          if options[:progress_proc] && Net::HTTPSuccess === resp
 
319
            options[:progress_proc].call(buf.size)
 
320
          end
 
321
        }
 
322
      }
 
323
    }
 
324
    io = buf.io
 
325
    io.rewind
 
326
    io.status = [resp.code, resp.message]
 
327
    resp.each {|name,value| buf.io.meta_add_field name, value }
 
328
    case resp
 
329
    when Net::HTTPSuccess
 
330
    when Net::HTTPMovedPermanently, # 301
 
331
         Net::HTTPFound, # 302
 
332
         Net::HTTPSeeOther, # 303
 
333
         Net::HTTPTemporaryRedirect # 307
 
334
      throw :open_uri_redirect, URI.parse(resp['location'])
 
335
    else
 
336
      raise OpenURI::HTTPError.new(io.status.join(' '), io)
 
337
    end
 
338
  end
 
339
 
 
340
  class HTTPError < StandardError
 
341
    def initialize(message, io)
 
342
      super(message)
 
343
      @io = io
 
344
    end
 
345
    attr_reader :io
 
346
  end
 
347
 
 
348
  class Buffer # :nodoc:
 
349
    def initialize
 
350
      @io = StringIO.new
 
351
      @size = 0
 
352
    end
 
353
    attr_reader :size
 
354
 
 
355
    StringMax = 10240
 
356
    def <<(str)
 
357
      @io << str
 
358
      @size += str.length
 
359
      if StringIO === @io && StringMax < @size
 
360
        require 'tempfile'
 
361
        io = Tempfile.new('open-uri')
 
362
        io.binmode
 
363
        Meta.init io, @io if @io.respond_to? :meta
 
364
        io << @io.string
 
365
        @io = io
 
366
      end
 
367
    end
 
368
 
 
369
    def io
 
370
      Meta.init @io unless @io.respond_to? :meta
 
371
      @io
 
372
    end
 
373
  end
 
374
 
 
375
  # Mixin for holding meta-information.
 
376
  module Meta
 
377
    def Meta.init(obj, src=nil) # :nodoc:
 
378
      obj.extend Meta
 
379
      obj.instance_eval {
 
380
        @base_uri = nil
 
381
        @meta = {}
 
382
      }
 
383
      if src
 
384
        obj.status = src.status
 
385
        obj.base_uri = src.base_uri
 
386
        src.meta.each {|name, value|
 
387
          obj.meta_add_field(name, value)
 
388
        }
 
389
      end
 
390
    end
 
391
 
 
392
    # returns an Array which consists status code and message.
 
393
    attr_accessor :status
 
394
 
 
395
    # returns a URI which is base of relative URIs in the data.
 
396
    # It may differ from the URI supplied by a user because redirection.
 
397
    attr_accessor :base_uri
 
398
 
 
399
    # returns a Hash which represents header fields.
 
400
    # The Hash keys are downcased for canonicalization.
 
401
    attr_reader :meta
 
402
 
 
403
    def meta_add_field(name, value) # :nodoc:
 
404
      @meta[name.downcase] = value
 
405
    end
 
406
 
 
407
    # returns a Time which represents Last-Modified field.
 
408
    def last_modified
 
409
      if v = @meta['last-modified']
 
410
        Time.httpdate(v)
 
411
      else
 
412
        nil
 
413
      end
 
414
    end
 
415
 
 
416
    RE_LWS = /[\r\n\t ]+/n
 
417
    RE_TOKEN = %r{[^\x00- ()<>@,;:\\"/\[\]?={}\x7f]+}n
 
418
    RE_QUOTED_STRING = %r{"(?:[\r\n\t !#-\[\]-~\x80-\xff]|\\[\x00-\x7f])*"}n
 
419
    RE_PARAMETERS = %r{(?:;#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?=#{RE_LWS}?(?:#{RE_TOKEN}|#{RE_QUOTED_STRING})#{RE_LWS}?)*}n
 
420
 
 
421
    def content_type_parse # :nodoc:
 
422
      v = @meta['content-type']
 
423
      # The last (?:;#{RE_LWS}?)? matches extra ";" which violates RFC2045.
 
424
      if v && %r{\A#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?/(#{RE_TOKEN})#{RE_LWS}?(#{RE_PARAMETERS})(?:;#{RE_LWS}?)?\z}no =~ v
 
425
        type = $1.downcase
 
426
        subtype = $2.downcase
 
427
        parameters = []
 
428
        $3.scan(/;#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?=#{RE_LWS}?(?:(#{RE_TOKEN})|(#{RE_QUOTED_STRING}))/no) {|att, val, qval|
 
429
          val = qval.gsub(/[\r\n\t !#-\[\]-~\x80-\xff]+|(\\[\x00-\x7f])/) { $1 ? $1[1,1] : $& } if qval
 
430
          parameters << [att.downcase, val]
 
431
        }
 
432
        ["#{type}/#{subtype}", *parameters]
 
433
      else
 
434
        nil
 
435
      end
 
436
    end
 
437
 
 
438
    # returns "type/subtype" which is MIME Content-Type.
 
439
    # It is downcased for canonicalization.
 
440
    # Content-Type parameters are stripped.
 
441
    def content_type
 
442
      type, *parameters = content_type_parse
 
443
      type || 'application/octet-stream'
 
444
    end
 
445
 
 
446
    # returns a charset parameter in Content-Type field.
 
447
    # It is downcased for canonicalization.
 
448
    #
 
449
    # If charset parameter is not given but a block is given,
 
450
    # the block is called and its result is returned.
 
451
    # It can be used to guess charset.
 
452
    #
 
453
    # If charset parameter and block is not given,
 
454
    # nil is returned except text type in HTTP.
 
455
    # In that case, "iso-8859-1" is returned as defined by RFC2616 3.7.1.
 
456
    def charset
 
457
      type, *parameters = content_type_parse
 
458
      if pair = parameters.assoc('charset')
 
459
        pair.last.downcase
 
460
      elsif block_given?
 
461
        yield
 
462
      elsif type && %r{\Atext/} =~ type &&
 
463
            @base_uri && /\Ahttp\z/i =~ @base_uri.scheme
 
464
        "iso-8859-1" # RFC2616 3.7.1
 
465
      else
 
466
        nil
 
467
      end
 
468
    end
 
469
 
 
470
    # returns a list of encodings in Content-Encoding field
 
471
    # as an Array of String.
 
472
    # The encodings are downcased for canonicalization.
 
473
    def content_encoding
 
474
      v = @meta['content-encoding']
 
475
      if v && %r{\A#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?(?:,#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?)*}o =~ v
 
476
        v.scan(RE_TOKEN).map {|content_coding| content_coding.downcase}
 
477
      else
 
478
        []
 
479
      end
 
480
    end
 
481
  end
 
482
 
 
483
  # Mixin for HTTP and FTP URIs.
 
484
  module OpenRead
 
485
    # OpenURI::OpenRead#open provides `open' for URI::HTTP and URI::FTP.
 
486
    #
 
487
    # OpenURI::OpenRead#open takes optional 3 arguments as:
 
488
    # OpenURI::OpenRead#open([mode [, perm]] [, options]) [{|io| ... }]
 
489
    #
 
490
    # `mode', `perm' is same as Kernel#open.
 
491
    #
 
492
    # However, `mode' must be read mode because OpenURI::OpenRead#open doesn't
 
493
    # support write mode (yet).
 
494
    # Also `perm' is just ignored because it is meaningful only for file
 
495
    # creation.
 
496
    #
 
497
    # `options' must be a hash.
 
498
    #
 
499
    # Each pairs which key is a string in the hash specify a extra header
 
500
    # field for HTTP.
 
501
    # I.e. it is ignored for FTP without HTTP proxy.
 
502
    #
 
503
    # The hash may include other options which key is a symbol:
 
504
    #
 
505
    # [:proxy]
 
506
    #  Synopsis:
 
507
    #    :proxy => "http://proxy.foo.com:8000/"
 
508
    #    :proxy => URI.parse("http://proxy.foo.com:8000/")
 
509
    #    :proxy => true
 
510
    #    :proxy => false
 
511
    #    :proxy => nil
 
512
    #   
 
513
    #  If :proxy option is specified, the value should be String, URI,
 
514
    #  boolean or nil.
 
515
    #  When String or URI is given, it is treated as proxy URI.
 
516
    #  When true is given or the option itself is not specified,
 
517
    #  environment variable `scheme_proxy' is examined.
 
518
    #  `scheme' is replaced by `http', `https' or `ftp'.
 
519
    #  When false or nil is given, the environment variables are ignored and
 
520
    #  connection will be made to a server directly.
 
521
    #
 
522
    # [:proxy_http_basic_authentication]
 
523
    #  Synopsis:
 
524
    #    :proxy_http_basic_authentication => ["http://proxy.foo.com:8000/", "proxy-user", "proxy-password"]
 
525
    #    :proxy_http_basic_authentication => [URI.parse("http://proxy.foo.com:8000/"), "proxy-user", "proxy-password"]
 
526
    #   
 
527
    #  If :proxy option is specified, the value should be an Array with 3 elements.
 
528
    #  It should contain a proxy URI, a proxy user name and a proxy password.
 
529
    #  The proxy URI should be a String, an URI or nil.
 
530
    #  The proxy user name and password should be a String.
 
531
    #
 
532
    #  If nil is given for the proxy URI, this option is just ignored.
 
533
    #
 
534
    #  If :proxy and :proxy_http_basic_authentication is specified, 
 
535
    #  ArgumentError is raised.
 
536
    #
 
537
    # [:http_basic_authentication]
 
538
    #  Synopsis:
 
539
    #    :http_basic_authentication=>[user, password]
 
540
    #
 
541
    #  If :http_basic_authentication is specified,
 
542
    #  the value should be an array which contains 2 strings:
 
543
    #  username and password.
 
544
    #  It is used for HTTP Basic authentication defined by RFC 2617.
 
545
    #
 
546
    # [:content_length_proc]
 
547
    #  Synopsis:
 
548
    #    :content_length_proc => lambda {|content_length| ... }
 
549
    # 
 
550
    #  If :content_length_proc option is specified, the option value procedure
 
551
    #  is called before actual transfer is started.
 
552
    #  It takes one argument which is expected content length in bytes.
 
553
    # 
 
554
    #  If two or more transfer is done by HTTP redirection, the procedure
 
555
    #  is called only one for a last transfer.
 
556
    # 
 
557
    #  When expected content length is unknown, the procedure is called with
 
558
    #  nil.
 
559
    #  It is happen when HTTP response has no Content-Length header.
 
560
    #
 
561
    # [:progress_proc]
 
562
    #  Synopsis:
 
563
    #    :progress_proc => lambda {|size| ...}
 
564
    #
 
565
    #  If :progress_proc option is specified, the proc is called with one
 
566
    #  argument each time when `open' gets content fragment from network.
 
567
    #  The argument `size' `size' is a accumulated transfered size in bytes.
 
568
    #
 
569
    #  If two or more transfer is done by HTTP redirection, the procedure
 
570
    #  is called only one for a last transfer.
 
571
    #
 
572
    #  :progress_proc and :content_length_proc are intended to be used for
 
573
    #  progress bar.
 
574
    #  For example, it can be implemented as follows using Ruby/ProgressBar.
 
575
    #
 
576
    #    pbar = nil
 
577
    #    open("http://...",
 
578
    #      :content_length_proc => lambda {|t|
 
579
    #        if t && 0 < t
 
580
    #          pbar = ProgressBar.new("...", t)
 
581
    #          pbar.file_transfer_mode
 
582
    #        end
 
583
    #      },
 
584
    #      :progress_proc => lambda {|s|
 
585
    #        pbar.set s if pbar
 
586
    #      }) {|f| ... }
 
587
    #
 
588
    # [:read_timeout]
 
589
    #  Synopsis:
 
590
    #    :read_timeout=>nil     (no timeout)
 
591
    #    :read_timeout=>10      (10 second)
 
592
    #
 
593
    #  :read_timeout option specifies a timeout of read for http connections.
 
594
    #
 
595
    # [:ssl_ca_cert]
 
596
    #  Synopsis:
 
597
    #    :ssl_ca_cert=>filename
 
598
    #
 
599
    #  :ssl_ca_cert is used to specify CA certificate for SSL.
 
600
    #  If it is given, default certificates are not used.
 
601
    #
 
602
    # [:ssl_verify_mode]
 
603
    #  Synopsis:
 
604
    #    :ssl_verify_mode=>mode
 
605
    #
 
606
    #  :ssl_verify_mode is used to specify openssl verify mode.
 
607
    #
 
608
    # OpenURI::OpenRead#open returns an IO like object if block is not given.
 
609
    # Otherwise it yields the IO object and return the value of the block.
 
610
    # The IO object is extended with OpenURI::Meta.
 
611
    def open(*rest, &block)
 
612
      OpenURI.open_uri(self, *rest, &block)
 
613
    end
 
614
 
 
615
    # OpenURI::OpenRead#read([options]) reads a content referenced by self and
 
616
    # returns the content as string.
 
617
    # The string is extended with OpenURI::Meta.
 
618
    # The argument `options' is same as OpenURI::OpenRead#open.
 
619
    def read(options={})
 
620
      self.open(options) {|f|
 
621
        str = f.read
 
622
        Meta.init str, f
 
623
        str
 
624
      }
 
625
    end
 
626
  end
 
627
end
 
628
 
 
629
module URI
 
630
  class Generic
 
631
    # returns a proxy URI.
 
632
    # The proxy URI is obtained from environment variables such as http_proxy,
 
633
    # ftp_proxy, no_proxy, etc.
 
634
    # If there is no proper proxy, nil is returned.
 
635
    #
 
636
    # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
 
637
    # are examined too.
 
638
    #
 
639
    # But http_proxy and HTTP_PROXY is treated specially under CGI environment.
 
640
    # It's because HTTP_PROXY may be set by Proxy: header.
 
641
    # So HTTP_PROXY is not used.
 
642
    # http_proxy is not used too if the variable is case insensitive.
 
643
    # CGI_HTTP_PROXY can be used instead.
 
644
    def find_proxy
 
645
      name = self.scheme.downcase + '_proxy'
 
646
      proxy_uri = nil
 
647
      if name == 'http_proxy' && ENV.include?('REQUEST_METHOD') # CGI?
 
648
        # HTTP_PROXY conflicts with *_proxy for proxy settings and
 
649
        # HTTP_* for header information in CGI.
 
650
        # So it should be careful to use it.
 
651
        pairs = ENV.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
 
652
        case pairs.length
 
653
        when 0 # no proxy setting anyway.
 
654
          proxy_uri = nil
 
655
        when 1
 
656
          k, v = pairs.shift
 
657
          if k == 'http_proxy' && ENV[k.upcase] == nil
 
658
            # http_proxy is safe to use because ENV is case sensitive.
 
659
            proxy_uri = ENV[name]
 
660
          else
 
661
            proxy_uri = nil
 
662
          end
 
663
        else # http_proxy is safe to use because ENV is case sensitive.
 
664
          proxy_uri = ENV[name]
 
665
        end
 
666
        if !proxy_uri
 
667
          # Use CGI_HTTP_PROXY.  cf. libwww-perl.
 
668
          proxy_uri = ENV["CGI_#{name.upcase}"]
 
669
        end
 
670
      elsif name == 'http_proxy'
 
671
        unless proxy_uri = ENV[name]
 
672
          if proxy_uri = ENV[name.upcase]
 
673
            warn 'The environment variable HTTP_PROXY is discouraged.  Use http_proxy.'
 
674
          end
 
675
        end
 
676
      else
 
677
        proxy_uri = ENV[name] || ENV[name.upcase]
 
678
      end
 
679
 
 
680
      if proxy_uri && self.host
 
681
        require 'socket'
 
682
        begin
 
683
          addr = IPSocket.getaddress(self.host)
 
684
          proxy_uri = nil if /\A127\.|\A::1\z/ =~ addr
 
685
        rescue SocketError
 
686
        end
 
687
      end
 
688
 
 
689
      if proxy_uri
 
690
        proxy_uri = URI.parse(proxy_uri)
 
691
        name = 'no_proxy'
 
692
        if no_proxy = ENV[name] || ENV[name.upcase]
 
693
          no_proxy.scan(/([^:,]*)(?::(\d+))?/) {|host, port|
 
694
            if /(\A|\.)#{Regexp.quote host}\z/i =~ self.host &&
 
695
               (!port || self.port == port.to_i)
 
696
              proxy_uri = nil
 
697
              break
 
698
            end
 
699
          }
 
700
        end
 
701
        proxy_uri
 
702
      else
 
703
        nil
 
704
      end
 
705
    end
 
706
  end
 
707
 
 
708
  class HTTP
 
709
    def buffer_open(buf, proxy, options) # :nodoc:
 
710
      OpenURI.open_http(buf, self, proxy, options)
 
711
    end
 
712
 
 
713
    include OpenURI::OpenRead
 
714
  end
 
715
 
 
716
  class FTP
 
717
    def buffer_open(buf, proxy, options) # :nodoc:
 
718
      if proxy
 
719
        OpenURI.open_http(buf, self, proxy, options)
 
720
        return
 
721
      end
 
722
      require 'net/ftp'
 
723
 
 
724
      directories = self.path.split(%r{/}, -1)
 
725
      directories.shift if directories[0] == '' # strip a field before leading slash
 
726
      directories.each {|d|
 
727
        d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") }
 
728
      }
 
729
      unless filename = directories.pop
 
730
        raise ArgumentError, "no filename: #{self.inspect}"
 
731
      end
 
732
      directories.each {|d|
 
733
        if /[\r\n]/ =~ d
 
734
          raise ArgumentError, "invalid directory: #{d.inspect}"
 
735
        end
 
736
      }
 
737
      if /[\r\n]/ =~ filename
 
738
        raise ArgumentError, "invalid filename: #{filename.inspect}"
 
739
      end
 
740
      typecode = self.typecode
 
741
      if typecode && /\A[aid]\z/ !~ typecode
 
742
        raise ArgumentError, "invalid typecode: #{typecode.inspect}"
 
743
      end
 
744
 
 
745
      # The access sequence is defined by RFC 1738
 
746
      ftp = Net::FTP.open(self.host)
 
747
      # todo: extract user/passwd from .netrc.
 
748
      user = 'anonymous'
 
749
      passwd = nil
 
750
      user, passwd = self.userinfo.split(/:/) if self.userinfo
 
751
      ftp.login(user, passwd)
 
752
      directories.each {|cwd|
 
753
        ftp.voidcmd("CWD #{cwd}")
 
754
      }
 
755
      if typecode
 
756
        # xxx: typecode D is not handled.
 
757
        ftp.voidcmd("TYPE #{typecode.upcase}")
 
758
      end
 
759
      if options[:content_length_proc]
 
760
        options[:content_length_proc].call(ftp.size(filename))
 
761
      end
 
762
      ftp.retrbinary("RETR #{filename}", 4096) { |str|
 
763
        buf << str
 
764
        options[:progress_proc].call(buf.size) if options[:progress_proc]
 
765
      }
 
766
      ftp.close
 
767
      buf.io.rewind
 
768
    end
 
769
 
 
770
    include OpenURI::OpenRead
 
771
  end
 
772
end
 
773
# :startdoc: