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