2 #***************************************************************************
4 # Project ___| | | | _ \| |
6 # | (__| |_| | _ <| |___
7 # \___|\___/|_| \_\_____|
9 # Copyright (C) 1998 - 2004, 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.40 2004/03/01 07:16:45 bagder 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.
41 open(FTPLOG, ">log/ftpd.log") ||
42 print STDERR "failed to open log file, runs without logging\n";
44 sub logmsg { print FTPLOG "$$: "; print FTPLOG @_; }
47 # append to the server.input file
48 open(INPUT, ">>log/server.input") ||
49 logmsg "failed to open log/server.input\n";
55 # use this, open->print->close system only to make the file
56 # open as little as possible, to make the test suite run
57 # better on windows/cygwin
60 my $verbose=0; # set to 1 for debugging
65 my $port = 8921; # just a default
67 if($ARGV[0] eq "-v") {
70 elsif($ARGV[0] eq "-s") {
74 elsif($ARGV[0] =~ /^(\d+)$/) {
79 my $proto = getprotobyname('tcp') || 6;
81 socket(Server, PF_INET, SOCK_STREAM, $proto)|| die "socket: $!";
82 setsockopt(Server, SOL_SOCKET, SO_REUSEADDR,
83 pack("l", 1)) || die "setsockopt: $!";
84 bind(Server, sockaddr_in($port, INADDR_ANY))|| die "bind: $!";
85 listen(Server,SOMAXCONN) || die "listen: $!";
87 #print "FTP server started on port $port\n";
89 open(PID, ">.ftp.pid");
98 $SIG{CHLD} = \&REAPER; # loathe sysV
99 logmsg "reaped $waitedpid" . ($? ? " with exit $?\n" : "\n");
102 # USER is ok in fresh state
106 'PASV' => 'loggedin|twosock',
107 'EPSV' => 'loggedin|twosock',
108 'PORT' => 'loggedin|twosock',
109 'TYPE' => 'loggedin|twosock',
116 'CWD' => 'loggedin|twosock',
117 'SYST' => 'loggedin',
118 'SIZE' => 'loggedin|twosock',
119 'PWD' => 'loggedin|twosock',
120 'MKD' => 'loggedin|twosock',
121 'QUIT' => 'loggedin|twosock',
122 'RNFR' => 'loggedin|twosock',
123 'RNTO' => 'loggedin|twosock',
124 'DELE' => 'loggedin|twosock',
125 'MDTM' => 'loggedin|twosock',
128 # initially, we're in 'fresh' state
129 my %statechange = ( 'USER' => 'passwd', # USER goes to passwd state
130 'PASS' => 'loggedin', # PASS goes to loggedin state
131 'PORT' => 'twosock', # PORT goes to twosock
132 'PASV' => 'twosock', # PASV goes to twosock
133 'EPSV' => 'twosock', # EPSV goes to twosock
136 # this text is shown before the function specified below is run
137 my %displaytext = ('USER' => '331 We are happy you popped in!',
138 'PASS' => '230 Welcome you silly person',
139 'PORT' => '200 You said PORT - I say FINE',
140 'TYPE' => '200 I modify TYPE as you wanted',
141 'LIST' => '150 here comes a directory',
142 'NLST' => '150 here comes a directory',
143 'CWD' => '250 CWD command successful.',
144 'SYST' => '215 UNIX Type: L8', # just fake something
145 'QUIT' => '221 bye bye baby', # just reply something
146 'PWD' => '257 "/nowhere/anywhere" is current directory',
147 'MKD' => '257 Created your requested directory',
148 'REST' => '350 Yeah yeah we set it there for you',
149 'DELE' => '200 OK OK OK whatever you say',
150 'RNFR' => '350 Received your order. Please provide more',
151 'RNTO' => '250 Ok, thanks. File renaming completed.',
154 # callback functions for certain commands
155 my %commandfunc = ( 'PORT' => \&PORT_command,
156 'LIST' => \&LIST_command,
157 'NLST' => \&NLST_command,
158 'PASV' => \&PASV_command,
159 'EPSV' => \&PASV_command,
160 'RETR' => \&RETR_command,
161 'SIZE' => \&SIZE_command,
162 'REST' => \&REST_command,
163 'STOR' => \&STOR_command,
164 'APPE' => \&STOR_command, # append looks like upload
165 'MDTM' => \&MDTM_command,
171 logmsg "Set REST position to $rest\n"
175 # print "150 ASCII data connection for /bin/ls (193.15.23.1,59196) (0 bytes)\r\n";
177 # this is a built-in fake-dir ;-)
178 my @ftpdir=("total 20\r\n",
179 "drwxr-xr-x 8 98 98 512 Oct 22 13:06 .\r\n",
180 "drwxr-xr-x 8 98 98 512 Oct 22 13:06 ..\r\n",
181 "drwxr-xr-x 2 98 98 512 May 2 1996 .NeXT\r\n",
182 "-r--r--r-- 1 0 1 35 Jul 16 1996 README\r\n",
183 "lrwxrwxrwx 1 0 1 7 Dec 9 1999 bin -> usr/bin\r\n",
184 "dr-xr-xr-x 2 0 1 512 Oct 1 1997 dev\r\n",
185 "drwxrwxrwx 2 98 98 512 May 29 16:04 download.html\r\n",
186 "dr-xr-xr-x 2 0 1 512 Nov 30 1995 etc\r\n",
187 "drwxrwxrwx 2 98 1 512 Oct 30 14:33 pub\r\n",
188 "dr-xr-xr-x 5 0 1 512 Oct 1 1997 usr\r\n");
190 logmsg "$$: pass data to child pid\n";
195 logmsg "$$: done passing data to child pid\n";
197 print "226 ASCII transfer complete\r\n";
202 my @ftpdir=("file", "with space", "fake", "..", " ..", "funny", "README");
207 print "226 ASCII transfer complete\r\n";
214 loadtest("$srcdir/data/test$testno");
216 logmsg "MDTM $testno\n";
218 my @data = getpart("reply", "mdtm");
220 my $reply = $data[0];
224 print "550 $testno: no such file.\r\n";
225 logmsg "MDTM $testno: no such file\n";
229 logmsg "MDTM $testno returned $reply\n";
232 print "500 MDTM: no such command.\r\n";
233 logmsg "MDTM: no such command\n";
241 loadtest("$srcdir/data/test$testno");
243 logmsg "SIZE number $testno\n";
245 my @data = getpart("reply", "size");
251 print "213 $size\r\n";
252 logmsg "SIZE $testno returned $size\n";
255 print "550 $testno: No such file or directory.\r\n";
256 logmsg "SIZE $testno: no such file\n";
261 @data = getpart("reply", "data");
266 print "213 $size\r\n";
267 logmsg "SIZE $testno returned $size\n";
270 print "550 $testno: No such file or directory.\r\n";
271 logmsg "SIZE $testno: no such file\n";
280 logmsg "RETR test number $testno\n";
282 if($testno =~ /^verifiedserver$/) {
283 # this is the secret command that verifies that this actually is
284 # the curl test server
285 my $response = "WE ROOLZ: $$\r\n";
286 my $len = length($response);
287 print "150 Binary junk ($len bytes).\r\n";
288 print SOCK "WE ROOLZ: $$\r\n";
290 print "226 File transfer complete\r\n";
292 print STDERR "FTPD: We returned proof we are the test server\n";
294 logmsg "we returned proof that we are the test server\n";
298 loadtest("$srcdir/data/test$testno");
300 my @data = getpart("reply", "data");
310 # move read pointer forward
312 logmsg "REST $rest was removed from size, makes $size left\n";
313 $rest = 0; # reset REST offset again
316 print "150 Binary data connection for $testno () ($size bytes).\r\n",
317 "226 File transfer complete\r\n";
318 logmsg "150+226 in one shot!\n";
325 $retrweirdo=0; # switch off the weirdo again!
328 my $sz = "($size bytes)";
333 print "150 Binary data connection for $testno () $sz.\r\n";
334 logmsg "150 Binary data connection for $testno () $sz.\n";
342 print "226 File transfer complete\r\n";
346 print "550 $testno: No such file or directory.\r\n";
347 logmsg "550 $testno: no such file\n";
355 my $filename = "log/upload.$testno";
357 logmsg "STOR test number $testno in $filename\n";
359 print "125 Gimme gimme gimme!\r\n";
361 open(FILE, ">$filename") ||
362 return 0; # failed to open output
366 while (defined($line = <SOCK>)) {
367 $ulsize += length($line);
373 logmsg "received $ulsize bytes upload\n";
375 print "226 File transfer complete\r\n";
383 socket(Server2, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
384 setsockopt(Server2, SOL_SOCKET, SO_REUSEADDR,
385 pack("l", 1)) || die "setsockopt: $!";
389 $pasvport++; # don't reuse the previous
391 if($pasvport > 65535) {
394 if(bind(Server2, sockaddr_in($pasvport, INADDR_ANY))) {
398 $pasvport+= 3; # try another port please
401 print "500 no free ports!\r\n";
402 logmsg "couldn't find free port\n";
405 listen(Server2,SOMAXCONN) || die "listen: $!";
409 logmsg "replying to a $cmd command\n";
410 printf("227 Entering Passive Mode (127,0,0,1,%d,%d)\n",
411 ($pasvport/256), ($pasvport%256));
415 logmsg "replying to a $cmd command\n";
416 printf("229 Entering Passive Mode (|||%d|)\n", $pasvport);
419 my $paddr = accept(SOCK, Server2);
420 my($iport,$iaddr) = sockaddr_in($paddr);
421 my $name = gethostbyaddr($iaddr,AF_INET);
423 close(Server2); # close the listener when its served its purpose!
425 logmsg "$$: data connection from $name [", inet_ntoa($iaddr), "] at port $iport\n";
434 if($arg !~ /(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)/) {
435 logmsg "bad PORT-line: $arg\n";
436 print "500 silly you, go away\r\n";
439 my $iaddr = inet_aton("$1.$2.$3.$4");
441 my $port = ($5<<8)+$6;
443 if(!$port || $port > 65535) {
444 print STDERR "very illegal PORT number: $port\n";
448 my $paddr = sockaddr_in($port, $iaddr);
449 my $proto = getprotobyname('tcp') || 6;
451 socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "major failure";
452 connect(SOCK, $paddr) || return 1;
457 $SIG{CHLD} = \&REAPER;
464 open(CUSTOM, "<log/ftpserver.cmd") ||
467 logmsg "FTPD: Getting commands from log/ftpserver.cmd\n";
470 if($_ =~ /REPLY ([A-Z]+) (.*)/) {
473 if($_ =~ /COUNT ([A-Z]+) (.*)/) {
474 # we blank the customreply for this command when having
475 # been used this number of times
478 elsif($_ =~ /DELAY ([A-Z]+) (\d*)/) {
481 elsif($_ =~ /RETRWEIRDO/) {
482 print "instructed to use RETRWEIRDO\n";
485 elsif($_ =~ /RETRNOSIZE/) {
486 print "instructed to use RETRNOSIZE\n";
494 '220- _ _ ____ _ '."\r\n",
495 '220- ___| | | | _ \| | '."\r\n",
496 '220- / __| | | | |_) | | '."\r\n",
497 '220- | (__| |_| | _ <| |___ '."\r\n",
498 '220 \___|\___/|_| \_\_____|'."\r\n");
500 for ( $waitedpid = 0;
501 ($paddr = accept(Client,Server)) || $waitedpid;
502 $waitedpid = 0, close Client)
504 next if $waitedpid and not $paddr;
505 my($port,$iaddr) = sockaddr_in($paddr);
506 my $name = gethostbyaddr($iaddr,AF_INET);
511 logmsg "connection from $name [", inet_ntoa($iaddr), "] at port $port\n";
513 open(STDIN, "<&Client") || die "can't dup client to stdin";
514 open(STDOUT, ">&Client") || die "can't dup client to stdout";
516 FTPLOG->autoflush(1);
517 &customize(); # read test control instructions
522 print STDERR "OUT: $_";
529 last unless defined ($_ = <STDIN>);
533 # Remove trailing CRLF.
536 unless (m/^([A-Z]{3,4})\s?(.*)/i) {
537 print "500 '$_': command not understood.\r\n";
538 logmsg "unknown crap received, bailing out hard\n";
545 logmsg "GOT: ($1) $_\n";
548 print STDERR "IN: $full\n";
551 my $ok = $commandok{$FTPCMD};
552 if($ok !~ /$state/) {
553 print "500 $FTPCMD not OK in state: $state!\r\n";
557 my $newstate=$statechange{$FTPCMD};
558 if($newstate eq "") {
559 # remain in the same state
565 my $delay = $delayreply{$FTPCMD};
567 # just go sleep this many seconds!
572 $text = $customreply{$FTPCMD};
575 $text = $displaytext{$FTPCMD};
578 if($customcount{$FTPCMD} && (!--$customcount{$FTPCMD})) {
579 # used enough number of times, now blank the customreply
580 $customreply{$FTPCMD}="";
582 logmsg "$FTPCMD made to send '$text'\n";
589 # only perform this if we're not faking a reply
590 # see if the new state is a function caller.
591 my $func = $commandfunc{$FTPCMD};
594 \&$func($FTPARG, $FTPCMD);
598 logmsg "set to state $state\n";