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