mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
7242e6a9b6
The TLS handshake timeout can be specified independently, because the TLS handshake doesn't start just after the underlying connection is established, but after the AUTH command is completed. It's also useful for testing purposes. However, if ssl_handshake_timeout is not specified, open_timeout is used instead for convenience. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@56919 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
1495 lines
41 KiB
Ruby
1495 lines
41 KiB
Ruby
# 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 "socket"
|
|
require "monitor"
|
|
require "net/protocol"
|
|
require "time"
|
|
begin
|
|
require "openssl"
|
|
rescue LoadError
|
|
end
|
|
|
|
module Net
|
|
|
|
# :stopdoc:
|
|
class FTPError < StandardError; end
|
|
class FTPReplyError < FTPError; end
|
|
class FTPTempError < FTPError; end
|
|
class FTPPermError < FTPError; end
|
|
class FTPProtoError < FTPError; end
|
|
class FTPConnectionError < FTPError; end
|
|
# :startdoc:
|
|
|
|
#
|
|
# 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.
|
|
#
|
|
# == Example
|
|
#
|
|
# require 'net/ftp'
|
|
#
|
|
# === Example 1
|
|
#
|
|
# ftp = Net::FTP.new('example.com')
|
|
# ftp.login
|
|
# files = ftp.chdir('pub/lang/ruby/contrib')
|
|
# files = ftp.list('n*')
|
|
# ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
|
|
# ftp.close
|
|
#
|
|
# === Example 2
|
|
#
|
|
# Net::FTP.open('example.com') do |ftp|
|
|
# ftp.login
|
|
# files = ftp.chdir('pub/lang/ruby/contrib')
|
|
# files = ftp.list('n*')
|
|
# ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
|
|
# end
|
|
#
|
|
# == Major Methods
|
|
#
|
|
# The following are the methods most likely to be useful to users:
|
|
# - FTP.open
|
|
# - #getbinaryfile
|
|
# - #gettextfile
|
|
# - #putbinaryfile
|
|
# - #puttextfile
|
|
# - #chdir
|
|
# - #nlst
|
|
# - #size
|
|
# - #rename
|
|
# - #delete
|
|
#
|
|
class FTP < Protocol
|
|
include MonitorMixin
|
|
if defined?(OpenSSL::SSL)
|
|
include OpenSSL
|
|
include SSL
|
|
end
|
|
|
|
# :stopdoc:
|
|
FTP_PORT = 21
|
|
CRLF = "\r\n"
|
|
DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
|
|
@@default_passive = true
|
|
# :startdoc:
|
|
|
|
# When +true+, transfers are performed in binary mode. Default: +true+.
|
|
attr_reader :binary
|
|
|
|
# When +true+, the connection is in passive mode. Default: +true+.
|
|
attr_accessor :passive
|
|
|
|
# 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+.
|
|
attr_accessor :resume
|
|
|
|
# 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.
|
|
def read_timeout=(sec)
|
|
@sock.read_timeout = sec
|
|
@read_timeout = sec
|
|
end
|
|
|
|
# The server's welcome message.
|
|
attr_reader :welcome
|
|
|
|
# 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.
|
|
# Default: +true+.
|
|
def self.default_passive=(value)
|
|
@@default_passive = value
|
|
end
|
|
|
|
# When +true+, connections are in passive mode per default.
|
|
# Default: +true+.
|
|
def self.default_passive
|
|
@@default_passive
|
|
end
|
|
|
|
#
|
|
# 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)
|
|
if block_given?
|
|
ftp = new(host, *args)
|
|
begin
|
|
yield ftp
|
|
ensure
|
|
ftp.close
|
|
end
|
|
else
|
|
new(host, *args)
|
|
end
|
|
end
|
|
|
|
# :call-seq:
|
|
# Net::FTP.new(host = nil, options = {})
|
|
#
|
|
# Creates and returns a new +FTP+ object. If a +host+ is given, a connection
|
|
# is made.
|
|
#
|
|
# +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
|
|
# as parameters.
|
|
# 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:
|
|
# +true+.
|
|
# 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
|
|
# handshake.
|
|
# See Net::FTP#ssl_handshake_timeout for
|
|
# details. Default: +nil+.
|
|
# 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)
|
|
super()
|
|
begin
|
|
options = user_or_options.to_hash
|
|
rescue NoMethodError
|
|
# for backward compatibility
|
|
options = {}
|
|
options[:username] = user_or_options
|
|
options[:password] = passwd
|
|
options[:account] = acct
|
|
end
|
|
@host = nil
|
|
if options[:ssl]
|
|
unless defined?(OpenSSL::SSL)
|
|
raise "SSL extension not installed"
|
|
end
|
|
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
|
|
end
|
|
@ssl_session = nil
|
|
if options[:private_data_connection].nil?
|
|
@private_data_connection = true
|
|
else
|
|
@private_data_connection = options[:private_data_connection]
|
|
end
|
|
else
|
|
@ssl_context = nil
|
|
if options[:private_data_connection]
|
|
raise ArgumentError,
|
|
"private_data_connection can be set to true only when ssl is enabled"
|
|
end
|
|
end
|
|
@binary = true
|
|
if options[:passive].nil?
|
|
@passive = @@default_passive
|
|
else
|
|
@passive = options[:passive]
|
|
end
|
|
if options[:debug_mode].nil?
|
|
@debug_mode = false
|
|
else
|
|
@debug_mode = options[:debug_mode]
|
|
end
|
|
@resume = false
|
|
@bare_sock = @sock = NullSocket.new
|
|
@logged_in = false
|
|
@open_timeout = options[:open_timeout]
|
|
@ssl_handshake_timeout = options[:ssl_handshake_timeout]
|
|
@read_timeout = options[:read_timeout] || 60
|
|
if host
|
|
if options[:port]
|
|
connect(host, options[:port] || FTP_PORT)
|
|
else
|
|
# spec/rubyspec/library/net/ftp/initialize_spec.rb depends on
|
|
# the number of arguments passed to connect....
|
|
connect(host)
|
|
end
|
|
if options[:username]
|
|
login(options[:username], options[:password], options[:account])
|
|
end
|
|
end
|
|
end
|
|
|
|
# A setter to toggle transfers in binary mode.
|
|
# +newmode+ is either +true+ or +false+
|
|
def binary=(newmode)
|
|
if newmode != @binary
|
|
@binary = newmode
|
|
send_type_command if @logged_in
|
|
end
|
|
end
|
|
|
|
# Sends a command to destination host, with the current binary sendmode
|
|
# type.
|
|
#
|
|
# If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
|
|
# A" (ascii) is sent.
|
|
def send_type_command # :nodoc:
|
|
if @binary
|
|
voidcmd("TYPE I")
|
|
else
|
|
voidcmd("TYPE A")
|
|
end
|
|
end
|
|
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:
|
|
oldmode = binary
|
|
self.binary = newmode
|
|
begin
|
|
yield
|
|
ensure
|
|
self.binary = oldmode
|
|
end
|
|
end
|
|
private :with_binary
|
|
|
|
# Obsolete
|
|
def return_code # :nodoc:
|
|
$stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing")
|
|
return "\n"
|
|
end
|
|
|
|
# Obsolete
|
|
def return_code=(s) # :nodoc:
|
|
$stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing")
|
|
end
|
|
|
|
# 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
|
|
# returned.
|
|
def open_socket(host, port) # :nodoc:
|
|
return Timeout.timeout(@open_timeout, OpenTimeout) {
|
|
if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
|
|
@passive = true
|
|
sock = SOCKSSocket.open(host, port)
|
|
else
|
|
sock = Socket.tcp(host, port)
|
|
end
|
|
}
|
|
end
|
|
private :open_socket
|
|
|
|
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=
|
|
if @ssl_session &&
|
|
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
|
|
end
|
|
ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout)
|
|
if @ssl_context.verify_mode != VERIFY_NONE
|
|
ssl_sock.post_connection_check(@host)
|
|
end
|
|
@ssl_session = ssl_sock.session
|
|
return ssl_sock
|
|
end
|
|
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)
|
|
if @debug_mode
|
|
print "connect: ", host, ", ", port, "\n"
|
|
end
|
|
synchronize do
|
|
@host = host
|
|
@bare_sock = open_socket(host, port)
|
|
@sock = BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)
|
|
voidresp
|
|
if @ssl_context
|
|
begin
|
|
voidcmd("AUTH TLS")
|
|
ssl_sock = start_tls_session(@bare_sock)
|
|
@sock = BufferedSSLSocket.new(ssl_sock, read_timeout: @read_timeout)
|
|
if @private_data_connection
|
|
voidcmd("PBSZ 0")
|
|
voidcmd("PROT P")
|
|
end
|
|
rescue OpenSSL::SSL::SSLError, OpenTimeout
|
|
@sock.close
|
|
raise
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# 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)
|
|
synchronize do
|
|
@sock = sock
|
|
if get_greeting
|
|
voidresp
|
|
end
|
|
end
|
|
end
|
|
|
|
# If string +s+ includes the PASS command (password), then the contents of
|
|
# the password are cleaned from the string using "*"
|
|
def sanitize(s) # :nodoc:
|
|
if s =~ /^PASS /i
|
|
return s[0, 5] + "*" * (s.length - 5)
|
|
else
|
|
return s
|
|
end
|
|
end
|
|
private :sanitize
|
|
|
|
# Ensures that +line+ has a control return / line feed (CRLF) and writes
|
|
# it to the socket.
|
|
def putline(line) # :nodoc:
|
|
if @debug_mode
|
|
print "put: ", sanitize(line), "\n"
|
|
end
|
|
if /[\r\n]/ =~ line
|
|
raise ArgumentError, "A line must not contain CR or LF"
|
|
end
|
|
line = line + CRLF
|
|
@sock.write(line)
|
|
end
|
|
private :putline
|
|
|
|
# Reads a line from the sock. If EOF, then it will raise EOFError
|
|
def getline # :nodoc:
|
|
line = @sock.readline # if get EOF, raise EOFError
|
|
line.sub!(/(\r\n|\n|\r)\z/n, "")
|
|
if @debug_mode
|
|
print "get: ", sanitize(line), "\n"
|
|
end
|
|
return line
|
|
end
|
|
private :getline
|
|
|
|
# Receive a section of lines until the response code's match.
|
|
def getmultiline # :nodoc:
|
|
lines = []
|
|
lines << getline
|
|
code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1)
|
|
if code
|
|
delimiter = code + " "
|
|
begin
|
|
lines << getline
|
|
end until lines.last.start_with?(delimiter)
|
|
end
|
|
return lines.join("\n") + "\n"
|
|
end
|
|
private :getmultiline
|
|
|
|
# Receives a response from the destination host.
|
|
#
|
|
# Returns the response code or raises FTPTempError, FTPPermError, or
|
|
# FTPProtoError
|
|
def getresp # :nodoc:
|
|
@last_response = getmultiline
|
|
@last_response_code = @last_response[0, 3]
|
|
case @last_response_code
|
|
when /\A[123]/
|
|
return @last_response
|
|
when /\A4/
|
|
raise FTPTempError, @last_response
|
|
when /\A5/
|
|
raise FTPPermError, @last_response
|
|
else
|
|
raise FTPProtoError, @last_response
|
|
end
|
|
end
|
|
private :getresp
|
|
|
|
# Receives a response.
|
|
#
|
|
# Raises FTPReplyError if the first position of the response code is not
|
|
# equal 2.
|
|
def voidresp # :nodoc:
|
|
resp = getresp
|
|
if !resp.start_with?("2")
|
|
raise FTPReplyError, resp
|
|
end
|
|
end
|
|
private :voidresp
|
|
|
|
#
|
|
# Sends a command and returns the response.
|
|
#
|
|
def sendcmd(cmd)
|
|
synchronize do
|
|
putline(cmd)
|
|
return getresp
|
|
end
|
|
end
|
|
|
|
#
|
|
# Sends a command and expect a response beginning with '2'.
|
|
#
|
|
def voidcmd(cmd)
|
|
synchronize do
|
|
putline(cmd)
|
|
voidresp
|
|
end
|
|
end
|
|
|
|
# Constructs and send the appropriate PORT (or EPRT) command
|
|
def sendport(host, port) # :nodoc:
|
|
remote_address = @bare_sock.remote_address
|
|
if remote_address.ipv4?
|
|
cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
|
|
elsif remote_address.ipv6?
|
|
cmd = sprintf("EPRT |2|%s|%d|", host, port)
|
|
else
|
|
raise FTPProtoError, host
|
|
end
|
|
voidcmd(cmd)
|
|
end
|
|
private :sendport
|
|
|
|
# Constructs a TCPServer socket
|
|
def makeport # :nodoc:
|
|
Addrinfo.tcp(@bare_sock.local_address.ip_address, 0).listen
|
|
end
|
|
private :makeport
|
|
|
|
# sends the appropriate command to enable a passive connection
|
|
def makepasv # :nodoc:
|
|
if @bare_sock.remote_address.ipv4?
|
|
host, port = parse227(sendcmd("PASV"))
|
|
else
|
|
host, port = parse229(sendcmd("EPSV"))
|
|
# host, port = parse228(sendcmd("LPSV"))
|
|
end
|
|
return host, port
|
|
end
|
|
private :makepasv
|
|
|
|
# Constructs a connection for transferring data
|
|
def transfercmd(cmd, rest_offset = nil) # :nodoc:
|
|
if @passive
|
|
host, port = makepasv
|
|
conn = open_socket(host, port)
|
|
if @resume and rest_offset
|
|
resp = sendcmd("REST " + rest_offset.to_s)
|
|
if !resp.start_with?("3")
|
|
raise FTPReplyError, resp
|
|
end
|
|
end
|
|
resp = sendcmd(cmd)
|
|
# skip 2XX for some ftp servers
|
|
resp = getresp if resp.start_with?("2")
|
|
if !resp.start_with?("1")
|
|
raise FTPReplyError, resp
|
|
end
|
|
else
|
|
sock = makeport
|
|
begin
|
|
addr = sock.local_address
|
|
sendport(addr.ip_address, addr.ip_port)
|
|
if @resume and rest_offset
|
|
resp = sendcmd("REST " + rest_offset.to_s)
|
|
if !resp.start_with?("3")
|
|
raise FTPReplyError, resp
|
|
end
|
|
end
|
|
resp = sendcmd(cmd)
|
|
# skip 2XX for some ftp servers
|
|
resp = getresp if resp.start_with?("2")
|
|
if !resp.start_with?("1")
|
|
raise FTPReplyError, resp
|
|
end
|
|
conn, = sock.accept
|
|
sock.shutdown(Socket::SHUT_WR) rescue nil
|
|
sock.read rescue nil
|
|
ensure
|
|
sock.close
|
|
end
|
|
end
|
|
if @private_data_connection
|
|
return BufferedSSLSocket.new(start_tls_session(conn),
|
|
read_timeout: @read_timeout)
|
|
else
|
|
return BufferedSocket.new(conn, read_timeout: @read_timeout)
|
|
end
|
|
end
|
|
private :transfercmd
|
|
|
|
#
|
|
# Logs in to the remote host. The session must have been
|
|
# previously connected. If +user+ is the string "anonymous" and
|
|
# the +password+ is +nil+, "anonymous@" is used as a password. If
|
|
# the +acct+ parameter is not +nil+, an FTP ACCT command is sent
|
|
# following the successful login. Raises an exception on error
|
|
# (typically <tt>Net::FTPPermError</tt>).
|
|
#
|
|
def login(user = "anonymous", passwd = nil, acct = nil)
|
|
if user == "anonymous" and passwd == nil
|
|
passwd = "anonymous@"
|
|
end
|
|
|
|
resp = ""
|
|
synchronize do
|
|
resp = sendcmd('USER ' + user)
|
|
if resp.start_with?("3")
|
|
raise FTPReplyError, resp if passwd.nil?
|
|
resp = sendcmd('PASS ' + passwd)
|
|
end
|
|
if resp.start_with?("3")
|
|
raise FTPReplyError, resp if acct.nil?
|
|
resp = sendcmd('ACCT ' + acct)
|
|
end
|
|
end
|
|
if !resp.start_with?("2")
|
|
raise FTPReplyError, resp
|
|
end
|
|
@welcome = resp
|
|
send_type_command
|
|
@logged_in = true
|
|
end
|
|
|
|
#
|
|
# Puts the connection into binary (image) mode, issues the given command,
|
|
# and fetches the data returned, passing it to the associated block in
|
|
# chunks of +blocksize+ characters. Note that +cmd+ is a server command
|
|
# (such as "RETR myfile").
|
|
#
|
|
def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
|
|
synchronize do
|
|
with_binary(true) do
|
|
begin
|
|
conn = transfercmd(cmd, rest_offset)
|
|
loop do
|
|
data = conn.read(blocksize)
|
|
break if data == nil
|
|
yield(data)
|
|
end
|
|
conn.shutdown(Socket::SHUT_WR)
|
|
conn.read_timeout = 1
|
|
conn.read
|
|
ensure
|
|
conn.close if conn
|
|
end
|
|
voidresp
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Puts the connection into ASCII (text) mode, issues the given command, and
|
|
# passes the resulting data, one line at a time, to the associated block. If
|
|
# no block is given, prints the lines. Note that +cmd+ is a server command
|
|
# (such as "RETR myfile").
|
|
#
|
|
def retrlines(cmd) # :yield: line
|
|
synchronize do
|
|
with_binary(false) do
|
|
begin
|
|
conn = transfercmd(cmd)
|
|
loop do
|
|
line = conn.gets
|
|
break if line == nil
|
|
yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
|
|
end
|
|
conn.shutdown(Socket::SHUT_WR)
|
|
conn.read_timeout = 1
|
|
conn.read
|
|
ensure
|
|
conn.close if conn
|
|
end
|
|
voidresp
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Puts the connection into binary (image) mode, issues the given server-side
|
|
# command (such as "STOR myfile"), and sends the contents of the file named
|
|
# +file+ to the server. If the optional block is given, it also passes it
|
|
# the data, in chunks of +blocksize+ characters.
|
|
#
|
|
def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data
|
|
if rest_offset
|
|
file.seek(rest_offset, IO::SEEK_SET)
|
|
end
|
|
synchronize do
|
|
with_binary(true) do
|
|
conn = transfercmd(cmd)
|
|
loop do
|
|
buf = file.read(blocksize)
|
|
break if buf == nil
|
|
conn.write(buf)
|
|
yield(buf) if block_given?
|
|
end
|
|
conn.close
|
|
voidresp
|
|
end
|
|
end
|
|
rescue Errno::EPIPE
|
|
# EPIPE, in this case, means that the data connection was unexpectedly
|
|
# terminated. Rather than just raising EPIPE to the caller, check the
|
|
# response on the control connection. If getresp doesn't raise a more
|
|
# appropriate exception, re-raise the original exception.
|
|
getresp
|
|
raise
|
|
end
|
|
|
|
#
|
|
# Puts the connection into ASCII (text) mode, issues the given server-side
|
|
# command (such as "STOR myfile"), and sends the contents of the file
|
|
# named +file+ to the server, one line at a time. If the optional block is
|
|
# given, it also passes it the lines.
|
|
#
|
|
def storlines(cmd, file) # :yield: line
|
|
synchronize do
|
|
with_binary(false) do
|
|
conn = transfercmd(cmd)
|
|
loop do
|
|
buf = file.gets
|
|
break if buf == nil
|
|
if buf[-2, 2] != CRLF
|
|
buf = buf.chomp + CRLF
|
|
end
|
|
conn.write(buf)
|
|
yield(buf) if block_given?
|
|
end
|
|
conn.close
|
|
voidresp
|
|
end
|
|
end
|
|
rescue Errno::EPIPE
|
|
# EPIPE, in this case, means that the data connection was unexpectedly
|
|
# terminated. Rather than just raising EPIPE to the caller, check the
|
|
# response on the control connection. If getresp doesn't raise a more
|
|
# appropriate exception, re-raise the original exception.
|
|
getresp
|
|
raise
|
|
end
|
|
|
|
#
|
|
# Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
|
|
# If +localfile+ is nil, returns retrieved data.
|
|
# If a block is supplied, it is passed the retrieved data in +blocksize+
|
|
# chunks.
|
|
#
|
|
def getbinaryfile(remotefile, localfile = File.basename(remotefile),
|
|
blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
|
|
f = nil
|
|
result = nil
|
|
if localfile
|
|
if @resume
|
|
rest_offset = File.size?(localfile)
|
|
f = open(localfile, "a")
|
|
else
|
|
rest_offset = nil
|
|
f = open(localfile, "w")
|
|
end
|
|
elsif !block_given?
|
|
result = String.new
|
|
end
|
|
begin
|
|
f&.binmode
|
|
retrbinary("RETR #{remotefile}", blocksize, rest_offset) do |data|
|
|
f&.write(data)
|
|
block&.(data)
|
|
result&.concat(data)
|
|
end
|
|
return result
|
|
ensure
|
|
f&.close
|
|
end
|
|
end
|
|
|
|
#
|
|
# Retrieves +remotefile+ in ASCII (text) mode, storing the result in
|
|
# +localfile+.
|
|
# If +localfile+ is nil, returns retrieved data.
|
|
# If a block is supplied, it is passed the retrieved data one
|
|
# line at a time.
|
|
#
|
|
def gettextfile(remotefile, localfile = File.basename(remotefile),
|
|
&block) # :yield: line
|
|
f = nil
|
|
result = nil
|
|
if localfile
|
|
f = open(localfile, "w")
|
|
elsif !block_given?
|
|
result = String.new
|
|
end
|
|
begin
|
|
retrlines("RETR #{remotefile}") do |line, newline|
|
|
l = newline ? line + "\n" : line
|
|
f&.print(l)
|
|
block&.(line, newline)
|
|
result&.concat(l)
|
|
end
|
|
return result
|
|
ensure
|
|
f&.close
|
|
end
|
|
end
|
|
|
|
#
|
|
# Retrieves +remotefile+ in whatever mode the session is set (text or
|
|
# binary). See #gettextfile and #getbinaryfile.
|
|
#
|
|
def get(remotefile, localfile = File.basename(remotefile),
|
|
blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
|
|
if @binary
|
|
getbinaryfile(remotefile, localfile, blocksize, &block)
|
|
else
|
|
gettextfile(remotefile, localfile, &block)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Transfers +localfile+ to the server in binary mode, storing the result in
|
|
# +remotefile+. If a block is supplied, calls it, passing in the transmitted
|
|
# data in +blocksize+ chunks.
|
|
#
|
|
def putbinaryfile(localfile, remotefile = File.basename(localfile),
|
|
blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
|
|
if @resume
|
|
begin
|
|
rest_offset = size(remotefile)
|
|
rescue Net::FTPPermError
|
|
rest_offset = nil
|
|
end
|
|
else
|
|
rest_offset = nil
|
|
end
|
|
f = open(localfile)
|
|
begin
|
|
f.binmode
|
|
if rest_offset
|
|
storbinary("APPE #{remotefile}", f, blocksize, rest_offset, &block)
|
|
else
|
|
storbinary("STOR #{remotefile}", f, blocksize, rest_offset, &block)
|
|
end
|
|
ensure
|
|
f.close
|
|
end
|
|
end
|
|
|
|
#
|
|
# Transfers +localfile+ to the server in ASCII (text) mode, storing the result
|
|
# in +remotefile+. If callback or an associated block is supplied, calls it,
|
|
# passing in the transmitted data one line at a time.
|
|
#
|
|
def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
|
|
f = open(localfile)
|
|
begin
|
|
storlines("STOR #{remotefile}", f, &block)
|
|
ensure
|
|
f.close
|
|
end
|
|
end
|
|
|
|
#
|
|
# Transfers +localfile+ to the server in whatever mode the session is set
|
|
# (text or binary). See #puttextfile and #putbinaryfile.
|
|
#
|
|
def put(localfile, remotefile = File.basename(localfile),
|
|
blocksize = DEFAULT_BLOCKSIZE, &block)
|
|
if @binary
|
|
putbinaryfile(localfile, remotefile, blocksize, &block)
|
|
else
|
|
puttextfile(localfile, remotefile, &block)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Sends the ACCT command.
|
|
#
|
|
# This is a less common FTP command, to send account
|
|
# information if the destination host requires it.
|
|
#
|
|
def acct(account)
|
|
cmd = "ACCT " + account
|
|
voidcmd(cmd)
|
|
end
|
|
|
|
#
|
|
# Returns an array of filenames in the remote directory.
|
|
#
|
|
def nlst(dir = nil)
|
|
cmd = "NLST"
|
|
if dir
|
|
cmd = "#{cmd} #{dir}"
|
|
end
|
|
files = []
|
|
retrlines(cmd) do |line|
|
|
files.push(line)
|
|
end
|
|
return files
|
|
end
|
|
|
|
#
|
|
# Returns an array of file information in the directory (the output is like
|
|
# `ls -l`). If a block is given, it iterates through the listing.
|
|
#
|
|
def list(*args, &block) # :yield: line
|
|
cmd = "LIST"
|
|
args.each do |arg|
|
|
cmd = "#{cmd} #{arg}"
|
|
end
|
|
lines = []
|
|
retrlines(cmd) do |line|
|
|
lines << line
|
|
end
|
|
if block
|
|
lines.each(&block)
|
|
end
|
|
return lines
|
|
end
|
|
alias ls list
|
|
alias dir list
|
|
|
|
#
|
|
# MLSxEntry represents an entry in responses of MLST/MLSD.
|
|
# Each entry has the facts (e.g., size, last modification time, etc.)
|
|
# and the pathname.
|
|
#
|
|
class MLSxEntry
|
|
attr_reader :facts, :pathname
|
|
|
|
def initialize(facts, pathname)
|
|
@facts = facts
|
|
@pathname = pathname
|
|
end
|
|
|
|
standard_facts = %w(size modify create type unique perm
|
|
lang media-type charset)
|
|
standard_facts.each do |factname|
|
|
define_method factname.gsub(/-/, "_") do
|
|
facts[factname]
|
|
end
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the entry is a file (i.e., the value of the type
|
|
# fact is file).
|
|
#
|
|
def file?
|
|
return facts["type"] == "file"
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the entry is a directory (i.e., the value of the
|
|
# type fact is dir, cdir, or pdir).
|
|
#
|
|
def directory?
|
|
if /\A[cp]?dir\z/.match(facts["type"])
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the APPE command may be applied to the file.
|
|
#
|
|
def appendable?
|
|
return facts["perm"].include?(?a)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if files may be created in the directory by STOU,
|
|
# STOR, APPE, and RNTO.
|
|
#
|
|
def creatable?
|
|
return facts["perm"].include?(?c)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the file or directory may be deleted by DELE/RMD.
|
|
#
|
|
def deletable?
|
|
return facts["perm"].include?(?d)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the directory may be entered by CWD/CDUP.
|
|
#
|
|
def enterable?
|
|
return facts["perm"].include?(?e)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the file or directory may be renamed by RNFR.
|
|
#
|
|
def renamable?
|
|
return facts["perm"].include?(?f)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the listing commands, LIST, NLST, and MLSD are
|
|
# applied to the directory.
|
|
#
|
|
def listable?
|
|
return facts["perm"].include?(?l)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the MKD command may be used to create a new
|
|
# directory within the directory.
|
|
#
|
|
def directory_makable?
|
|
return facts["perm"].include?(?m)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the objects in the directory may be deleted, or
|
|
# the directory may be purged.
|
|
#
|
|
def purgeable?
|
|
return facts["perm"].include?(?p)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the RETR command may be applied to the file.
|
|
#
|
|
def readable?
|
|
return facts["perm"].include?(?r)
|
|
end
|
|
|
|
#
|
|
# Returns +true+ if the STOR command may be applied to the file.
|
|
#
|
|
def writable?
|
|
return facts["perm"].include?(?w)
|
|
end
|
|
end
|
|
|
|
CASE_DEPENDENT_PARSER = ->(value) { value }
|
|
CASE_INDEPENDENT_PARSER = ->(value) { value.downcase }
|
|
DECIMAL_PARSER = ->(value) { value.to_i }
|
|
OCTAL_PARSER = ->(value) { value.to_i(8) }
|
|
TIME_PARSER = ->(value, local = false) {
|
|
unless /\A(?<year>\d{4})(?<month>\d{2})(?<day>\d{2})
|
|
(?<hour>\d{2})(?<min>\d{2})(?<sec>\d{2})
|
|
(\.(?<fractions>\d+))?/x =~ value
|
|
raise FTPProtoError, "invalid time-val: #{value}"
|
|
end
|
|
usec = fractions.to_i * 10 ** (6 - fractions.to_s.size)
|
|
Time.send(local ? :local : :utc, year, month, day, hour, min, sec, usec)
|
|
}
|
|
FACT_PARSERS = Hash.new(CASE_DEPENDENT_PARSER)
|
|
FACT_PARSERS["size"] = DECIMAL_PARSER
|
|
FACT_PARSERS["modify"] = TIME_PARSER
|
|
FACT_PARSERS["create"] = TIME_PARSER
|
|
FACT_PARSERS["type"] = CASE_INDEPENDENT_PARSER
|
|
FACT_PARSERS["unique"] = CASE_DEPENDENT_PARSER
|
|
FACT_PARSERS["perm"] = CASE_INDEPENDENT_PARSER
|
|
FACT_PARSERS["lang"] = CASE_INDEPENDENT_PARSER
|
|
FACT_PARSERS["media-type"] = CASE_INDEPENDENT_PARSER
|
|
FACT_PARSERS["charset"] = CASE_INDEPENDENT_PARSER
|
|
FACT_PARSERS["unix.mode"] = OCTAL_PARSER
|
|
FACT_PARSERS["unix.owner"] = DECIMAL_PARSER
|
|
FACT_PARSERS["unix.group"] = DECIMAL_PARSER
|
|
FACT_PARSERS["unix.ctime"] = TIME_PARSER
|
|
FACT_PARSERS["unix.atime"] = TIME_PARSER
|
|
|
|
def parse_mlsx_entry(entry)
|
|
facts, pathname = entry.chomp.split(/ /, 2)
|
|
unless pathname
|
|
raise FTPProtoError, entry
|
|
end
|
|
return MLSxEntry.new(
|
|
facts.scan(/(.*?)=(.*?);/).each_with_object({}) {
|
|
|(factname, value), h|
|
|
name = factname.downcase
|
|
h[name] = FACT_PARSERS[name].(value)
|
|
},
|
|
pathname)
|
|
end
|
|
private :parse_mlsx_entry
|
|
|
|
#
|
|
# Returns data (e.g., size, last modification time, entry type, etc.)
|
|
# about the file or directory specified by +pathname+.
|
|
# If +pathname+ is omitted, the current directory is assumed.
|
|
#
|
|
def mlst(pathname = nil)
|
|
cmd = pathname ? "MLST #{pathname}" : "MLST"
|
|
resp = sendcmd(cmd)
|
|
if !resp.start_with?("250")
|
|
raise FTPReplyError, resp
|
|
end
|
|
line = resp.lines[1]
|
|
unless line
|
|
raise FTPProtoError, resp
|
|
end
|
|
entry = line.sub(/\A(250-| *)/, "")
|
|
return parse_mlsx_entry(entry)
|
|
end
|
|
|
|
#
|
|
# Returns an array of the entries of the directory specified by
|
|
# +pathname+.
|
|
# Each entry has the facts (e.g., size, last modification time, etc.)
|
|
# and the pathname.
|
|
# If a block is given, it iterates through the listing.
|
|
# If +pathname+ is omitted, the current directory is assumed.
|
|
#
|
|
def mlsd(pathname = nil, &block) # :yield: entry
|
|
cmd = pathname ? "MLSD #{pathname}" : "MLSD"
|
|
entries = []
|
|
retrlines(cmd) do |line|
|
|
entries << parse_mlsx_entry(line)
|
|
end
|
|
if block
|
|
entries.each(&block)
|
|
end
|
|
return entries
|
|
end
|
|
|
|
#
|
|
# Renames a file on the server.
|
|
#
|
|
def rename(fromname, toname)
|
|
resp = sendcmd("RNFR #{fromname}")
|
|
if !resp.start_with?("3")
|
|
raise FTPReplyError, resp
|
|
end
|
|
voidcmd("RNTO #{toname}")
|
|
end
|
|
|
|
#
|
|
# Deletes a file on the server.
|
|
#
|
|
def delete(filename)
|
|
resp = sendcmd("DELE #{filename}")
|
|
if resp.start_with?("250")
|
|
return
|
|
elsif resp.start_with?("5")
|
|
raise FTPPermError, resp
|
|
else
|
|
raise FTPReplyError, resp
|
|
end
|
|
end
|
|
|
|
#
|
|
# Changes the (remote) directory.
|
|
#
|
|
def chdir(dirname)
|
|
if dirname == ".."
|
|
begin
|
|
voidcmd("CDUP")
|
|
return
|
|
rescue FTPPermError => e
|
|
if e.message[0, 3] != "500"
|
|
raise e
|
|
end
|
|
end
|
|
end
|
|
cmd = "CWD #{dirname}"
|
|
voidcmd(cmd)
|
|
end
|
|
|
|
def get_body(resp) # :nodoc:
|
|
resp.slice(/\A[0-9a-zA-Z]{3} (.*)$/, 1)
|
|
end
|
|
private :get_body
|
|
|
|
#
|
|
# Returns the size of the given (remote) filename.
|
|
#
|
|
def size(filename)
|
|
with_binary(true) do
|
|
resp = sendcmd("SIZE #{filename}")
|
|
if !resp.start_with?("213")
|
|
raise FTPReplyError, resp
|
|
end
|
|
return get_body(resp).to_i
|
|
end
|
|
end
|
|
|
|
#
|
|
# Returns the last modification time of the (remote) file. If +local+ is
|
|
# +true+, it is returned as a local time, otherwise it's a UTC time.
|
|
#
|
|
def mtime(filename, local = false)
|
|
return TIME_PARSER.(mdtm(filename), local)
|
|
end
|
|
|
|
#
|
|
# Creates a remote directory.
|
|
#
|
|
def mkdir(dirname)
|
|
resp = sendcmd("MKD #{dirname}")
|
|
return parse257(resp)
|
|
end
|
|
|
|
#
|
|
# Removes a remote directory.
|
|
#
|
|
def rmdir(dirname)
|
|
voidcmd("RMD #{dirname}")
|
|
end
|
|
|
|
#
|
|
# Returns the current remote directory.
|
|
#
|
|
def pwd
|
|
resp = sendcmd("PWD")
|
|
return parse257(resp)
|
|
end
|
|
alias getdir pwd
|
|
|
|
#
|
|
# Returns system information.
|
|
#
|
|
def system
|
|
resp = sendcmd("SYST")
|
|
if !resp.start_with?("215")
|
|
raise FTPReplyError, resp
|
|
end
|
|
return get_body(resp)
|
|
end
|
|
|
|
#
|
|
# Aborts the previous command (ABOR command).
|
|
#
|
|
def abort
|
|
line = "ABOR" + CRLF
|
|
print "put: ABOR\n" if @debug_mode
|
|
@sock.send(line, Socket::MSG_OOB)
|
|
resp = getmultiline
|
|
unless ["426", "226", "225"].include?(resp[0, 3])
|
|
raise FTPProtoError, resp
|
|
end
|
|
return resp
|
|
end
|
|
|
|
#
|
|
# Returns the status (STAT command).
|
|
# pathname - when stat is invoked with pathname as a parameter it acts like
|
|
# list but alot faster and over the same tcp session.
|
|
#
|
|
def status(pathname = nil)
|
|
line = pathname ? "STAT #{pathname}" : "STAT"
|
|
if /[\r\n]/ =~ line
|
|
raise ArgumentError, "A line must not contain CR or LF"
|
|
end
|
|
print "put: #{line}\n" if @debug_mode
|
|
@sock.send(line + CRLF, Socket::MSG_OOB)
|
|
return getresp
|
|
end
|
|
|
|
#
|
|
# Returns the raw last modification time of the (remote) file in the format
|
|
# "YYYYMMDDhhmmss" (MDTM command).
|
|
#
|
|
# Use +mtime+ if you want a parsed Time instance.
|
|
#
|
|
def mdtm(filename)
|
|
resp = sendcmd("MDTM #{filename}")
|
|
if resp.start_with?("213")
|
|
return get_body(resp)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Issues the HELP command.
|
|
#
|
|
def help(arg = nil)
|
|
cmd = "HELP"
|
|
if arg
|
|
cmd = cmd + " " + arg
|
|
end
|
|
sendcmd(cmd)
|
|
end
|
|
|
|
#
|
|
# Exits the FTP session.
|
|
#
|
|
def quit
|
|
voidcmd("QUIT")
|
|
end
|
|
|
|
#
|
|
# Issues a NOOP command.
|
|
#
|
|
# Does nothing except return a response.
|
|
#
|
|
def noop
|
|
voidcmd("NOOP")
|
|
end
|
|
|
|
#
|
|
# Issues a SITE command.
|
|
#
|
|
def site(arg)
|
|
cmd = "SITE " + arg
|
|
voidcmd(cmd)
|
|
end
|
|
|
|
#
|
|
# Closes the connection. Further operations are impossible until you open
|
|
# a new connection with #connect.
|
|
#
|
|
def close
|
|
if @sock and not @sock.closed?
|
|
begin
|
|
@sock.shutdown(Socket::SHUT_WR) rescue nil
|
|
orig, self.read_timeout = self.read_timeout, 3
|
|
@sock.read rescue nil
|
|
ensure
|
|
@sock.close
|
|
self.read_timeout = orig
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Returns +true+ iff the connection is closed.
|
|
#
|
|
def closed?
|
|
@sock == nil or @sock.closed?
|
|
end
|
|
|
|
# handler for response code 227
|
|
# (Entering Passive Mode (h1,h2,h3,h4,p1,p2))
|
|
#
|
|
# Returns host and port.
|
|
def parse227(resp) # :nodoc:
|
|
if !resp.start_with?("227")
|
|
raise FTPReplyError, resp
|
|
end
|
|
if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
|
|
return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
|
|
else
|
|
raise FTPProtoError, resp
|
|
end
|
|
end
|
|
private :parse227
|
|
|
|
# handler for response code 228
|
|
# (Entering Long Passive Mode)
|
|
#
|
|
# Returns host and port.
|
|
def parse228(resp) # :nodoc:
|
|
if !resp.start_with?("228")
|
|
raise FTPReplyError, resp
|
|
end
|
|
if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp)
|
|
return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
|
|
elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp)
|
|
return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"])
|
|
else
|
|
raise FTPProtoError, resp
|
|
end
|
|
end
|
|
private :parse228
|
|
|
|
def parse_pasv_ipv4_host(s)
|
|
return s.tr(",", ".")
|
|
end
|
|
private :parse_pasv_ipv4_host
|
|
|
|
def parse_pasv_ipv6_host(s)
|
|
return s.split(/,/).map { |i|
|
|
"%02x" % i.to_i
|
|
}.each_slice(2).map(&:join).join(":")
|
|
end
|
|
private :parse_pasv_ipv6_host
|
|
|
|
def parse_pasv_port(s)
|
|
return s.split(/,/).map(&:to_i).inject { |x, y|
|
|
(x << 8) + y
|
|
}
|
|
end
|
|
private :parse_pasv_port
|
|
|
|
# handler for response code 229
|
|
# (Extended Passive Mode Entered)
|
|
#
|
|
# Returns host and port.
|
|
def parse229(resp) # :nodoc:
|
|
if !resp.start_with?("229")
|
|
raise FTPReplyError, resp
|
|
end
|
|
if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp)
|
|
return @bare_sock.remote_address.ip_address, m["port"].to_i
|
|
else
|
|
raise FTPProtoError, resp
|
|
end
|
|
end
|
|
private :parse229
|
|
|
|
# handler for response code 257
|
|
# ("PATHNAME" created)
|
|
#
|
|
# Returns host and port.
|
|
def parse257(resp) # :nodoc:
|
|
if !resp.start_with?("257")
|
|
raise FTPReplyError, resp
|
|
end
|
|
return resp.slice(/"(([^"]|"")*)"/, 1).to_s.gsub(/""/, '"')
|
|
end
|
|
private :parse257
|
|
|
|
# :stopdoc:
|
|
class NullSocket
|
|
def read_timeout=(sec)
|
|
end
|
|
|
|
def closed?
|
|
true
|
|
end
|
|
|
|
def close
|
|
end
|
|
|
|
def method_missing(mid, *args)
|
|
raise FTPConnectionError, "not connected"
|
|
end
|
|
end
|
|
|
|
class BufferedSocket < BufferedIO
|
|
[:local_address, :remote_address, :addr, :peeraddr, :send, :shutdown].each do |method|
|
|
define_method(method) { |*args|
|
|
@io.__send__(method, *args)
|
|
}
|
|
end
|
|
|
|
def read(len = nil)
|
|
if len
|
|
s = super(len, String.new, true)
|
|
return s.empty? ? nil : s
|
|
else
|
|
result = ""
|
|
while s = super(DEFAULT_BLOCKSIZE, String.new, true)
|
|
break if s.empty?
|
|
result << s
|
|
end
|
|
return result
|
|
end
|
|
end
|
|
|
|
def gets
|
|
line = readuntil("\n", true)
|
|
return line.empty? ? nil : line
|
|
end
|
|
|
|
def readline
|
|
line = gets
|
|
if line.nil?
|
|
raise EOFError, "end of file reached"
|
|
end
|
|
return line
|
|
end
|
|
end
|
|
|
|
if defined?(OpenSSL::SSL::SSLSocket)
|
|
class BufferedSSLSocket < BufferedSocket
|
|
def initialize(*args)
|
|
super
|
|
@is_shutdown = false
|
|
end
|
|
|
|
def shutdown(*args)
|
|
# SSL_shutdown() will be called from SSLSocket#close, and
|
|
# SSL_shutdown() will send the "close notify" alert to the peer,
|
|
# so shutdown(2) should not be called.
|
|
@is_shutdown = true
|
|
end
|
|
|
|
def send(mesg, flags, dest = nil)
|
|
# Ignore flags and dest.
|
|
@io.write(mesg)
|
|
end
|
|
|
|
private
|
|
|
|
def rbuf_fill
|
|
if @is_shutdown
|
|
raise EOFError, "shutdown has been called"
|
|
else
|
|
super
|
|
end
|
|
end
|
|
end
|
|
end
|
|
# :startdoc:
|
|
end
|
|
end
|
|
|
|
|
|
# Documentation comments:
|
|
# - sourced from pickaxe and nutshell, with improvements (hopefully)
|