├── .github ├── dependabot.yml └── workflows │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── BSDL ├── COPYING ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── net │ └── ftp.rb ├── net-ftp.gemspec └── test ├── lib └── helper.rb └── net ├── fixtures ├── Makefile ├── cacert.pem ├── dhparams.pem ├── server.crt └── server.key └── ftp ├── test_buffered_socket.rb ├── test_ftp.rb └── test_mlsx_entry.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/net-ftp' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/net-ftp 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | # Set up 26 | - name: Harden Runner 27 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 28 | with: 29 | egress-policy: audit 30 | 31 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 32 | 33 | - name: Set up Ruby 34 | uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 35 | with: 36 | bundler-cache: true 37 | ruby-version: ruby 38 | 39 | # Release 40 | - name: Publish to RubyGems 41 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 42 | 43 | - name: Create GitHub release 44 | run: | 45 | tag_name="$(git describe --tags --abbrev=0)" 46 | gh release create "${tag_name}" --verify-tag --draft --generate-notes 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: ubuntu 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 2.6 11 | 12 | build: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest, windows-latest ] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | - name: Install dependencies 27 | run: bundle install 28 | - name: Run test 29 | run: rake test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "test-unit" 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | All the files in this distribution are covered under either the Ruby license or 2 | the BSD-2-Clause license (see the file COPYING). 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::FTP 2 | 3 | This class implements the File Transfer Protocol. If you have used a 4 | command-line FTP program, and are familiar with the commands, you will be 5 | able to use this class easily. Some extra features are included to take 6 | advantage of Ruby's style and strengths. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'net-ftp' 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle install 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install net-ftp 23 | 24 | ## Usage 25 | 26 | ### Example 1 27 | 28 | ```ruby 29 | ftp = Net::FTP.new('example.com') 30 | ftp.login 31 | files = ftp.chdir('pub/lang/ruby/contrib') 32 | files = ftp.list('n*') 33 | ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) 34 | ftp.close 35 | ``` 36 | 37 | ### Example 2 38 | 39 | ```ruby 40 | Net::FTP.open('example.com') do |ftp| 41 | ftp.login 42 | files = ftp.chdir('pub/lang/ruby/contrib') 43 | files = ftp.list('n*') 44 | ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) 45 | end 46 | ``` 47 | 48 | ## Development 49 | 50 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 51 | 52 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 53 | 54 | ## Contributing 55 | 56 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/net-ftp. 57 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "net/ftp" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/net/ftp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # = net/ftp.rb - FTP Client Library 4 | # 5 | # Written by Shugo Maeda . 6 | # 7 | # Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas) 8 | # and "Ruby In a Nutshell" (Matsumoto), used with permission. 9 | # 10 | # This library is distributed under the terms of the Ruby license. 11 | # You can freely distribute/modify this library. 12 | # 13 | # It is included in the Ruby standard library. 14 | # 15 | # See the Net::FTP class for an overview. 16 | # 17 | 18 | require "socket" 19 | require "monitor" 20 | require "net/protocol" 21 | require "time" 22 | begin 23 | require "openssl" 24 | rescue LoadError 25 | end 26 | 27 | module Net 28 | 29 | # :stopdoc: 30 | class FTPError < StandardError; end 31 | class FTPReplyError < FTPError; end 32 | class FTPTempError < FTPError; end 33 | class FTPPermError < FTPError; end 34 | class FTPProtoError < FTPError; end 35 | class FTPConnectionError < FTPError; end 36 | # :startdoc: 37 | 38 | # 39 | # This class implements the File Transfer Protocol. If you have used a 40 | # command-line FTP program, and are familiar with the commands, you will be 41 | # able to use this class easily. Some extra features are included to take 42 | # advantage of Ruby's style and strengths. 43 | # 44 | # == Example 45 | # 46 | # require 'net/ftp' 47 | # 48 | # === Example 1 49 | # 50 | # ftp = Net::FTP.new('example.com') 51 | # ftp.login 52 | # files = ftp.chdir('pub/lang/ruby/contrib') 53 | # files = ftp.list('n*') 54 | # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) 55 | # ftp.close 56 | # 57 | # === Example 2 58 | # 59 | # Net::FTP.open('example.com') do |ftp| 60 | # ftp.login 61 | # files = ftp.chdir('pub/lang/ruby/contrib') 62 | # files = ftp.list('n*') 63 | # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) 64 | # end 65 | # 66 | # == Major Methods 67 | # 68 | # The following are the methods most likely to be useful to users: 69 | # - FTP.open 70 | # - #getbinaryfile 71 | # - #gettextfile 72 | # - #putbinaryfile 73 | # - #puttextfile 74 | # - #chdir 75 | # - #nlst 76 | # - #size 77 | # - #rename 78 | # - #delete 79 | # 80 | # == Mainframe User Support 81 | # - #literal 82 | # - #quote 83 | # 84 | class FTP < Protocol 85 | include MonitorMixin 86 | if defined?(OpenSSL::SSL) 87 | include OpenSSL 88 | include SSL 89 | end 90 | 91 | # :stopdoc: 92 | VERSION = "0.3.8" 93 | FTP_PORT = 21 94 | CRLF = "\r\n" 95 | DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE 96 | @@default_passive = true 97 | # :startdoc: 98 | 99 | # When +true+, transfers are performed in binary mode. Default: +true+. 100 | attr_reader :binary 101 | 102 | # When +true+, the connection is in passive mode. Default: +true+. 103 | attr_accessor :passive 104 | 105 | # When +true+, use the IP address in PASV responses. Otherwise, it uses 106 | # the same IP address for the control connection. Default: +false+. 107 | attr_accessor :use_pasv_ip 108 | 109 | # When +true+, all traffic to and from the server is written 110 | # to +$stdout+. Default: +false+. 111 | attr_accessor :debug_mode 112 | 113 | # Sets or retrieves the output stream for debugging. 114 | # Output stream will be used only when +debug_mode+ is set to true. 115 | # The default value is +$stdout+. 116 | attr_accessor :debug_output 117 | 118 | # Sets or retrieves the +resume+ status, which decides whether incomplete 119 | # transfers are resumed or restarted. Default: +false+. 120 | attr_accessor :resume 121 | 122 | # Number of seconds to wait for the connection to open. Any number 123 | # may be used, including Floats for fractional seconds. If the FTP 124 | # object cannot open a connection in this many seconds, it raises a 125 | # Net::OpenTimeout exception. The default value is +nil+. 126 | attr_accessor :open_timeout 127 | 128 | # Number of seconds to wait for the TLS handshake. Any number 129 | # may be used, including Floats for fractional seconds. If the FTP 130 | # object cannot complete the TLS handshake in this many seconds, it 131 | # raises a Net::OpenTimeout exception. The default value is +nil+. 132 | # If +ssl_handshake_timeout+ is +nil+, +open_timeout+ is used instead. 133 | attr_accessor :ssl_handshake_timeout 134 | 135 | # Number of seconds to wait for one block to be read (via one read(2) 136 | # call). Any number may be used, including Floats for fractional 137 | # seconds. If the FTP object cannot read data in this many seconds, 138 | # it raises a Timeout::Error exception. The default value is 60 seconds. 139 | attr_reader :read_timeout 140 | 141 | # Setter for the read_timeout attribute. 142 | def read_timeout=(sec) 143 | @sock.read_timeout = sec 144 | @read_timeout = sec 145 | end 146 | 147 | # The server's welcome message. 148 | attr_reader :welcome 149 | 150 | # The server's last response code. 151 | attr_reader :last_response_code 152 | alias lastresp last_response_code 153 | 154 | # The server's last response. 155 | attr_reader :last_response 156 | 157 | # When +true+, connections are in passive mode per default. 158 | # Default: +true+. 159 | def self.default_passive=(value) 160 | @@default_passive = value 161 | end 162 | 163 | # When +true+, connections are in passive mode per default. 164 | # Default: +true+. 165 | def self.default_passive 166 | @@default_passive 167 | end 168 | 169 | # 170 | # A synonym for FTP.new, but with a mandatory host parameter. 171 | # 172 | # If a block is given, it is passed the +FTP+ object, which will be closed 173 | # when the block finishes, or when an exception is raised. 174 | # 175 | def FTP.open(host, *args) 176 | if block_given? 177 | ftp = new(host, *args) 178 | begin 179 | yield ftp 180 | ensure 181 | ftp.close 182 | end 183 | else 184 | new(host, *args) 185 | end 186 | end 187 | 188 | # :call-seq: 189 | # Net::FTP.new(host = nil, options = {}) 190 | # 191 | # Creates and returns a new +FTP+ object. If a +host+ is given, a connection 192 | # is made. 193 | # 194 | # +options+ is an option hash, each key of which is a symbol. 195 | # 196 | # The available options are: 197 | # 198 | # port:: Port number (default value is 21) 199 | # ssl:: If +options+[:ssl] is true, then an attempt will be made 200 | # to use SSL (now TLS) to connect to the server. For this 201 | # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] 202 | # extensions need to be installed. If +options+[:ssl] is a 203 | # hash, it's passed to OpenSSL::SSL::SSLContext#set_params 204 | # as parameters. 205 | # private_data_connection:: If true, TLS is used for data connections. 206 | # Default: +true+ when +options+[:ssl] is true. 207 | # implicit_ftps:: If true, TLS is established on initial connection. 208 | # Default: +false+ 209 | # username:: Username for login. If +options+[:username] is the string 210 | # "anonymous" and the +options+[:password] is +nil+, 211 | # "anonymous@" is used as a password. 212 | # password:: Password for login. 213 | # account:: Account information for ACCT. 214 | # passive:: When +true+, the connection is in passive mode. Default: 215 | # +true+. 216 | # open_timeout:: Number of seconds to wait for the connection to open. 217 | # See Net::FTP#open_timeout for details. Default: +nil+. 218 | # read_timeout:: Number of seconds to wait for one block to be read. 219 | # See Net::FTP#read_timeout for details. Default: +60+. 220 | # ssl_handshake_timeout:: Number of seconds to wait for the TLS 221 | # handshake. 222 | # See Net::FTP#ssl_handshake_timeout for 223 | # details. Default: +nil+. 224 | # use_pasv_ip:: When +true+, use the IP address in PASV responses. 225 | # Otherwise, it uses the same IP address for the control 226 | # connection. Default: +false+. 227 | # debug_mode:: When +true+, all traffic to and from the server is 228 | # written to +$stdout+. Default: +false+. 229 | # 230 | def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil) 231 | super() 232 | begin 233 | options = user_or_options.to_hash 234 | rescue NoMethodError 235 | # for backward compatibility 236 | options = {} 237 | options[:username] = user_or_options 238 | options[:password] = passwd 239 | options[:account] = acct 240 | end 241 | @host = nil 242 | if options[:ssl] 243 | unless defined?(OpenSSL::SSL) 244 | raise "SSL extension not installed" 245 | end 246 | ssl_params = options[:ssl] == true ? {} : options[:ssl] 247 | @ssl_context = SSLContext.new 248 | @ssl_context.set_params(ssl_params) 249 | if defined?(VerifyCallbackProc) 250 | @ssl_context.verify_callback = VerifyCallbackProc 251 | end 252 | 253 | # jruby-openssl does not support session caching 254 | unless RUBY_ENGINE == "jruby" 255 | @ssl_context.session_cache_mode = 256 | OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | 257 | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE 258 | @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess } 259 | end 260 | 261 | @ssl_session = nil 262 | if options[:private_data_connection].nil? 263 | @private_data_connection = true 264 | else 265 | @private_data_connection = options[:private_data_connection] 266 | end 267 | if options[:implicit_ftps].nil? 268 | @implicit_ftps = false 269 | else 270 | @implicit_ftps = options[:implicit_ftps] 271 | end 272 | else 273 | @ssl_context = nil 274 | if options[:private_data_connection] 275 | raise ArgumentError, 276 | "private_data_connection can be set to true only when ssl is enabled" 277 | end 278 | @private_data_connection = false 279 | if options[:implicit_ftps] 280 | raise ArgumentError, 281 | "implicit_ftps can be set to true only when ssl is enabled" 282 | end 283 | @implicit_ftps = false 284 | end 285 | @binary = true 286 | if options[:passive].nil? 287 | @passive = @@default_passive 288 | else 289 | @passive = options[:passive] 290 | end 291 | if options[:debug_mode].nil? 292 | @debug_mode = false 293 | else 294 | @debug_mode = options[:debug_mode] 295 | end 296 | @debug_output = $stdout 297 | @resume = false 298 | @bare_sock = @sock = NullSocket.new 299 | @logged_in = false 300 | @open_timeout = options[:open_timeout] 301 | @ssl_handshake_timeout = options[:ssl_handshake_timeout] 302 | @read_timeout = options[:read_timeout] || 60 303 | @use_pasv_ip = options[:use_pasv_ip] || false 304 | if host 305 | connect(host, options[:port] || FTP_PORT) 306 | if options[:username] 307 | login(options[:username], options[:password], options[:account]) 308 | end 309 | end 310 | end 311 | 312 | # A setter to toggle transfers in binary mode. 313 | # +newmode+ is either +true+ or +false+ 314 | def binary=(newmode) 315 | if newmode != @binary 316 | @binary = newmode 317 | send_type_command if @logged_in 318 | end 319 | end 320 | 321 | # Sends a command to destination host, with the current binary sendmode 322 | # type. 323 | # 324 | # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE 325 | # A" (ascii) is sent. 326 | def send_type_command # :nodoc: 327 | if @binary 328 | voidcmd("TYPE I") 329 | else 330 | voidcmd("TYPE A") 331 | end 332 | end 333 | private :send_type_command 334 | 335 | # Toggles transfers in binary mode and yields to a block. 336 | # This preserves your current binary send mode, but allows a temporary 337 | # transaction with binary sendmode of +newmode+. 338 | # 339 | # +newmode+ is either +true+ or +false+ 340 | def with_binary(newmode) # :nodoc: 341 | oldmode = binary 342 | self.binary = newmode 343 | begin 344 | yield 345 | ensure 346 | self.binary = oldmode 347 | end 348 | end 349 | private :with_binary 350 | 351 | # Obsolete 352 | def return_code # :nodoc: 353 | warn("Net::FTP#return_code is obsolete and do nothing", uplevel: 1) 354 | return "\n" 355 | end 356 | 357 | # Obsolete 358 | def return_code=(s) # :nodoc: 359 | warn("Net::FTP#return_code= is obsolete and do nothing", uplevel: 1) 360 | end 361 | 362 | # Constructs a socket with +host+ and +port+. 363 | # 364 | # If SOCKSSocket is defined and the environment (ENV) defines 365 | # SOCKS_SERVER, then a SOCKSSocket is returned, else a Socket is 366 | # returned. 367 | def open_socket(host, port) # :nodoc: 368 | return Timeout.timeout(@open_timeout, OpenTimeout) { 369 | if defined? SOCKSSocket and ENV["SOCKS_SERVER"] 370 | @passive = true 371 | SOCKSSocket.open(host, port) 372 | else 373 | Socket.tcp(host, port) 374 | end 375 | } 376 | end 377 | private :open_socket 378 | 379 | def start_tls_session(sock) 380 | ssl_sock = SSLSocket.new(sock, @ssl_context) 381 | ssl_sock.sync_close = true 382 | ssl_sock.hostname = @host if ssl_sock.respond_to? :hostname= 383 | if @ssl_session && 384 | Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout 385 | # ProFTPD returns 425 for data connections if session is not reused. 386 | ssl_sock.session = @ssl_session 387 | end 388 | ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout) 389 | if @ssl_context.verify_mode != VERIFY_NONE 390 | ssl_sock.post_connection_check(@host) 391 | end 392 | return ssl_sock 393 | end 394 | private :start_tls_session 395 | 396 | # 397 | # Establishes an FTP connection to host, optionally overriding the default 398 | # port. If the environment variable +SOCKS_SERVER+ is set, sets up the 399 | # connection through a SOCKS proxy. Raises an exception (typically 400 | # Errno::ECONNREFUSED) if the connection cannot be established. 401 | # 402 | def connect(host, port = FTP_PORT) 403 | debug_print "connect: #{host}:#{port}" 404 | synchronize do 405 | @host = host 406 | @bare_sock = open_socket(host, port) 407 | if @ssl_context 408 | begin 409 | unless @implicit_ftps 410 | set_socket(BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)) 411 | voidcmd("AUTH TLS") 412 | end 413 | set_socket(BufferedSSLSocket.new(start_tls_session(@bare_sock), read_timeout: @read_timeout), @implicit_ftps) 414 | if @private_data_connection 415 | voidcmd("PBSZ 0") 416 | voidcmd("PROT P") 417 | end 418 | rescue OpenSSL::SSL::SSLError, OpenTimeout 419 | @sock.close 420 | raise 421 | end 422 | else 423 | set_socket(BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)) 424 | end 425 | end 426 | end 427 | 428 | # 429 | # Set the socket used to connect to the FTP server. 430 | # 431 | # May raise FTPReplyError if +get_greeting+ is false. 432 | def set_socket(sock, get_greeting = true) 433 | synchronize do 434 | @sock = sock 435 | if get_greeting 436 | voidresp 437 | end 438 | end 439 | end 440 | 441 | # If string +s+ includes the PASS command (password), then the contents of 442 | # the password are cleaned from the string using "*" 443 | def sanitize(s) # :nodoc: 444 | if s =~ /^PASS /i 445 | return s[0, 5] + "*" * (s.length - 5) 446 | else 447 | return s 448 | end 449 | end 450 | private :sanitize 451 | 452 | # Ensures that +line+ has a control return / line feed (CRLF) and writes 453 | # it to the socket. 454 | def putline(line) # :nodoc: 455 | debug_print "put: #{sanitize(line)}" 456 | if /[\r\n]/ =~ line 457 | raise ArgumentError, "A line must not contain CR or LF" 458 | end 459 | line = line + CRLF 460 | @sock.write(line) 461 | end 462 | private :putline 463 | 464 | # Reads a line from the sock. If EOF, then it will raise EOFError 465 | def getline # :nodoc: 466 | line = @sock.readline # if get EOF, raise EOFError 467 | line.sub!(/(\r\n|\n|\r)\z/n, "") 468 | debug_print "get: #{sanitize(line)}" 469 | return line 470 | end 471 | private :getline 472 | 473 | # Receive a section of lines until the response code's match. 474 | def getmultiline # :nodoc: 475 | lines = [] 476 | lines << getline 477 | code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1) 478 | if code 479 | delimiter = code + " " 480 | begin 481 | lines << getline 482 | end until lines.last.start_with?(delimiter) 483 | end 484 | return lines.join("\n") + "\n" 485 | end 486 | private :getmultiline 487 | 488 | # Receives a response from the destination host. 489 | # 490 | # Returns the response code or raises FTPTempError, FTPPermError, or 491 | # FTPProtoError 492 | def getresp # :nodoc: 493 | @last_response = getmultiline 494 | @last_response_code = @last_response[0, 3] 495 | case @last_response_code 496 | when /\A[123]/ 497 | return @last_response 498 | when /\A4/ 499 | raise FTPTempError, @last_response 500 | when /\A5/ 501 | raise FTPPermError, @last_response 502 | else 503 | raise FTPProtoError, @last_response 504 | end 505 | end 506 | private :getresp 507 | 508 | # Receives a response. 509 | # 510 | # Raises FTPReplyError if the first position of the response code is not 511 | # equal 2. 512 | def voidresp # :nodoc: 513 | resp = getresp 514 | if !resp.start_with?("2") 515 | raise FTPReplyError, resp 516 | end 517 | end 518 | private :voidresp 519 | 520 | # 521 | # Sends a command and returns the response. 522 | # 523 | def sendcmd(cmd) 524 | synchronize do 525 | putline(cmd) 526 | return getresp 527 | end 528 | end 529 | 530 | # 531 | # Sends a command and expect a response beginning with '2'. 532 | # 533 | def voidcmd(cmd) 534 | synchronize do 535 | putline(cmd) 536 | voidresp 537 | end 538 | end 539 | 540 | # Constructs and send the appropriate PORT (or EPRT) command 541 | def sendport(host, port) # :nodoc: 542 | remote_address = @bare_sock.remote_address 543 | if remote_address.ipv4? 544 | cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",") 545 | elsif remote_address.ipv6? 546 | cmd = sprintf("EPRT |2|%s|%d|", host, port) 547 | else 548 | raise FTPProtoError, host 549 | end 550 | voidcmd(cmd) 551 | end 552 | private :sendport 553 | 554 | # Constructs a TCPServer socket 555 | def makeport # :nodoc: 556 | Addrinfo.tcp(@bare_sock.local_address.ip_address, 0).listen 557 | end 558 | private :makeport 559 | 560 | # sends the appropriate command to enable a passive connection 561 | def makepasv # :nodoc: 562 | if @bare_sock.remote_address.ipv4? 563 | host, port = parse227(sendcmd("PASV")) 564 | else 565 | host, port = parse229(sendcmd("EPSV")) 566 | end 567 | return host, port 568 | end 569 | private :makepasv 570 | 571 | # Constructs a connection for transferring data 572 | def transfercmd(cmd, rest_offset = nil) # :nodoc: 573 | if @passive 574 | host, port = makepasv 575 | succeeded = false 576 | begin 577 | conn = open_socket(host, port) 578 | if @resume and rest_offset 579 | resp = sendcmd("REST " + rest_offset.to_s) 580 | if !resp.start_with?("3") 581 | raise FTPReplyError, resp 582 | end 583 | end 584 | resp = sendcmd(cmd) 585 | # skip 2XX for some ftp servers 586 | resp = getresp if resp.start_with?("2") 587 | if !resp.start_with?("1") 588 | raise FTPReplyError, resp 589 | end 590 | succeeded = true 591 | ensure 592 | conn&.close if !succeeded 593 | end 594 | else 595 | sock = makeport 596 | begin 597 | addr = sock.local_address 598 | sendport(addr.ip_address, addr.ip_port) 599 | if @resume and rest_offset 600 | resp = sendcmd("REST " + rest_offset.to_s) 601 | if !resp.start_with?("3") 602 | raise FTPReplyError, resp 603 | end 604 | end 605 | resp = sendcmd(cmd) 606 | # skip 2XX for some ftp servers 607 | resp = getresp if resp.start_with?("2") 608 | if !resp.start_with?("1") 609 | raise FTPReplyError, resp 610 | end 611 | conn, = sock.accept 612 | sock.shutdown(Socket::SHUT_WR) rescue nil 613 | sock.read rescue nil 614 | ensure 615 | sock.close 616 | end 617 | end 618 | if @private_data_connection 619 | return BufferedSSLSocket.new(start_tls_session(conn), 620 | read_timeout: @read_timeout) 621 | else 622 | return BufferedSocket.new(conn, read_timeout: @read_timeout) 623 | end 624 | end 625 | private :transfercmd 626 | 627 | # 628 | # Logs in to the remote host. The session must have been 629 | # previously connected. If +user+ is the string "anonymous" and 630 | # the +password+ is +nil+, "anonymous@" is used as a password. If 631 | # the +acct+ parameter is not +nil+, an FTP ACCT command is sent 632 | # following the successful login. Raises an exception on error 633 | # (typically Net::FTPPermError). 634 | # 635 | def login(user = "anonymous", passwd = nil, acct = nil) 636 | if user == "anonymous" and passwd == nil 637 | passwd = "anonymous@" 638 | end 639 | 640 | resp = "" 641 | synchronize do 642 | resp = sendcmd('USER ' + user) 643 | if resp.start_with?("3") 644 | raise FTPReplyError, resp if passwd.nil? 645 | resp = sendcmd('PASS ' + passwd) 646 | end 647 | if resp.start_with?("3") 648 | raise FTPReplyError, resp if acct.nil? 649 | resp = sendcmd('ACCT ' + acct) 650 | end 651 | end 652 | if !resp.start_with?("2") 653 | raise FTPReplyError, resp 654 | end 655 | @welcome = resp 656 | send_type_command 657 | @logged_in = true 658 | end 659 | 660 | # 661 | # Puts the connection into binary (image) mode, issues the given command, 662 | # and fetches the data returned, passing it to the associated block in 663 | # chunks of +blocksize+ characters. Note that +cmd+ is a server command 664 | # (such as "RETR myfile"). 665 | # 666 | def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data 667 | synchronize do 668 | with_binary(true) do 669 | begin 670 | conn = transfercmd(cmd, rest_offset) 671 | while data = conn.read(blocksize) 672 | yield(data) 673 | end 674 | conn.shutdown(Socket::SHUT_WR) rescue nil 675 | conn.read_timeout = 1 676 | conn.read rescue nil 677 | ensure 678 | conn.close if conn 679 | end 680 | voidresp 681 | end 682 | end 683 | end 684 | 685 | # 686 | # Puts the connection into ASCII (text) mode, issues the given command, and 687 | # passes the resulting data, one line at a time, to the associated block. If 688 | # no block is given, prints the lines. Note that +cmd+ is a server command 689 | # (such as "RETR myfile"). 690 | # 691 | def retrlines(cmd) # :yield: line 692 | synchronize do 693 | with_binary(false) do 694 | begin 695 | conn = transfercmd(cmd) 696 | while line = conn.gets 697 | yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?) 698 | end 699 | conn.shutdown(Socket::SHUT_WR) rescue nil 700 | conn.read_timeout = 1 701 | conn.read rescue nil 702 | ensure 703 | conn.close if conn 704 | end 705 | voidresp 706 | end 707 | end 708 | end 709 | 710 | # 711 | # Puts the connection into binary (image) mode, issues the given server-side 712 | # command (such as "STOR myfile"), and sends the contents of the file named 713 | # +file+ to the server. If the optional block is given, it also passes it 714 | # the data, in chunks of +blocksize+ characters. 715 | # 716 | def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data 717 | if rest_offset 718 | file.seek(rest_offset, IO::SEEK_SET) 719 | end 720 | synchronize do 721 | with_binary(true) do 722 | begin 723 | conn = transfercmd(cmd) 724 | while buf = file.read(blocksize) 725 | conn.write(buf) 726 | yield(buf) if block_given? 727 | end 728 | conn.shutdown(Socket::SHUT_WR) rescue nil 729 | conn.read_timeout = 1 730 | conn.read rescue nil 731 | ensure 732 | conn.close if conn 733 | end 734 | voidresp 735 | end 736 | end 737 | rescue Errno::EPIPE 738 | # EPIPE, in this case, means that the data connection was unexpectedly 739 | # terminated. Rather than just raising EPIPE to the caller, check the 740 | # response on the control connection. If getresp doesn't raise a more 741 | # appropriate exception, re-raise the original exception. 742 | getresp 743 | raise 744 | end 745 | 746 | # 747 | # Puts the connection into ASCII (text) mode, issues the given server-side 748 | # command (such as "STOR myfile"), and sends the contents of the file 749 | # named +file+ to the server, one line at a time. If the optional block is 750 | # given, it also passes it the lines. 751 | # 752 | def storlines(cmd, file) # :yield: line 753 | synchronize do 754 | with_binary(false) do 755 | begin 756 | conn = transfercmd(cmd) 757 | while buf = file.gets 758 | if buf[-2, 2] != CRLF 759 | buf = buf.chomp + CRLF 760 | end 761 | conn.write(buf) 762 | yield(buf) if block_given? 763 | end 764 | conn.shutdown(Socket::SHUT_WR) rescue nil 765 | conn.read_timeout = 1 766 | conn.read rescue nil 767 | ensure 768 | conn.close if conn 769 | end 770 | getresp # The response might be important when connected to a mainframe 771 | end 772 | end 773 | rescue Errno::EPIPE 774 | # EPIPE, in this case, means that the data connection was unexpectedly 775 | # terminated. Rather than just raising EPIPE to the caller, check the 776 | # response on the control connection. If getresp doesn't raise a more 777 | # appropriate exception, re-raise the original exception. 778 | getresp 779 | raise 780 | end 781 | 782 | # 783 | # Retrieves +remotefile+ in binary mode, storing the result in +localfile+. 784 | # If +localfile+ is nil, returns retrieved data. 785 | # If a block is supplied, it is passed the retrieved data in +blocksize+ 786 | # chunks. 787 | # 788 | def getbinaryfile(remotefile, localfile = File.basename(remotefile), 789 | blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data 790 | f = nil 791 | result = nil 792 | if localfile 793 | if @resume 794 | rest_offset = File.size?(localfile) 795 | f = File.open(localfile, "a") 796 | else 797 | rest_offset = nil 798 | f = File.open(localfile, "w") 799 | end 800 | elsif !block_given? 801 | result = String.new 802 | end 803 | begin 804 | f&.binmode 805 | retrbinary("RETR #{remotefile}", blocksize, rest_offset) do |data| 806 | f&.write(data) 807 | block&.(data) 808 | result&.concat(data) 809 | end 810 | return result 811 | ensure 812 | f&.close 813 | end 814 | end 815 | 816 | # 817 | # Retrieves +remotefile+ in ASCII (text) mode, storing the result in 818 | # +localfile+. 819 | # If +localfile+ is nil, returns retrieved data. 820 | # If a block is supplied, it is passed the retrieved data one 821 | # line at a time. 822 | # 823 | def gettextfile(remotefile, localfile = File.basename(remotefile), 824 | &block) # :yield: line 825 | f = nil 826 | result = nil 827 | if localfile 828 | f = File.open(localfile, "w") 829 | elsif !block_given? 830 | result = String.new 831 | end 832 | begin 833 | retrlines("RETR #{remotefile}") do |line, newline| 834 | l = newline ? line + "\n" : line 835 | f&.print(l) 836 | block&.(line, newline) 837 | result&.concat(l) 838 | end 839 | return result 840 | ensure 841 | f&.close 842 | end 843 | end 844 | 845 | # 846 | # Retrieves +remotefile+ in whatever mode the session is set (text or 847 | # binary). See #gettextfile and #getbinaryfile. 848 | # 849 | def get(remotefile, localfile = File.basename(remotefile), 850 | blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data 851 | if @binary 852 | getbinaryfile(remotefile, localfile, blocksize, &block) 853 | else 854 | gettextfile(remotefile, localfile, &block) 855 | end 856 | end 857 | 858 | # 859 | # Transfers +localfile+ to the server in binary mode, storing the result in 860 | # +remotefile+. If a block is supplied, calls it, passing in the transmitted 861 | # data in +blocksize+ chunks. 862 | # 863 | def putbinaryfile(localfile, remotefile = File.basename(localfile), 864 | blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data 865 | if @resume 866 | begin 867 | rest_offset = size(remotefile) 868 | rescue Net::FTPPermError 869 | rest_offset = nil 870 | end 871 | else 872 | rest_offset = nil 873 | end 874 | f = File.open(localfile) 875 | begin 876 | f.binmode 877 | if rest_offset 878 | storbinary("APPE #{remotefile}", f, blocksize, rest_offset, &block) 879 | else 880 | storbinary("STOR #{remotefile}", f, blocksize, rest_offset, &block) 881 | end 882 | ensure 883 | f.close 884 | end 885 | end 886 | 887 | # 888 | # Transfers +localfile+ to the server in ASCII (text) mode, storing the result 889 | # in +remotefile+. If callback or an associated block is supplied, calls it, 890 | # passing in the transmitted data one line at a time. 891 | # 892 | # Returns the response which will contain a job number if the user was communicating with a mainframe in ASCII mode 893 | # after issuing 'quote site filetype=jes' 894 | # 895 | def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line 896 | f = File.open(localfile) 897 | response = '' 898 | begin 899 | response = storlines("STOR #{remotefile}", f, &block) 900 | ensure 901 | f.close 902 | end 903 | response 904 | end 905 | 906 | # 907 | # Transfers +localfile+ to the server in whatever mode the session is set 908 | # (text or binary). See #puttextfile and #putbinaryfile. 909 | # 910 | def put(localfile, remotefile = File.basename(localfile), 911 | blocksize = DEFAULT_BLOCKSIZE, &block) 912 | if @binary 913 | putbinaryfile(localfile, remotefile, blocksize, &block) 914 | else 915 | puttextfile(localfile, remotefile, &block) 916 | end 917 | end 918 | 919 | # 920 | # Sends the ACCT command. 921 | # 922 | # This is a less common FTP command, to send account 923 | # information if the destination host requires it. 924 | # 925 | def acct(account) 926 | cmd = "ACCT " + account 927 | voidcmd(cmd) 928 | end 929 | 930 | # 931 | # Returns an array of filenames in the remote directory. 932 | # 933 | def nlst(dir = nil) 934 | cmd = "NLST" 935 | if dir 936 | cmd = "#{cmd} #{dir}" 937 | end 938 | files = [] 939 | retrlines(cmd) do |line| 940 | files.push(line) 941 | end 942 | return files 943 | end 944 | 945 | # 946 | # Returns an array of file information in the directory (the output is like 947 | # `ls -l`). If a block is given, it iterates through the listing. 948 | # 949 | def list(*args, &block) # :yield: line 950 | cmd = "LIST" 951 | args.each do |arg| 952 | cmd = "#{cmd} #{arg}" 953 | end 954 | lines = [] 955 | retrlines(cmd) do |line| 956 | lines << line 957 | end 958 | if block 959 | lines.each(&block) 960 | end 961 | return lines 962 | end 963 | alias ls list 964 | alias dir list 965 | 966 | # 967 | # MLSxEntry represents an entry in responses of MLST/MLSD. 968 | # Each entry has the facts (e.g., size, last modification time, etc.) 969 | # and the pathname. 970 | # 971 | class MLSxEntry 972 | attr_reader :facts, :pathname 973 | 974 | def initialize(facts, pathname) 975 | @facts = facts 976 | @pathname = pathname 977 | end 978 | 979 | standard_facts = %w(size modify create type unique perm 980 | lang media-type charset) 981 | standard_facts.each do |factname| 982 | define_method factname.gsub(/-/, "_") do 983 | facts[factname] 984 | end 985 | end 986 | 987 | # 988 | # Returns +true+ if the entry is a file (i.e., the value of the type 989 | # fact is file). 990 | # 991 | def file? 992 | return facts["type"] == "file" 993 | end 994 | 995 | # 996 | # Returns +true+ if the entry is a directory (i.e., the value of the 997 | # type fact is dir, cdir, or pdir). 998 | # 999 | def directory? 1000 | if /\A[cp]?dir\z/.match(facts["type"]) 1001 | return true 1002 | else 1003 | return false 1004 | end 1005 | end 1006 | 1007 | # 1008 | # Returns +true+ if the APPE command may be applied to the file. 1009 | # 1010 | def appendable? 1011 | return facts["perm"].include?(?a) 1012 | end 1013 | 1014 | # 1015 | # Returns +true+ if files may be created in the directory by STOU, 1016 | # STOR, APPE, and RNTO. 1017 | # 1018 | def creatable? 1019 | return facts["perm"].include?(?c) 1020 | end 1021 | 1022 | # 1023 | # Returns +true+ if the file or directory may be deleted by DELE/RMD. 1024 | # 1025 | def deletable? 1026 | return facts["perm"].include?(?d) 1027 | end 1028 | 1029 | # 1030 | # Returns +true+ if the directory may be entered by CWD/CDUP. 1031 | # 1032 | def enterable? 1033 | return facts["perm"].include?(?e) 1034 | end 1035 | 1036 | # 1037 | # Returns +true+ if the file or directory may be renamed by RNFR. 1038 | # 1039 | def renamable? 1040 | return facts["perm"].include?(?f) 1041 | end 1042 | 1043 | # 1044 | # Returns +true+ if the listing commands, LIST, NLST, and MLSD are 1045 | # applied to the directory. 1046 | # 1047 | def listable? 1048 | return facts["perm"].include?(?l) 1049 | end 1050 | 1051 | # 1052 | # Returns +true+ if the MKD command may be used to create a new 1053 | # directory within the directory. 1054 | # 1055 | def directory_makable? 1056 | return facts["perm"].include?(?m) 1057 | end 1058 | 1059 | # 1060 | # Returns +true+ if the objects in the directory may be deleted, or 1061 | # the directory may be purged. 1062 | # 1063 | def purgeable? 1064 | return facts["perm"].include?(?p) 1065 | end 1066 | 1067 | # 1068 | # Returns +true+ if the RETR command may be applied to the file. 1069 | # 1070 | def readable? 1071 | return facts["perm"].include?(?r) 1072 | end 1073 | 1074 | # 1075 | # Returns +true+ if the STOR command may be applied to the file. 1076 | # 1077 | def writable? 1078 | return facts["perm"].include?(?w) 1079 | end 1080 | end 1081 | 1082 | CASE_DEPENDENT_PARSER = ->(value) { value } 1083 | CASE_INDEPENDENT_PARSER = ->(value) { value.downcase } 1084 | DECIMAL_PARSER = ->(value) { value.to_i } 1085 | OCTAL_PARSER = ->(value) { value.to_i(8) } 1086 | TIME_PARSER = ->(value, local = false) { 1087 | unless /\A(?\d{4})(?\d{2})(?\d{2}) 1088 | (?\d{2})(?\d{2})(?\d{2}) 1089 | (?:\.(?\d{1,17}))?/x =~ value 1090 | value = value[0, 97] + "..." if value.size > 100 1091 | raise FTPProtoError, "invalid time-val: #{value}" 1092 | end 1093 | usec = ".#{fractions}".to_r * 1_000_000 if fractions 1094 | Time.public_send(local ? :local : :utc, year, month, day, hour, min, sec, usec) 1095 | } 1096 | FACT_PARSERS = Hash.new(CASE_DEPENDENT_PARSER) 1097 | FACT_PARSERS["size"] = DECIMAL_PARSER 1098 | FACT_PARSERS["modify"] = TIME_PARSER 1099 | FACT_PARSERS["create"] = TIME_PARSER 1100 | FACT_PARSERS["type"] = CASE_INDEPENDENT_PARSER 1101 | FACT_PARSERS["unique"] = CASE_DEPENDENT_PARSER 1102 | FACT_PARSERS["perm"] = CASE_INDEPENDENT_PARSER 1103 | FACT_PARSERS["lang"] = CASE_INDEPENDENT_PARSER 1104 | FACT_PARSERS["media-type"] = CASE_INDEPENDENT_PARSER 1105 | FACT_PARSERS["charset"] = CASE_INDEPENDENT_PARSER 1106 | FACT_PARSERS["unix.mode"] = OCTAL_PARSER 1107 | FACT_PARSERS["unix.owner"] = DECIMAL_PARSER 1108 | FACT_PARSERS["unix.group"] = DECIMAL_PARSER 1109 | FACT_PARSERS["unix.ctime"] = TIME_PARSER 1110 | FACT_PARSERS["unix.atime"] = TIME_PARSER 1111 | 1112 | def parse_mlsx_entry(entry) 1113 | facts, pathname = entry.chomp.split(/ /, 2) 1114 | unless pathname 1115 | raise FTPProtoError, entry 1116 | end 1117 | return MLSxEntry.new( 1118 | facts.scan(/(.*?)=(.*?);/).each_with_object({}) { 1119 | |(factname, value), h| 1120 | name = factname.downcase 1121 | h[name] = FACT_PARSERS[name].(value) 1122 | }, 1123 | pathname) 1124 | end 1125 | private :parse_mlsx_entry 1126 | 1127 | # 1128 | # Returns data (e.g., size, last modification time, entry type, etc.) 1129 | # about the file or directory specified by +pathname+. 1130 | # If +pathname+ is omitted, the current directory is assumed. 1131 | # 1132 | def mlst(pathname = nil) 1133 | cmd = pathname ? "MLST #{pathname}" : "MLST" 1134 | resp = sendcmd(cmd) 1135 | if !resp.start_with?("250") 1136 | raise FTPReplyError, resp 1137 | end 1138 | line = resp.lines[1] 1139 | unless line 1140 | raise FTPProtoError, resp 1141 | end 1142 | entry = line.sub(/\A(250-| *)/, "") 1143 | return parse_mlsx_entry(entry) 1144 | end 1145 | 1146 | # 1147 | # Returns an array of the entries of the directory specified by 1148 | # +pathname+. 1149 | # Each entry has the facts (e.g., size, last modification time, etc.) 1150 | # and the pathname. 1151 | # If a block is given, it iterates through the listing. 1152 | # If +pathname+ is omitted, the current directory is assumed. 1153 | # 1154 | def mlsd(pathname = nil, &block) # :yield: entry 1155 | cmd = pathname ? "MLSD #{pathname}" : "MLSD" 1156 | entries = [] 1157 | retrlines(cmd) do |line| 1158 | entries << parse_mlsx_entry(line) 1159 | end 1160 | if block 1161 | entries.each(&block) 1162 | end 1163 | return entries 1164 | end 1165 | 1166 | # 1167 | # Renames a file on the server. 1168 | # 1169 | def rename(fromname, toname) 1170 | resp = sendcmd("RNFR #{fromname}") 1171 | if !resp.start_with?("3") 1172 | raise FTPReplyError, resp 1173 | end 1174 | voidcmd("RNTO #{toname}") 1175 | end 1176 | 1177 | # 1178 | # Deletes a file on the server. 1179 | # 1180 | def delete(filename) 1181 | resp = sendcmd("DELE #{filename}") 1182 | if resp.start_with?("250") 1183 | return 1184 | elsif resp.start_with?("5") 1185 | raise FTPPermError, resp 1186 | else 1187 | raise FTPReplyError, resp 1188 | end 1189 | end 1190 | 1191 | # 1192 | # Changes the (remote) directory. 1193 | # 1194 | def chdir(dirname) 1195 | if dirname == ".." 1196 | begin 1197 | voidcmd("CDUP") 1198 | return 1199 | rescue FTPPermError => e 1200 | if e.message[0, 3] != "500" 1201 | raise e 1202 | end 1203 | end 1204 | end 1205 | cmd = "CWD #{dirname}" 1206 | voidcmd(cmd) 1207 | end 1208 | 1209 | def get_body(resp) # :nodoc: 1210 | resp.slice(/\A[0-9a-zA-Z]{3} (.*)$/, 1) 1211 | end 1212 | private :get_body 1213 | 1214 | # 1215 | # Returns the size of the given (remote) filename. 1216 | # 1217 | def size(filename) 1218 | with_binary(true) do 1219 | resp = sendcmd("SIZE #{filename}") 1220 | if !resp.start_with?("213") 1221 | raise FTPReplyError, resp 1222 | end 1223 | return get_body(resp).to_i 1224 | end 1225 | end 1226 | 1227 | # 1228 | # Returns the last modification time of the (remote) file. If +local+ is 1229 | # +true+, it is returned as a local time, otherwise it's a UTC time. 1230 | # 1231 | def mtime(filename, local = false) 1232 | return TIME_PARSER.(mdtm(filename), local) 1233 | end 1234 | 1235 | # 1236 | # Creates a remote directory. 1237 | # 1238 | def mkdir(dirname) 1239 | resp = sendcmd("MKD #{dirname}") 1240 | return parse257(resp) 1241 | end 1242 | 1243 | # 1244 | # The "quote" subcommand sends arguments verbatim to the remote ftp server. 1245 | # The "literal" subcommand is an alias for "quote". 1246 | # @param arguments Array[String] to be sent verbatim to the remote ftp server 1247 | # 1248 | def quote(arguments) 1249 | sendcmd(arguments) 1250 | end 1251 | alias literal quote 1252 | 1253 | # 1254 | # Removes a remote directory. 1255 | # 1256 | def rmdir(dirname) 1257 | voidcmd("RMD #{dirname}") 1258 | end 1259 | 1260 | # 1261 | # Returns the current remote directory. 1262 | # 1263 | def pwd 1264 | resp = sendcmd("PWD") 1265 | return parse257(resp) 1266 | end 1267 | alias getdir pwd 1268 | 1269 | # 1270 | # Returns system information. 1271 | # 1272 | def system 1273 | resp = sendcmd("SYST") 1274 | if !resp.start_with?("215") 1275 | raise FTPReplyError, resp 1276 | end 1277 | return get_body(resp) 1278 | end 1279 | 1280 | # 1281 | # Aborts the previous command (ABOR command). 1282 | # 1283 | def abort 1284 | line = "ABOR" + CRLF 1285 | debug_print "put: ABOR" 1286 | @sock.send(line, Socket::MSG_OOB) 1287 | resp = getmultiline 1288 | unless ["426", "226", "225"].include?(resp[0, 3]) 1289 | raise FTPProtoError, resp 1290 | end 1291 | return resp 1292 | end 1293 | 1294 | # 1295 | # Returns the status (STAT command). 1296 | # 1297 | # pathname:: when stat is invoked with pathname as a parameter it acts like 1298 | # list but a lot faster and over the same tcp session. 1299 | # 1300 | def status(pathname = nil) 1301 | line = pathname ? "STAT #{pathname}" : "STAT" 1302 | if /[\r\n]/ =~ line 1303 | raise ArgumentError, "A line must not contain CR or LF" 1304 | end 1305 | debug_print "put: #{line}" 1306 | @sock.send(line + CRLF, Socket::MSG_OOB) 1307 | return getresp 1308 | end 1309 | 1310 | # 1311 | # Returns the raw last modification time of the (remote) file in the format 1312 | # "YYYYMMDDhhmmss" (MDTM command). 1313 | # 1314 | # Use +mtime+ if you want a parsed Time instance. 1315 | # 1316 | def mdtm(filename) 1317 | resp = sendcmd("MDTM #{filename}") 1318 | if resp.start_with?("213") 1319 | return get_body(resp) 1320 | end 1321 | end 1322 | 1323 | # 1324 | # Issues the HELP command. 1325 | # 1326 | def help(arg = nil) 1327 | cmd = "HELP" 1328 | if arg 1329 | cmd = cmd + " " + arg 1330 | end 1331 | sendcmd(cmd) 1332 | end 1333 | 1334 | # 1335 | # Exits the FTP session. 1336 | # 1337 | def quit 1338 | voidcmd("QUIT") 1339 | end 1340 | 1341 | # 1342 | # Issues a NOOP command. 1343 | # 1344 | # Does nothing except return a response. 1345 | # 1346 | def noop 1347 | voidcmd("NOOP") 1348 | end 1349 | 1350 | # 1351 | # Issues a SITE command. 1352 | # 1353 | def site(arg) 1354 | cmd = "SITE " + arg 1355 | voidcmd(cmd) 1356 | end 1357 | 1358 | # 1359 | # Issues a FEAT command 1360 | # 1361 | # Returns an array of supported optional features 1362 | # 1363 | def features 1364 | resp = sendcmd("FEAT") 1365 | if !resp.start_with?("211") 1366 | raise FTPReplyError, resp 1367 | end 1368 | 1369 | feats = [] 1370 | resp.each_line do |line| 1371 | next if !line.start_with?(' ') # skip status lines 1372 | 1373 | feats << line.strip 1374 | end 1375 | 1376 | return feats 1377 | end 1378 | 1379 | # 1380 | # Issues an OPTS command 1381 | # - name Should be the name of the option to set 1382 | # - params is any optional parameters to supply with the option 1383 | # 1384 | # example: option('UTF8', 'ON') => 'OPTS UTF8 ON' 1385 | # 1386 | def option(name, params = nil) 1387 | cmd = "OPTS #{name}" 1388 | cmd += " #{params}" if params 1389 | 1390 | voidcmd(cmd) 1391 | end 1392 | 1393 | # 1394 | # Closes the connection. Further operations are impossible until you open 1395 | # a new connection with #connect. 1396 | # 1397 | def close 1398 | if @sock and not @sock.closed? 1399 | begin 1400 | @sock.shutdown(Socket::SHUT_WR) rescue nil 1401 | orig, self.read_timeout = self.read_timeout, 3 1402 | @sock.read rescue nil 1403 | ensure 1404 | @sock.close 1405 | self.read_timeout = orig 1406 | end 1407 | end 1408 | end 1409 | 1410 | # 1411 | # Returns +true+ if and only if the connection is closed. 1412 | # 1413 | def closed? 1414 | @sock == nil or @sock.closed? 1415 | end 1416 | 1417 | # handler for response code 227 1418 | # (Entering Passive Mode (h1,h2,h3,h4,p1,p2)) 1419 | # 1420 | # Returns host and port. 1421 | def parse227(resp) # :nodoc: 1422 | if !resp.start_with?("227") 1423 | raise FTPReplyError, resp 1424 | end 1425 | if m = /(?\d+(?:,\d+){3}),(?\d+,\d+)/.match(resp) 1426 | if @use_pasv_ip 1427 | host = parse_pasv_ipv4_host(m["host"]) 1428 | else 1429 | host = @bare_sock.remote_address.ip_address 1430 | end 1431 | return host, parse_pasv_port(m["port"]) 1432 | else 1433 | raise FTPProtoError, resp 1434 | end 1435 | end 1436 | private :parse227 1437 | 1438 | def parse_pasv_ipv4_host(s) 1439 | return s.tr(",", ".") 1440 | end 1441 | private :parse_pasv_ipv4_host 1442 | 1443 | def parse_pasv_ipv6_host(s) 1444 | return s.split(/,/).map { |i| 1445 | "%02x" % i.to_i 1446 | }.each_slice(2).map(&:join).join(":") 1447 | end 1448 | private :parse_pasv_ipv6_host 1449 | 1450 | def parse_pasv_port(s) 1451 | return s.split(/,/).map(&:to_i).inject { |x, y| 1452 | (x << 8) + y 1453 | } 1454 | end 1455 | private :parse_pasv_port 1456 | 1457 | # handler for response code 229 1458 | # (Extended Passive Mode Entered) 1459 | # 1460 | # Returns host and port. 1461 | def parse229(resp) # :nodoc: 1462 | if !resp.start_with?("229") 1463 | raise FTPReplyError, resp 1464 | end 1465 | if m = /\((?[!-~])\k\k(?\d+)\k\)/.match(resp) 1466 | return @bare_sock.remote_address.ip_address, m["port"].to_i 1467 | else 1468 | raise FTPProtoError, resp 1469 | end 1470 | end 1471 | private :parse229 1472 | 1473 | # handler for response code 257 1474 | # ("PATHNAME" created) 1475 | # 1476 | # Returns host and port. 1477 | def parse257(resp) # :nodoc: 1478 | if !resp.start_with?("257") 1479 | raise FTPReplyError, resp 1480 | end 1481 | return resp.slice(/"(([^"]|"")*)"/, 1).to_s.gsub(/""/, '"') 1482 | end 1483 | private :parse257 1484 | 1485 | # 1486 | # Writes debug message to the debug output stream 1487 | # 1488 | def debug_print(msg) 1489 | @debug_output << msg + "\n" if @debug_mode && @debug_output 1490 | end 1491 | 1492 | # :stopdoc: 1493 | class NullSocket 1494 | def read_timeout=(sec) 1495 | end 1496 | 1497 | def closed? 1498 | true 1499 | end 1500 | 1501 | def close 1502 | end 1503 | 1504 | def method_missing(mid, *args) 1505 | raise FTPConnectionError, "not connected" 1506 | end 1507 | end 1508 | 1509 | class BufferedSocket < BufferedIO 1510 | [:local_address, :remote_address, :addr, :peeraddr, :send, :shutdown].each do |method| 1511 | define_method(method) { |*args| 1512 | @io.__send__(method, *args) 1513 | } 1514 | end 1515 | 1516 | def read(len = nil) 1517 | if len 1518 | s = super(len, String.new, true) 1519 | return s.empty? ? nil : s 1520 | else 1521 | result = String.new 1522 | while s = super(DEFAULT_BLOCKSIZE, String.new, true) 1523 | break if s.empty? 1524 | result << s 1525 | end 1526 | return result 1527 | end 1528 | end 1529 | 1530 | def gets 1531 | line = readuntil("\n", true) 1532 | return line.empty? ? nil : line 1533 | end 1534 | 1535 | def readline 1536 | line = gets 1537 | if line.nil? 1538 | raise EOFError, "end of file reached" 1539 | end 1540 | return line 1541 | end 1542 | end 1543 | 1544 | if defined?(OpenSSL::SSL::SSLSocket) 1545 | class BufferedSSLSocket < BufferedSocket 1546 | 1547 | def shutdown(*args) 1548 | # Invokes SSL_shutdown, which sends a close_notify message to the peer. 1549 | @io.__send__(:stop) 1550 | end 1551 | 1552 | def send(mesg, flags, dest = nil) 1553 | # Ignore flags and dest. 1554 | @io.write(mesg) 1555 | end 1556 | 1557 | end 1558 | end 1559 | # :startdoc: 1560 | end 1561 | end 1562 | 1563 | 1564 | # Documentation comments: 1565 | # - sourced from pickaxe and nutshell, with improvements (hopefully) 1566 | -------------------------------------------------------------------------------- /net-ftp.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-"), "..").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Shugo Maeda"] 14 | spec.email = ["shugo@ruby-lang.org"] 15 | 16 | spec.summary = %q{Support for the File Transfer Protocol.} 17 | spec.description = %q{Support for the File Transfer Protocol.} 18 | spec.homepage = "https://github.com/ruby/net-ftp" 19 | spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0") 20 | spec.licenses = ["Ruby", "BSD-2-Clause"] 21 | 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 28 | `git ls-files -z 2>/dev/null`.split("\x0").reject { |f| f.match(%r{^(bin|test|spec|features)/}) } 29 | end 30 | spec.require_paths = ["lib"] 31 | 32 | spec.add_dependency "net-protocol" 33 | spec.add_dependency "time" 34 | end 35 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | 3 | module Test 4 | module Unit 5 | class TestCase 6 | def windows? platform = RUBY_PLATFORM 7 | /mswin|mingw/ =~ platform 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/net/fixtures/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | regen_certs: 4 | touch server.key 5 | make server.crt 6 | 7 | cacert.pem: server.key 8 | openssl req -new -x509 -days 3650 -key server.key -out cacert.pem -subj "/C=JP/ST=Shimane/L=Matz-e city/O=Ruby Core Team/CN=Ruby Test CA/emailAddress=security@ruby-lang.org" 9 | 10 | server.csr: 11 | openssl req -new -key server.key -out server.csr -subj "/C=JP/ST=Shimane/O=Ruby Core Team/OU=Ruby Test/CN=localhost" 12 | 13 | server.crt: server.csr cacert.pem 14 | openssl x509 -days 3650 -CA cacert.pem -CAkey server.key -set_serial 00 -in server.csr -req -out server.crt 15 | rm server.csr 16 | -------------------------------------------------------------------------------- /test/net/fixtures/cacert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+zCCAuOgAwIBAgIUGMvHl3EhtKPKcgc3NQSAYfFuC+8wDQYJKoZIhvcNAQEL 3 | BQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRQwEgYDVQQHDAtN 4 | YXR6LWUgY2l0eTEXMBUGA1UECgwOUnVieSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1 5 | YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJARYWc2VjdXJpdHlAcnVieS1sYW5nLm9y 6 | ZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEyMjkxMTQ3MjNaMIGMMQswCQYDVQQGEwJK 7 | UDEQMA4GA1UECAwHU2hpbWFuZTEUMBIGA1UEBwwLTWF0ei1lIGNpdHkxFzAVBgNV 8 | BAoMDlJ1YnkgQ29yZSBUZWFtMRUwEwYDVQQDDAxSdWJ5IFRlc3QgQ0ExJTAjBgkq 9 | hkiG9w0BCQEWFnNlY3VyaXR5QHJ1YnktbGFuZy5vcmcwggEiMA0GCSqGSIb3DQEB 10 | AQUAA4IBDwAwggEKAoIBAQCw+egZQ6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI 11 | +1GSqyi1bFBgsRjM0THllIdMbKmJtWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0f 12 | qXmG8UTz0VTWdlAXXmhUs6lSADvAaIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0 13 | yg+801SXzoFTTa+UGIRLE66jH51aa5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIe 14 | NWMF32wHqIOOPvQcWV3M5D2vxJEj702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1 15 | JNPc/n3dVUm+fM6NoDXPoLP7j55G9zKyqGtGAWXAj1MTAgMBAAGjUzBRMB0GA1Ud 16 | DgQWBBSJGVleDvFp9cu9R+E0/OKYzGkwkTAfBgNVHSMEGDAWgBSJGVleDvFp9cu9 17 | R+E0/OKYzGkwkTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBl 18 | 8GLB8skAWlkSw/FwbUmEV3zyqu+p7PNP5YIYoZs0D74e7yVulGQ6PKMZH5hrZmHo 19 | orFSQU+VUUirG8nDGj7Rzce8WeWBxsaDGC8CE2dq6nC6LuUwtbdMnBrH0LRWAz48 20 | jGFF3jHtVz8VsGfoZTZCjukWqNXvU6hETT9GsfU+PZqbqcTVRPH52+XgYayKdIbD 21 | r97RM4X3+aXBHcUW0b76eyyi65RR/Xtvn8ioZt2AdX7T2tZzJyXJN3Hupp77s6Ui 22 | AZR35SToHCZeTZD12YBvLBdaTPLZN7O/Q/aAO9ZiJaZ7SbFOjz813B2hxXab4Fob 23 | 2uJX6eMWTVxYK5D4M9lm 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /test/net/fixtures/dhparams.pem: -------------------------------------------------------------------------------- 1 | DH Parameters: (2048 bit) 2 | prime: 3 | 00:ec:4e:a4:06:b6:22:ca:f9:8a:00:cc:d0:ee:2f: 4 | 16:bf:05:64:f5:8f:fe:7f:c4:bb:b0:24:cd:ef:5d: 5 | 8a:90:ad:dc:a9:dd:63:84:90:d8:25:ba:d8:78:d5: 6 | 77:91:42:0a:84:fc:56:1e:13:9b:1c:aa:43:d5:1f: 7 | 38:52:92:fe:b3:66:f9:e7:e8:8c:77:a1:a6:2f:b3: 8 | 98:98:d2:13:fc:57:1c:2a:14:dc:bd:e6:9b:54:19: 9 | 99:4f:ce:81:64:a6:32:7f:8e:61:50:5f:45:3a:e5: 10 | 0c:f7:13:f3:b8:ad:d5:77:ca:09:42:f7:d8:30:27: 11 | 7b:2c:f0:b4:b5:a0:04:96:34:0b:47:81:1d:7f:c1: 12 | 3a:62:86:8e:7d:f8:13:7f:9a:b1:8b:09:23:9e:55: 13 | 59:41:cd:f0:86:09:c4:b7:d1:69:54:cb:d0:f5:e9: 14 | 27:c9:e1:81:e4:a1:df:6b:20:1c:df:e8:54:02:f2: 15 | 37:fc:2a:f7:d5:b3:6f:79:7e:70:22:78:79:18:3c: 16 | 75:14:68:4a:05:9f:ac:d4:7f:9a:79:db:9d:0a:6e: 17 | ec:0a:04:70:bf:c9:4a:59:81:a2:1f:33:9b:4a:66: 18 | bc:03:ce:8a:1b:e3:03:ec:ba:39:26:ab:90:dc:39: 19 | 41:a1:d8:f7:20:3c:8f:af:12:2f:f7:a9:6f:44:f1: 20 | 6d:03 21 | generator: 2 (0x2) 22 | -----BEGIN DH PARAMETERS----- 23 | MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY 24 | JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab 25 | VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6 26 | YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3 27 | 1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD 28 | 7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg== 29 | -----END DH PARAMETERS----- 30 | -------------------------------------------------------------------------------- /test/net/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYTCCAkkCAQAwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYD 3 | VQQIDAdTaGltYW5lMRQwEgYDVQQHDAtNYXR6LWUgY2l0eTEXMBUGA1UECgwOUnVi 4 | eSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJ 5 | ARYWc2VjdXJpdHlAcnVieS1sYW5nLm9yZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEy 6 | MjkxMTQ3MjNaMGAxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRcwFQYD 7 | VQQKDA5SdWJ5IENvcmUgVGVhbTESMBAGA1UECwwJUnVieSBUZXN0MRIwEAYDVQQD 8 | DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw+egZ 9 | Q6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI+1GSqyi1bFBgsRjM0THllIdMbKmJ 10 | tWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0fqXmG8UTz0VTWdlAXXmhUs6lSADvA 11 | aIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0yg+801SXzoFTTa+UGIRLE66jH51a 12 | a5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIeNWMF32wHqIOOPvQcWV3M5D2vxJEj 13 | 702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1JNPc/n3dVUm+fM6NoDXPoLP7j55G 14 | 9zKyqGtGAWXAj1MTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACtGNdj5TEtnJBYp 15 | M+LhBeU3oNteldfycEm993gJp6ghWZFg23oX8fVmyEeJr/3Ca9bAgDqg0t9a0npN 16 | oWKEY6wVKqcHgu3gSvThF5c9KhGbeDDmlTSVVNQmXWX0K2d4lS2cwZHH8mCm2mrY 17 | PDqlEkSc7k4qSiqigdS8i80Yk+lDXWsm8CjsiC93qaRM7DnS0WPQR0c16S95oM6G 18 | VklFKUSDAuFjw9aVWA/nahOucjn0w5fVW6lyIlkBslC1ChlaDgJmvhz+Ol3iMsE0 19 | kAmFNu2KKPVrpMWaBID49QwQTDyhetNLaVVFM88iUdA9JDoVMEuP1mm39JqyzHTu 20 | uBrdP4Q= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /test/net/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAsPnoGUOnrpiSqt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqso 3 | tWxQYLEYzNEx5ZSHTGypibVsJylvCfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE 4 | 89FU1nZQF15oVLOpUgA7wGiHuEVawrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNU 5 | l86BU02vlBiESxOuox+dWmuVV7vfYZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9s 6 | B6iDjj70HFldzOQ9r8SRI+9NirupPTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P59 7 | 3VVJvnzOjaA1z6Cz+4+eRvcysqhrRgFlwI9TEwIDAQABAoIBAEEYiyDP29vCzx/+ 8 | dS3LqnI5BjUuJhXUnc6AWX/PCgVAO+8A+gZRgvct7PtZb0sM6P9ZcLrweomlGezI 9 | FrL0/6xQaa8bBr/ve/a8155OgcjFo6fZEw3Dz7ra5fbSiPmu4/b/kvrg+Br1l77J 10 | aun6uUAs1f5B9wW+vbR7tzbT/mxaUeDiBzKpe15GwcvbJtdIVMa2YErtRjc1/5B2 11 | BGVXyvlJv0SIlcIEMsHgnAFOp1ZgQ08aDzvilLq8XVMOahAhP1O2A3X8hKdXPyrx 12 | IVWE9bS9ptTo+eF6eNl+d7htpKGEZHUxinoQpWEBTv+iOoHsVunkEJ3vjLP3lyI/ 13 | fY0NQ1ECgYEA3RBXAjgvIys2gfU3keImF8e/TprLge1I2vbWmV2j6rZCg5r/AS0u 14 | pii5CvJ5/T5vfJPNgPBy8B/yRDs+6PJO1GmnlhOkG9JAIPkv0RBZvR0PMBtbp6nT 15 | Y3yo1lwamBVBfY6rc0sLTzosZh2aGoLzrHNMQFMGaauORzBFpY5lU50CgYEAzPHl 16 | u5DI6Xgep1vr8QvCUuEesCOgJg8Yh1UqVoY/SmQh6MYAv1I9bLGwrb3WW/7kqIoD 17 | fj0aQV5buVZI2loMomtU9KY5SFIsPV+JuUpy7/+VE01ZQM5FdY8wiYCQiVZYju9X 18 | Wz5LxMNoz+gT7pwlLCsC4N+R8aoBk404aF1gum8CgYAJ7VTq7Zj4TFV7Soa/T1eE 19 | k9y8a+kdoYk3BASpCHJ29M5R2KEA7YV9wrBklHTz8VzSTFTbKHEQ5W5csAhoL5Fo 20 | qoHzFFi3Qx7MHESQb9qHyolHEMNx6QdsHUn7rlEnaTTyrXh3ifQtD6C0yTmFXUIS 21 | CW9wKApOrnyKJ9nI0HcuZQKBgQCMtoV6e9VGX4AEfpuHvAAnMYQFgeBiYTkBKltQ 22 | XwozhH63uMMomUmtSG87Sz1TmrXadjAhy8gsG6I0pWaN7QgBuFnzQ/HOkwTm+qKw 23 | AsrZt4zeXNwsH7QXHEJCFnCmqw9QzEoZTrNtHJHpNboBuVnYcoueZEJrP8OnUG3r 24 | UjmopwKBgAqB2KYYMUqAOvYcBnEfLDmyZv9BTVNHbR2lKkMYqv5LlvDaBxVfilE0 25 | 2riO4p6BaAdvzXjKeRrGNEKoHNBpOSfYCOM16NjL8hIZB1CaV3WbT5oY+jp7Mzd5 26 | 7d56RZOE+ERK2uz/7JX9VSsM/LbH9pJibd4e8mikDS9ntciqOH/3 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/net/ftp/test_buffered_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/ftp" 4 | require "test/unit" 5 | require "ostruct" 6 | require "stringio" 7 | 8 | class BufferedSocketTest < Test::Unit::TestCase 9 | def test_gets_empty 10 | sock = create_buffered_socket("") 11 | assert_equal(nil, sock.gets) 12 | end 13 | 14 | def test_gets_one_line 15 | sock = create_buffered_socket("foo\n") 16 | assert_equal("foo\n", sock.gets) 17 | end 18 | 19 | def test_gets_one_line_without_term 20 | sock = create_buffered_socket("foo") 21 | assert_equal("foo", sock.gets) 22 | end 23 | 24 | def test_gets_two_lines 25 | sock = create_buffered_socket("foo\nbar\n") 26 | assert_equal("foo\n", sock.gets) 27 | assert_equal("bar\n", sock.gets) 28 | end 29 | 30 | def test_gets_two_lines_without_term 31 | sock = create_buffered_socket("foo\nbar") 32 | assert_equal("foo\n", sock.gets) 33 | assert_equal("bar", sock.gets) 34 | end 35 | 36 | def test_read_nil 37 | sock = create_buffered_socket("foo\nbar") 38 | assert_equal("foo\nbar", sock.read) 39 | assert_equal("", sock.read) 40 | end 41 | 42 | private 43 | 44 | def create_buffered_socket(s) 45 | io = StringIO.new(s) 46 | return Net::FTP::BufferedSocket.new(io) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/net/ftp/test_ftp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/ftp" 4 | require "test/unit" 5 | require "ostruct" 6 | require "stringio" 7 | require "tempfile" 8 | require "tmpdir" 9 | 10 | class FTPTest < Test::Unit::TestCase 11 | SERVER_NAME = "localhost" 12 | SERVER_ADDR = 13 | begin 14 | Addrinfo.getaddrinfo(SERVER_NAME, 0, nil, :STREAM)[0].ip_address 15 | rescue SocketError 16 | "127.0.0.1" 17 | end 18 | CA_FILE = File.expand_path("../fixtures/cacert.pem", __dir__) 19 | SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) 20 | SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) 21 | 22 | def mjit_enabled? 23 | if defined?(RubyVM::JIT) 24 | RubyVM::JIT.enabled? 25 | elsif defined?(RubyVM::MJIT) 26 | RubyVM::MJIT.enabled? 27 | else 28 | false 29 | end 30 | end 31 | 32 | def setup 33 | @thread = nil 34 | @default_passive = Net::FTP.default_passive 35 | Net::FTP.default_passive = false 36 | end 37 | 38 | def teardown 39 | Net::FTP.default_passive = @default_passive 40 | if @thread 41 | @thread.join 42 | end 43 | end 44 | 45 | def test_not_connected 46 | ftp = Net::FTP.new 47 | assert_raise(Net::FTPConnectionError) do 48 | ftp.quit 49 | end 50 | end 51 | 52 | def test_closed_when_not_connected 53 | ftp = Net::FTP.new 54 | assert_equal(true, ftp.closed?) 55 | assert_nothing_raised(Net::FTPConnectionError) do 56 | ftp.close 57 | end 58 | end 59 | 60 | def test_connect_fail 61 | server = create_ftp_server { |sock| 62 | sock.print("421 Service not available, closing control connection.\r\n") 63 | } 64 | begin 65 | ftp = Net::FTP.new 66 | assert_raise(Net::FTPTempError){ ftp.connect(SERVER_ADDR, server.port) } 67 | ensure 68 | ftp.close if ftp 69 | server.close 70 | end 71 | end 72 | 73 | def test_parse227 74 | ftp = Net::FTP.new(nil, use_pasv_ip: true) 75 | host, port = ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34)") 76 | assert_equal("192.168.0.1", host) 77 | assert_equal(3106, port) 78 | host, port = ftp.send(:parse227, "227 192,168,0,1,12,34") 79 | assert_equal("192.168.0.1", host) 80 | assert_equal(3106, port) 81 | assert_raise(Net::FTPReplyError) do 82 | ftp.send(:parse227, "500 Syntax error") 83 | end 84 | assert_raise(Net::FTPProtoError) do 85 | ftp.send(:parse227, "227 Entering Passive Mode") 86 | end 87 | assert_raise(Net::FTPProtoError) do 88 | ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1)") 89 | end 90 | assert_raise(Net::FTPProtoError) do 91 | ftp.send(:parse227, "227 ) foo bar (") 92 | end 93 | 94 | ftp = Net::FTP.new 95 | sock = OpenStruct.new 96 | sock.remote_address = OpenStruct.new 97 | sock.remote_address.ip_address = "10.0.0.1" 98 | ftp.instance_variable_set(:@bare_sock, sock) 99 | host, port = ftp.send(:parse227, "227 Entering Passive Mode (192,168,0,1,12,34)") 100 | assert_equal("10.0.0.1", host) 101 | end 102 | 103 | def test_parse229 104 | ftp = Net::FTP.new 105 | sock = OpenStruct.new 106 | sock.remote_address = OpenStruct.new 107 | sock.remote_address.ip_address = "1080:0000:0000:0000:0008:0800:200c:417a" 108 | ftp.instance_variable_set(:@bare_sock, sock) 109 | host, port = ftp.send(:parse229, "229 Entering Passive Mode (|||3106|)") 110 | assert_equal("1080:0000:0000:0000:0008:0800:200c:417a", host) 111 | assert_equal(3106, port) 112 | host, port = ftp.send(:parse229, "229 Entering Passive Mode (!!!3106!)") 113 | assert_equal("1080:0000:0000:0000:0008:0800:200c:417a", host) 114 | assert_equal(3106, port) 115 | host, port = ftp.send(:parse229, "229 Entering Passive Mode (~~~3106~)") 116 | assert_equal("1080:0000:0000:0000:0008:0800:200c:417a", host) 117 | assert_equal(3106, port) 118 | assert_raise(Net::FTPReplyError) do 119 | ftp.send(:parse229, "500 Syntax error") 120 | end 121 | assert_raise(Net::FTPProtoError) do 122 | ftp.send(:parse229, "229 Entering Passive Mode") 123 | end 124 | assert_raise(Net::FTPProtoError) do 125 | ftp.send(:parse229, "229 Entering Passive Mode (|!!3106!)") 126 | end 127 | assert_raise(Net::FTPProtoError) do 128 | ftp.send(:parse229, "229 Entering Passive Mode ( 3106 )") 129 | end 130 | assert_raise(Net::FTPProtoError) do 131 | ftp.send(:parse229, "229 Entering Passive Mode (\x7f\x7f\x7f3106\x7f)") 132 | end 133 | assert_raise(Net::FTPProtoError) do 134 | ftp.send(:parse229, "229 ) foo bar (") 135 | end 136 | end 137 | 138 | def test_parse_pasv_port 139 | ftp = Net::FTP.new 140 | assert_equal(12, ftp.send(:parse_pasv_port, "12")) 141 | assert_equal(3106, ftp.send(:parse_pasv_port, "12,34")) 142 | assert_equal(795192, ftp.send(:parse_pasv_port, "12,34,56")) 143 | assert_equal(203569230, ftp.send(:parse_pasv_port, "12,34,56,78")) 144 | end 145 | 146 | def test_login 147 | commands = [] 148 | server = create_ftp_server { |sock| 149 | sock.print("220 (test_ftp).\r\n") 150 | commands.push(sock.gets) 151 | sock.print("331 Please specify the password.\r\n") 152 | commands.push(sock.gets) 153 | sock.print("230 Login successful.\r\n") 154 | commands.push(sock.gets) 155 | sock.print("200 Switching to Binary mode.\r\n") 156 | } 157 | begin 158 | begin 159 | ftp = Net::FTP.new 160 | ftp.connect(SERVER_ADDR, server.port) 161 | ftp.login 162 | assert_match(/\AUSER /, commands.shift) 163 | assert_match(/\APASS /, commands.shift) 164 | assert_equal("TYPE I\r\n", commands.shift) 165 | assert_equal(nil, commands.shift) 166 | ensure 167 | ftp.close if ftp 168 | end 169 | ensure 170 | server.close 171 | end 172 | end 173 | 174 | def test_login_fail1 175 | commands = [] 176 | server = create_ftp_server { |sock| 177 | sock.print("220 (test_ftp).\r\n") 178 | commands.push(sock.gets) 179 | sock.print("502 Command not implemented.\r\n") 180 | } 181 | begin 182 | begin 183 | ftp = Net::FTP.new 184 | ftp.connect(SERVER_ADDR, server.port) 185 | assert_raise(Net::FTPPermError){ ftp.login } 186 | ensure 187 | ftp.close if ftp 188 | end 189 | ensure 190 | server.close 191 | end 192 | end 193 | 194 | def test_login_fail2 195 | commands = [] 196 | server = create_ftp_server { |sock| 197 | sock.print("220 (test_ftp).\r\n") 198 | commands.push(sock.gets) 199 | sock.print("331 Please specify the password.\r\n") 200 | commands.push(sock.gets) 201 | sock.print("530 Not logged in.\r\n") 202 | } 203 | begin 204 | begin 205 | ftp = Net::FTP.new 206 | ftp.connect(SERVER_ADDR, server.port) 207 | assert_raise(Net::FTPPermError){ ftp.login } 208 | ensure 209 | ftp.close if ftp 210 | end 211 | ensure 212 | server.close 213 | end 214 | end 215 | 216 | def test_implicit_login 217 | commands = [] 218 | server = create_ftp_server { |sock| 219 | sock.print("220 (test_ftp).\r\n") 220 | commands.push(sock.gets) 221 | sock.print("331 Please specify the password.\r\n") 222 | commands.push(sock.gets) 223 | sock.print("332 Need account for login.\r\n") 224 | commands.push(sock.gets) 225 | sock.print("230 Login successful.\r\n") 226 | commands.push(sock.gets) 227 | sock.print("200 Switching to Binary mode.\r\n") 228 | } 229 | begin 230 | begin 231 | ftp = Net::FTP.new(SERVER_ADDR, 232 | port: server.port, 233 | username: "foo", 234 | password: "bar", 235 | account: "baz") 236 | assert_equal("USER foo\r\n", commands.shift) 237 | assert_equal("PASS bar\r\n", commands.shift) 238 | assert_equal("ACCT baz\r\n", commands.shift) 239 | assert_equal("TYPE I\r\n", commands.shift) 240 | assert_equal(nil, commands.shift) 241 | ensure 242 | ftp.close if ftp 243 | end 244 | ensure 245 | server.close 246 | end 247 | end 248 | 249 | def test_s_open 250 | commands = [] 251 | server = create_ftp_server { |sock| 252 | sock.print("220 (test_ftp).\r\n") 253 | commands.push(sock.gets) 254 | sock.print("331 Please specify the password.\r\n") 255 | commands.push(sock.gets) 256 | sock.print("230 Login successful.\r\n") 257 | commands.push(sock.gets) 258 | sock.print("200 Switching to Binary mode.\r\n") 259 | } 260 | begin 261 | Net::FTP.open(SERVER_ADDR, port: server.port, username: "anonymous") do 262 | end 263 | assert_equal("USER anonymous\r\n", commands.shift) 264 | assert_equal("PASS anonymous@\r\n", commands.shift) 265 | assert_equal("TYPE I\r\n", commands.shift) 266 | assert_equal(nil, commands.shift) 267 | ensure 268 | server.close 269 | end 270 | end 271 | 272 | def test_s_new_timeout_options 273 | ftp = Net::FTP.new 274 | assert_equal(nil, ftp.open_timeout) 275 | assert_equal(60, ftp.read_timeout) 276 | 277 | ftp = Net::FTP.new(nil, open_timeout: 123, read_timeout: 234) 278 | assert_equal(123, ftp.open_timeout) 279 | assert_equal(234, ftp.read_timeout) 280 | end 281 | 282 | # TODO: How can we test open_timeout? sleep before accept cannot delay 283 | # connections. 284 | def _test_open_timeout_exceeded 285 | commands = [] 286 | server = create_ftp_server(0.2) { |sock| 287 | sock.print("220 (test_ftp).\r\n") 288 | commands.push(sock.gets) 289 | sock.print("331 Please specify the password.\r\n") 290 | commands.push(sock.gets) 291 | sock.print("230 Login successful.\r\n") 292 | commands.push(sock.gets) 293 | sock.print("200 Switching to Binary mode.\r\n") 294 | } 295 | begin 296 | begin 297 | ftp = Net::FTP.new 298 | ftp.open_timeout = 0.1 299 | ftp.connect(SERVER_ADDR, server.port) 300 | assert_raise(Net::OpenTimeout) do 301 | ftp.login 302 | end 303 | assert_match(/\AUSER /, commands.shift) 304 | assert_match(/\APASS /, commands.shift) 305 | assert_equal(nil, commands.shift) 306 | ensure 307 | ftp.close if ftp 308 | end 309 | ensure 310 | server.close 311 | end 312 | end 313 | 314 | def test_read_timeout_exceeded 315 | commands = [] 316 | server = create_ftp_server { |sock| 317 | sock.print("220 (test_ftp).\r\n") 318 | commands.push(sock.gets) 319 | sleep(0.1) 320 | sock.print("331 Please specify the password.\r\n") 321 | commands.push(sock.gets) 322 | sleep(2.0) # Net::ReadTimeout 323 | sock.print("230 Login successful.\r\n") 324 | commands.push(sock.gets) 325 | sleep(0.1) 326 | sock.print("200 Switching to Binary mode.\r\n") 327 | } 328 | begin 329 | begin 330 | ftp = Net::FTP.new 331 | ftp.read_timeout = 0.4 332 | ftp.connect(SERVER_ADDR, server.port) 333 | assert_raise(Net::ReadTimeout) do 334 | ftp.login 335 | end 336 | assert_match(/\AUSER /, commands.shift) 337 | assert_match(/\APASS /, commands.shift) 338 | assert_equal(nil, commands.shift) 339 | ensure 340 | ftp.close if ftp 341 | end 342 | ensure 343 | server.close 344 | end 345 | end 346 | 347 | def test_read_timeout_not_exceeded 348 | commands = [] 349 | server = create_ftp_server { |sock| 350 | sock.print("220 (test_ftp).\r\n") 351 | commands.push(sock.gets) 352 | sleep(0.1) 353 | sock.print("331 Please specify the password.\r\n") 354 | commands.push(sock.gets) 355 | sleep(0.1) 356 | sock.print("230 Login successful.\r\n") 357 | commands.push(sock.gets) 358 | sleep(0.1) 359 | sock.print("200 Switching to Binary mode.\r\n") 360 | } 361 | begin 362 | begin 363 | ftp = Net::FTP.new 364 | ftp.read_timeout = 1.0 365 | ftp.connect(SERVER_ADDR, server.port) 366 | ftp.login 367 | assert_match(/\AUSER /, commands.shift) 368 | assert_match(/\APASS /, commands.shift) 369 | assert_equal("TYPE I\r\n", commands.shift) 370 | assert_equal(nil, commands.shift) 371 | ensure 372 | ftp.close 373 | assert_equal(1.0, ftp.read_timeout) 374 | end 375 | ensure 376 | server.close 377 | end 378 | end 379 | 380 | def test_list_read_timeout_exceeded 381 | commands = [] 382 | list_lines = [ 383 | "-rw-r--r-- 1 0 0 0 Mar 30 11:22 foo.txt", 384 | "-rw-r--r-- 1 0 0 0 Mar 30 11:22 bar.txt", 385 | "-rw-r--r-- 1 0 0 0 Mar 30 11:22 baz.txt" 386 | ] 387 | server = create_ftp_server { |sock| 388 | sock.print("220 (test_ftp).\r\n") 389 | commands.push(sock.gets) 390 | sock.print("331 Please specify the password.\r\n") 391 | commands.push(sock.gets) 392 | sock.print("230 Login successful.\r\n") 393 | commands.push(sock.gets) 394 | sock.print("200 Switching to Binary mode.\r\n") 395 | commands.push(sock.gets) 396 | sock.print("200 Switching to ASCII mode.\r\n") 397 | line = sock.gets 398 | commands.push(line) 399 | host, port = process_port_or_eprt(sock, line) 400 | commands.push(sock.gets) 401 | sock.print("150 Here comes the directory listing.\r\n") 402 | begin 403 | conn = TCPSocket.new(host, port) 404 | list_lines.each_with_index do |l, i| 405 | if i == 1 406 | sleep(0.5) 407 | else 408 | sleep(0.1) 409 | end 410 | conn.print(l, "\r\n") 411 | end 412 | rescue Errno::EPIPE, Errno::ECONNRESET 413 | ensure 414 | assert_nil($!) 415 | conn.close 416 | end 417 | sock.print("226 Directory send OK.\r\n") 418 | } 419 | begin 420 | begin 421 | ftp = Net::FTP.new 422 | ftp.read_timeout = 0.2 423 | ftp.connect(SERVER_ADDR, server.port) 424 | ftp.login 425 | assert_match(/\AUSER /, commands.shift) 426 | assert_match(/\APASS /, commands.shift) 427 | assert_equal("TYPE I\r\n", commands.shift) 428 | assert_raise(Net::ReadTimeout) do 429 | ftp.list 430 | end 431 | assert_equal("TYPE A\r\n", commands.shift) 432 | assert_match(/\A(PORT|EPRT) /, commands.shift) 433 | assert_equal("LIST\r\n", commands.shift) 434 | assert_equal(nil, commands.shift) 435 | ensure 436 | ftp.close if ftp 437 | end 438 | ensure 439 | server.close 440 | end 441 | end 442 | 443 | def test_list_read_timeout_not_exceeded 444 | commands = [] 445 | list_lines = [ 446 | "-rw-r--r-- 1 0 0 0 Mar 30 11:22 foo.txt", 447 | "-rw-r--r-- 1 0 0 0 Mar 30 11:22 bar.txt", 448 | "-rw-r--r-- 1 0 0 0 Mar 30 11:22 baz.txt" 449 | ] 450 | server = create_ftp_server { |sock| 451 | sock.print("220 (test_ftp).\r\n") 452 | commands.push(sock.gets) 453 | sock.print("331 Please specify the password.\r\n") 454 | commands.push(sock.gets) 455 | sock.print("230 Login successful.\r\n") 456 | commands.push(sock.gets) 457 | sock.print("200 Switching to Binary mode.\r\n") 458 | commands.push(sock.gets) 459 | sock.print("200 Switching to ASCII mode.\r\n") 460 | line = sock.gets 461 | commands.push(line) 462 | host, port = process_port_or_eprt(sock, line) 463 | commands.push(sock.gets) 464 | sock.print("150 Here comes the directory listing.\r\n") 465 | conn = TCPSocket.new(host, port) 466 | list_lines.each do |l| 467 | sleep(0.1) 468 | conn.print(l, "\r\n") 469 | end 470 | conn.close 471 | sock.print("226 Directory send OK.\r\n") 472 | commands.push(sock.gets) 473 | sock.print("200 Switching to Binary mode.\r\n") 474 | } 475 | begin 476 | begin 477 | ftp = Net::FTP.new 478 | ftp.read_timeout = 1.0 479 | ftp.connect(SERVER_ADDR, server.port) 480 | ftp.login 481 | assert_match(/\AUSER /, commands.shift) 482 | assert_match(/\APASS /, commands.shift) 483 | assert_equal("TYPE I\r\n", commands.shift) 484 | assert_equal(list_lines, ftp.list) 485 | assert_equal("TYPE A\r\n", commands.shift) 486 | assert_match(/\A(PORT|EPRT) /, commands.shift) 487 | assert_equal("LIST\r\n", commands.shift) 488 | assert_equal("TYPE I\r\n", commands.shift) 489 | assert_equal(nil, commands.shift) 490 | ensure 491 | ftp.close if ftp 492 | end 493 | ensure 494 | server.close 495 | end 496 | end 497 | 498 | def test_list_fail 499 | commands = [] 500 | server = create_ftp_server { |sock| 501 | sock.print("220 (test_ftp).\r\n") 502 | commands.push(sock.gets) 503 | sock.print("331 Please specify the password.\r\n") 504 | commands.push(sock.gets) 505 | sock.print("230 Login successful.\r\n") 506 | commands.push(sock.gets) 507 | sock.print("200 Switching to Binary mode.\r\n") 508 | commands.push(sock.gets) 509 | sock.print("200 Switching to ASCII mode.\r\n") 510 | line = sock.gets 511 | commands.push(line) 512 | host, port = process_port_or_eprt(sock, line) 513 | commands.push(sock.gets) 514 | sock.print("553 Requested action not taken.\r\n") 515 | commands.push(sock.gets) 516 | sock.print("200 Switching to Binary mode.\r\n") 517 | [host, port] 518 | } 519 | begin 520 | begin 521 | ftp = Net::FTP.new 522 | ftp.connect(SERVER_ADDR, server.port) 523 | ftp.login 524 | assert_match(/\AUSER /, commands.shift) 525 | assert_match(/\APASS /, commands.shift) 526 | assert_equal("TYPE I\r\n", commands.shift) 527 | assert_raise(Net::FTPPermError){ ftp.list } 528 | assert_equal("TYPE A\r\n", commands.shift) 529 | assert_match(/\A(PORT|EPRT) /, commands.shift) 530 | assert_equal("LIST\r\n", commands.shift) 531 | assert_equal("TYPE I\r\n", commands.shift) 532 | assert_equal(nil, commands.shift) 533 | ensure 534 | ftp.close if ftp 535 | end 536 | ensure 537 | server.close 538 | end 539 | end 540 | 541 | def test_open_data_port_fail_no_leak 542 | commands = [] 543 | server = create_ftp_server { |sock| 544 | sock.print("220 (test_ftp).\r\n") 545 | commands.push(sock.gets) 546 | sock.print("331 Please specify the password.\r\n") 547 | commands.push(sock.gets) 548 | sock.print("230 Login successful.\r\n") 549 | commands.push(sock.gets) 550 | sock.print("200 Switching to Binary mode.\r\n") 551 | commands.push(sock.gets) 552 | sock.print("200 Switching to ASCII mode.\r\n") 553 | line = sock.gets 554 | commands.push(line) 555 | sock.print("421 Service not available, closing control connection.\r\n") 556 | commands.push(sock.gets) 557 | sock.print("200 Switching to Binary mode.\r\n") 558 | } 559 | begin 560 | begin 561 | ftp = Net::FTP.new 562 | ftp.connect(SERVER_ADDR, server.port) 563 | ftp.login 564 | assert_match(/\AUSER /, commands.shift) 565 | assert_match(/\APASS /, commands.shift) 566 | assert_equal("TYPE I\r\n", commands.shift) 567 | assert_raise(Net::FTPTempError){ ftp.list } 568 | assert_equal("TYPE A\r\n", commands.shift) 569 | assert_match(/\A(PORT|EPRT) /, commands.shift) 570 | assert_equal("TYPE I\r\n", commands.shift) 571 | assert_equal(nil, commands.shift) 572 | ensure 573 | ftp.close if ftp 574 | end 575 | ensure 576 | server.close 577 | end 578 | end 579 | 580 | def test_retrbinary_read_timeout_exceeded 581 | commands = [] 582 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 583 | server = create_ftp_server { |sock| 584 | sock.print("220 (test_ftp).\r\n") 585 | commands.push(sock.gets) 586 | sock.print("331 Please specify the password.\r\n") 587 | commands.push(sock.gets) 588 | sock.print("230 Login successful.\r\n") 589 | commands.push(sock.gets) 590 | sock.print("200 Switching to Binary mode.\r\n") 591 | line = sock.gets 592 | commands.push(line) 593 | host, port = process_port_or_eprt(sock, line) 594 | commands.push(sock.gets) 595 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 596 | conn = TCPSocket.new(host, port) 597 | sleep(0.1) 598 | conn.print(binary_data[0,1024]) 599 | sleep(1.0) 600 | conn.print(binary_data[1024, 1024]) rescue nil # may raise EPIPE or something 601 | conn.close 602 | sock.print("226 Transfer complete.\r\n") 603 | } 604 | begin 605 | begin 606 | ftp = Net::FTP.new 607 | ftp.read_timeout = 0.5 608 | ftp.connect(SERVER_ADDR, server.port) 609 | ftp.login 610 | assert_match(/\AUSER /, commands.shift) 611 | assert_match(/\APASS /, commands.shift) 612 | assert_equal("TYPE I\r\n", commands.shift) 613 | buf = String.new 614 | assert_raise(Net::ReadTimeout) do 615 | ftp.retrbinary("RETR foo", 1024) do |s| 616 | buf << s 617 | end 618 | end 619 | assert_equal(1024, buf.bytesize) 620 | assert_equal(binary_data[0, 1024], buf) 621 | assert_match(/\A(PORT|EPRT) /, commands.shift) 622 | assert_equal("RETR foo\r\n", commands.shift) 623 | assert_equal(nil, commands.shift) 624 | ensure 625 | ftp.close unless ftp.closed? 626 | end 627 | ensure 628 | server.close 629 | end 630 | end 631 | 632 | def test_retrbinary_read_timeout_not_exceeded 633 | commands = [] 634 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 635 | server = create_ftp_server { |sock| 636 | sock.print("220 (test_ftp).\r\n") 637 | commands.push(sock.gets) 638 | sock.print("331 Please specify the password.\r\n") 639 | commands.push(sock.gets) 640 | sock.print("230 Login successful.\r\n") 641 | commands.push(sock.gets) 642 | sock.print("200 Switching to Binary mode.\r\n") 643 | line = sock.gets 644 | commands.push(line) 645 | host, port = process_port_or_eprt(sock, line) 646 | commands.push(sock.gets) 647 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 648 | conn = TCPSocket.new(host, port) 649 | binary_data.scan(/.{1,1024}/nm) do |s| 650 | sleep(0.2) 651 | conn.print(s) 652 | end 653 | conn.shutdown(Socket::SHUT_WR) 654 | conn.read 655 | conn.close 656 | sock.print("226 Transfer complete.\r\n") 657 | } 658 | begin 659 | begin 660 | ftp = Net::FTP.new 661 | ftp.read_timeout = 1.0 662 | ftp.connect(SERVER_ADDR, server.port) 663 | ftp.login 664 | assert_match(/\AUSER /, commands.shift) 665 | assert_match(/\APASS /, commands.shift) 666 | assert_equal("TYPE I\r\n", commands.shift) 667 | buf = String.new 668 | ftp.retrbinary("RETR foo", 1024) do |s| 669 | buf << s 670 | end 671 | assert_equal(binary_data.bytesize, buf.bytesize) 672 | assert_equal(binary_data, buf) 673 | assert_match(/\A(PORT|EPRT) /, commands.shift) 674 | assert_equal("RETR foo\r\n", commands.shift) 675 | assert_equal(nil, commands.shift) 676 | ensure 677 | ftp.close if ftp 678 | end 679 | ensure 680 | server.close 681 | end 682 | end 683 | 684 | def test_retrbinary_fail 685 | commands = [] 686 | server = create_ftp_server { |sock| 687 | sock.print("220 (test_ftp).\r\n") 688 | commands.push(sock.gets) 689 | sock.print("331 Please specify the password.\r\n") 690 | commands.push(sock.gets) 691 | sock.print("230 Login successful.\r\n") 692 | commands.push(sock.gets) 693 | sock.print("200 Switching to Binary mode.\r\n") 694 | line = sock.gets 695 | commands.push(line) 696 | host, port = process_port_or_eprt(sock, line) 697 | commands.push(sock.gets) 698 | sock.print("550 Requested action not taken.\r\n") 699 | [host, port] 700 | } 701 | begin 702 | begin 703 | ftp = Net::FTP.new 704 | ftp.connect(SERVER_ADDR, server.port) 705 | ftp.login 706 | assert_match(/\AUSER /, commands.shift) 707 | assert_match(/\APASS /, commands.shift) 708 | assert_equal("TYPE I\r\n", commands.shift) 709 | assert_raise(Net::FTPPermError){ ftp.retrbinary("RETR foo", 1024) } 710 | assert_match(/\A(PORT|EPRT) /, commands.shift) 711 | assert_equal("RETR foo\r\n", commands.shift) 712 | assert_equal(nil, commands.shift) 713 | ensure 714 | ftp.close if ftp 715 | end 716 | ensure 717 | server.close 718 | end 719 | end 720 | 721 | def test_getbinaryfile 722 | commands = [] 723 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 724 | server = create_ftp_server { |sock| 725 | sock.print("220 (test_ftp).\r\n") 726 | commands.push(sock.gets) 727 | sock.print("331 Please specify the password.\r\n") 728 | commands.push(sock.gets) 729 | sock.print("230 Login successful.\r\n") 730 | commands.push(sock.gets) 731 | sock.print("200 Switching to Binary mode.\r\n") 732 | line = sock.gets 733 | commands.push(line) 734 | host, port = process_port_or_eprt(sock, line) 735 | commands.push(sock.gets) 736 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 737 | conn = TCPSocket.new(host, port) 738 | binary_data.scan(/.{1,1024}/nm) do |s| 739 | conn.print(s) 740 | end 741 | conn.shutdown(Socket::SHUT_WR) 742 | conn.read 743 | conn.close 744 | sock.print("226 Transfer complete.\r\n") 745 | } 746 | begin 747 | begin 748 | ftp = Net::FTP.new 749 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 750 | ftp.connect(SERVER_ADDR, server.port) 751 | ftp.login 752 | assert_match(/\AUSER /, commands.shift) 753 | assert_match(/\APASS /, commands.shift) 754 | assert_equal("TYPE I\r\n", commands.shift) 755 | buf = ftp.getbinaryfile("foo", nil) 756 | assert_equal(binary_data, buf) 757 | assert_equal(Encoding::ASCII_8BIT, buf.encoding) 758 | assert_match(/\A(PORT|EPRT) /, commands.shift) 759 | assert_equal("RETR foo\r\n", commands.shift) 760 | assert_equal(nil, commands.shift) 761 | ensure 762 | ftp.close if ftp 763 | end 764 | ensure 765 | server.close 766 | end 767 | end 768 | 769 | def test_getbinaryfile_empty 770 | commands = [] 771 | binary_data = "" 772 | server = create_ftp_server { |sock| 773 | sock.print("220 (test_ftp).\r\n") 774 | commands.push(sock.gets) 775 | sock.print("331 Please specify the password.\r\n") 776 | commands.push(sock.gets) 777 | sock.print("230 Login successful.\r\n") 778 | commands.push(sock.gets) 779 | sock.print("200 Switching to Binary mode.\r\n") 780 | line = sock.gets 781 | commands.push(line) 782 | host, port = process_port_or_eprt(sock, line) 783 | commands.push(sock.gets) 784 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 785 | conn = TCPSocket.new(host, port) 786 | conn.shutdown(Socket::SHUT_WR) 787 | conn.read 788 | conn.close 789 | sock.print("226 Transfer complete.\r\n") 790 | } 791 | begin 792 | begin 793 | ftp = Net::FTP.new 794 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 795 | ftp.connect(SERVER_ADDR, server.port) 796 | ftp.login 797 | assert_match(/\AUSER /, commands.shift) 798 | assert_match(/\APASS /, commands.shift) 799 | assert_equal("TYPE I\r\n", commands.shift) 800 | buf = ftp.getbinaryfile("foo", nil) 801 | assert_equal(binary_data, buf) 802 | assert_equal(Encoding::ASCII_8BIT, buf.encoding) 803 | assert_match(/\A(PORT|EPRT) /, commands.shift) 804 | assert_equal("RETR foo\r\n", commands.shift) 805 | assert_equal(nil, commands.shift) 806 | ensure 807 | ftp.close if ftp 808 | end 809 | ensure 810 | server.close 811 | end 812 | end 813 | 814 | def test_getbinaryfile_with_filename_and_block 815 | commands = [] 816 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 817 | server = create_ftp_server { |sock| 818 | sock.print("220 (test_ftp).\r\n") 819 | commands.push(sock.gets) 820 | sock.print("331 Please specify the password.\r\n") 821 | commands.push(sock.gets) 822 | sock.print("230 Login successful.\r\n") 823 | commands.push(sock.gets) 824 | sock.print("200 Switching to Binary mode.\r\n") 825 | line = sock.gets 826 | commands.push(line) 827 | host, port = process_port_or_eprt(sock, line) 828 | commands.push(sock.gets) 829 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 830 | conn = TCPSocket.new(host, port) 831 | binary_data.scan(/.{1,1024}/nm) do |s| 832 | conn.print(s) 833 | end 834 | conn.shutdown(Socket::SHUT_WR) 835 | conn.read 836 | conn.close 837 | sock.print("226 Transfer complete.\r\n") 838 | } 839 | begin 840 | begin 841 | ftp = Net::FTP.new 842 | ftp.connect(SERVER_ADDR, server.port) 843 | ftp.login 844 | assert_match(/\AUSER /, commands.shift) 845 | assert_match(/\APASS /, commands.shift) 846 | assert_equal("TYPE I\r\n", commands.shift) 847 | Tempfile.create("foo", external_encoding: "ASCII-8BIT") do |f| 848 | f.binmode 849 | buf = String.new 850 | res = ftp.getbinaryfile("foo", f.path) { |s| 851 | buf << s 852 | } 853 | assert_equal(nil, res) 854 | assert_equal(binary_data, buf) 855 | assert_equal(Encoding::ASCII_8BIT, buf.encoding) 856 | assert_equal(binary_data, f.read) 857 | end 858 | assert_match(/\A(PORT|EPRT) /, commands.shift) 859 | assert_equal("RETR foo\r\n", commands.shift) 860 | assert_equal(nil, commands.shift) 861 | ensure 862 | ftp.close if ftp 863 | end 864 | ensure 865 | server.close 866 | end 867 | end 868 | 869 | def test_getbinaryfile_error 870 | commands = [] 871 | server = create_ftp_server { |sock| 872 | sock.print("220 (test_ftp).\r\n") 873 | commands.push(sock.gets) 874 | sock.print("331 Please specify the password.\r\n") 875 | commands.push(sock.gets) 876 | sock.print("230 Login successful.\r\n") 877 | commands.push(sock.gets) 878 | sock.print("200 Switching to Binary mode.\r\n") 879 | line = sock.gets 880 | commands.push(line) 881 | sock.print("450 No Dice\r\n") 882 | } 883 | begin 884 | begin 885 | ftp = Net::FTP.new 886 | ftp.passive = true 887 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 888 | ftp.connect(SERVER_ADDR, server.port) 889 | ftp.login 890 | assert_match(/\AUSER /, commands.shift) 891 | assert_match(/\APASS /, commands.shift) 892 | assert_equal("TYPE I\r\n", commands.shift) 893 | assert_raise(Net::FTPTempError) {ftp.getbinaryfile("foo", nil)} 894 | assert_match(/\A(PASV|EPSV)\r\n/, commands.shift) 895 | ensure 896 | ftp.close if ftp 897 | end 898 | ensure 899 | server.close 900 | end 901 | end 902 | 903 | def test_storbinary 904 | commands = [] 905 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 906 | stored_data = nil 907 | server = create_ftp_server { |sock| 908 | sock.print("220 (test_ftp).\r\n") 909 | commands.push(sock.gets) 910 | sock.print("331 Please specify the password.\r\n") 911 | commands.push(sock.gets) 912 | sock.print("230 Login successful.\r\n") 913 | commands.push(sock.gets) 914 | sock.print("200 Switching to Binary mode.\r\n") 915 | line = sock.gets 916 | commands.push(line) 917 | host, port = process_port_or_eprt(sock, line) 918 | commands.push(sock.gets) 919 | sock.print("150 Opening BINARY mode data connection for foo\r\n") 920 | conn = TCPSocket.new(host, port) 921 | stored_data = conn.read 922 | conn.close 923 | sock.print("226 Transfer complete.\r\n") 924 | } 925 | begin 926 | begin 927 | ftp = Net::FTP.new 928 | ftp.connect(SERVER_ADDR, server.port) 929 | ftp.login 930 | assert_match(/\AUSER /, commands.shift) 931 | assert_match(/\APASS /, commands.shift) 932 | assert_equal("TYPE I\r\n", commands.shift) 933 | ftp.storbinary("STOR foo", StringIO.new(binary_data), 1024) 934 | assert_equal(binary_data, stored_data) 935 | assert_match(/\A(PORT|EPRT) /, commands.shift) 936 | assert_equal("STOR foo\r\n", commands.shift) 937 | assert_equal(nil, commands.shift) 938 | ensure 939 | ftp.close if ftp 940 | end 941 | ensure 942 | server.close 943 | end 944 | end 945 | 946 | def test_storbinary_fail 947 | commands = [] 948 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 949 | server = create_ftp_server { |sock| 950 | sock.print("220 (test_ftp).\r\n") 951 | commands.push(sock.gets) 952 | sock.print("331 Please specify the password.\r\n") 953 | commands.push(sock.gets) 954 | sock.print("230 Login successful.\r\n") 955 | commands.push(sock.gets) 956 | sock.print("200 Switching to Binary mode.\r\n") 957 | line = sock.gets 958 | commands.push(line) 959 | host, port = process_port_or_eprt(sock, line) 960 | commands.push(sock.gets) 961 | sock.print("452 Requested file action aborted.\r\n") 962 | [host, port] 963 | } 964 | begin 965 | begin 966 | ftp = Net::FTP.new 967 | ftp.connect(SERVER_ADDR, server.port) 968 | ftp.login 969 | assert_match(/\AUSER /, commands.shift) 970 | assert_match(/\APASS /, commands.shift) 971 | assert_equal("TYPE I\r\n", commands.shift) 972 | assert_raise(Net::FTPTempError){ ftp.storbinary("STOR foo", StringIO.new(binary_data), 1024) } 973 | assert_match(/\A(PORT|EPRT) /, commands.shift) 974 | assert_equal("STOR foo\r\n", commands.shift) 975 | assert_equal(nil, commands.shift) 976 | ensure 977 | ftp.close if ftp 978 | end 979 | ensure 980 | server.close 981 | end 982 | end 983 | 984 | def test_retrlines 985 | commands = [] 986 | text_data = < port, 1835 | :ssl => true) 1836 | rescue SystemCallError 1837 | omit $! 1838 | end 1839 | end 1840 | end 1841 | end 1842 | 1843 | def test_tls_with_ca_file 1844 | assert_nothing_raised do 1845 | tls_test do |port| 1846 | begin 1847 | Net::FTP.new(SERVER_NAME, 1848 | :port => port, 1849 | :ssl => { :ca_file => CA_FILE }) 1850 | rescue SystemCallError 1851 | omit $! 1852 | end 1853 | end 1854 | end 1855 | end 1856 | 1857 | def test_tls_verify_none 1858 | assert_nothing_raised do 1859 | tls_test do |port| 1860 | Net::FTP.new(SERVER_ADDR, 1861 | :port => port, 1862 | :ssl => { :verify_mode => OpenSSL::SSL::VERIFY_NONE }) 1863 | end 1864 | end 1865 | end 1866 | 1867 | def test_tls_post_connection_check 1868 | assert_raise(OpenSSL::SSL::SSLError) do 1869 | tls_test do |port| 1870 | # SERVER_ADDR is different from the hostname in the certificate, 1871 | # so the following code should raise a SSLError. 1872 | Net::FTP.new(SERVER_ADDR, 1873 | :port => port, 1874 | :ssl => { :ca_file => CA_FILE }) 1875 | end 1876 | end 1877 | end 1878 | 1879 | def test_active_private_data_connection 1880 | server = TCPServer.new(SERVER_ADDR, 0) 1881 | port = server.addr[1] 1882 | commands = [] 1883 | session_reused_for_data_connection = nil 1884 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 1885 | @thread = Thread.start do 1886 | sock = server.accept 1887 | begin 1888 | sock.print("220 (test_ftp).\r\n") 1889 | commands.push(sock.gets) 1890 | sock.print("234 AUTH success.\r\n") 1891 | ctx = OpenSSL::SSL::SSLContext.new 1892 | ctx.ca_file = CA_FILE 1893 | ctx.key = File.open(SERVER_KEY) { |f| 1894 | OpenSSL::PKey::RSA.new(f) 1895 | } 1896 | ctx.cert = File.open(SERVER_CERT) { |f| 1897 | OpenSSL::X509::Certificate.new(f) 1898 | } 1899 | sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) 1900 | sock.sync_close = true 1901 | begin 1902 | sock.accept 1903 | commands.push(sock.gets) 1904 | sock.print("200 PSBZ success.\r\n") 1905 | commands.push(sock.gets) 1906 | sock.print("200 PROT success.\r\n") 1907 | commands.push(sock.gets) 1908 | sock.print("331 Please specify the password.\r\n") 1909 | commands.push(sock.gets) 1910 | sock.print("230 Login successful.\r\n") 1911 | commands.push(sock.gets) 1912 | sock.print("200 Switching to Binary mode.\r\n") 1913 | line = sock.gets 1914 | commands.push(line) 1915 | host, port = process_port_or_eprt(sock, line) 1916 | commands.push(sock.gets) 1917 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 1918 | conn = TCPSocket.new(host, port) 1919 | conn = OpenSSL::SSL::SSLSocket.new(conn, ctx) 1920 | conn.sync_close = true 1921 | conn.accept 1922 | session_reused_for_data_connection = conn.session_reused? 1923 | binary_data.scan(/.{1,1024}/nm) do |s| 1924 | conn.print(s) 1925 | end 1926 | conn.close 1927 | sock.print("226 Transfer complete.\r\n") 1928 | rescue OpenSSL::SSL::SSLError 1929 | end 1930 | ensure 1931 | sock.close 1932 | server.close 1933 | end 1934 | end 1935 | ftp = Net::FTP.new(SERVER_NAME, 1936 | port: port, 1937 | ssl: { ca_file: CA_FILE }, 1938 | passive: false) 1939 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 1940 | begin 1941 | assert_equal("AUTH TLS\r\n", commands.shift) 1942 | assert_equal("PBSZ 0\r\n", commands.shift) 1943 | assert_equal("PROT P\r\n", commands.shift) 1944 | ftp.login 1945 | assert_match(/\AUSER /, commands.shift) 1946 | assert_match(/\APASS /, commands.shift) 1947 | assert_equal("TYPE I\r\n", commands.shift) 1948 | buf = ftp.getbinaryfile("foo", nil) 1949 | assert_equal(binary_data, buf) 1950 | assert_equal(Encoding::ASCII_8BIT, buf.encoding) 1951 | assert_match(/\A(PORT|EPRT) /, commands.shift) 1952 | assert_equal("RETR foo\r\n", commands.shift) 1953 | assert_equal(nil, commands.shift) 1954 | # FIXME: The new_session_cb is known broken for clients in OpenSSL 1.1.0h. 1955 | # See https://github.com/openssl/openssl/pull/5967 for details. 1956 | if RUBY_ENGINE != "jruby" && OpenSSL::OPENSSL_LIBRARY_VERSION !~ /OpenSSL 1.1.0h|LibreSSL/ 1957 | assert_equal(true, session_reused_for_data_connection) 1958 | end 1959 | ensure 1960 | ftp.close 1961 | end 1962 | end 1963 | 1964 | def test_passive_private_data_connection 1965 | server = TCPServer.new(SERVER_ADDR, 0) 1966 | port = server.addr[1] 1967 | commands = [] 1968 | session_reused_for_data_connection = nil 1969 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 1970 | @thread = Thread.start do 1971 | sock = server.accept 1972 | begin 1973 | sock.print("220 (test_ftp).\r\n") 1974 | commands.push(sock.gets) 1975 | sock.print("234 AUTH success.\r\n") 1976 | ctx = OpenSSL::SSL::SSLContext.new 1977 | ctx.ca_file = CA_FILE 1978 | ctx.key = File.open(SERVER_KEY) { |f| 1979 | OpenSSL::PKey::RSA.new(f) 1980 | } 1981 | ctx.cert = File.open(SERVER_CERT) { |f| 1982 | OpenSSL::X509::Certificate.new(f) 1983 | } 1984 | sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) 1985 | sock.sync_close = true 1986 | begin 1987 | sock.accept 1988 | commands.push(sock.gets) 1989 | sock.print("200 PSBZ success.\r\n") 1990 | commands.push(sock.gets) 1991 | sock.print("200 PROT success.\r\n") 1992 | commands.push(sock.gets) 1993 | sock.print("331 Please specify the password.\r\n") 1994 | commands.push(sock.gets) 1995 | sock.print("230 Login successful.\r\n") 1996 | commands.push(sock.gets) 1997 | sock.print("200 Switching to Binary mode.\r\n") 1998 | commands.push(sock.gets) 1999 | data_server = create_data_connection_server(sock) 2000 | commands.push(sock.gets) 2001 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 2002 | conn = data_server.accept 2003 | conn = OpenSSL::SSL::SSLSocket.new(conn, ctx) 2004 | conn.sync_close = true 2005 | conn.accept 2006 | session_reused_for_data_connection = conn.session_reused? 2007 | binary_data.scan(/.{1,1024}/nm) do |s| 2008 | conn.print(s) 2009 | end 2010 | conn.close 2011 | data_server.close 2012 | sock.print("226 Transfer complete.\r\n") 2013 | rescue OpenSSL::SSL::SSLError 2014 | end 2015 | ensure 2016 | sock.close 2017 | server.close 2018 | end 2019 | end 2020 | ftp = Net::FTP.new(SERVER_NAME, 2021 | port: port, 2022 | ssl: { ca_file: CA_FILE }, 2023 | passive: true) 2024 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 2025 | begin 2026 | assert_equal("AUTH TLS\r\n", commands.shift) 2027 | assert_equal("PBSZ 0\r\n", commands.shift) 2028 | assert_equal("PROT P\r\n", commands.shift) 2029 | ftp.login 2030 | assert_match(/\AUSER /, commands.shift) 2031 | assert_match(/\APASS /, commands.shift) 2032 | assert_equal("TYPE I\r\n", commands.shift) 2033 | buf = ftp.getbinaryfile("foo", nil) 2034 | assert_equal(binary_data, buf) 2035 | assert_equal(Encoding::ASCII_8BIT, buf.encoding) 2036 | assert_match(/\A(PASV|EPSV)\r\n/, commands.shift) 2037 | assert_equal("RETR foo\r\n", commands.shift) 2038 | assert_equal(nil, commands.shift) 2039 | # FIXME: The new_session_cb is known broken for clients in OpenSSL 1.1.0h. 2040 | if OpenSSL::OPENSSL_LIBRARY_VERSION !~ /OpenSSL 1.1.0h|LibreSSL/ 2041 | assert_equal(true, session_reused_for_data_connection) 2042 | end 2043 | ensure 2044 | ftp.close 2045 | end 2046 | end 2047 | 2048 | def test_active_clear_data_connection 2049 | server = TCPServer.new(SERVER_ADDR, 0) 2050 | port = server.addr[1] 2051 | commands = [] 2052 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 2053 | @thread = Thread.start do 2054 | sock = server.accept 2055 | begin 2056 | sock.print("220 (test_ftp).\r\n") 2057 | commands.push(sock.gets) 2058 | sock.print("234 AUTH success.\r\n") 2059 | ctx = OpenSSL::SSL::SSLContext.new 2060 | ctx.ca_file = CA_FILE 2061 | ctx.key = File.open(SERVER_KEY) { |f| 2062 | OpenSSL::PKey::RSA.new(f) 2063 | } 2064 | ctx.cert = File.open(SERVER_CERT) { |f| 2065 | OpenSSL::X509::Certificate.new(f) 2066 | } 2067 | sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) 2068 | sock.sync_close = true 2069 | begin 2070 | sock.accept 2071 | commands.push(sock.gets) 2072 | sock.print("331 Please specify the password.\r\n") 2073 | commands.push(sock.gets) 2074 | sock.print("230 Login successful.\r\n") 2075 | commands.push(sock.gets) 2076 | sock.print("200 Switching to Binary mode.\r\n") 2077 | line = sock.gets 2078 | commands.push(line) 2079 | host, port = process_port_or_eprt(sock, line) 2080 | commands.push(sock.gets) 2081 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 2082 | conn = TCPSocket.new(host, port) 2083 | binary_data.scan(/.{1,1024}/nm) do |s| 2084 | conn.print(s) 2085 | end 2086 | conn.close 2087 | sock.print("226 Transfer complete.\r\n") 2088 | rescue OpenSSL::SSL::SSLError 2089 | end 2090 | ensure 2091 | sock.close 2092 | server.close 2093 | end 2094 | end 2095 | ftp = Net::FTP.new(SERVER_NAME, 2096 | port: port, 2097 | ssl: { ca_file: CA_FILE }, 2098 | private_data_connection: false, 2099 | passive: false) 2100 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 2101 | begin 2102 | assert_equal("AUTH TLS\r\n", commands.shift) 2103 | ftp.login 2104 | assert_match(/\AUSER /, commands.shift) 2105 | assert_match(/\APASS /, commands.shift) 2106 | assert_equal("TYPE I\r\n", commands.shift) 2107 | buf = ftp.getbinaryfile("foo", nil) 2108 | assert_equal(binary_data, buf) 2109 | assert_equal(Encoding::ASCII_8BIT, buf.encoding) 2110 | assert_match(/\A(PORT|EPRT) /, commands.shift) 2111 | assert_equal("RETR foo\r\n", commands.shift) 2112 | assert_equal(nil, commands.shift) 2113 | ensure 2114 | ftp.close 2115 | end 2116 | end 2117 | 2118 | def test_passive_clear_data_connection 2119 | server = TCPServer.new(SERVER_ADDR, 0) 2120 | port = server.addr[1] 2121 | commands = [] 2122 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 2123 | @thread = Thread.start do 2124 | sock = server.accept 2125 | begin 2126 | sock.print("220 (test_ftp).\r\n") 2127 | commands.push(sock.gets) 2128 | sock.print("234 AUTH success.\r\n") 2129 | ctx = OpenSSL::SSL::SSLContext.new 2130 | ctx.ca_file = CA_FILE 2131 | ctx.key = File.open(SERVER_KEY) { |f| 2132 | OpenSSL::PKey::RSA.new(f) 2133 | } 2134 | ctx.cert = File.open(SERVER_CERT) { |f| 2135 | OpenSSL::X509::Certificate.new(f) 2136 | } 2137 | sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) 2138 | sock.sync_close = true 2139 | begin 2140 | sock.accept 2141 | commands.push(sock.gets) 2142 | sock.print("331 Please specify the password.\r\n") 2143 | commands.push(sock.gets) 2144 | sock.print("230 Login successful.\r\n") 2145 | commands.push(sock.gets) 2146 | sock.print("200 Switching to Binary mode.\r\n") 2147 | commands.push(sock.gets) 2148 | data_server = create_data_connection_server(sock) 2149 | commands.push(sock.gets) 2150 | sock.print("150 Opening BINARY mode data connection for foo (#{binary_data.size} bytes)\r\n") 2151 | conn = data_server.accept 2152 | binary_data.scan(/.{1,1024}/nm) do |s| 2153 | conn.print(s) 2154 | end 2155 | conn.close 2156 | data_server.close 2157 | sock.print("226 Transfer complete.\r\n") 2158 | rescue OpenSSL::SSL::SSLError 2159 | end 2160 | ensure 2161 | sock.close 2162 | server.close 2163 | end 2164 | end 2165 | ftp = Net::FTP.new(SERVER_NAME, 2166 | port: port, 2167 | ssl: { ca_file: CA_FILE }, 2168 | private_data_connection: false, 2169 | passive: true) 2170 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 2171 | begin 2172 | assert_equal("AUTH TLS\r\n", commands.shift) 2173 | ftp.login 2174 | assert_match(/\AUSER /, commands.shift) 2175 | assert_match(/\APASS /, commands.shift) 2176 | assert_equal("TYPE I\r\n", commands.shift) 2177 | buf = ftp.getbinaryfile("foo", nil) 2178 | assert_equal(binary_data, buf) 2179 | assert_equal(Encoding::ASCII_8BIT, buf.encoding) 2180 | assert_match(/\A(PASV|EPSV)\r\n/, commands.shift) 2181 | assert_equal("RETR foo\r\n", commands.shift) 2182 | assert_equal(nil, commands.shift) 2183 | ensure 2184 | ftp.close 2185 | end 2186 | end 2187 | 2188 | def test_tls_connect_timeout 2189 | server = TCPServer.new(SERVER_ADDR, 0) 2190 | port = server.addr[1] 2191 | commands = [] 2192 | sock = nil 2193 | @thread = Thread.start do 2194 | sock = server.accept 2195 | sock.print("220 (test_ftp).\r\n") 2196 | commands.push(sock.gets) 2197 | sock.print("234 AUTH success.\r\n") 2198 | end 2199 | begin 2200 | assert_raise(Net::OpenTimeout) do 2201 | Net::FTP.new(SERVER_NAME, 2202 | port: port, 2203 | ssl: { ca_file: CA_FILE }, 2204 | ssl_handshake_timeout: 0.1) 2205 | end 2206 | @thread.join 2207 | ensure 2208 | sock.close if sock 2209 | server.close 2210 | end 2211 | end 2212 | end 2213 | 2214 | def test_abort_tls 2215 | return unless defined?(OpenSSL) 2216 | 2217 | commands = [] 2218 | server = create_ftp_server { |sock| 2219 | sock.print("220 (test_ftp).\r\n") 2220 | commands.push(sock.gets) 2221 | sock.print("234 AUTH success.\r\n") 2222 | ctx = OpenSSL::SSL::SSLContext.new 2223 | ctx.ca_file = CA_FILE 2224 | ctx.key = File.open(SERVER_KEY) { |f| 2225 | OpenSSL::PKey::RSA.new(f) 2226 | } 2227 | ctx.cert = File.open(SERVER_CERT) { |f| 2228 | OpenSSL::X509::Certificate.new(f) 2229 | } 2230 | sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) 2231 | sock.sync_close = true 2232 | sock.accept 2233 | commands.push(sock.gets) 2234 | sock.print("200 PSBZ success.\r\n") 2235 | commands.push(sock.gets) 2236 | sock.print("200 PROT success.\r\n") 2237 | commands.push(sock.gets) 2238 | sock.print("331 Please specify the password.\r\n") 2239 | commands.push(sock.gets) 2240 | sock.print("230 Login successful.\r\n") 2241 | commands.push(sock.gets) 2242 | sock.print("200 Switching to Binary mode.\r\n") 2243 | commands.push(sock.gets) 2244 | sock.print("225 No transfer to ABOR.\r\n") 2245 | } 2246 | begin 2247 | begin 2248 | ftp = Net::FTP.new(SERVER_NAME, 2249 | port: server.port, 2250 | ssl: { ca_file: CA_FILE }) 2251 | ftp.read_timeout *= 5 if mjit_enabled? # for --jit-wait 2252 | assert_equal("AUTH TLS\r\n", commands.shift) 2253 | assert_equal("PBSZ 0\r\n", commands.shift) 2254 | assert_equal("PROT P\r\n", commands.shift) 2255 | ftp.login 2256 | assert_match(/\AUSER /, commands.shift) 2257 | assert_match(/\APASS /, commands.shift) 2258 | assert_equal("TYPE I\r\n", commands.shift) 2259 | ftp.abort 2260 | assert_equal("ABOR\r\n", commands.shift) 2261 | assert_equal(nil, commands.shift) 2262 | rescue RuntimeError, LoadError 2263 | # skip (require openssl) 2264 | ensure 2265 | ftp.close if ftp 2266 | end 2267 | ensure 2268 | server.close 2269 | end 2270 | end 2271 | 2272 | def test_getbinaryfile_command_injection 2273 | omit "| is not allowed in filename on Windows" if windows? 2274 | [false, true].each do |resume| 2275 | commands = [] 2276 | binary_data = (0..0xff).map {|i| i.chr}.join * 4 * 3 2277 | server = create_ftp_server { |sock| 2278 | sock.print("220 (test_ftp).\r\n") 2279 | commands.push(sock.gets) 2280 | sock.print("331 Please specify the password.\r\n") 2281 | commands.push(sock.gets) 2282 | sock.print("230 Login successful.\r\n") 2283 | commands.push(sock.gets) 2284 | sock.print("200 Switching to Binary mode.\r\n") 2285 | line = sock.gets 2286 | commands.push(line) 2287 | host, port = process_port_or_eprt(sock, line) 2288 | commands.push(sock.gets) 2289 | sock.print("150 Opening BINARY mode data connection for |echo hello (#{binary_data.size} bytes)\r\n") 2290 | conn = TCPSocket.new(host, port) 2291 | binary_data.scan(/.{1,1024}/nm) do |s| 2292 | conn.print(s) 2293 | end 2294 | conn.shutdown(Socket::SHUT_WR) 2295 | conn.read 2296 | conn.close 2297 | sock.print("226 Transfer complete.\r\n") 2298 | } 2299 | begin 2300 | chdir_to_tmpdir do 2301 | begin 2302 | ftp = Net::FTP.new 2303 | ftp.resume = resume 2304 | ftp.read_timeout = (mjit_enabled?) ? 300 : 0.2 # use large timeout for --jit-wait 2305 | ftp.connect(SERVER_ADDR, server.port) 2306 | ftp.login 2307 | assert_match(/\AUSER /, commands.shift) 2308 | assert_match(/\APASS /, commands.shift) 2309 | assert_equal("TYPE I\r\n", commands.shift) 2310 | ftp.getbinaryfile("|echo hello") 2311 | assert_equal(binary_data, File.binread("./|echo hello")) 2312 | assert_match(/\A(PORT|EPRT) /, commands.shift) 2313 | assert_equal("RETR |echo hello\r\n", commands.shift) 2314 | assert_equal(nil, commands.shift) 2315 | ensure 2316 | ftp.close if ftp 2317 | end 2318 | end 2319 | ensure 2320 | server.close 2321 | end 2322 | end 2323 | end 2324 | 2325 | def test_gettextfile_command_injection 2326 | omit "| is not allowed in filename on Windows" if windows? 2327 | commands = [] 2328 | text_data = <"file"}, "foo").file?) 11 | assert_equal(false, Net::FTP::MLSxEntry.new({"type"=>"dir"}, "foo").file?) 12 | assert_equal(false, Net::FTP::MLSxEntry.new({"type"=>"cdir"}, "foo").file?) 13 | assert_equal(false, Net::FTP::MLSxEntry.new({"type"=>"pdir"}, "foo").file?) 14 | end 15 | 16 | def test_directory? 17 | assert_equal(false, 18 | Net::FTP::MLSxEntry.new({"type"=>"file"}, "foo").directory?) 19 | assert_equal(true, 20 | Net::FTP::MLSxEntry.new({"type"=>"dir"}, "foo").directory?) 21 | assert_equal(true, 22 | Net::FTP::MLSxEntry.new({"type"=>"cdir"}, "foo").directory?) 23 | assert_equal(true, 24 | Net::FTP::MLSxEntry.new({"type"=>"pdir"}, "foo").directory?) 25 | end 26 | 27 | def test_appendable? 28 | assert_equal(true, 29 | Net::FTP::MLSxEntry.new({"perm"=>"a"}, "foo").appendable?) 30 | assert_equal(false, 31 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").appendable?) 32 | end 33 | 34 | def test_creatable? 35 | assert_equal(true, 36 | Net::FTP::MLSxEntry.new({"perm"=>"c"}, "foo").creatable?) 37 | assert_equal(false, 38 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").creatable?) 39 | end 40 | 41 | def test_deletable? 42 | assert_equal(true, 43 | Net::FTP::MLSxEntry.new({"perm"=>"d"}, "foo").deletable?) 44 | assert_equal(false, 45 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").deletable?) 46 | end 47 | 48 | def test_enterable? 49 | assert_equal(true, 50 | Net::FTP::MLSxEntry.new({"perm"=>"e"}, "foo").enterable?) 51 | assert_equal(false, 52 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").enterable?) 53 | end 54 | 55 | def test_renamable? 56 | assert_equal(true, 57 | Net::FTP::MLSxEntry.new({"perm"=>"f"}, "foo").renamable?) 58 | assert_equal(false, 59 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").renamable?) 60 | end 61 | 62 | def test_listable? 63 | assert_equal(true, 64 | Net::FTP::MLSxEntry.new({"perm"=>"l"}, "foo").listable?) 65 | assert_equal(false, 66 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").listable?) 67 | end 68 | 69 | def test_directory_makable? 70 | assert_equal(true, 71 | Net::FTP::MLSxEntry.new({"perm"=>"m"}, "foo"). 72 | directory_makable?) 73 | assert_equal(false, 74 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo"). 75 | directory_makable?) 76 | end 77 | 78 | def test_purgeable? 79 | assert_equal(true, 80 | Net::FTP::MLSxEntry.new({"perm"=>"p"}, "foo").purgeable?) 81 | assert_equal(false, 82 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").purgeable?) 83 | end 84 | 85 | def test_readable? 86 | assert_equal(true, 87 | Net::FTP::MLSxEntry.new({"perm"=>"r"}, "foo").readable?) 88 | assert_equal(false, 89 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").readable?) 90 | end 91 | 92 | def test_writable? 93 | assert_equal(true, 94 | Net::FTP::MLSxEntry.new({"perm"=>"w"}, "foo").writable?) 95 | assert_equal(false, 96 | Net::FTP::MLSxEntry.new({"perm"=>""}, "foo").writable?) 97 | end 98 | end 99 | --------------------------------------------------------------------------------