1 # Part of this code was borrowed from Richard Jones's Net::FTPServer
2 # http://www.annexia.org/freeware/netftpserver
13 use POSIX qw(strftime);
21 my %_connection_states = (
28 # subset of FTP commands supported by these server and the respective
29 # connection states in which they are allowed
31 # Standard commands from RFC 959.
32 'CWD' => $_connection_states{LOGGEDIN} |
33 $_connection_states{TWOSOCKS},
34 # 'EPRT' => $_connection_states{LOGGEDIN},
35 # 'EPSV' => $_connection_states{LOGGEDIN},
36 'LIST' => $_connection_states{TWOSOCKS},
37 # 'LPRT' => $_connection_states{LOGGEDIN},
38 # 'LPSV' => $_connection_states{LOGGEDIN},
39 'PASS' => $_connection_states{WAIT4PWD},
40 'PASV' => $_connection_states{LOGGEDIN},
41 'PORT' => $_connection_states{LOGGEDIN},
42 'PWD' => $_connection_states{LOGGEDIN} |
43 $_connection_states{TWOSOCKS},
44 'QUIT' => $_connection_states{LOGGEDIN} |
45 $_connection_states{TWOSOCKS},
46 'REST' => $_connection_states{TWOSOCKS},
47 'RETR' => $_connection_states{TWOSOCKS},
48 'SYST' => $_connection_states{LOGGEDIN},
49 'TYPE' => $_connection_states{LOGGEDIN} |
50 $_connection_states{TWOSOCKS},
51 'USER' => $_connection_states{NEWCONN},
52 # From ftpexts Internet Draft.
53 'SIZE' => $_connection_states{LOGGEDIN} |
54 $_connection_states{TWOSOCKS},
59 # COMMAND-HANDLING ROUTINES
63 my ($conn, $cmd, $path) = @_;
66 my $newdir = $conn->{dir};
68 # If the path starts with a "/" then it's an absolute path.
69 if (substr ($path, 0, 1) eq "/") {
74 # Split the path into its component parts and process each separately.
75 my @elems = split /\//, $path;
78 if ($_ eq "" || $_ eq ".") {
81 } elsif ($_ eq "..") {
82 # Go to parent directory.
84 print {$conn->{socket}} "550 Directory not found.\r\n";
87 $newdir = substr ($newdir, 0, rindex ($newdir, "/"));
89 # Go into subdirectory, if it exists.
90 $newdir .= ("/" . $_);
91 if (! -d $conn->{rootdir} . $newdir) {
92 print {$conn->{socket}} "550 Directory not found.\r\n";
98 $conn->{dir} = $newdir;
103 my ($conn, $cmd, $path) = @_;
105 # This is something of a hack. Some clients expect a Unix server
106 # to respond to flags on the 'ls command line'. Remove these flags
107 # and ignore them. This is particularly an issue with ncftp 2.4.3.
108 $path =~ s/^-[a-zA-Z0-9]+\s?//;
110 my $dir = $conn->{dir};
112 print STDERR "_LIST_command - dir is: $dir\n";
115 if (substr ($path, 0, 1) eq "/") {
120 # Parse the first elements of the path until we find the appropriate
122 my @elems = split /\//, $path;
123 my ($wildcard, $filename);
126 for (my $i = 0; $i < @elems; ++$i) {
128 my $lastelement = $i == @elems-1;
130 if ($_ eq "" || $_ eq ".") { next } # Ignore these.
132 # Go to parent directory.
133 unless ($dir eq "/") {
134 $dir = substr ($dir, 0, rindex ($dir, "/"));
137 if (!$lastelement) { # These elements can only be directories.
138 unless (-d $conn->{rootdir} . $dir . $_) {
139 print {$conn->{socket}} "550 File or directory not found.\r\n";
143 } else { # It's the last element: check if it's a file, directory or wildcard.
144 if (-f $conn->{rootdir} . $dir . $_) {
147 } elsif (-d $conn->{rootdir} . $dir . $_) {
150 } elsif (/\*/ || /\?/) {
154 print {$conn->{socket}} "550 File or directory not found.\r\n";
161 print STDERR "_LIST_command - dir is: $dir\n" if $log;
163 print {$conn->{socket}} "150 Opening data connection for file listing.\r\n";
165 # Open a path back to the client.
166 my $sock = __open_data_connection ($conn);
169 print {$conn->{socket}} "425 Can't open data connection.\r\n";
173 # If the path contains a directory name, extract it so that
174 # we can prefix it to every filename listed.
175 my $prefix = (($filename || $wildcard) && $path =~ /(.*\/).*/) ? $1 : "";
177 print STDERR "_LIST_command - prefix is: $prefix\n" if $log;
179 # OK, we're either listing a full directory, listing a single
180 # file or listing a wildcard.
181 if ($filename) { # Single file.
182 __list_file ($sock, $prefix . $filename);
183 } else { # Wildcard or full directory $dirh.
185 # Synthesize (fake) "total" field for directory listing.
186 print $sock "total 1 \r\n";
189 foreach (__get_file_list ($conn->{rootdir} . $dir, $wildcard)) {
190 __list_file ($sock, $prefix . $_);
194 unless ($sock->close) {
195 print {$conn->{socket}} "550 Error closing data connection: $!\r\n";
199 print {$conn->{socket}} "226 Listing complete. Data connection has been closed.\r\n";
204 my ($conn, $cmd, $pass) = @_;
206 # TODO: implement authentication?
208 print STDERR "switching to LOGGEDIN state\n" if $log;
209 $conn->{state} = $_connection_states{LOGGEDIN};
211 if ($conn->{username} eq "anonymous") {
212 print {$conn->{socket}} "202 Anonymous user access is always granted.\r\n";
214 print {$conn->{socket}} "230 Authentication not implemented yet, access is always granted.\r\n";
220 my ($conn, $cmd, $rest) = @_;
222 # Open a listening socket - but don't actually accept on it yet.
223 "0" =~ /(0)/; # Perl 5.7 / IO::Socket::INET bug workaround.
224 my $sock = IO::Socket::INET->new (LocalHost => '127.0.0.1',
229 Type => SOCK_STREAM);
232 # Return a code 550 here, even though this is not in the RFC. XXX
233 print {$conn->{socket}} "550 Can't open a listening socket.\r\n";
237 $conn->{passive} = 1;
238 $conn->{passive_socket} = $sock;
240 # Get our port number.
241 my $sockport = $sock->sockport;
243 # Split the port number into high and low components.
244 my $p1 = int ($sockport / 256);
245 my $p2 = $sockport % 256;
247 $conn->{state} = $_connection_states{TWOSOCKS};
249 # We only accept connections from localhost.
250 print {$conn->{socket}} "227 Entering Passive Mode (127,0,0,1,$p1,$p2)\r\n";
255 my ($conn, $cmd, $rest) = @_;
257 # The arguments to PORT are a1,a2,a3,a4,p1,p2 where a1 is the
258 # most significant part of the address (eg. 127,0,0,1) and
259 # p1 is the most significant part of the port.
260 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})/) {
261 print {$conn->{socket}} "501 Syntax error in PORT command.\r\n";
265 # Check host address.
266 unless ($1 > 0 && $1 < 224 &&
267 $2 >= 0 && $2 < 256 &&
268 $3 >= 0 && $3 < 256 &&
269 $4 >= 0 && $4 < 256) {
270 print {$conn->{socket}} "501 Invalid host address.\r\n";
274 # Construct host address and port number.
275 my $peeraddrstring = "$1.$2.$3.$4";
276 my $peerport = $5 * 256 + $6;
279 unless ($peerport > 0 && $peerport < 65536) {
280 print {$conn->{socket}} "501 Invalid port number.\r\n";
283 $conn->{peeraddrstring} = $peeraddrstring;
284 $conn->{peeraddr} = inet_aton ($peeraddrstring);
285 $conn->{peerport} = $peerport;
286 $conn->{passive} = 0;
288 $conn->{state} = $_connection_states{TWOSOCKS};
290 print {$conn->{socket}} "200 PORT command OK.\r\n";
295 my ($conn, $cmd, $rest) = @_;
297 # See RFC 959 Appendix II and draft-ietf-ftpext-mlst-11.txt section 6.2.1.
298 my $pathname = $conn->{dir};
299 $pathname =~ s,/+$,, unless $pathname eq "/";
300 $pathname =~ tr,/,/,s;
302 print {$conn->{socket}} "257 \"$pathname\"\r\n";
307 my ($conn, $cmd, $restart_from) = @_;
309 unless ($restart_from =~ /^([1-9][0-9]*|0)$/) {
310 print {$conn->{socket}} "501 REST command needs a numeric argument.\r\n";
314 $conn->{restart} = $1;
316 print {$conn->{socket}} "350 Restarting next transfer at $1.\r\n";
321 my ($conn, $cmd, $path) = @_;
323 my $dir = $conn->{dir};
326 if (substr ($path, 0, 1) eq "/") {
329 $path = "." if $path eq "";
332 # Parse the first elements of path until we find the appropriate
334 my @elems = split /\//, $path;
335 my $filename = pop @elems;
338 if ($_ eq "" || $_ eq ".") {
340 } elsif ($_ eq "..") {
341 # Go to parent directory.
342 unless ($dir eq "/") {
343 $dir = substr ($dir, 0, rindex ($dir, "/"));
346 unless (-d $conn->{rootdir} . $dir . $_) {
347 print {$conn->{socket}} "550 File or directory not found.\r\n";
354 unless (defined $filename && length $filename) {
355 print {$conn->{socket}} "550 File or directory not found.\r\n";
359 if ($filename eq "." || $filename eq "..") {
360 print {$conn->{socket}} "550 RETR command is not supported on directories.\r\n";
364 my $fullname = $conn->{rootdir} . $dir . $filename;
365 unless (-f $fullname) {
366 print {$conn->{socket}} "550 RETR command is only supported on plain files.\r\n";
370 # Try to open the file.
371 unless (open (FILE, '<', $fullname)) {
372 print {$conn->{socket}} "550 File or directory not found.\r\n";
376 print {$conn->{socket}} "150 Opening " .
377 ($conn->{type} eq 'A' ? "ASCII mode" : "BINARY mode") .
378 " data connection for file $filename.\r\n";
380 # Open a path back to the client.
381 my $sock = __open_data_connection ($conn);
384 print {$conn->{socket}} "425 Can't open data connection.\r\n";
388 # What mode are we sending this file in?
389 unless ($conn->{type} eq 'A') # Binary type.
391 my ($r, $buffer, $n, $w);
393 # Restart the connection from previous point?
394 if ($conn->{restart}) {
395 # VFS seek method only required to support relative forward seeks
397 # In Perl = 5.00503, SEEK_CUR is exported by IO::Seekable,
398 # in Perl >= 5.6, SEEK_CUR is exported by both IO::Seekable
399 # and Fcntl. Hence we 'use IO::Seekable' at the top of the
400 # file to get this symbol reliably in both cases.
401 sysseek (FILE, $conn->{restart}, SEEK_CUR);
402 $conn->{restart} = 0;
406 while ($r = sysread (FILE, $buffer, 65536))
408 # Restart alarm clock timer.
409 alarm $conn->{idle_timeout};
411 for ($n = 0; $n < $r; )
413 $w = syswrite ($sock, $buffer, $r - $n, $n);
415 # Cleanup and exit if there was an error.
416 unless (defined $w) {
419 print {$conn->{socket}} "426 File retrieval error: $!. Data connection has been closed.\r\n";
426 # Transfer aborted by client?
431 print {$conn->{socket}} "426 Transfer aborted. Data connection closed.\r\n";
436 # Cleanup and exit if there was an error.
437 unless (defined $r) {
440 print {$conn->{socket}} "426 File retrieval error: $!. Data connection has been closed.\r\n";
443 } else { # ASCII type.
444 # Restart the connection from previous point?
445 if ($conn->{restart}) {
446 for (my $i = 0; $i < $conn->{restart}; ++$i) {
449 $conn->{restart} = 0;
453 while (defined ($_ = <FILE>)) {
454 # Remove any native line endings.
457 # Restart alarm clock timer.
458 alarm $conn->{idle_timeout};
460 # Write the line with telnet-format line endings.
461 print $sock "$_\r\n";
463 # Transfer aborted by client?
468 print {$conn->{socket}} "426 Transfer aborted. Data connection closed.\r\n";
474 unless (close ($sock) && close (FILE)) {
475 print {$conn->{socket}} "550 File retrieval error: $!.\r\n";
479 print {$conn->{socket}} "226 File retrieval complete. Data connection has been closed.\r\n";
484 my ($conn, $cmd, $path) = @_;
486 my $dir = $conn->{dir};
489 if (substr ($path, 0, 1) eq "/") {
492 $path = "." if $path eq "";
495 # Parse the first elements of path until we find the appropriate
497 my @elems = split /\//, $path;
498 my $filename = pop @elems;
501 if ($_ eq "" || $_ eq ".") {
503 } elsif ($_ eq "..") {
504 # Go to parent directory.
505 unless ($dir eq "/") {
506 $dir = substr ($dir, 0, rindex ($dir, "/"));
509 unless (-d $conn->{rootdir} . $dir . $_) {
510 print {$conn->{socket}} "550 File or directory not found.\r\n";
517 unless (defined $filename && length $filename) {
518 print {$conn->{socket}} "550 File or directory not found.\r\n";
522 if ($filename eq "." || $filename eq "..") {
523 print {$conn->{socket}} "550 SIZE command is not supported on directories.\r\n";
527 my $fullname = $conn->{rootdir} . $dir . $filename;
528 unless (-f $fullname) {
529 print {$conn->{socket}} "550 SIZE command is only supported on plain files.\r\n";
534 if ($conn->{type} eq 'A') {
535 # ASCII mode: we have to count the characters by hand.
536 unless (open (FILE, '<', $filename)) {
537 print {$conn->{socket}} "550 Cannot calculate size of $filename.\r\n";
540 $size++ while (defined (getc(FILE)));
543 # BINARY mode: we can use stat
544 $size = (stat($filename))[7];
547 print {$conn->{socket}} "213 $size\r\n";
552 my ($conn, $cmd, $dummy) = @_;
554 print {$conn->{socket}} "215 UNIX Type: L8\r\n";
559 my ($conn, $cmd, $type) = @_;
561 # See RFC 959 section 5.3.2.
562 if ($type =~ /^([AI])$/i) {
564 } elsif ($type =~ /^([AI])\sN$/i) {
566 } elsif ($type =~ /^L\s8$/i) {
567 $conn->{type} = 'L8';
569 print {$conn->{socket}} "504 This server does not support TYPE $type.\r\n";
573 print {$conn->{socket}} "200 TYPE changed to $type.\r\n";
578 my ($conn, $cmd, $username) = @_;
580 print STDERR "username: $username\n" if $log;
581 $conn->{username} = $username;
583 print STDERR "switching to WAIT4PWD state\n" if $log;
584 $conn->{state} = $_connection_states{WAIT4PWD};
586 if ($conn->{username} eq "anonymous") {
587 print {$conn->{socket}} "230 Anonymous user access granted.\r\n";
589 print {$conn->{socket}} "331 Password required.\r\n";
596 sub __open_data_connection
602 if ($conn->{passive}) {
603 # Passive mode - wait for a connection from the client.
604 accept ($sock, $conn->{passive_socket}) or return undef;
606 # Active mode - connect back to the client.
607 "0" =~ /(0)/; # Perl 5.7 / IO::Socket::INET bug workaround.
608 $sock = IO::Socket::INET->new (LocalAddr => '127.0.0.1',
609 PeerAddr => $conn->{peeraddrstring},
610 PeerPort => $conn->{peerport},
612 Type => SOCK_STREAM) or return undef;
622 my $filename = shift;
624 # Get the status information.
625 my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
626 $atime, $mtime, $ctime, $blksize, $blocks)
629 # If the file has been removed since we created this
630 # handle, then $dev will be undefined. Return immediately.
631 return unless defined $dev;
633 # Generate printable user/group.
634 my $user = getpwuid ($uid) || "-";
635 my $group = getgrgid ($gid) || "-";
637 # Permissions from mode.
638 my $perms = $mode & 0777;
640 # Work out the mode using special "_" operator which causes Perl
641 # to use the result of the previous stat call.
642 $mode = (-f _ ? 'f' :
648 (-c _ ? 'c' : '?')))))));
650 # Generate printable date (this logic is taken from GNU fileutils:
651 # src/ls.c: print_long_format).
654 if ($time > $mtime + 6 * 30 * 24 * 60 * 60 || $time < $mtime - 60 * 60) {
657 $fmt = "%b %e %H:%M";
660 my $fmt_time = strftime $fmt, localtime ($mtime);
662 # Generate printable permissions.
663 my $fmt_perms = join "",
664 ($perms & 0400 ? 'r' : '-'),
665 ($perms & 0200 ? 'w' : '-'),
666 ($perms & 0100 ? 'x' : '-'),
667 ($perms & 040 ? 'r' : '-'),
668 ($perms & 020 ? 'w' : '-'),
669 ($perms & 010 ? 'x' : '-'),
670 ($perms & 04 ? 'r' : '-'),
671 ($perms & 02 ? 'w' : '-'),
672 ($perms & 01 ? 'x' : '-');
674 # Printable file type.
675 my $fmt_mode = $mode eq 'f' ? '-' : $mode;
677 # If it's a symbolic link, display the link.
680 $link = readlink $filename;
681 die "readlink: $!" unless defined $link;
683 my $fmt_link = defined $link ? " -> $link" : "";
687 ("%s%s%4d %-8s %-8s %8d %s %s%s\r\n",
697 $sock->print ($line);
704 my $wildcard = shift;
706 opendir (DIRHANDLE, $dir)
707 or die "Cannot open directory!!!";
709 my @allfiles = readdir DIRHANDLE;
713 # Get rid of . and ..
714 @allfiles = grep !/^\.{1,2}$/, @allfiles;
716 # Convert wildcard to a regular expression.
717 $wildcard = __wildcard_to_regex ($wildcard);
719 @filenames = grep /$wildcard/, @allfiles;
721 @filenames = @allfiles;
724 closedir (DIRHANDLE);
726 return sort @filenames;
730 sub __wildcard_to_regex
732 my $wildcard = shift;
734 $wildcard =~ s,([^?*a-zA-Z0-9]),\\$1,g; # Escape punctuation.
735 $wildcard =~ s,\*,.*,g; # Turn * into .*
736 $wildcard =~ s,\?,.,g; # Turn ? into .
737 $wildcard = "^$wildcard\$"; # Bracket it.
743 ###########################################################################
745 ###########################################################################
748 my %_attr_data = ( # DEFAULT
749 _localAddr => 'localhost',
752 _rootDir => Cwd::getcwd(),
757 my ($self, $attr) = @_;
769 my ($caller, %args) = @_;
770 my $caller_is_obj = ref($caller);
771 my $class = $caller_is_obj || $caller;
772 my $self = bless {}, $class;
773 foreach my $attrname ($self->_standard_keys()) {
774 my ($argname) = ($attrname =~ /^_(.*)/);
775 if (exists $args{$argname}) {
776 $self->{$attrname} = $args{$argname};
777 } elsif ($caller_is_obj) {
778 $self->{$attrname} = $caller->{$attrname};
780 $self->{$attrname} = $self->_default_for($attrname);
783 # create server socket
784 "0" =~ /(0)/; # Perl 5.7 / IO::Socket::INET bug workaround.
785 $self->{_server_sock}
786 = IO::Socket::INET->new (LocalHost => $self->{_localAddr},
787 LocalPort => $self->{_localPort},
789 Reuse => $self->{_reuseAddr},
799 my ($self, $synch_callback) = @_;
802 # turn buffering off on STDERR
803 select((select(STDERR), $|=1)[0]);
805 # initialize command table
806 my $command_table = {};
807 foreach (keys %_commands) {
808 my $subname = "_${_}_command";
809 $command_table->{$_} = \&$subname;
820 $SIG{CHLD} = sub { wait };
821 my $server_sock = $self->{_server_sock};
824 while (my $client_addr = accept (my $socket, $server_sock))
826 # turn buffering off on $socket
827 select((select($socket), $|=1)[0]);
829 # find out who connected
830 my ($client_port, $client_ip) = sockaddr_in ($client_addr);
831 my $client_ipnum = inet_ntoa ($client_ip);
833 # print who connected
834 print STDERR "got a connection from: $client_ipnum\n" if $log;
836 # fork off a process to handle this connection.
838 # unless (defined $pid) {
840 # sleep 5; # Back off in case system is overloaded.
844 if (1) { # Child process.
852 print STDERR "Client closed connection abruptly.\n";
857 print STDERR "Connection idle timeout expired. Closing server.\n";
861 #$SIG{CHLD} = 'IGNORE';
864 print STDERR "in child\n" if $log;
868 'state' => $_connection_states{NEWCONN},
871 'idle_timeout' => 60, # 1 minute timeout
872 'rootdir' => $self->{_rootDir},
875 print {$conn->{socket}} "220 GNU Wget Testing FTP Server ready.\r\n";
877 # command handling loop
879 print STDERR "waiting for request\n" if $log;
881 last unless defined (my $req = <$socket>);
883 # Remove trailing CRLF.
884 $req =~ s/[\n\r]+$//;
886 print STDERR "received request $req\n" if $log;
889 # See also RFC 2640 section 3.1.
890 unless ($req =~ m/^([A-Z]{3,4})\s?(.*)/i) {
891 # badly formed command
895 # The following strange 'eval' is necessary to work around a
896 # very odd bug in Perl 5.6.0. The following assignment to
897 # $cmd will fail in some cases unless you use $1 in some sort
898 # of an expression beforehand.
902 my ($cmd, $rest) = (uc $1, $2);
904 # Got a command which matches in the table?
905 unless (exists $command_table->{$cmd}) {
906 print {$conn->{socket}} "500 Unrecognized command.\r\n";
910 # Command requires user to be authenticated?
911 unless ($_commands{$cmd} | $conn->{state}) {
912 print {$conn->{socket}} "530 Not logged in.\r\n";
916 # Handle the QUIT command specially.
917 if ($cmd eq "QUIT") {
918 print {$conn->{socket}} "221 Goodbye. Service closing connection.\r\n";
923 &{$command_table->{$cmd}} ($conn, $cmd, $rest);
935 return $self->{_server_sock}->sockport;