2007-06-14 12:05:59 -04:00
|
|
|
#!/usr/bin/perl -w
|
|
|
|
|
|
|
|
# Part of this code was borrowed from Richard Jones's Net::FTPServer
|
|
|
|
# http://www.annexia.org/freeware/netftpserver
|
|
|
|
|
|
|
|
package FTPServer;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
|
|
|
|
use Cwd;
|
|
|
|
use Socket;
|
|
|
|
use IO::Socket::INET;
|
|
|
|
use IO::Seekable;
|
|
|
|
use POSIX qw(strftime);
|
|
|
|
|
|
|
|
my $log = undef;
|
|
|
|
my $GOT_SIGURG = 0;
|
|
|
|
|
|
|
|
# CONSTANTS
|
|
|
|
|
|
|
|
# connection states
|
|
|
|
my %_connection_states = (
|
|
|
|
'NEWCONN' => 0x01,
|
|
|
|
'WAIT4PWD' => 0x02,
|
|
|
|
'LOGGEDIN' => 0x04,
|
|
|
|
'TWOSOCKS' => 0x08,
|
|
|
|
);
|
|
|
|
|
|
|
|
# subset of FTP commands supported by these server and the respective
|
|
|
|
# connection states in which they are allowed
|
|
|
|
my %_commands = (
|
|
|
|
# Standard commands from RFC 959.
|
|
|
|
'CWD' => $_connection_states{LOGGEDIN} |
|
|
|
|
$_connection_states{TWOSOCKS},
|
|
|
|
# 'EPRT' => $_connection_states{LOGGEDIN},
|
|
|
|
# 'EPSV' => $_connection_states{LOGGEDIN},
|
|
|
|
'LIST' => $_connection_states{TWOSOCKS},
|
|
|
|
# 'LPRT' => $_connection_states{LOGGEDIN},
|
|
|
|
# 'LPSV' => $_connection_states{LOGGEDIN},
|
|
|
|
'PASS' => $_connection_states{WAIT4PWD},
|
|
|
|
'PASV' => $_connection_states{LOGGEDIN},
|
|
|
|
'PORT' => $_connection_states{LOGGEDIN},
|
|
|
|
'PWD' => $_connection_states{LOGGEDIN} |
|
|
|
|
$_connection_states{TWOSOCKS},
|
|
|
|
'QUIT' => $_connection_states{LOGGEDIN} |
|
|
|
|
$_connection_states{TWOSOCKS},
|
|
|
|
'REST' => $_connection_states{TWOSOCKS},
|
|
|
|
'RETR' => $_connection_states{TWOSOCKS},
|
|
|
|
'SYST' => $_connection_states{LOGGEDIN},
|
|
|
|
'TYPE' => $_connection_states{LOGGEDIN} |
|
|
|
|
$_connection_states{TWOSOCKS},
|
|
|
|
'USER' => $_connection_states{NEWCONN},
|
|
|
|
# From ftpexts Internet Draft.
|
|
|
|
'SIZE' => $_connection_states{LOGGEDIN} |
|
|
|
|
$_connection_states{TWOSOCKS},
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# COMMAND-HANDLING ROUTINES
|
|
|
|
|
|
|
|
sub _CWD_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $path) = @_;
|
|
|
|
|
|
|
|
local $_;
|
|
|
|
my $newdir = $conn->{dir};
|
|
|
|
|
|
|
|
# If the path starts with a "/" then it's an absolute path.
|
|
|
|
if (substr ($path, 0, 1) eq "/") {
|
|
|
|
$newdir = "";
|
|
|
|
$path =~ s,^/+,,;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Split the path into its component parts and process each separately.
|
|
|
|
my @elems = split /\//, $path;
|
|
|
|
|
|
|
|
foreach (@elems) {
|
|
|
|
if ($_ eq "" || $_ eq ".") {
|
|
|
|
# Ignore these.
|
|
|
|
next;
|
|
|
|
} elsif ($_ eq "..") {
|
|
|
|
# Go to parent directory.
|
|
|
|
if ($newdir eq "") {
|
|
|
|
print {$conn->{socket}} "550 Directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$newdir = substr ($newdir, 0, rindex ($newdir, "/"));
|
|
|
|
} else {
|
|
|
|
# Go into subdirectory, if it exists.
|
|
|
|
$newdir .= ("/" . $_);
|
|
|
|
if (! -d $conn->{rootdir} . $newdir) {
|
|
|
|
print {$conn->{socket}} "550 Directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$conn->{dir} = $newdir;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _LIST_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $path) = @_;
|
|
|
|
|
|
|
|
# This is something of a hack. Some clients expect a Unix server
|
|
|
|
# to respond to flags on the 'ls command line'. Remove these flags
|
|
|
|
# and ignore them. This is particularly an issue with ncftp 2.4.3.
|
|
|
|
$path =~ s/^-[a-zA-Z0-9]+\s?//;
|
|
|
|
|
|
|
|
my $dir = $conn->{dir};
|
|
|
|
|
|
|
|
print STDERR "_LIST_command - dir is: $dir\n";
|
|
|
|
|
|
|
|
# Absolute path?
|
|
|
|
if (substr ($path, 0, 1) eq "/") {
|
|
|
|
$dir = "/";
|
|
|
|
$path =~ s,^/+,,;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Parse the first elements of the path until we find the appropriate
|
|
|
|
# working directory.
|
|
|
|
my @elems = split /\//, $path;
|
|
|
|
my ($wildcard, $filename);
|
|
|
|
local $_;
|
|
|
|
|
|
|
|
for (my $i = 0; $i < @elems; ++$i) {
|
|
|
|
$_ = $elems[$i];
|
|
|
|
my $lastelement = $i == @elems-1;
|
|
|
|
|
|
|
|
if ($_ eq "" || $_ eq ".") { next } # Ignore these.
|
|
|
|
elsif ($_ eq "..") {
|
|
|
|
# Go to parent directory.
|
|
|
|
unless ($dir eq "/") {
|
|
|
|
$dir = substr ($dir, 0, rindex ($dir, "/"));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!$lastelement) { # These elements can only be directories.
|
|
|
|
unless (-d $conn->{rootdir} . $dir . $_) {
|
|
|
|
print {$conn->{socket}} "550 File or directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$dir .= $_;
|
|
|
|
} else { # It's the last element: check if it's a file, directory or wildcard.
|
|
|
|
if (-f $conn->{rootdir} . $dir . $_) {
|
|
|
|
# It's a file.
|
|
|
|
$filename = $_;
|
|
|
|
} elsif (-d $conn->{rootdir} . $dir . $_) {
|
|
|
|
# It's a directory.
|
|
|
|
$dir .= $_;
|
|
|
|
} elsif (/\*/ || /\?/) {
|
|
|
|
# It is a wildcard.
|
|
|
|
$wildcard = $_;
|
|
|
|
} else {
|
|
|
|
print {$conn->{socket}} "550 File or directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
print STDERR "_LIST_command - dir is: $dir\n" if $log;
|
|
|
|
|
|
|
|
print {$conn->{socket}} "150 Opening data connection for file listing.\r\n";
|
|
|
|
|
|
|
|
# Open a path back to the client.
|
|
|
|
my $sock = __open_data_connection ($conn);
|
|
|
|
|
|
|
|
unless ($sock) {
|
|
|
|
print {$conn->{socket}} "425 Can't open data connection.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
# If the path contains a directory name, extract it so that
|
|
|
|
# we can prefix it to every filename listed.
|
|
|
|
my $prefix = (($filename || $wildcard) && $path =~ /(.*\/).*/) ? $1 : "";
|
|
|
|
|
|
|
|
print STDERR "_LIST_command - prefix is: $prefix\n" if $log;
|
|
|
|
|
|
|
|
# OK, we're either listing a full directory, listing a single
|
|
|
|
# file or listing a wildcard.
|
|
|
|
if ($filename) { # Single file.
|
|
|
|
__list_file ($sock, $prefix . $filename);
|
|
|
|
} else { # Wildcard or full directory $dirh.
|
|
|
|
unless ($wildcard) {
|
|
|
|
# Synthesize (fake) "total" field for directory listing.
|
|
|
|
print $sock "total 1 \r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (__get_file_list ($conn->{rootdir} . $dir, $wildcard)) {
|
|
|
|
__list_file ($sock, $prefix . $_);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unless ($sock->close) {
|
|
|
|
print {$conn->{socket}} "550 Error closing data connection: $!\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
print {$conn->{socket}} "226 Listing complete. Data connection has been closed.\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _PASS_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $pass) = @_;
|
|
|
|
|
|
|
|
# TODO: implement authentication?
|
|
|
|
|
|
|
|
print STDERR "switching to LOGGEDIN state\n" if $log;
|
|
|
|
$conn->{state} = $_connection_states{LOGGEDIN};
|
|
|
|
|
|
|
|
if ($conn->{username} eq "anonymous") {
|
|
|
|
print {$conn->{socket}} "202 Anonymous user access is always granted.\r\n";
|
|
|
|
} else {
|
|
|
|
print {$conn->{socket}} "230 Authentication not implemented yet, access is always granted.\r\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _PASV_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $rest) = @_;
|
|
|
|
|
|
|
|
# Open a listening socket - but don't actually accept on it yet.
|
|
|
|
"0" =~ /(0)/; # Perl 5.7 / IO::Socket::INET bug workaround.
|
|
|
|
my $sock = IO::Socket::INET->new (LocalHost => '127.0.0.1',
|
|
|
|
LocalPort => '0',
|
|
|
|
Listen => 1,
|
|
|
|
Reuse => 1,
|
|
|
|
Proto => 'tcp',
|
|
|
|
Type => SOCK_STREAM);
|
|
|
|
|
|
|
|
unless ($sock) {
|
|
|
|
# Return a code 550 here, even though this is not in the RFC. XXX
|
|
|
|
print {$conn->{socket}} "550 Can't open a listening socket.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$conn->{passive} = 1;
|
|
|
|
$conn->{passive_socket} = $sock;
|
|
|
|
|
|
|
|
# Get our port number.
|
|
|
|
my $sockport = $sock->sockport;
|
|
|
|
|
|
|
|
# Split the port number into high and low components.
|
|
|
|
my $p1 = int ($sockport / 256);
|
|
|
|
my $p2 = $sockport % 256;
|
|
|
|
|
|
|
|
$conn->{state} = $_connection_states{TWOSOCKS};
|
|
|
|
|
|
|
|
# We only accept connections from localhost.
|
|
|
|
print {$conn->{socket}} "227 Entering Passive Mode (127,0,0,1,$p1,$p2)\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _PORT_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $rest) = @_;
|
|
|
|
|
|
|
|
# The arguments to PORT are a1,a2,a3,a4,p1,p2 where a1 is the
|
|
|
|
# most significant part of the address (eg. 127,0,0,1) and
|
|
|
|
# p1 is the most significant part of the port.
|
|
|
|
unless ($rest =~ /^\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})/) {
|
|
|
|
print {$conn->{socket}} "501 Syntax error in PORT command.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check host address.
|
|
|
|
unless ($1 > 0 && $1 < 224 &&
|
|
|
|
$2 >= 0 && $2 < 256 &&
|
|
|
|
$3 >= 0 && $3 < 256 &&
|
|
|
|
$4 >= 0 && $4 < 256) {
|
|
|
|
print {$conn->{socket}} "501 Invalid host address.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Construct host address and port number.
|
|
|
|
my $peeraddrstring = "$1.$2.$3.$4";
|
|
|
|
my $peerport = $5 * 256 + $6;
|
|
|
|
|
|
|
|
# Check port number.
|
|
|
|
unless ($peerport > 0 && $peerport < 65536) {
|
|
|
|
print {$conn->{socket}} "501 Invalid port number.\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
$conn->{peeraddrstring} = $peeraddrstring;
|
|
|
|
$conn->{peeraddr} = inet_aton ($peeraddrstring);
|
|
|
|
$conn->{peerport} = $peerport;
|
|
|
|
$conn->{passive} = 0;
|
|
|
|
|
|
|
|
$conn->{state} = $_connection_states{TWOSOCKS};
|
|
|
|
|
|
|
|
print {$conn->{socket}} "200 PORT command OK.\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _PWD_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $rest) = @_;
|
|
|
|
|
|
|
|
# See RFC 959 Appendix II and draft-ietf-ftpext-mlst-11.txt section 6.2.1.
|
|
|
|
my $pathname = $conn->{dir};
|
|
|
|
$pathname =~ s,/+$,, unless $pathname eq "/";
|
|
|
|
$pathname =~ tr,/,/,s;
|
|
|
|
|
|
|
|
print {$conn->{socket}} "257 \"$pathname\"\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _REST_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $restart_from) = @_;
|
|
|
|
|
|
|
|
unless ($restart_from =~ /^([1-9][0-9]*|0)$/) {
|
|
|
|
print {$conn->{socket}} "501 REST command needs a numeric argument.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$conn->{restart} = $1;
|
|
|
|
|
|
|
|
print {$conn->{socket}} "350 Restarting next transfer at $1.\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _RETR_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $path) = @_;
|
|
|
|
|
|
|
|
my $dir = $conn->{dir};
|
|
|
|
|
|
|
|
# Absolute path?
|
|
|
|
if (substr ($path, 0, 1) eq "/") {
|
|
|
|
$dir = "/";
|
|
|
|
$path =~ s,^/+,,;
|
|
|
|
$path = "." if $path eq "";
|
|
|
|
}
|
|
|
|
|
|
|
|
# Parse the first elements of path until we find the appropriate
|
|
|
|
# working directory.
|
|
|
|
my @elems = split /\//, $path;
|
|
|
|
my $filename = pop @elems;
|
|
|
|
|
|
|
|
foreach (@elems) {
|
|
|
|
if ($_ eq "" || $_ eq ".") {
|
|
|
|
next # Ignore these.
|
|
|
|
} elsif ($_ eq "..") {
|
|
|
|
# Go to parent directory.
|
|
|
|
unless ($dir eq "/") {
|
|
|
|
$dir = substr ($dir, 0, rindex ($dir, "/"));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
unless (-d $conn->{rootdir} . $dir . $_) {
|
|
|
|
print {$conn->{socket}} "550 File or directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$dir .= $_;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unless (defined $filename && length $filename) {
|
|
|
|
print {$conn->{socket}} "550 File or directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($filename eq "." || $filename eq "..") {
|
|
|
|
print {$conn->{socket}} "550 RETR command is not supported on directories.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
my $fullname = $conn->{rootdir} . $dir . $filename;
|
|
|
|
unless (-f $fullname) {
|
|
|
|
print {$conn->{socket}} "550 RETR command is only supported on plain files.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Try to open the file.
|
|
|
|
unless (open (FILE, '<', $fullname)) {
|
|
|
|
print {$conn->{socket}} "550 File or directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
print {$conn->{socket}} "150 Opening " .
|
|
|
|
($conn->{type} eq 'A' ? "ASCII mode" : "BINARY mode") .
|
|
|
|
" data connection for file $filename.\r\n";
|
|
|
|
|
|
|
|
# Open a path back to the client.
|
|
|
|
my $sock = __open_data_connection ($conn);
|
|
|
|
|
|
|
|
unless ($sock) {
|
|
|
|
print {$conn->{socket}} "425 Can't open data connection.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
# What mode are we sending this file in?
|
|
|
|
unless ($conn->{type} eq 'A') # Binary type.
|
|
|
|
{
|
|
|
|
my ($r, $buffer, $n, $w);
|
|
|
|
|
|
|
|
# Restart the connection from previous point?
|
|
|
|
if ($conn->{restart}) {
|
|
|
|
# VFS seek method only required to support relative forward seeks
|
|
|
|
#
|
|
|
|
# In Perl = 5.00503, SEEK_CUR is exported by IO::Seekable,
|
|
|
|
# in Perl >= 5.6, SEEK_CUR is exported by both IO::Seekable
|
|
|
|
# and Fcntl. Hence we 'use IO::Seekable' at the top of the
|
|
|
|
# file to get this symbol reliably in both cases.
|
|
|
|
sysseek (FILE, $conn->{restart}, SEEK_CUR);
|
|
|
|
$conn->{restart} = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Copy data.
|
|
|
|
while ($r = sysread (FILE, $buffer, 65536))
|
|
|
|
{
|
|
|
|
# Restart alarm clock timer.
|
|
|
|
alarm $conn->{idle_timeout};
|
|
|
|
|
|
|
|
for ($n = 0; $n < $r; )
|
|
|
|
{
|
|
|
|
$w = syswrite ($sock, $buffer, $r - $n, $n);
|
|
|
|
|
|
|
|
# Cleanup and exit if there was an error.
|
|
|
|
unless (defined $w) {
|
|
|
|
close $sock;
|
|
|
|
close FILE;
|
|
|
|
print {$conn->{socket}} "426 File retrieval error: $!. Data connection has been closed.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$n += $w;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Transfer aborted by client?
|
|
|
|
if ($GOT_SIGURG) {
|
|
|
|
$GOT_SIGURG = 0;
|
|
|
|
close $sock;
|
|
|
|
close FILE;
|
|
|
|
print {$conn->{socket}} "426 Transfer aborted. Data connection closed.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Cleanup and exit if there was an error.
|
|
|
|
unless (defined $r) {
|
|
|
|
close $sock;
|
|
|
|
close FILE;
|
|
|
|
print {$conn->{socket}} "426 File retrieval error: $!. Data connection has been closed.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else { # ASCII type.
|
|
|
|
# Restart the connection from previous point?
|
|
|
|
if ($conn->{restart}) {
|
|
|
|
for (my $i = 0; $i < $conn->{restart}; ++$i) {
|
|
|
|
getc FILE;
|
|
|
|
}
|
|
|
|
$conn->{restart} = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Copy data.
|
|
|
|
while (defined ($_ = <FILE>)) {
|
|
|
|
# Remove any native line endings.
|
|
|
|
s/[\n\r]+$//;
|
|
|
|
|
|
|
|
# Restart alarm clock timer.
|
|
|
|
alarm $conn->{idle_timeout};
|
|
|
|
|
|
|
|
# Write the line with telnet-format line endings.
|
|
|
|
print $sock "$_\r\n";
|
|
|
|
|
|
|
|
# Transfer aborted by client?
|
|
|
|
if ($GOT_SIGURG) {
|
|
|
|
$GOT_SIGURG = 0;
|
|
|
|
close $sock;
|
|
|
|
close FILE;
|
|
|
|
print {$conn->{socket}} "426 Transfer aborted. Data connection closed.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unless (close ($sock) && close (FILE)) {
|
|
|
|
print {$conn->{socket}} "550 File retrieval error: $!.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
print {$conn->{socket}} "226 File retrieval complete. Data connection has been closed.\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _SIZE_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $path) = @_;
|
|
|
|
|
|
|
|
my $dir = $conn->{dir};
|
|
|
|
|
|
|
|
# Absolute path?
|
|
|
|
if (substr ($path, 0, 1) eq "/") {
|
|
|
|
$dir = "/";
|
|
|
|
$path =~ s,^/+,,;
|
|
|
|
$path = "." if $path eq "";
|
|
|
|
}
|
|
|
|
|
|
|
|
# Parse the first elements of path until we find the appropriate
|
|
|
|
# working directory.
|
|
|
|
my @elems = split /\//, $path;
|
|
|
|
my $filename = pop @elems;
|
|
|
|
|
|
|
|
foreach (@elems) {
|
|
|
|
if ($_ eq "" || $_ eq ".") {
|
|
|
|
next # Ignore these.
|
|
|
|
} elsif ($_ eq "..") {
|
|
|
|
# Go to parent directory.
|
|
|
|
unless ($dir eq "/") {
|
|
|
|
$dir = substr ($dir, 0, rindex ($dir, "/"));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
unless (-d $conn->{rootdir} . $dir . $_) {
|
|
|
|
print {$conn->{socket}} "550 File or directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$dir .= $_;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
unless (defined $filename && length $filename) {
|
|
|
|
print {$conn->{socket}} "550 File or directory not found.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($filename eq "." || $filename eq "..") {
|
|
|
|
print {$conn->{socket}} "550 SIZE command is not supported on directories.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
my $fullname = $conn->{rootdir} . $dir . $filename;
|
|
|
|
unless (-f $fullname) {
|
|
|
|
print {$conn->{socket}} "550 SIZE command is only supported on plain files.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
my $size = 0;
|
|
|
|
if ($conn->{type} eq 'A') {
|
|
|
|
# ASCII mode: we have to count the characters by hand.
|
|
|
|
unless (open (FILE, '<', $filename)) {
|
|
|
|
print {$conn->{socket}} "550 Cannot calculate size of $filename.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$size++ while (defined (getc(FILE)));
|
|
|
|
close FILE;
|
|
|
|
} else {
|
|
|
|
# BINARY mode: we can use stat
|
|
|
|
$size = (stat($filename))[7];
|
|
|
|
}
|
|
|
|
|
|
|
|
print {$conn->{socket}} "213 $size\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _SYST_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $dummy) = @_;
|
|
|
|
|
|
|
|
print {$conn->{socket}} "215 UNIX Type: L8\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _TYPE_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $type) = @_;
|
|
|
|
|
|
|
|
# See RFC 959 section 5.3.2.
|
|
|
|
if ($type =~ /^([AI])$/i) {
|
|
|
|
$conn->{type} = 'A';
|
|
|
|
} elsif ($type =~ /^([AI])\sN$/i) {
|
|
|
|
$conn->{type} = 'A';
|
|
|
|
} elsif ($type =~ /^L\s8$/i) {
|
|
|
|
$conn->{type} = 'L8';
|
|
|
|
} else {
|
|
|
|
print {$conn->{socket}} "504 This server does not support TYPE $type.\r\n";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
print {$conn->{socket}} "200 TYPE changed to $type.\r\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _USER_command
|
|
|
|
{
|
|
|
|
my ($conn, $cmd, $username) = @_;
|
|
|
|
|
|
|
|
print STDERR "username: $username\n" if $log;
|
|
|
|
$conn->{username} = $username;
|
|
|
|
|
|
|
|
print STDERR "switching to WAIT4PWD state\n" if $log;
|
|
|
|
$conn->{state} = $_connection_states{WAIT4PWD};
|
|
|
|
|
|
|
|
if ($conn->{username} eq "anonymous") {
|
|
|
|
print {$conn->{socket}} "230 Anonymous user access granted.\r\n";
|
|
|
|
} else {
|
|
|
|
print {$conn->{socket}} "331 Password required.\r\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# HELPER ROUTINES
|
|
|
|
|
|
|
|
sub __open_data_connection
|
|
|
|
{
|
|
|
|
my $conn = shift;
|
|
|
|
|
|
|
|
my $sock;
|
|
|
|
|
|
|
|
if ($conn->{passive}) {
|
|
|
|
# Passive mode - wait for a connection from the client.
|
|
|
|
accept ($sock, $conn->{passive_socket}) or return undef;
|
|
|
|
} else {
|
|
|
|
# Active mode - connect back to the client.
|
|
|
|
"0" =~ /(0)/; # Perl 5.7 / IO::Socket::INET bug workaround.
|
|
|
|
$sock = IO::Socket::INET->new (LocalAddr => '127.0.0.1',
|
|
|
|
PeerAddr => $conn->{peeraddrstring},
|
|
|
|
PeerPort => $conn->{peerport},
|
|
|
|
Proto => 'tcp',
|
|
|
|
Type => SOCK_STREAM) or return undef;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $sock;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub __list_file
|
|
|
|
{
|
|
|
|
my $sock = shift;
|
|
|
|
my $filename = shift;
|
|
|
|
|
|
|
|
# Get the status information.
|
|
|
|
my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
|
|
|
|
$atime, $mtime, $ctime, $blksize, $blocks)
|
|
|
|
= lstat $filename;
|
|
|
|
|
|
|
|
# If the file has been removed since we created this
|
|
|
|
# handle, then $dev will be undefined. Return immediately.
|
|
|
|
return unless defined $dev;
|
|
|
|
|
|
|
|
# Generate printable user/group.
|
|
|
|
my $user = getpwuid ($uid) || "-";
|
|
|
|
my $group = getgrgid ($gid) || "-";
|
|
|
|
|
|
|
|
# Permissions from mode.
|
|
|
|
my $perms = $mode & 0777;
|
|
|
|
|
|
|
|
# Work out the mode using special "_" operator which causes Perl
|
|
|
|
# to use the result of the previous stat call.
|
|
|
|
$mode = (-f _ ? 'f' :
|
|
|
|
(-d _ ? 'd' :
|
|
|
|
(-l _ ? 'l' :
|
|
|
|
(-p _ ? 'p' :
|
|
|
|
(-S _ ? 's' :
|
|
|
|
(-b _ ? 'b' :
|
|
|
|
(-c _ ? 'c' : '?')))))));
|
|
|
|
|
|
|
|
# Generate printable date (this logic is taken from GNU fileutils:
|
|
|
|
# src/ls.c: print_long_format).
|
|
|
|
my $time = time;
|
|
|
|
my $fmt;
|
|
|
|
if ($time > $mtime + 6 * 30 * 24 * 60 * 60 || $time < $mtime - 60 * 60) {
|
|
|
|
$fmt = "%b %e %Y";
|
|
|
|
} else {
|
|
|
|
$fmt = "%b %e %H:%M";
|
|
|
|
}
|
|
|
|
|
|
|
|
my $fmt_time = strftime $fmt, localtime ($mtime);
|
|
|
|
|
|
|
|
# Generate printable permissions.
|
|
|
|
my $fmt_perms = join "",
|
|
|
|
($perms & 0400 ? 'r' : '-'),
|
|
|
|
($perms & 0200 ? 'w' : '-'),
|
|
|
|
($perms & 0100 ? 'x' : '-'),
|
|
|
|
($perms & 040 ? 'r' : '-'),
|
|
|
|
($perms & 020 ? 'w' : '-'),
|
|
|
|
($perms & 010 ? 'x' : '-'),
|
|
|
|
($perms & 04 ? 'r' : '-'),
|
|
|
|
($perms & 02 ? 'w' : '-'),
|
|
|
|
($perms & 01 ? 'x' : '-');
|
|
|
|
|
|
|
|
# Printable file type.
|
|
|
|
my $fmt_mode = $mode eq 'f' ? '-' : $mode;
|
|
|
|
|
|
|
|
# If it's a symbolic link, display the link.
|
|
|
|
my $link;
|
|
|
|
if ($mode eq 'l') {
|
|
|
|
$link = readlink $filename;
|
|
|
|
die "readlink: $!" unless defined $link;
|
|
|
|
}
|
|
|
|
my $fmt_link = defined $link ? " -> $link" : "";
|
|
|
|
|
|
|
|
# Display the file.
|
|
|
|
my $line = sprintf
|
|
|
|
("%s%s%4d %-8s %-8s %8d %s %s%s\r\n",
|
|
|
|
$fmt_mode,
|
|
|
|
$fmt_perms,
|
|
|
|
$nlink,
|
|
|
|
$user,
|
|
|
|
$group,
|
|
|
|
$size,
|
|
|
|
$fmt_time,
|
|
|
|
$filename,
|
|
|
|
$fmt_link);
|
|
|
|
$sock->print ($line);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub __get_file_list
|
|
|
|
{
|
|
|
|
my $dir = shift;
|
|
|
|
my $wildcard = shift;
|
|
|
|
|
|
|
|
opendir (DIRHANDLE, $dir)
|
|
|
|
or die "Cannot open directory!!!";
|
|
|
|
|
|
|
|
my @allfiles = readdir DIRHANDLE;
|
|
|
|
my @filenames = ();
|
|
|
|
|
|
|
|
if ($wildcard) {
|
|
|
|
# Get rid of . and ..
|
|
|
|
@allfiles = grep !/^\.{1,2}$/, @allfiles;
|
|
|
|
|
|
|
|
# Convert wildcard to a regular expression.
|
|
|
|
$wildcard = __wildcard_to_regex ($wildcard);
|
|
|
|
|
|
|
|
@filenames = grep /$wildcard/, @allfiles;
|
|
|
|
} else {
|
|
|
|
@filenames = @allfiles;
|
|
|
|
}
|
|
|
|
|
|
|
|
closedir (DIRHANDLE);
|
|
|
|
|
|
|
|
return sort @filenames;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub __wildcard_to_regex
|
|
|
|
{
|
|
|
|
my $wildcard = shift;
|
|
|
|
|
|
|
|
$wildcard =~ s,([^?*a-zA-Z0-9]),\\$1,g; # Escape punctuation.
|
|
|
|
$wildcard =~ s,\*,.*,g; # Turn * into .*
|
|
|
|
$wildcard =~ s,\?,.,g; # Turn ? into .
|
|
|
|
$wildcard = "^$wildcard\$"; # Bracket it.
|
|
|
|
|
|
|
|
return $wildcard;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
###########################################################################
|
|
|
|
# FTPSERVER CLASS
|
|
|
|
###########################################################################
|
|
|
|
|
|
|
|
{
|
|
|
|
my %_attr_data = ( # DEFAULT
|
|
|
|
_localAddr => 'localhost',
|
|
|
|
_localPort => 8021,
|
|
|
|
_reuseAddr => 1,
|
|
|
|
_rootDir => Cwd::getcwd(),
|
|
|
|
);
|
|
|
|
|
|
|
|
sub _default_for
|
|
|
|
{
|
|
|
|
my ($self, $attr) = @_;
|
|
|
|
$_attr_data{$attr};
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _standard_keys
|
|
|
|
{
|
|
|
|
keys %_attr_data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub new {
|
|
|
|
my ($caller, %args) = @_;
|
|
|
|
my $caller_is_obj = ref($caller);
|
|
|
|
my $class = $caller_is_obj || $caller;
|
|
|
|
my $self = bless {}, $class;
|
|
|
|
foreach my $attrname ($self->_standard_keys()) {
|
|
|
|
my ($argname) = ($attrname =~ /^_(.*)/);
|
|
|
|
if (exists $args{$argname}) {
|
|
|
|
$self->{$attrname} = $args{$argname};
|
|
|
|
} elsif ($caller_is_obj) {
|
|
|
|
$self->{$attrname} = $caller->{$attrname};
|
|
|
|
} else {
|
|
|
|
$self->{$attrname} = $self->_default_for($attrname);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $self;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sub run
|
|
|
|
{
|
|
|
|
my ($self, $synch_callback) = @_;
|
|
|
|
my $initialized = 0;
|
|
|
|
|
|
|
|
# turn buffering off on STDERR
|
|
|
|
select((select(STDERR), $|=1)[0]);
|
|
|
|
|
|
|
|
# initialize command table
|
|
|
|
my $command_table = {};
|
|
|
|
foreach (keys %_commands) {
|
|
|
|
my $subname = "_${_}_command";
|
|
|
|
$command_table->{$_} = \&$subname;
|
|
|
|
}
|
|
|
|
|
|
|
|
my $old_ils = $/;
|
|
|
|
$/ = "\r\n";
|
|
|
|
|
|
|
|
# create server socket
|
|
|
|
"0" =~ /(0)/; # Perl 5.7 / IO::Socket::INET bug workaround.
|
|
|
|
my $server_sock = IO::Socket::INET->new (LocalHost => $self->{_localAddr},
|
|
|
|
LocalPort => $self->{_localPort},
|
|
|
|
Listen => 1,
|
|
|
|
Reuse => $self->{_reuseAddr},
|
|
|
|
Proto => 'tcp',
|
|
|
|
Type => SOCK_STREAM) or die "bind: $!";
|
|
|
|
|
|
|
|
if (!$initialized) {
|
|
|
|
$synch_callback->();
|
|
|
|
$initialized = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
$SIG{CHLD} = sub { wait };
|
|
|
|
|
|
|
|
# the accept loop
|
|
|
|
while (my $client_addr = accept (my $socket, $server_sock))
|
|
|
|
{
|
|
|
|
# turn buffering off on $socket
|
|
|
|
select((select($socket), $|=1)[0]);
|
|
|
|
|
|
|
|
# find out who connected
|
|
|
|
my ($client_port, $client_ip) = sockaddr_in ($client_addr);
|
|
|
|
my $client_ipnum = inet_ntoa ($client_ip);
|
|
|
|
|
|
|
|
# print who connected
|
|
|
|
print STDERR "got a connection from: $client_ipnum\n" if $log;
|
|
|
|
|
|
|
|
# fork off a process to handle this connection.
|
2008-05-23 02:40:56 -04:00
|
|
|
# my $pid = fork();
|
|
|
|
# unless (defined $pid) {
|
|
|
|
# warn "fork: $!";
|
|
|
|
# sleep 5; # Back off in case system is overloaded.
|
|
|
|
# next;
|
|
|
|
# }
|
|
|
|
|
|
|
|
if (1) { # Child process.
|
2007-06-14 12:05:59 -04:00
|
|
|
|
|
|
|
# install signals
|
|
|
|
$SIG{URG} = sub {
|
|
|
|
$GOT_SIGURG = 1;
|
|
|
|
};
|
|
|
|
|
|
|
|
$SIG{PIPE} = sub {
|
|
|
|
print STDERR "Client closed connection abruptly.\n";
|
|
|
|
exit;
|
|
|
|
};
|
|
|
|
|
|
|
|
$SIG{ALRM} = sub {
|
|
|
|
print STDERR "Connection idle timeout expired. Closing server.\n";
|
|
|
|
exit;
|
|
|
|
};
|
|
|
|
|
|
|
|
#$SIG{CHLD} = 'IGNORE';
|
|
|
|
|
|
|
|
|
|
|
|
print STDERR "in child\n" if $log;
|
|
|
|
|
|
|
|
my $conn = {
|
|
|
|
'socket' => $socket,
|
|
|
|
'state' => $_connection_states{NEWCONN},
|
|
|
|
'dir' => '/',
|
|
|
|
'restart' => 0,
|
|
|
|
'idle_timeout' => 60, # 1 minute timeout
|
|
|
|
'rootdir' => $self->{_rootDir},
|
|
|
|
};
|
|
|
|
|
|
|
|
print {$conn->{socket}} "220 GNU Wget Testing FTP Server ready.\r\n";
|
|
|
|
|
|
|
|
# command handling loop
|
|
|
|
for (;;) {
|
|
|
|
print STDERR "waiting for request\n" if $log;
|
|
|
|
|
|
|
|
last unless defined (my $req = <$socket>);
|
|
|
|
|
|
|
|
# Remove trailing CRLF.
|
|
|
|
$req =~ s/[\n\r]+$//;
|
|
|
|
|
|
|
|
print STDERR "received request $req\n" if $log;
|
|
|
|
|
|
|
|
# Get the command.
|
|
|
|
# See also RFC 2640 section 3.1.
|
|
|
|
unless ($req =~ m/^([A-Z]{3,4})\s?(.*)/i) {
|
|
|
|
# badly formed command
|
|
|
|
exit 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
# The following strange 'eval' is necessary to work around a
|
|
|
|
# very odd bug in Perl 5.6.0. The following assignment to
|
|
|
|
# $cmd will fail in some cases unless you use $1 in some sort
|
|
|
|
# of an expression beforehand.
|
|
|
|
# - RWMJ 2002-07-05.
|
|
|
|
eval '$1 eq $1';
|
|
|
|
|
|
|
|
my ($cmd, $rest) = (uc $1, $2);
|
|
|
|
|
|
|
|
# Got a command which matches in the table?
|
|
|
|
unless (exists $command_table->{$cmd}) {
|
|
|
|
print {$conn->{socket}} "500 Unrecognized command.\r\n";
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Command requires user to be authenticated?
|
|
|
|
unless ($_commands{$cmd} | $conn->{state}) {
|
|
|
|
print {$conn->{socket}} "530 Not logged in.\r\n";
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Handle the QUIT command specially.
|
|
|
|
if ($cmd eq "QUIT") {
|
|
|
|
print {$conn->{socket}} "221 Goodbye. Service closing connection.\r\n";
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Run the command.
|
|
|
|
&{$command_table->{$cmd}} ($conn, $cmd, $rest);
|
|
|
|
}
|
|
|
|
} else { # Father
|
|
|
|
close $socket;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$/ = $old_ils;
|
|
|
|
}
|
|
|
|
|
|
|
|
1;
|
|
|
|
|
|
|
|
# vim: et ts=4 sw=4
|
|
|
|
|