ftpserver.pl 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. #!/usr/bin/perl
  2. #***************************************************************************
  3. # _ _ ____ _
  4. # Project ___| | | | _ \| |
  5. # / __| | | | |_) | |
  6. # | (__| |_| | _ <| |___
  7. # \___|\___/|_| \_\_____|
  8. #
  9. # Copyright (C) 1998 - 2004, Daniel Stenberg, <daniel@haxx.se>, et al.
  10. #
  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.
  14. #
  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.
  18. #
  19. # This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
  20. # KIND, either express or implied.
  21. #
  22. # $Id: ftpserver.pl,v 1.40 2004/03/01 07:16:45 bagder Exp $
  23. ###########################################################################
  24. # This is the FTP server designed for the curl test suite.
  25. #
  26. # It is meant to exercise curl, it is not meant to be a fully working
  27. # or even very standard compliant server.
  28. #
  29. # You may optionally specify port on the command line, otherwise it'll
  30. # default to port 8921.
  31. #
  32. use Socket;
  33. use FileHandle;
  34. use strict;
  35. require "getpart.pm";
  36. open(FTPLOG, ">log/ftpd.log") ||
  37. print STDERR "failed to open log file, runs without logging\n";
  38. sub logmsg { print FTPLOG "$$: "; print FTPLOG @_; }
  39. sub ftpmsg {
  40. # append to the server.input file
  41. open(INPUT, ">>log/server.input") ||
  42. logmsg "failed to open log/server.input\n";
  43. INPUT->autoflush(1);
  44. print INPUT @_;
  45. close(INPUT);
  46. # use this, open->print->close system only to make the file
  47. # open as little as possible, to make the test suite run
  48. # better on windows/cygwin
  49. }
  50. my $verbose=0; # set to 1 for debugging
  51. my $retrweirdo=0;
  52. my $retrnosize=0;
  53. my $srcdir=".";
  54. my $port = 8921; # just a default
  55. do {
  56. if($ARGV[0] eq "-v") {
  57. $verbose=1;
  58. }
  59. elsif($ARGV[0] eq "-s") {
  60. $srcdir=$ARGV[1];
  61. shift @ARGV;
  62. }
  63. elsif($ARGV[0] =~ /^(\d+)$/) {
  64. $port = $1;
  65. }
  66. } while(shift @ARGV);
  67. my $proto = getprotobyname('tcp') || 6;
  68. socket(Server, PF_INET, SOCK_STREAM, $proto)|| die "socket: $!";
  69. setsockopt(Server, SOL_SOCKET, SO_REUSEADDR,
  70. pack("l", 1)) || die "setsockopt: $!";
  71. bind(Server, sockaddr_in($port, INADDR_ANY))|| die "bind: $!";
  72. listen(Server,SOMAXCONN) || die "listen: $!";
  73. #print "FTP server started on port $port\n";
  74. open(PID, ">.ftp.pid");
  75. print PID $$;
  76. close(PID);
  77. my $waitedpid = 0;
  78. my $paddr;
  79. sub REAPER {
  80. $waitedpid = wait;
  81. $SIG{CHLD} = \&REAPER; # loathe sysV
  82. logmsg "reaped $waitedpid" . ($? ? " with exit $?\n" : "\n");
  83. }
  84. # USER is ok in fresh state
  85. my %commandok = (
  86. 'USER' => 'fresh',
  87. 'PASS' => 'passwd',
  88. 'PASV' => 'loggedin|twosock',
  89. 'EPSV' => 'loggedin|twosock',
  90. 'PORT' => 'loggedin|twosock',
  91. 'TYPE' => 'loggedin|twosock',
  92. 'LIST' => 'twosock',
  93. 'NLST' => 'twosock',
  94. 'RETR' => 'twosock',
  95. 'STOR' => 'twosock',
  96. 'APPE' => 'twosock',
  97. 'REST' => 'twosock',
  98. 'CWD' => 'loggedin|twosock',
  99. 'SYST' => 'loggedin',
  100. 'SIZE' => 'loggedin|twosock',
  101. 'PWD' => 'loggedin|twosock',
  102. 'MKD' => 'loggedin|twosock',
  103. 'QUIT' => 'loggedin|twosock',
  104. 'RNFR' => 'loggedin|twosock',
  105. 'RNTO' => 'loggedin|twosock',
  106. 'DELE' => 'loggedin|twosock',
  107. 'MDTM' => 'loggedin|twosock',
  108. );
  109. # initially, we're in 'fresh' state
  110. my %statechange = ( 'USER' => 'passwd', # USER goes to passwd state
  111. 'PASS' => 'loggedin', # PASS goes to loggedin state
  112. 'PORT' => 'twosock', # PORT goes to twosock
  113. 'PASV' => 'twosock', # PASV goes to twosock
  114. 'EPSV' => 'twosock', # EPSV goes to twosock
  115. );
  116. # this text is shown before the function specified below is run
  117. my %displaytext = ('USER' => '331 We are happy you popped in!',
  118. 'PASS' => '230 Welcome you silly person',
  119. 'PORT' => '200 You said PORT - I say FINE',
  120. 'TYPE' => '200 I modify TYPE as you wanted',
  121. 'LIST' => '150 here comes a directory',
  122. 'NLST' => '150 here comes a directory',
  123. 'CWD' => '250 CWD command successful.',
  124. 'SYST' => '215 UNIX Type: L8', # just fake something
  125. 'QUIT' => '221 bye bye baby', # just reply something
  126. 'PWD' => '257 "/nowhere/anywhere" is current directory',
  127. 'MKD' => '257 Created your requested directory',
  128. 'REST' => '350 Yeah yeah we set it there for you',
  129. 'DELE' => '200 OK OK OK whatever you say',
  130. 'RNFR' => '350 Received your order. Please provide more',
  131. 'RNTO' => '250 Ok, thanks. File renaming completed.',
  132. );
  133. # callback functions for certain commands
  134. my %commandfunc = ( 'PORT' => \&PORT_command,
  135. 'LIST' => \&LIST_command,
  136. 'NLST' => \&NLST_command,
  137. 'PASV' => \&PASV_command,
  138. 'EPSV' => \&PASV_command,
  139. 'RETR' => \&RETR_command,
  140. 'SIZE' => \&SIZE_command,
  141. 'REST' => \&REST_command,
  142. 'STOR' => \&STOR_command,
  143. 'APPE' => \&STOR_command, # append looks like upload
  144. 'MDTM' => \&MDTM_command,
  145. );
  146. my $rest=0;
  147. sub REST_command {
  148. $rest = $_[0];
  149. logmsg "Set REST position to $rest\n"
  150. }
  151. sub LIST_command {
  152. # print "150 ASCII data connection for /bin/ls (193.15.23.1,59196) (0 bytes)\r\n";
  153. # this is a built-in fake-dir ;-)
  154. my @ftpdir=("total 20\r\n",
  155. "drwxr-xr-x 8 98 98 512 Oct 22 13:06 .\r\n",
  156. "drwxr-xr-x 8 98 98 512 Oct 22 13:06 ..\r\n",
  157. "drwxr-xr-x 2 98 98 512 May 2 1996 .NeXT\r\n",
  158. "-r--r--r-- 1 0 1 35 Jul 16 1996 README\r\n",
  159. "lrwxrwxrwx 1 0 1 7 Dec 9 1999 bin -> usr/bin\r\n",
  160. "dr-xr-xr-x 2 0 1 512 Oct 1 1997 dev\r\n",
  161. "drwxrwxrwx 2 98 98 512 May 29 16:04 download.html\r\n",
  162. "dr-xr-xr-x 2 0 1 512 Nov 30 1995 etc\r\n",
  163. "drwxrwxrwx 2 98 1 512 Oct 30 14:33 pub\r\n",
  164. "dr-xr-xr-x 5 0 1 512 Oct 1 1997 usr\r\n");
  165. logmsg "$$: pass data to child pid\n";
  166. for(@ftpdir) {
  167. print SOCK $_;
  168. }
  169. close(SOCK);
  170. logmsg "$$: done passing data to child pid\n";
  171. print "226 ASCII transfer complete\r\n";
  172. return 0;
  173. }
  174. sub NLST_command {
  175. my @ftpdir=("file", "with space", "fake", "..", " ..", "funny", "README");
  176. for(@ftpdir) {
  177. print SOCK "$_\r\n";
  178. }
  179. close(SOCK);
  180. print "226 ASCII transfer complete\r\n";
  181. return 0;
  182. }
  183. sub MDTM_command {
  184. my $testno = $_[0];
  185. loadtest("$srcdir/data/test$testno");
  186. logmsg "MDTM $testno\n";
  187. my @data = getpart("reply", "mdtm");
  188. my $reply = $data[0];
  189. chomp $reply;
  190. if($reply <0) {
  191. print "550 $testno: no such file.\r\n";
  192. logmsg "MDTM $testno: no such file\n";
  193. }
  194. elsif($reply) {
  195. print "$reply\r\n";
  196. logmsg "MDTM $testno returned $reply\n";
  197. }
  198. else {
  199. print "500 MDTM: no such command.\r\n";
  200. logmsg "MDTM: no such command\n";
  201. }
  202. return 0;
  203. }
  204. sub SIZE_command {
  205. my $testno = $_[0];
  206. loadtest("$srcdir/data/test$testno");
  207. logmsg "SIZE number $testno\n";
  208. my @data = getpart("reply", "size");
  209. my $size = $data[0];
  210. if($size) {
  211. if($size > -1) {
  212. print "213 $size\r\n";
  213. logmsg "SIZE $testno returned $size\n";
  214. }
  215. else {
  216. print "550 $testno: No such file or directory.\r\n";
  217. logmsg "SIZE $testno: no such file\n";
  218. }
  219. }
  220. else {
  221. $size=0;
  222. @data = getpart("reply", "data");
  223. for(@data) {
  224. $size += length($_);
  225. }
  226. if($size) {
  227. print "213 $size\r\n";
  228. logmsg "SIZE $testno returned $size\n";
  229. }
  230. else {
  231. print "550 $testno: No such file or directory.\r\n";
  232. logmsg "SIZE $testno: no such file\n";
  233. }
  234. }
  235. return 0;
  236. }
  237. sub RETR_command {
  238. my $testno = $_[0];
  239. logmsg "RETR test number $testno\n";
  240. if($testno =~ /^verifiedserver$/) {
  241. # this is the secret command that verifies that this actually is
  242. # the curl test server
  243. my $response = "WE ROOLZ: $$\r\n";
  244. my $len = length($response);
  245. print "150 Binary junk ($len bytes).\r\n";
  246. print SOCK "WE ROOLZ: $$\r\n";
  247. close(SOCK);
  248. print "226 File transfer complete\r\n";
  249. if($verbose) {
  250. print STDERR "FTPD: We returned proof we are the test server\n";
  251. }
  252. logmsg "we returned proof that we are the test server\n";
  253. return 0;
  254. }
  255. loadtest("$srcdir/data/test$testno");
  256. my @data = getpart("reply", "data");
  257. my $size=0;
  258. for(@data) {
  259. $size += length($_);
  260. }
  261. if($size) {
  262. if($rest) {
  263. # move read pointer forward
  264. $size -= $rest;
  265. logmsg "REST $rest was removed from size, makes $size left\n";
  266. $rest = 0; # reset REST offset again
  267. }
  268. if($retrweirdo) {
  269. print "150 Binary data connection for $testno () ($size bytes).\r\n",
  270. "226 File transfer complete\r\n";
  271. logmsg "150+226 in one shot!\n";
  272. for(@data) {
  273. my $send = $_;
  274. print SOCK $send;
  275. }
  276. close(SOCK);
  277. $retrweirdo=0; # switch off the weirdo again!
  278. }
  279. else {
  280. my $sz = "($size bytes)";
  281. if($retrnosize) {
  282. $sz = "size?";
  283. }
  284. print "150 Binary data connection for $testno () $sz.\r\n";
  285. logmsg "150 Binary data connection for $testno () $sz.\n";
  286. for(@data) {
  287. my $send = $_;
  288. print SOCK $send;
  289. }
  290. close(SOCK);
  291. print "226 File transfer complete\r\n";
  292. }
  293. }
  294. else {
  295. print "550 $testno: No such file or directory.\r\n";
  296. logmsg "550 $testno: no such file\n";
  297. }
  298. return 0;
  299. }
  300. sub STOR_command {
  301. my $testno=$_[0];
  302. my $filename = "log/upload.$testno";
  303. logmsg "STOR test number $testno in $filename\n";
  304. print "125 Gimme gimme gimme!\r\n";
  305. open(FILE, ">$filename") ||
  306. return 0; # failed to open output
  307. my $line;
  308. my $ulsize=0;
  309. while (defined($line = <SOCK>)) {
  310. $ulsize += length($line);
  311. print FILE $line;
  312. }
  313. close(FILE);
  314. close(SOCK);
  315. logmsg "received $ulsize bytes upload\n";
  316. print "226 File transfer complete\r\n";
  317. return 0;
  318. }
  319. my $pasvport=9000;
  320. sub PASV_command {
  321. my ($arg, $cmd)=@_;
  322. socket(Server2, PF_INET, SOCK_STREAM, $proto) || die "socket: $!";
  323. setsockopt(Server2, SOL_SOCKET, SO_REUSEADDR,
  324. pack("l", 1)) || die "setsockopt: $!";
  325. my $ok=0;
  326. $pasvport++; # don't reuse the previous
  327. for(1 .. 10) {
  328. if($pasvport > 65535) {
  329. $pasvport = 1025;
  330. }
  331. if(bind(Server2, sockaddr_in($pasvport, INADDR_ANY))) {
  332. $ok=1;
  333. last;
  334. }
  335. $pasvport+= 3; # try another port please
  336. }
  337. if(!$ok) {
  338. print "500 no free ports!\r\n";
  339. logmsg "couldn't find free port\n";
  340. return 0;
  341. }
  342. listen(Server2,SOMAXCONN) || die "listen: $!";
  343. if($cmd ne "EPSV") {
  344. # PASV reply
  345. logmsg "replying to a $cmd command\n";
  346. printf("227 Entering Passive Mode (127,0,0,1,%d,%d)\n",
  347. ($pasvport/256), ($pasvport%256));
  348. }
  349. else {
  350. # EPSV reply
  351. logmsg "replying to a $cmd command\n";
  352. printf("229 Entering Passive Mode (|||%d|)\n", $pasvport);
  353. }
  354. my $paddr = accept(SOCK, Server2);
  355. my($iport,$iaddr) = sockaddr_in($paddr);
  356. my $name = gethostbyaddr($iaddr,AF_INET);
  357. close(Server2); # close the listener when its served its purpose!
  358. logmsg "$$: data connection from $name [", inet_ntoa($iaddr), "] at port $iport\n";
  359. return;
  360. }
  361. sub PORT_command {
  362. my $arg = $_[0];
  363. if($arg !~ /(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)/) {
  364. logmsg "bad PORT-line: $arg\n";
  365. print "500 silly you, go away\r\n";
  366. return 0;
  367. }
  368. my $iaddr = inet_aton("$1.$2.$3.$4");
  369. my $port = ($5<<8)+$6;
  370. if(!$port || $port > 65535) {
  371. print STDERR "very illegal PORT number: $port\n";
  372. return 1;
  373. }
  374. my $paddr = sockaddr_in($port, $iaddr);
  375. my $proto = getprotobyname('tcp') || 6;
  376. socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "major failure";
  377. connect(SOCK, $paddr) || return 1;
  378. return \&SOCK;
  379. }
  380. $SIG{CHLD} = \&REAPER;
  381. my %customreply;
  382. my %customcount;
  383. my %delayreply;
  384. sub customize {
  385. undef %customreply;
  386. open(CUSTOM, "<log/ftpserver.cmd") ||
  387. return 1;
  388. logmsg "FTPD: Getting commands from log/ftpserver.cmd\n";
  389. while(<CUSTOM>) {
  390. if($_ =~ /REPLY ([A-Z]+) (.*)/) {
  391. $customreply{$1}=$2;
  392. }
  393. if($_ =~ /COUNT ([A-Z]+) (.*)/) {
  394. # we blank the customreply for this command when having
  395. # been used this number of times
  396. $customcount{$1}=$2;
  397. }
  398. elsif($_ =~ /DELAY ([A-Z]+) (\d*)/) {
  399. $delayreply{$1}=$2;
  400. }
  401. elsif($_ =~ /RETRWEIRDO/) {
  402. print "instructed to use RETRWEIRDO\n";
  403. $retrweirdo=1;
  404. }
  405. elsif($_ =~ /RETRNOSIZE/) {
  406. print "instructed to use RETRNOSIZE\n";
  407. $retrnosize=1;
  408. }
  409. }
  410. close(CUSTOM);
  411. }
  412. my @welcome=(
  413. '220- _ _ ____ _ '."\r\n",
  414. '220- ___| | | | _ \| | '."\r\n",
  415. '220- / __| | | | |_) | | '."\r\n",
  416. '220- | (__| |_| | _ <| |___ '."\r\n",
  417. '220 \___|\___/|_| \_\_____|'."\r\n");
  418. for ( $waitedpid = 0;
  419. ($paddr = accept(Client,Server)) || $waitedpid;
  420. $waitedpid = 0, close Client)
  421. {
  422. next if $waitedpid and not $paddr;
  423. my($port,$iaddr) = sockaddr_in($paddr);
  424. my $name = gethostbyaddr($iaddr,AF_INET);
  425. # flush data:
  426. $| = 1;
  427. logmsg "connection from $name [", inet_ntoa($iaddr), "] at port $port\n";
  428. open(STDIN, "<&Client") || die "can't dup client to stdin";
  429. open(STDOUT, ">&Client") || die "can't dup client to stdout";
  430. FTPLOG->autoflush(1);
  431. &customize(); # read test control instructions
  432. print @welcome;
  433. if($verbose) {
  434. for(@welcome) {
  435. print STDERR "OUT: $_";
  436. }
  437. }
  438. my $state="fresh";
  439. while(1) {
  440. last unless defined ($_ = <STDIN>);
  441. ftpmsg $_;
  442. # Remove trailing CRLF.
  443. s/[\n\r]+$//;
  444. unless (m/^([A-Z]{3,4})\s?(.*)/i) {
  445. print "500 '$_': command not understood.\r\n";
  446. logmsg "unknown crap received, bailing out hard\n";
  447. last;
  448. }
  449. my $FTPCMD=$1;
  450. my $FTPARG=$2;
  451. my $full=$_;
  452. logmsg "GOT: ($1) $_\n";
  453. if($verbose) {
  454. print STDERR "IN: $full\n";
  455. }
  456. my $ok = $commandok{$FTPCMD};
  457. if($ok !~ /$state/) {
  458. print "500 $FTPCMD not OK in state: $state!\r\n";
  459. next;
  460. }
  461. my $newstate=$statechange{$FTPCMD};
  462. if($newstate eq "") {
  463. # remain in the same state
  464. }
  465. else {
  466. $state = $newstate;
  467. }
  468. my $delay = $delayreply{$FTPCMD};
  469. if($delay) {
  470. # just go sleep this many seconds!
  471. sleep($delay);
  472. }
  473. my $text;
  474. $text = $customreply{$FTPCMD};
  475. my $fake = $text;
  476. if($text eq "") {
  477. $text = $displaytext{$FTPCMD};
  478. }
  479. else {
  480. if($customcount{$FTPCMD} && (!--$customcount{$FTPCMD})) {
  481. # used enough number of times, now blank the customreply
  482. $customreply{$FTPCMD}="";
  483. }
  484. logmsg "$FTPCMD made to send '$text'\n";
  485. }
  486. if($text) {
  487. print "$text\r\n";
  488. }
  489. if($fake eq "") {
  490. # only perform this if we're not faking a reply
  491. # see if the new state is a function caller.
  492. my $func = $commandfunc{$FTPCMD};
  493. if($func) {
  494. # it is!
  495. \&$func($FTPARG, $FTPCMD);
  496. }
  497. }
  498. logmsg "set to state $state\n";
  499. } # while(1)
  500. close(Client);
  501. }