# frozen_string_literal: true
# = net/ftp.rb - FTP Client Library
# Written by Shugo Maeda <shugo@ruby-lang.org>.
# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
# and "Ruby In a Nutshell" (Matsumoto), used with permission.
# This library is distributed under the terms of the Ruby license.
# You can freely distribute/modify this library.
# It is included in the Ruby standard library.
# See the Net::FTP class for an overview.
require_relative "protocol"
class FTPError < StandardError; end
class FTPReplyError < FTPError; end
class FTPTempError < FTPError; end
class FTPPermError < FTPError; end
class FTPProtoError < FTPError; end
class FTPConnectionError < FTPError; end
# This class implements the File Transfer Protocol. If you have used a
# command-line FTP program, and are familiar with the commands, you will be
# able to use this class easily. Some extra features are included to take
# advantage of Ruby's style and strengths.
# ftp = Net::FTP.new('example.com')
# files = ftp.chdir('pub/lang/ruby/contrib')
# ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
# Net::FTP.open('example.com') do |ftp|
# files = ftp.chdir('pub/lang/ruby/contrib')
# ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
# The following are the methods most likely to be useful to users:
if defined?(OpenSSL::SSL)
DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
# When +true+, transfers are performed in binary mode. Default: +true+.
# When +true+, the connection is in passive mode. Default: +true+.
# When +true+, use the IP address in PASV responses. Otherwise, it uses
# the same IP address for the control connection. Default: +false+.
attr_accessor :use_pasv_ip
# When +true+, all traffic to and from the server is written
# to +$stdout+. Default: +false+.
attr_accessor :debug_mode
# Sets or retrieves the +resume+ status, which decides whether incomplete
# transfers are resumed or restarted. Default: +false+.
# Number of seconds to wait for the connection to open. Any number
# may be used, including Floats for fractional seconds. If the FTP
# object cannot open a connection in this many seconds, it raises a
# Net::OpenTimeout exception. The default value is +nil+.
attr_accessor :open_timeout
# Number of seconds to wait for the TLS handshake. Any number
# may be used, including Floats for fractional seconds. If the FTP
# object cannot complete the TLS handshake in this many seconds, it
# raises a Net::OpenTimeout exception. The default value is +nil+.
# If +ssl_handshake_timeout+ is +nil+, +open_timeout+ is used instead.
attr_accessor :ssl_handshake_timeout
# Number of seconds to wait for one block to be read (via one read(2)
# call). Any number may be used, including Floats for fractional
# seconds. If the FTP object cannot read data in this many seconds,
# it raises a Timeout::Error exception. The default value is 60 seconds.
attr_reader :read_timeout
# Setter for the read_timeout attribute.
# The server's welcome message.
# The server's last response code.
attr_reader :last_response_code
alias lastresp last_response_code
# The server's last response.
attr_reader :last_response
# When +true+, connections are in passive mode per default.
def self.default_passive=(value)
@@default_passive = value
# When +true+, connections are in passive mode per default.
# A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
# If a block is given, it is passed the +FTP+ object, which will be closed
# when the block finishes, or when an exception is raised.
def FTP.open(host, *args)
# Net::FTP.new(host = nil, options = {})
# Creates and returns a new +FTP+ object. If a +host+ is given, a connection
# +options+ is an option hash, each key of which is a symbol.
# The available options are:
# port:: Port number (default value is 21)
# ssl:: If options[:ssl] is true, then an attempt will be made
# to use SSL (now TLS) to connect to the server. For this
# to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL]
# extensions need to be installed. If options[:ssl] is a
# hash, it's passed to OpenSSL::SSL::SSLContext#set_params
# private_data_connection:: If true, TLS is used for data connections.
# Default: +true+ when options[:ssl] is true.
# username:: Username for login. If options[:username] is the string
# "anonymous" and the options[:password] is +nil+,
# "anonymous@" is used as a password.
# password:: Password for login.
# account:: Account information for ACCT.
# passive:: When +true+, the connection is in passive mode. Default:
# open_timeout:: Number of seconds to wait for the connection to open.
# See Net::FTP#open_timeout for details. Default: +nil+.
# read_timeout:: Number of seconds to wait for one block to be read.
# See Net::FTP#read_timeout for details. Default: +60+.
# ssl_handshake_timeout:: Number of seconds to wait for the TLS
# See Net::FTP#ssl_handshake_timeout for
# details. Default: +nil+.
# use_pasv_ip:: When +true+, use the IP address in PASV responses.
# Otherwise, it uses the same IP address for the control
# connection. Default: +false+.
# debug_mode:: When +true+, all traffic to and from the server is
# written to +$stdout+. Default: +false+.
def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil)
options = user_or_options.to_hash
# for backward compatibility
options[:username] = user_or_options
options[:password] = passwd
unless defined?(OpenSSL::SSL)
raise "SSL extension not installed"
ssl_params = options[:ssl] == true ? {} : options[:ssl]
@ssl_context = SSLContext.new
@ssl_context.set_params(ssl_params)
if defined?(VerifyCallbackProc)
@ssl_context.verify_callback = VerifyCallbackProc
@ssl_context.session_cache_mode =
OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
@ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
if options[:private_data_connection].nil?
@private_data_connection = true
@private_data_connection = options[:private_data_connection]
if options[:private_data_connection]
"private_data_connection can be set to true only when ssl is enabled"
@private_data_connection = false
if options[:passive].nil?
@passive = @@default_passive
@passive = options[:passive]
if options[:debug_mode].nil?
@debug_mode = options[:debug_mode]
@bare_sock = @sock = NullSocket.new
@open_timeout = options[:open_timeout]
@ssl_handshake_timeout = options[:ssl_handshake_timeout]
@read_timeout = options[:read_timeout] || 60
@use_pasv_ip = options[:use_pasv_ip] || false
connect(host, options[:port] || FTP_PORT)
login(options[:username], options[:password], options[:account])
# A setter to toggle transfers in binary mode.
# +newmode+ is either +true+ or +false+
send_type_command if @logged_in
# Sends a command to destination host, with the current binary sendmode
# If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
def send_type_command # :nodoc:
private :send_type_command
# Toggles transfers in binary mode and yields to a block.
# This preserves your current binary send mode, but allows a temporary
# transaction with binary sendmode of +newmode+.
# +newmode+ is either +true+ or +false+
def with_binary(newmode) # :nodoc:
def return_code # :nodoc:
warn("Net::FTP#return_code is obsolete and do nothing", uplevel: 1)
def return_code=(s) # :nodoc:
warn("Net::FTP#return_code= is obsolete and do nothing", uplevel: 1)
# Constructs a socket with +host+ and +port+.
# If SOCKSSocket is defined and the environment (ENV) defines
# SOCKS_SERVER, then a SOCKSSocket is returned, else a Socket is
def open_socket(host, port) # :nodoc:
return Timeout.timeout(@open_timeout, OpenTimeout) {
if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
SOCKSSocket.open(host, port)
def start_tls_session(sock)
ssl_sock = SSLSocket.new(sock, @ssl_context)
ssl_sock.sync_close = true
ssl_sock.hostname = @host if ssl_sock.respond_to? :hostname=
Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
# ProFTPD returns 425 for data connections if session is not reused.
ssl_sock.session = @ssl_session
ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout)
if @ssl_context.verify_mode != VERIFY_NONE
ssl_sock.post_connection_check(@host)
private :start_tls_session
# Establishes an FTP connection to host, optionally overriding the default
# port. If the environment variable +SOCKS_SERVER+ is set, sets up the
# connection through a SOCKS proxy. Raises an exception (typically
# <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
def connect(host, port = FTP_PORT)
print "connect: ", host, ", ", port, "\n"
@bare_sock = open_socket(host, port)
@sock = BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)
ssl_sock = start_tls_session(@bare_sock)
@sock = BufferedSSLSocket.new(ssl_sock, read_timeout: @read_timeout)
if @private_data_connection
rescue OpenSSL::SSL::SSLError, OpenTimeout
# Set the socket used to connect to the FTP server.
# May raise FTPReplyError if +get_greeting+ is false.
def set_socket(sock, get_greeting = true)
# If string +s+ includes the PASS command (password), then the contents of
# the password are cleaned from the string using "*"
def sanitize(s) # :nodoc:
return s[0, 5] + "*" * (s.length - 5)
# Ensures that +line+ has a control return / line feed (CRLF) and writes
def putline(line) # :nodoc:
print "put: ", sanitize(line), "\n"
raise ArgumentError, "A line must not contain CR or LF"
# Reads a line from the sock. If EOF, then it will raise EOFError
line = @sock.readline # if get EOF, raise EOFError
line.sub!(/(\r\n|\n|\r)\z/n, "")
print "get: ", sanitize(line), "\n"
# Receive a section of lines until the response code's match.
def getmultiline # :nodoc:
code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1)
end until lines.last.start_with?(delimiter)
return lines.join("\n") + "\n"
# Receives a response from the destination host.
# Returns the response code or raises FTPTempError, FTPPermError, or
@last_response = getmultiline
@last_response_code = @last_response[0, 3]
raise FTPTempError, @last_response
raise FTPPermError, @last_response
raise FTPProtoError, @last_response
# Raises FTPReplyError if the first position of the response code is not
if !resp.start_with?("2")
raise FTPReplyError, resp
# Sends a command and returns the response.