21
20
# connection states
22
21
my %_connection_states = (
25
24
'LOGGEDIN' => 0x04,
26
25
'TWOSOCKS' => 0x08,
29
28
# subset of FTP commands supported by these server and the respective
30
29
# connection states in which they are allowed
32
31
# Standard commands from RFC 959.
33
32
'CWD' => $_connection_states{LOGGEDIN} |
34
$_connection_states{TWOSOCKS},
33
$_connection_states{TWOSOCKS},
35
34
# 'EPRT' => $_connection_states{LOGGEDIN},
36
# 'EPSV' => $_connection_states{LOGGEDIN},
37
'LIST' => $_connection_states{TWOSOCKS},
35
# 'EPSV' => $_connection_states{LOGGEDIN},
36
'LIST' => $_connection_states{TWOSOCKS},
38
37
# 'LPRT' => $_connection_states{LOGGEDIN},
39
# 'LPSV' => $_connection_states{LOGGEDIN},
40
'PASS' => $_connection_states{WAIT4PWD},
41
'PASV' => $_connection_states{LOGGEDIN},
42
'PORT' => $_connection_states{LOGGEDIN},
38
# 'LPSV' => $_connection_states{LOGGEDIN},
39
'PASS' => $_connection_states{WAIT4PWD},
40
'PASV' => $_connection_states{LOGGEDIN},
41
'PORT' => $_connection_states{LOGGEDIN},
43
42
'PWD' => $_connection_states{LOGGEDIN} |
44
$_connection_states{TWOSOCKS},
43
$_connection_states{TWOSOCKS},
45
44
'QUIT' => $_connection_states{LOGGEDIN} |
46
$_connection_states{TWOSOCKS},
47
'REST' => $_connection_states{TWOSOCKS},
48
'RETR' => $_connection_states{TWOSOCKS},
45
$_connection_states{TWOSOCKS},
46
'REST' => $_connection_states{TWOSOCKS},
47
'RETR' => $_connection_states{TWOSOCKS},
49
48
'SYST' => $_connection_states{LOGGEDIN},
50
49
'TYPE' => $_connection_states{LOGGEDIN} |
51
50
$_connection_states{TWOSOCKS},
52
'USER' => $_connection_states{NEWCONN},
51
'USER' => $_connection_states{NEWCONN},
53
52
# From ftpexts Internet Draft.
54
53
'SIZE' => $_connection_states{LOGGEDIN} |
55
54
$_connection_states{TWOSOCKS},
64
63
my ($conn, $cmd, $path) = @_;
64
my $paths = $conn->{'paths'};
67
my $newdir = $conn->{dir};
69
# If the path starts with a "/" then it's an absolute path.
70
if (substr ($path, 0, 1) eq "/") {
67
my $new_path = FTPPaths::path_merge($conn->{'dir'}, $path);
75
69
# Split the path into its component parts and process each separately.
76
my @elems = split /\//, $path;
79
if ($_ eq "" || $_ eq ".") {
82
} elsif ($_ eq "..") {
83
# Go to parent directory.
85
print {$conn->{socket}} "550 Directory not found.\r\n";
88
$newdir = substr ($newdir, 0, rindex ($newdir, "/"));
90
# Go into subdirectory, if it exists.
91
$newdir .= ("/" . $_);
92
if (! -d $conn->{rootdir} . $newdir) {
93
print {$conn->{socket}} "550 Directory not found.\r\n";
70
if (! $paths->dir_exists($new_path)) {
71
print {$conn->{socket}} "550 Directory not found.\r\n";
99
$conn->{dir} = $newdir;
75
$conn->{'dir'} = $new_path;
76
print {$conn->{socket}} "200 directory changed to $new_path.\r\n";
104
81
my ($conn, $cmd, $path) = @_;
82
my $paths = $conn->{'paths'};
106
84
# This is something of a hack. Some clients expect a Unix server
107
85
# to respond to flags on the 'ls command line'. Remove these flags
108
86
# and ignore them. This is particularly an issue with ncftp 2.4.3.
109
87
$path =~ s/^-[a-zA-Z0-9]+\s?//;
111
my $dir = $conn->{dir};
89
my $dir = $conn->{'dir'};
113
91
print STDERR "_LIST_command - dir is: $dir\n";
116
if (substr ($path, 0, 1) eq "/") {
121
93
# Parse the first elements of the path until we find the appropriate
122
94
# working directory.
123
my @elems = split /\//, $path;
124
my ($wildcard, $filename);
127
for (my $i = 0; $i < @elems; ++$i) {
129
my $lastelement = $i == @elems-1;
131
if ($_ eq "" || $_ eq ".") { next } # Ignore these.
133
# Go to parent directory.
134
unless ($dir eq "/") {
135
$dir = substr ($dir, 0, rindex ($dir, "/"));
138
if (!$lastelement) { # These elements can only be directories.
139
unless (-d $conn->{rootdir} . $dir . $_) {
140
print {$conn->{socket}} "550 File or directory not found.\r\n";
144
} else { # It's the last element: check if it's a file, directory or wildcard.
145
if (-f $conn->{rootdir} . $dir . $_) {
148
} elsif (-d $conn->{rootdir} . $dir . $_) {
151
} elsif (/\*/ || /\?/) {
155
print {$conn->{socket}} "550 File or directory not found.\r\n";
97
$dir = FTPPaths::path_merge($dir, $path);
98
my $listing = $paths->get_list($dir);
100
print {$conn->{socket}} "550 File or directory not found.\r\n";
162
104
print STDERR "_LIST_command - dir is: $dir\n" if $log;
164
106
print {$conn->{socket}} "150 Opening data connection for file listing.\r\n";
166
108
# Open a path back to the client.
167
109
my $sock = __open_data_connection ($conn);
170
111
print {$conn->{socket}} "425 Can't open data connection.\r\n";
174
# If the path contains a directory name, extract it so that
175
# we can prefix it to every filename listed.
176
my $prefix = (($filename || $wildcard) && $path =~ /(.*\/).*/) ? $1 : "";
178
print STDERR "_LIST_command - prefix is: $prefix\n" if $log;
180
# OK, we're either listing a full directory, listing a single
181
# file or listing a wildcard.
182
if ($filename) { # Single file.
183
__list_file ($sock, $prefix . $filename);
184
} else { # Wildcard or full directory $dirh.
186
# Synthesize (fake) "total" field for directory listing.
187
print $sock "total 1 \r\n";
190
foreach (__get_file_list ($conn->{rootdir} . $dir, $wildcard)) {
191
__list_file ($sock, $prefix . $_);
115
for my $item (@$listing) {
116
print $sock "$item\r\n";
195
119
unless ($sock->close) {
196
120
print {$conn->{socket}} "550 Error closing data connection: $!\r\n";
320
244
sub _RETR_command
322
246
my ($conn, $cmd, $path) = @_;
324
my $dir = $conn->{dir};
327
if (substr ($path, 0, 1) eq "/") {
330
$path = "." if $path eq "";
333
# Parse the first elements of path until we find the appropriate
335
my @elems = split /\//, $path;
336
my $filename = pop @elems;
339
if ($_ eq "" || $_ eq ".") {
341
} elsif ($_ eq "..") {
342
# Go to parent directory.
343
unless ($dir eq "/") {
344
$dir = substr ($dir, 0, rindex ($dir, "/"));
347
unless (-d $conn->{rootdir} . $dir . $_) {
348
print {$conn->{socket}} "550 File or directory not found.\r\n";
355
unless (defined $filename && length $filename) {
356
print {$conn->{socket}} "550 File or directory not found.\r\n";
360
if ($filename eq "." || $filename eq "..") {
361
print {$conn->{socket}} "550 RETR command is not supported on directories.\r\n";
365
my $fullname = $conn->{rootdir} . $dir . $filename;
366
unless (-f $fullname) {
367
print {$conn->{socket}} "550 RETR command is only supported on plain files.\r\n";
371
# Try to open the file.
372
unless (open (FILE, '<', $fullname)) {
373
print {$conn->{socket}} "550 File or directory not found.\r\n";
248
$path = FTPPaths::path_merge($conn->{dir}, $path);
249
my $info = $conn->{'paths'}->get_info($path);
251
unless ($info->{'_type'} eq 'f') {
252
print {$conn->{socket}} "550 File not found.\r\n";
377
256
print {$conn->{socket}} "150 Opening " .
378
257
($conn->{type} eq 'A' ? "ASCII mode" : "BINARY mode") .
379
" data connection for file $filename.\r\n";
258
" data connection.\r\n";
381
260
# Open a path back to the client.
382
261
my $sock = __open_data_connection ($conn);
268
my $content = $info->{'content'};
270
# Restart the connection from previous point?
271
if ($conn->{restart}) {
272
$content = substr($content, $conn->{restart});
273
$conn->{restart} = 0;
389
276
# What mode are we sending this file in?
390
277
unless ($conn->{type} eq 'A') # Binary type.
392
279
my ($r, $buffer, $n, $w);
394
# Restart the connection from previous point?
395
if ($conn->{restart}) {
396
# VFS seek method only required to support relative forward seeks
398
# In Perl = 5.00503, SEEK_CUR is exported by IO::Seekable,
399
# in Perl >= 5.6, SEEK_CUR is exported by both IO::Seekable
400
# and Fcntl. Hence we 'use IO::Seekable' at the top of the
401
# file to get this symbol reliably in both cases.
402
sysseek (FILE, $conn->{restart}, SEEK_CUR);
403
$conn->{restart} = 0;
407
while ($r = sysread (FILE, $buffer, 65536))
283
while ($buffer = substr($content, 0, 65536))
409
287
# Restart alarm clock timer.
410
288
alarm $conn->{idle_timeout};
483
350
sub _SIZE_command
485
352
my ($conn, $cmd, $path) = @_;
487
my $dir = $conn->{dir};
490
if (substr ($path, 0, 1) eq "/") {
493
$path = "." if $path eq "";
496
# Parse the first elements of path until we find the appropriate
498
my @elems = split /\//, $path;
499
my $filename = pop @elems;
502
if ($_ eq "" || $_ eq ".") {
504
} elsif ($_ eq "..") {
505
# Go to parent directory.
506
unless ($dir eq "/") {
507
$dir = substr ($dir, 0, rindex ($dir, "/"));
510
unless (-d $conn->{rootdir} . $dir . $_) {
511
print {$conn->{socket}} "550 File or directory not found.\r\n";
518
unless (defined $filename && length $filename) {
354
$path = FTPPaths::path_merge($conn->{dir}, $path);
355
my $info = $conn->{'paths'}->get_info($path);
519
357
print {$conn->{socket}} "550 File or directory not found.\r\n";
523
if ($filename eq "." || $filename eq "..") {
361
if ($info->{'_type'} eq 'd') {
524
362
print {$conn->{socket}} "550 SIZE command is not supported on directories.\r\n";
528
my $fullname = $conn->{rootdir} . $dir . $filename;
529
unless (-f $fullname) {
530
print {$conn->{socket}} "550 SIZE command is only supported on plain files.\r\n";
535
if ($conn->{type} eq 'A') {
536
# ASCII mode: we have to count the characters by hand.
537
unless (open (FILE, '<', $filename)) {
538
print {$conn->{socket}} "550 Cannot calculate size of $filename.\r\n";
541
$size++ while (defined (getc(FILE)));
544
# BINARY mode: we can use stat
545
$size = (stat($filename))[7];
366
my $size = length $info->{'content'};
548
368
print {$conn->{socket}} "213 $size\r\n";
623
my $filename = shift;
625
# Get the status information.
626
my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
627
$atime, $mtime, $ctime, $blksize, $blocks)
630
# If the file has been removed since we created this
631
# handle, then $dev will be undefined. Return immediately.
632
return unless defined $dev;
634
# Generate printable user/group.
635
my $user = getpwuid ($uid) || "-";
636
my $group = getgrgid ($gid) || "-";
638
# Permissions from mode.
639
my $perms = $mode & 0777;
641
# Work out the mode using special "_" operator which causes Perl
642
# to use the result of the previous stat call.
643
$mode = (-f _ ? 'f' :
649
(-c _ ? 'c' : '?')))))));
651
# Generate printable date (this logic is taken from GNU fileutils:
652
# src/ls.c: print_long_format).
655
if ($time > $mtime + 6 * 30 * 24 * 60 * 60 || $time < $mtime - 60 * 60) {
658
$fmt = "%b %e %H:%M";
661
my $fmt_time = strftime $fmt, localtime ($mtime);
663
# Generate printable permissions.
664
my $fmt_perms = join "",
665
($perms & 0400 ? 'r' : '-'),
666
($perms & 0200 ? 'w' : '-'),
667
($perms & 0100 ? 'x' : '-'),
668
($perms & 040 ? 'r' : '-'),
669
($perms & 020 ? 'w' : '-'),
670
($perms & 010 ? 'x' : '-'),
671
($perms & 04 ? 'r' : '-'),
672
($perms & 02 ? 'w' : '-'),
673
($perms & 01 ? 'x' : '-');
675
# Printable file type.
676
my $fmt_mode = $mode eq 'f' ? '-' : $mode;
678
# If it's a symbolic link, display the link.
681
$link = readlink $filename;
682
die "readlink: $!" unless defined $link;
684
my $fmt_link = defined $link ? " -> $link" : "";
688
("%s%s%4d %-8s %-8s %8d %s %s%s\r\n",
698
$sock->print ($line);
705
my $wildcard = shift;
707
opendir (DIRHANDLE, $dir)
708
or die "Cannot open directory!!!";
710
my @allfiles = readdir DIRHANDLE;
714
# Get rid of . and ..
715
@allfiles = grep !/^\.{1,2}$/, @allfiles;
717
# Convert wildcard to a regular expression.
718
$wildcard = __wildcard_to_regex ($wildcard);
720
@filenames = grep /$wildcard/, @allfiles;
722
@filenames = @allfiles;
725
closedir (DIRHANDLE);
727
return sort @filenames;
731
sub __wildcard_to_regex
733
my $wildcard = shift;
735
$wildcard =~ s,([^?*a-zA-Z0-9]),\\$1,g; # Escape punctuation.
736
$wildcard =~ s,\*,.*,g; # Turn * into .*
737
$wildcard =~ s,\?,.,g; # Turn ? into .
738
$wildcard = "^$wildcard\$"; # Bracket it.
744
440
###########################################################################
745
441
# FTPSERVER CLASS
746
442
###########################################################################
749
445
my %_attr_data = ( # DEFAULT
750
_localAddr => 'localhost',
753
_rootDir => Cwd::getcwd(),
447
_localAddr => 'localhost',
450
_rootDir => Cwd::getcwd(),
451
_server_behavior => {},
758
456
my ($self, $attr) = @_;
759
457
$_attr_data{$attr};
764
462
keys %_attr_data;
803
511
my $old_ils = $/;
806
# create server socket
807
"0" =~ /(0)/; # Perl 5.7 / IO::Socket::INET bug workaround.
808
my $server_sock = IO::Socket::INET->new (LocalHost => $self->{_localAddr},
809
LocalPort => $self->{_localPort},
811
Reuse => $self->{_reuseAddr},
813
Type => SOCK_STREAM) or die "bind: $!";
815
514
if (!$initialized) {
816
515
$synch_callback->();
817
516
$initialized = 1;
820
519
$SIG{CHLD} = sub { wait };
520
my $server_sock = $self->{_server_sock};
822
522
# the accept loop
823
523
while (my $client_addr = accept (my $socket, $server_sock))
825
525
# turn buffering off on $socket
826
526
select((select($socket), $|=1)[0]);
828
# find out who connected
528
# find out who connected
829
529
my ($client_port, $client_ip) = sockaddr_in ($client_addr);
830
530
my $client_ipnum = inet_ntoa ($client_ip);
856
556
print STDERR "Connection idle timeout expired. Closing server.\n";
860
560
#$SIG{CHLD} = 'IGNORE';
863
563
print STDERR "in child\n" if $log;
867
'state' => $_connection_states{NEWCONN},
870
'idle_timeout' => 60, # 1 minute timeout
871
'rootdir' => $self->{_rootDir},
566
'paths' => FTPPaths->new($self->{'_input'},
567
$self->{'_server_behavior'}),
569
'state' => $_connection_states{NEWCONN},
572
'idle_timeout' => 60, # 1 minute timeout
573
'rootdir' => $self->{_rootDir},
874
576
print {$conn->{socket}} "220 GNU Wget Testing FTP Server ready.\r\n";
876
578
# command handling loop
911
613
print {$conn->{socket}} "530 Not logged in.\r\n";
915
617
# Handle the QUIT command specially.
916
618
if ($cmd eq "QUIT") {
917
619
print {$conn->{socket}} "221 Goodbye. Service closing connection.\r\n";
623
if (defined ($self->{_server_behavior}{fail_on_pasv})
625
undef $self->{_server_behavior}{fail_on_pasv};
921
630
# Run the command.
922
631
&{$command_table->{$cmd}} ($conn, $cmd, $rest);
924
633
} else { # Father
643
return $self->{_server_sock}->sockport;
649
use POSIX qw(strftime);
652
sub final_component {
671
my @components = split('/', $b);
673
foreach my $c (@components) {
676
} elsif ($c eq '..') {
688
my ($this, @args) = @_;
689
my $class = ref($this) || $this;
692
$self->initialize(@args);
697
my ($self, $urls, $behavior) = @_;
698
my $paths = {_type => 'd'};
700
# From a path like '/foo/bar/baz.txt', construct $paths such that
701
# $paths->{'foo'}->{'bar'}->{'baz.txt'} is
702
# $urls->{'/foo/bar/baz.txt'}.
703
for my $path (keys %$urls) {
704
my @components = split('/', $path);
707
for my $c (@components) {
708
unless (exists $x->{$c}) {
709
$x->{$c} = {_type => 'd'};
713
%$x = %{$urls->{$path}};
717
$self->{'_paths'} = $paths;
718
$self->{'_behavior'} = $behavior;
722
my ($self, $path, $node) = @_;
723
$node = $self->{'_paths'} unless $node;
724
my @components = split('/', $path);
725
shift @components if @components && $components[0] eq '';
727
for my $c (@components) {
728
if ($node->{'_type'} eq 'd') {
738
my ($self, $path) = @_;
739
return $self->exists($path, 'd');
743
# type is optional, in which case we don't check it.
744
my ($self, $path, $type) = @_;
745
my $paths = $self->{'_paths'};
747
die "Invalid path $path (not absolute).\n" unless $path =~ m.^/.;
748
my $info = $self->get_info($path);
749
return 0 unless defined($info);
750
return $info->{'_type'} eq $type if defined($type);
754
sub _format_for_list {
755
my ($self, $name, $info) = @_;
757
# XXX: mode should be specifyable as part of the node info.
759
if ($info->{'_type'} eq 'd') {
760
$mode_str = 'dr-xr-xr-x';
762
$mode_str = '-r--r--r--';
766
if ($info->{'_type'} eq 'f') {
767
$size = length $info->{'content'};
768
if ($self->{'_behavior'}{'bad_list'}) {
772
my $date = strftime ("%b %e %H:%M", localtime);
773
return "$mode_str 1 0 0 $size $date $name";
777
my ($self, $path) = @_;
778
my $info = $self->get_info($path);
779
return undef unless defined $info;
782
if ($info->{'_type'} eq 'd') {
783
for my $item (keys %$info) {
784
next if $item =~ /^_/;
785
push @$list, $self->_format_for_list($item, $info->{$item});
788
push @$list, $self->_format_for_list(final_component($path), $info);
934
796
# vim: et ts=4 sw=4