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