2 #***************************************************************************
4 # Project ___| | | | _ \| |
6 # | (__| |_| | _ <| |___
7 # \___|\___/|_| \_\_____|
9 # Copyright (C) 1998 - 2008, Daniel Stenberg, <daniel@haxx.se>, et al.
11 # This software is licensed as described in the file COPYING, which
12 # you should have received as part of this distribution. The terms
13 # are also available at http://curl.haxx.se/docs/copyright.html.
15 # You may opt to use, copy, modify, merge, publish, distribute and/or sell
16 # copies of the Software, and permit persons to whom the Software is
17 # furnished to do so, under the terms of the COPYING file.
19 # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20 # KIND, either express or implied.
22 # $Id: ftpserver.pl,v 1.1.1.1 2008-09-23 16:32:06 hoffman Exp $
23 ###########################################################################
25 # This is the FTP server designed for the curl test suite.
27 # It is meant to exercise curl, it is not meant to be a fully working
28 # or even very standard compliant server.
30 # You may optionally specify port on the command line, otherwise it'll
31 # default to port 8921.
33 # All socket/network/TCP related stuff is done by the 'sockfilt' program.
38 #use Time::HiRes qw( gettimeofday ); # not available in perl 5.6
46 # open and close each time to allow removal at any time
48 # if later than perl 5.6 is used
49 # my ($seconds, $microseconds) = gettimeofday;
51 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
53 open(FTPLOG
, ">>log/ftpd$ftpdnum.log");
54 printf FTPLOG
("%02d:%02d:%02d ", $hour, $min, $sec);
60 # append to the server.input file
61 open(INPUT
, ">>log/server$ftpdnum.input") ||
62 logmsg
"failed to open log/server$ftpdnum.input\n";
67 # use this, open->print->close system only to make the file
68 # open as little as possible, to make the test suite run
69 # better on windows/cygwin
72 my $verbose=0; # set to 1 for debugging
78 my $controldelay=0; # set to 1 to delay the control connect data sending to
79 # test that curl deals with that nicely
80 my $slavepid; # for the DATA connection sockfilt slave process
82 my $ext; # append to log/pid file names
84 my $port = 8921; # just a default
85 my $listenaddr = "127.0.0.1"; # just a default
86 my $pidfile = ".ftpd.pid"; # a default, use --pidfile
88 my $SERVERLOGS_LOCK="log/serverlogs.lock"; # server logs advisor read lock
91 if($ARGV[0] eq "-v") {
94 elsif($ARGV[0] eq "-s") {
98 elsif($ARGV[0] eq "--id") {
102 elsif($ARGV[0] eq "--pidfile") {
106 elsif($ARGV[0] eq "--ipv6") {
111 elsif($ARGV[0] eq "--port") {
115 elsif($ARGV[0] eq "--addr") {
116 $listenaddr = $ARGV[1];
117 $listenaddr =~ s/^\[(.*)\]$/\1/;
120 } while(shift @ARGV);
124 print STDERR
"ftpserver.pl received SIG$signame, exiting\n";
126 clear_advisor_read_lock
($SERVERLOGS_LOCK);
127 die "Somebody sent me a SIG$signame";
129 $SIG{INT
} = \
&catch_zap
;
130 $SIG{KILL
} = \
&catch_zap
;
134 local(*SFREAD
, *SFWRITE
);
144 $result = sysread($$FH, $$scalar, $length);
146 if(not defined $result) {
147 ($fcaller, $lcaller) = (caller)[1,2];
148 logmsg
"Failed to read input\n";
149 logmsg
"Error: ftp$ftpdnum$ext sysread error: $!\n";
152 clear_advisor_read_lock
($SERVERLOGS_LOCK);
153 die "Died in sysread_or_die() at $fcaller " .
154 "line $lcaller. ftp$ftpdnum$ext sysread error: $!\n";
156 elsif($result == 0) {
157 ($fcaller, $lcaller) = (caller)[1,2];
158 logmsg
"Failed to read input\n";
159 logmsg
"Error: ftp$ftpdnum$ext read zero\n";
162 clear_advisor_read_lock
($SERVERLOGS_LOCK);
163 die "Died in sysread_or_die() at $fcaller " .
164 "line $lcaller. ftp$ftpdnum$ext read zero\n";
171 my $cmd="./server/sockfilt --port $port --logfile log/sockctrl$ftpdnum$ext.log --pidfile .sockfilt$ftpdnum$ext.pid $ipv6";
172 $sfpid = open2
(*SFREAD
, *SFWRITE
, $cmd);
174 print STDERR
"$cmd\n" if($verbose);
176 print SFWRITE
"PING\n";
178 sysread SFREAD
, $pong, 5;
180 if($pong !~ /^PONG/) {
181 logmsg
"Failed sockfilt command: $cmd\n";
184 clear_advisor_read_lock
($SERVERLOGS_LOCK);
185 die "Failed to start sockfilt!";
189 # remove the file here so that if startsf() fails, it is very noticeable
194 logmsg
sprintf("FTP server listens on port IPv%d/$port\n", $ipv6?
6:4);
195 open(PID
, ">$pidfile");
199 logmsg
("logged pid $$ in $pidfile\n");
204 printf SFWRITE
"DATA\n%04x\n", length($l);
210 # Send data to the client on the control stream, which happens to be plain
215 # spit it all out at once
219 my $a = join("", @_);
220 my @a = split("", $a);
224 select(undef, undef, undef, 0.01);
235 # Send data to the client on the data stream
240 printf DWRITE
"DATA\n%04x\n", length($l);
245 # this text is shown before the function specified below is run
246 my %displaytext = ('USER' => '331 We are happy you popped in!',
247 'PASS' => '230 Welcome you silly person',
248 'PORT' => '200 You said PORT - I say FINE',
249 'TYPE' => '200 I modify TYPE as you wanted',
250 'LIST' => '150 here comes a directory',
251 'NLST' => '150 here comes a directory',
252 'CWD' => '250 CWD command successful.',
253 'SYST' => '215 UNIX Type: L8', # just fake something
254 'QUIT' => '221 bye bye baby', # just reply something
255 'PWD' => '257 "/nowhere/anywhere" is current directory',
256 'MKD' => '257 Created your requested directory',
257 'REST' => '350 Yeah yeah we set it there for you',
258 'DELE' => '200 OK OK OK whatever you say',
259 'RNFR' => '350 Received your order. Please provide more',
260 'RNTO' => '250 Ok, thanks. File renaming completed.',
261 'NOOP' => '200 Yes, I\'m very good at doing nothing.',
262 'PBSZ' => '500 PBSZ not implemented',
263 'PROT' => '500 PROT not implemented',
266 # callback functions for certain commands
267 my %commandfunc = ( 'PORT' => \
&PORT_command
,
268 'EPRT' => \
&PORT_command
,
269 'LIST' => \
&LIST_command
,
270 'NLST' => \
&NLST_command
,
271 'PASV' => \
&PASV_command
,
272 'EPSV' => \
&PASV_command
,
273 'RETR' => \
&RETR_command
,
274 'SIZE' => \
&SIZE_command
,
275 'REST' => \
&REST_command
,
276 'STOR' => \
&STOR_command
,
277 'APPE' => \
&STOR_command
, # append looks like upload
278 'MDTM' => \
&MDTM_command
,
283 my ($closed)=@_; # non-zero if already disconnected
286 logmsg
"* disconnect data connection\n";
287 print DWRITE
"DISC\n";
289 sysread DREAD
, $i, 5;
292 logmsg
"data connection already disconnected\n";
294 logmsg
"=====> Closed data connection\n";
296 logmsg
"* quit sockfilt for data (pid $slavepid)\n";
297 print DWRITE
"QUIT\n";
298 waitpid $slavepid, 0;
305 logmsg
"Set REST position to $rest\n"
309 # print "150 ASCII data connection for /bin/ls (193.15.23.1,59196) (0 bytes)\r\n";
311 # this is a built-in fake-dir ;-)
312 my @ftpdir=("total 20\r\n",
313 "drwxr-xr-x 8 98 98 512 Oct 22 13:06 .\r\n",
314 "drwxr-xr-x 8 98 98 512 Oct 22 13:06 ..\r\n",
315 "drwxr-xr-x 2 98 98 512 May 2 1996 .NeXT\r\n",
316 "-r--r--r-- 1 0 1 35 Jul 16 1996 README\r\n",
317 "lrwxrwxrwx 1 0 1 7 Dec 9 1999 bin -> usr/bin\r\n",
318 "dr-xr-xr-x 2 0 1 512 Oct 1 1997 dev\r\n",
319 "drwxrwxrwx 2 98 98 512 May 29 16:04 download.html\r\n",
320 "dr-xr-xr-x 2 0 1 512 Nov 30 1995 etc\r\n",
321 "drwxrwxrwx 2 98 1 512 Oct 30 14:33 pub\r\n",
322 "dr-xr-xr-x 5 0 1 512 Oct 1 1997 usr\r\n");
324 logmsg
"pass LIST data on data connection\n";
329 sendcontrol
"226 ASCII transfer complete\r\n";
334 my @ftpdir=("file", "with space", "fake", "..", " ..", "funny", "README");
335 logmsg
"pass NLST data on data connection\n";
340 sendcontrol
"226 ASCII transfer complete\r\n";
347 if ($testno > 10000) {
348 $testpart = $testno % 10000;
349 $testno = int($testno / 10000);
352 loadtest
("$srcdir/data/test$testno");
354 my @data = getpart
("reply", "mdtm");
356 my $reply = $data[0];
360 sendcontrol
"550 $testno: no such file.\r\n";
363 sendcontrol
"$reply\r\n";
366 sendcontrol
"500 MDTM: no such command.\r\n";
374 if ($testno > 10000) {
375 $testpart = $testno % 10000;
376 $testno = int($testno / 10000);
379 loadtest
("$srcdir/data/test$testno");
381 if($testno eq "verifiedserver") {
382 my $response = "WE ROOLZ: $$\r\n";
383 my $size = length($response);
384 sendcontrol
"213 $size\r\n";
388 my @data = getpart
("reply", "size");
394 sendcontrol
"213 $size\r\n";
397 sendcontrol
"550 $testno: No such file or directory.\r\n";
402 @data = getpart
("reply", "data$testpart");
407 sendcontrol
"213 $size\r\n";
410 sendcontrol
"550 $testno: No such file or directory.\r\n";
419 if($testno =~ /^verifiedserver$/) {
420 # this is the secret command that verifies that this actually is
421 # the curl test server
422 my $response = "WE ROOLZ: $$\r\n";
423 my $len = length($response);
424 sendcontrol
"150 Binary junk ($len bytes).\r\n";
425 senddata
"WE ROOLZ: $$\r\n";
427 sendcontrol
"226 File transfer complete\r\n";
429 print STDERR
"FTPD: We returned proof we are the test server\n";
434 $testno =~ s/^([^0-9]*)//;
436 if ($testno > 10000) {
437 $testpart = $testno % 10000;
438 $testno = int($testno / 10000);
441 loadtest
("$srcdir/data/test$testno");
443 my @data = getpart
("reply", "data$testpart");
450 my %hash = getpartattr
("reply", "data$testpart");
452 if($size || $hash{'sendzero'}) {
455 # move read pointer forward
457 logmsg
"REST $rest was removed from size, makes $size left\n";
458 $rest = 0; # reset REST offset again
461 sendcontrol
"150 Binary data connection for $testno () ($size bytes).\r\n",
462 "226 File transfer complete\r\n";
469 $retrweirdo=0; # switch off the weirdo again!
472 my $sz = "($size bytes)";
477 sendcontrol
"150 Binary data connection for $testno () $sz.\r\n";
484 sendcontrol
"226 File transfer complete\r\n";
488 sendcontrol
"550 $testno: No such file or directory.\r\n";
496 my $filename = "log/upload.$testno";
498 logmsg
"STOR test number $testno in $filename\n";
500 sendcontrol
"125 Gimme gimme gimme!\r\n";
502 open(FILE
, ">$filename") ||
503 return 0; # failed to open output
508 while (5 == (sysread DREAD
, $line, 5)) {
509 if($line eq "DATA\n") {
511 sysread DREAD
, $i, 5;
513 #print STDERR " GOT: $i";
516 sysread DREAD
, $line, $size;
518 #print STDERR " GOT: $size bytes\n";
521 print FILE
$line if(!$nosave);
522 logmsg
"> Appending $size bytes to file\n";
524 elsif($line eq "DISC\n") {
530 logmsg
"No support for: $line";
535 print FILE
"$ulsize bytes would've been stored here\n";
538 close_dataconn
($disc);
539 logmsg
"received $ulsize bytes upload\n";
540 sendcontrol
"226 File transfer complete\r\n";
547 my $pidf=".sockdata$ftpdnum$ext.pid";
549 my $prev = checkserver
($pidf);
551 print "kill existing server: $prev\n" if($verbose);
556 # We fire up a new sockfilt to do the data transfer for us.
557 $slavepid = open2
(\
*DREAD
, \
*DWRITE
,
558 "./server/sockfilt --port 0 --logfile log/sockdata$ftpdnum$ext.log --pidfile $pidf $ipv6");
560 print DWRITE
"PING\n";
563 sysread_or_die
(\
*DREAD
, \
$pong, 5);
565 if($pong !~ /^PONG/) {
567 waitpid($slavepid, 0);
568 sendcontrol
"500 no free ports!\r\n";
569 logmsg
"failed to run sockfilt for data connection\n";
573 logmsg
"Run sockfilt for data on pid $slavepid\n";
575 # Find out what port we listen on
577 print DWRITE
"PORT\n";
579 # READ the response code
580 sysread_or_die
(\
*DREAD
, \
$i, 5);
582 # READ the response size
583 sysread_or_die
(\
*DREAD
, \
$i, 5);
587 # READ the response data
588 sysread_or_die
(\
*DREAD
, \
$i, $size);
590 # The data is in the format
593 if($i =~ /IPv(\d)\/(\d
+)/) {
594 # FIX: deal with IP protocol version
605 sendcontrol
sprintf("227 Entering Passive Mode ($p,%d,%d)\n",
606 ($pasvport/256), ($pasvport%256));
610 sendcontrol
sprintf("229 Entering Passive Mode (|||%d|)\n", $pasvport);
614 local $SIG{ALRM
} = sub { die "alarm\n" };
616 # assume swift operations unless explicitly slow
617 alarm ($controldelay?
20:10);
622 while(sysread(DREAD
, $input, 5)) {
624 if($input !~ /^CNCT/) {
625 # we wait for a connected client
626 logmsg
"Odd, we got $input from client\n";
629 logmsg
"====> Client DATA connect\n";
637 print DWRITE
"QUIT\n";
638 waitpid $slavepid, 0;
639 logmsg
"accept failed\n";
644 logmsg
"data connection setup on port $pasvport\n";
650 # Support both PORT and EPRT here. Consider LPRT too.
653 my ($arg, $cmd) = @_;
657 # We always ignore the given IP and use localhost.
660 if($arg !~ /(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)/) {
661 logmsg
"bad PORT-line: $arg\n";
662 sendcontrol
"500 silly you, go away\r\n";
666 $addr = "$1.$2.$3.$4";
669 elsif(($cmd eq "EPRT") && ($grok_eprt)) {
670 if($arg !~ /(\d+)\|([^\|]+)\|(\d+)/) {
671 sendcontrol
"500 silly you, go away\r\n";
674 sendcontrol
"200 Thanks for dropping by. We contact you later\r\n";
679 sendcontrol
"500 we don't like $cmd now\r\n";
683 if(!$port || $port > 65535) {
684 print STDERR
"very illegal PORT number: $port\n";
688 # We fire up a new sockfilt to do the data transfer for us.
689 # FIX: make it use IPv6 if need be
690 my $filtcmd="./server/sockfilt --connect $port --addr $addr --logfile log/sockdata$ftpdnum$ext.log --pidfile .sockdata$ftpdnum$ext.pid $ipv6";
691 $slavepid = open2
(\
*DREAD
, \
*DWRITE
, $filtcmd);
693 print STDERR
"$filtcmd\n" if($verbose);
695 print DWRITE
"PING\n";
697 sysread DREAD
, $pong, 5;
699 if($pong !~ /^PONG/) {
700 logmsg
"Failed sockfilt for data connection\n";
702 waitpid($slavepid, 0);
705 logmsg
"====> Client DATA connect to port $port\n";
714 $nosave = 0; # default is to save as normal
715 $controldelay = 0; # default is no delaying the responses
724 open(CUSTOM
, "<log/ftpserver.cmd") ||
727 logmsg
"FTPD: Getting commands from log/ftpserver.cmd\n";
730 if($_ =~ /REPLY ([A-Z]+) (.*)/) {
731 $customreply{$1}=eval "qq{$2}";
732 logmsg
"FTPD: set custom reply for $1\n";
734 if($_ =~ /COUNT ([A-Z]+) (.*)/) {
735 # we blank the customreply for this command when having
736 # been used this number of times
738 logmsg
"FTPD: blank custom reply for $1 after $2 uses\n";
740 elsif($_ =~ /DELAY ([A-Z]+) (\d*)/) {
742 logmsg
"FTPD: delay reply for $1 with $2 seconds\n";
744 elsif($_ =~ /SLOWDOWN/) {
746 logmsg
"FTPD: send response with 0.1 sec delay between each byte\n";
748 elsif($_ =~ /RETRWEIRDO/) {
749 logmsg
"FTPD: instructed to use RETRWEIRDO\n";
752 elsif($_ =~ /RETRNOSIZE/) {
753 logmsg
"FTPD: instructed to use RETRNOSIZE\n";
756 elsif($_ =~ /PASVBADIP/) {
757 logmsg
"FTPD: instructed to use PASVBADIP\n";
760 elsif($_ =~ /NOSAVE/) {
761 # don't actually store the file we upload - to be used when
762 # uploading insanely huge amounts
764 logmsg
"FTPD: NOSAVE prevents saving of uploaded data\n";
771 '220- _ _ ____ _ '."\r\n",
772 '220- ___| | | | _ \| | '."\r\n",
773 '220- / __| | | | |_) | | '."\r\n",
774 '220- | (__| |_| | _ <| |___ '."\r\n",
775 '220 \___|\___/|_| \_\_____|'."\r\n");
780 # We read 'sockfilt' commands.
784 logmsg
"Awaiting input\n";
785 sysread_or_die
(\
*SFREAD
, \
$input, 5);
787 if($input !~ /^CNCT/) {
788 # we wait for a connected client
789 logmsg
"sockfilt said: $input";
792 logmsg
"====> Client connect\n";
794 set_advisor_read_lock
($SERVERLOGS_LOCK);
799 kill(9, $slavepid) if($slavepid);
800 waitpid($slavepid, 0) if($slavepid);
803 &customize
(); # read test control instructions
805 sendcontrol
@welcome;
808 print STDERR
"OUT: $_";
815 # Now we expect to read DATA\n[hex size]\n[prot], where the [prot]
816 # part only is FTP lingo.
819 sysread_or_die
(\
*SFREAD
, \
$i, 5);
822 logmsg
"sockfilt said $i";
831 sysread_or_die
(\
*SFREAD
, \
$i, 5);
836 sysread SFREAD
, $_, $size;
840 # Remove trailing CRLF.
843 unless (m/^([A-Z]{3,4})\s?(.*)/i) {
844 sendcontrol
"500 '$_': command not understood.\r\n";
851 logmsg
"< \"$full\"\n";
854 print STDERR
"IN: $full\n";
857 my $delay = $delayreply{$FTPCMD};
859 # just go sleep this many seconds!
860 logmsg
("Sleep for $delay seconds\n");
865 $text = $customreply{$FTPCMD};
868 $text = $displaytext{$FTPCMD};
871 if($customcount{$FTPCMD} && (!--$customcount{$FTPCMD})) {
872 # used enough number of times, now blank the customreply
873 $customreply{$FTPCMD}="";
878 sendcontrol
"$text\r\n";
881 $check=1; # no response yet
885 # only perform this if we're not faking a reply
886 my $func = $commandfunc{$FTPCMD};
888 &$func($FTPARG, $FTPCMD);
889 $check=0; # taken care of
894 logmsg
"$FTPCMD wasn't handled!\n";
895 sendcontrol
"500 $FTPCMD is not dealt with!\r\n";
899 logmsg
"====> Client disconnected\n";
901 clear_advisor_read_lock
($SERVERLOGS_LOCK);
904 print SFWRITE
"QUIT\n";
907 clear_advisor_read_lock
($SERVERLOGS_LOCK);