• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) 1998 - 2021, 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 https://curl.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###########################################################################
23
24# This is a server designed for the curl test suite.
25#
26# In December 2009 we started remaking the server to support more protocols
27# that are similar in spirit. Like POP3, IMAP and SMTP in addition to the FTP
28# it already supported since a long time. Note that it still only supports one
29# protocol per invoke. You need to start multiple servers to support multiple
30# protocols simultaneously.
31#
32# It is meant to exercise curl, it is not meant to be a fully working
33# or even very standard compliant server.
34#
35# You may optionally specify port on the command line, otherwise it'll
36# default to port 8921.
37#
38# All socket/network/TCP related stuff is done by the 'sockfilt' program.
39#
40
41BEGIN {
42    push(@INC, $ENV{'srcdir'}) if(defined $ENV{'srcdir'});
43    push(@INC, ".");
44    # sub second timestamping needs Time::HiRes
45    eval {
46        no warnings "all";
47        require Time::HiRes;
48        import  Time::HiRes qw( gettimeofday );
49    }
50}
51
52use strict;
53use warnings;
54use IPC::Open2;
55use Digest::MD5;
56
57require "getpart.pm";
58require "ftp.pm";
59require "directories.pm";
60
61use serverhelp qw(
62    servername_str
63    server_pidfilename
64    server_logfilename
65    mainsockf_pidfilename
66    mainsockf_logfilename
67    datasockf_pidfilename
68    datasockf_logfilename
69    );
70
71use sshhelp qw(
72    exe_ext
73    );
74
75#**********************************************************************
76# global vars...
77#
78my $verbose = 0;    # set to 1 for debugging
79my $idstr = "";     # server instance string
80my $idnum = 1;      # server instance number
81my $ipvnum = 4;     # server IPv number (4 or 6)
82my $proto = 'ftp';  # default server protocol
83my $srcdir;         # directory where ftpserver.pl is located
84my $srvrname;       # server name for presentation purposes
85my $cwd_testno;     # test case numbers extracted from CWD command
86my $testno = 0;     # test case number (read from ftpserver.cmd)
87my $path   = '.';
88my $logdir = $path .'/log';
89
90#**********************************************************************
91# global vars used for server address and primary listener port
92#
93my $port = 8921;               # default primary listener port
94my $listenaddr = '127.0.0.1';  # default address for listener port
95
96#**********************************************************************
97# global vars used for file names
98#
99my $pidfile;            # server pid file name
100my $portfile=".ftpserver.port"; # server port file name
101my $logfile;            # server log file name
102my $mainsockf_pidfile;  # pid file for primary connection sockfilt process
103my $mainsockf_logfile;  # log file for primary connection sockfilt process
104my $datasockf_pidfile;  # pid file for secondary connection sockfilt process
105my $datasockf_logfile;  # log file for secondary connection sockfilt process
106
107#**********************************************************************
108# global vars used for server logs advisor read lock handling
109#
110my $SERVERLOGS_LOCK = 'log/serverlogs.lock';
111my $serverlogslocked = 0;
112
113#**********************************************************************
114# global vars used for child processes PID tracking
115#
116my $sfpid;        # PID for primary connection sockfilt process
117my $slavepid;     # PID for secondary connection sockfilt process
118
119#**********************************************************************
120# global typeglob filehandle vars to read/write from/to sockfilters
121#
122local *SFREAD;    # used to read from primary connection
123local *SFWRITE;   # used to write to primary connection
124local *DREAD;     # used to read from secondary connection
125local *DWRITE;    # used to write to secondary connection
126
127my $sockfilt_timeout = 5;  # default timeout for sockfilter eXsysreads
128
129#**********************************************************************
130# global vars which depend on server protocol selection
131#
132my %commandfunc;   # protocol command specific function callbacks
133my %displaytext;   # text returned to client before callback runs
134
135#**********************************************************************
136# global vars customized for each test from the server commands file
137#
138my $ctrldelay;     # set if server should throttle ctrl stream
139my $datadelay;     # set if server should throttle data stream
140my $retrweirdo;    # set if ftp server should use RETRWEIRDO
141my $retrnosize;    # set if ftp server should use RETRNOSIZE
142my $pasvbadip;     # set if ftp server should use PASVBADIP
143my $nosave;        # set if ftp server should not save uploaded data
144my $nodataconn;    # set if ftp srvr doesn't establish or accepts data channel
145my $nodataconn425; # set if ftp srvr doesn't establish data ch and replies 425
146my $nodataconn421; # set if ftp srvr doesn't establish data ch and replies 421
147my $nodataconn150; # set if ftp srvr doesn't establish data ch and replies 150
148my $storeresp;
149my @capabilities;  # set if server supports capability commands
150my @auth_mechs;    # set if server supports authentication commands
151my %fulltextreply; #
152my %commandreply;  #
153my %customcount;   #
154my %delayreply;    #
155
156#**********************************************************************
157# global variables for to test ftp wildcardmatching or other test that
158# need flexible LIST responses.. and corresponding files.
159# $ftptargetdir is keeping the fake "name" of LIST directory.
160#
161my $ftplistparserstate;
162my $ftptargetdir="";
163
164#**********************************************************************
165# global variables used when running a ftp server to keep state info
166# relative to the secondary or data sockfilt process. Values of these
167# variables should only be modified using datasockf_state() sub, given
168# that they are closely related and relationship is a bit awkward.
169#
170my $datasockf_state = 'STOPPED'; # see datasockf_state() sub
171my $datasockf_mode = 'none';     # ['none','active','passive']
172my $datasockf_runs = 'no';       # ['no','yes']
173my $datasockf_conn = 'no';       # ['no','yes']
174
175#**********************************************************************
176# global vars used for signal handling
177#
178my $got_exit_signal = 0; # set if program should finish execution ASAP
179my $exit_signal;         # first signal handled in exit_signal_handler
180
181#**********************************************************************
182# Mail related definitions
183#
184my $TEXT_PASSWORD = "secret";
185my $POP3_TIMESTAMP = "<1972.987654321\@curl>";
186
187#**********************************************************************
188# exit_signal_handler will be triggered to indicate that the program
189# should finish its execution in a controlled way as soon as possible.
190# For now, program will also terminate from within this handler.
191#
192sub exit_signal_handler {
193    my $signame = shift;
194    # For now, simply mimic old behavior.
195    killsockfilters($proto, $ipvnum, $idnum, $verbose);
196    unlink($pidfile);
197    unlink($portfile);
198    if($serverlogslocked) {
199        $serverlogslocked = 0;
200        clear_advisor_read_lock($SERVERLOGS_LOCK);
201    }
202    exit;
203}
204
205#**********************************************************************
206# logmsg is general message logging subroutine for our test servers.
207#
208sub logmsg {
209    my $now;
210    # sub second timestamping needs Time::HiRes
211    if($Time::HiRes::VERSION) {
212        my ($seconds, $usec) = gettimeofday();
213        my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
214            localtime($seconds);
215        $now = sprintf("%02d:%02d:%02d.%06d ", $hour, $min, $sec, $usec);
216    }
217    else {
218        my $seconds = time();
219        my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
220            localtime($seconds);
221        $now = sprintf("%02d:%02d:%02d ", $hour, $min, $sec);
222    }
223    if(open(LOGFILEFH, ">>$logfile")) {
224        print LOGFILEFH $now;
225        print LOGFILEFH @_;
226        close(LOGFILEFH);
227    }
228}
229
230sub ftpmsg {
231  # append to the server.input file
232  open(INPUT, ">>log/server$idstr.input") ||
233    logmsg "failed to open log/server$idstr.input\n";
234
235  print INPUT @_;
236  close(INPUT);
237
238  # use this, open->print->close system only to make the file
239  # open as little as possible, to make the test suite run
240  # better on windows/cygwin
241}
242
243#**********************************************************************
244# eXsysread is a wrapper around perl's sysread() function. This will
245# repeat the call to sysread() until it has actually read the complete
246# number of requested bytes or an unrecoverable condition occurs.
247# On success returns a positive value, the number of bytes requested.
248# On failure or timeout returns zero.
249#
250sub eXsysread {
251    my $FH      = shift;
252    my $scalar  = shift;
253    my $nbytes  = shift;
254    my $timeout = shift; # A zero timeout disables eXsysread() time limit
255    #
256    my $time_limited = 0;
257    my $timeout_rest = 0;
258    my $start_time = 0;
259    my $nread  = 0;
260    my $rc;
261
262    $$scalar = "";
263
264    if((not defined $nbytes) || ($nbytes < 1)) {
265        logmsg "Error: eXsysread() failure: " .
266               "length argument must be positive\n";
267        return 0;
268    }
269    if((not defined $timeout) || ($timeout < 0)) {
270        logmsg "Error: eXsysread() failure: " .
271               "timeout argument must be zero or positive\n";
272        return 0;
273    }
274    if($timeout > 0) {
275        # caller sets eXsysread() time limit
276        $time_limited = 1;
277        $timeout_rest = $timeout;
278        $start_time = int(time());
279    }
280
281    while($nread < $nbytes) {
282        if($time_limited) {
283            eval {
284                local $SIG{ALRM} = sub { die "alarm\n"; };
285                alarm $timeout_rest;
286                $rc = sysread($FH, $$scalar, $nbytes - $nread, $nread);
287                alarm 0;
288            };
289            $timeout_rest = $timeout - (int(time()) - $start_time);
290            if($timeout_rest < 1) {
291                logmsg "Error: eXsysread() failure: timed out\n";
292                return 0;
293            }
294        }
295        else {
296            $rc = sysread($FH, $$scalar, $nbytes - $nread, $nread);
297        }
298        if($got_exit_signal) {
299            logmsg "Error: eXsysread() failure: signalled to die\n";
300            return 0;
301        }
302        if(not defined $rc) {
303            if($!{EINTR}) {
304                logmsg "Warning: retrying sysread() interrupted system call\n";
305                next;
306            }
307            if($!{EAGAIN}) {
308                logmsg "Warning: retrying sysread() due to EAGAIN\n";
309                next;
310            }
311            if($!{EWOULDBLOCK}) {
312                logmsg "Warning: retrying sysread() due to EWOULDBLOCK\n";
313                next;
314            }
315            logmsg "Error: sysread() failure: $!\n";
316            return 0;
317        }
318        if($rc < 0) {
319            logmsg "Error: sysread() failure: returned negative value $rc\n";
320            return 0;
321        }
322        if($rc == 0) {
323            logmsg "Error: sysread() failure: read zero bytes\n";
324            return 0;
325        }
326        $nread += $rc;
327    }
328    return $nread;
329}
330
331#**********************************************************************
332# read_mainsockf attempts to read the given amount of output from the
333# sockfilter which is in use for the main or primary connection. This
334# reads untranslated sockfilt lingo which may hold data read from the
335# main or primary socket. On success returns 1, otherwise zero.
336#
337sub read_mainsockf {
338    my $scalar  = shift;
339    my $nbytes  = shift;
340    my $timeout = shift; # Optional argument, if zero blocks indefinitely
341    my $FH = \*SFREAD;
342
343    if(not defined $timeout) {
344        $timeout = $sockfilt_timeout + ($nbytes >> 12);
345    }
346    if(eXsysread($FH, $scalar, $nbytes, $timeout) != $nbytes) {
347        my ($fcaller, $lcaller) = (caller)[1,2];
348        logmsg "Error: read_mainsockf() failure at $fcaller " .
349               "line $lcaller. Due to eXsysread() failure\n";
350        return 0;
351    }
352    return 1;
353}
354
355#**********************************************************************
356# read_datasockf attempts to read the given amount of output from the
357# sockfilter which is in use for the data or secondary connection. This
358# reads untranslated sockfilt lingo which may hold data read from the
359# data or secondary socket. On success returns 1, otherwise zero.
360#
361sub read_datasockf {
362    my $scalar = shift;
363    my $nbytes = shift;
364    my $timeout = shift; # Optional argument, if zero blocks indefinitely
365    my $FH = \*DREAD;
366
367    if(not defined $timeout) {
368        $timeout = $sockfilt_timeout + ($nbytes >> 12);
369    }
370    if(eXsysread($FH, $scalar, $nbytes, $timeout) != $nbytes) {
371        my ($fcaller, $lcaller) = (caller)[1,2];
372        logmsg "Error: read_datasockf() failure at $fcaller " .
373               "line $lcaller. Due to eXsysread() failure\n";
374        return 0;
375    }
376    return 1;
377}
378
379sub sysread_or_die {
380    my $FH     = shift;
381    my $scalar = shift;
382    my $length = shift;
383    my $fcaller;
384    my $lcaller;
385    my $result;
386
387    $result = sysread($$FH, $$scalar, $length);
388
389    if(not defined $result) {
390        ($fcaller, $lcaller) = (caller)[1,2];
391        logmsg "Failed to read input\n";
392        logmsg "Error: $srvrname server, sysread error: $!\n";
393        logmsg "Exited from sysread_or_die() at $fcaller " .
394               "line $lcaller. $srvrname server, sysread error: $!\n";
395        killsockfilters($proto, $ipvnum, $idnum, $verbose);
396        unlink($pidfile);
397        unlink($portfile);
398        if($serverlogslocked) {
399            $serverlogslocked = 0;
400            clear_advisor_read_lock($SERVERLOGS_LOCK);
401        }
402        exit;
403    }
404    elsif($result == 0) {
405        ($fcaller, $lcaller) = (caller)[1,2];
406        logmsg "Failed to read input\n";
407        logmsg "Error: $srvrname server, read zero\n";
408        logmsg "Exited from sysread_or_die() at $fcaller " .
409               "line $lcaller. $srvrname server, read zero\n";
410        killsockfilters($proto, $ipvnum, $idnum, $verbose);
411        unlink($pidfile);
412        unlink($portfile);
413        if($serverlogslocked) {
414            $serverlogslocked = 0;
415            clear_advisor_read_lock($SERVERLOGS_LOCK);
416        }
417        exit;
418    }
419
420    return $result;
421}
422
423sub startsf {
424    my $mainsockfcmd = "./server/sockfilt".exe_ext('SRV')." " .
425        "--ipv$ipvnum --port $port " .
426        "--pidfile \"$mainsockf_pidfile\" " .
427        "--portfile \"$portfile\" " .
428        "--logfile \"$mainsockf_logfile\"";
429    $sfpid = open2(*SFREAD, *SFWRITE, $mainsockfcmd);
430
431    print STDERR "$mainsockfcmd\n" if($verbose);
432
433    print SFWRITE "PING\n";
434    my $pong;
435    sysread_or_die(\*SFREAD, \$pong, 5);
436
437    if($pong !~ /^PONG/) {
438        logmsg "Failed sockfilt command: $mainsockfcmd\n";
439        killsockfilters($proto, $ipvnum, $idnum, $verbose);
440        unlink($pidfile);
441        unlink($portfile);
442        if($serverlogslocked) {
443            $serverlogslocked = 0;
444            clear_advisor_read_lock($SERVERLOGS_LOCK);
445        }
446        die "Failed to start sockfilt!";
447    }
448}
449
450#**********************************************************************
451# Returns the given test's reply data
452#
453sub getreplydata {
454    my ($num) = @_;
455    my $testpart = "";
456
457    $num =~ s/^([^0-9]*)//;
458    if($num > 10000) {
459       $testpart = $num % 10000;
460    }
461
462    my @data = getpart("reply", "data$testpart");
463    if((!@data) && ($testpart ne "")) {
464        @data = getpart("reply", "data");
465    }
466
467    return @data;
468}
469
470sub sockfilt {
471    my $l;
472    foreach $l (@_) {
473        printf SFWRITE "DATA\n%04x\n", length($l);
474        print SFWRITE $l;
475    }
476}
477
478sub sockfiltsecondary {
479    my $l;
480    foreach $l (@_) {
481        printf DWRITE "DATA\n%04x\n", length($l);
482        print DWRITE $l;
483    }
484}
485
486#**********************************************************************
487# Send data to the client on the control stream, which happens to be plain
488# stdout.
489#
490sub sendcontrol {
491    if(!$ctrldelay) {
492        # spit it all out at once
493        sockfilt @_;
494    }
495    else {
496        my $a = join("", @_);
497        my @a = split("", $a);
498
499        for(@a) {
500            sockfilt $_;
501            portable_sleep(0.01);
502        }
503    }
504    my $log;
505    foreach $log (@_) {
506        my $l = $log;
507        $l =~ s/\r/[CR]/g;
508        $l =~ s/\n/[LF]/g;
509        logmsg "> \"$l\"\n";
510    }
511}
512
513#**********************************************************************
514# Send data to the FTP client on the data stream when data connection
515# is actually established. Given that this sub should only be called
516# when a data connection is supposed to be established, calling this
517# without a data connection is an indication of weak logic somewhere.
518#
519sub senddata {
520    my $l;
521    if($datasockf_conn eq 'no') {
522        logmsg "WARNING: Detected data sending attempt without DATA channel\n";
523        foreach $l (@_) {
524            logmsg "WARNING: Data swallowed: $l\n"
525        }
526        return;
527    }
528
529    foreach $l (@_) {
530        if(!$datadelay) {
531            # spit it all out at once
532            sockfiltsecondary $l;
533        }
534        else {
535            # pause between each byte
536            for (split(//,$l)) {
537                sockfiltsecondary $_;
538                portable_sleep(0.01);
539            }
540        }
541    }
542}
543
544#**********************************************************************
545# protocolsetup initializes the 'displaytext' and 'commandfunc' hashes
546# for the given protocol. References to protocol command callbacks are
547# stored in 'commandfunc' hash, and text which will be returned to the
548# client before the command callback runs is stored in 'displaytext'.
549#
550sub protocolsetup {
551    my $proto = $_[0];
552
553    if($proto eq 'ftp') {
554        %commandfunc = (
555            'PORT' => \&PORT_ftp,
556            'EPRT' => \&PORT_ftp,
557            'LIST' => \&LIST_ftp,
558            'NLST' => \&NLST_ftp,
559            'PASV' => \&PASV_ftp,
560            'CWD'  => \&CWD_ftp,
561            'PWD'  => \&PWD_ftp,
562            'EPSV' => \&PASV_ftp,
563            'RETR' => \&RETR_ftp,
564            'SIZE' => \&SIZE_ftp,
565            'REST' => \&REST_ftp,
566            'STOR' => \&STOR_ftp,
567            'APPE' => \&STOR_ftp, # append looks like upload
568            'MDTM' => \&MDTM_ftp,
569        );
570        %displaytext = (
571            'USER' => '331 We are happy you popped in!',
572            'PASS' => '230 Welcome you silly person',
573            'PORT' => '200 You said PORT - I say FINE',
574            'TYPE' => '200 I modify TYPE as you wanted',
575            'LIST' => '150 here comes a directory',
576            'NLST' => '150 here comes a directory',
577            'CWD'  => '250 CWD command successful.',
578            'SYST' => '215 UNIX Type: L8', # just fake something
579            'QUIT' => '221 bye bye baby', # just reply something
580            'MKD'  => '257 Created your requested directory',
581            'REST' => '350 Yeah yeah we set it there for you',
582            'DELE' => '200 OK OK OK whatever you say',
583            'RNFR' => '350 Received your order. Please provide more',
584            'RNTO' => '250 Ok, thanks. File renaming completed.',
585            'NOOP' => '200 Yes, I\'m very good at doing nothing.',
586            'PBSZ' => '500 PBSZ not implemented',
587            'PROT' => '500 PROT not implemented',
588            'welcome' => join("",
589            '220-        _   _ ____  _     '."\r\n",
590            '220-    ___| | | |  _ \| |    '."\r\n",
591            '220-   / __| | | | |_) | |    '."\r\n",
592            '220-  | (__| |_| |  _ {| |___ '."\r\n",
593            '220    \___|\___/|_| \_\_____|'."\r\n")
594        );
595    }
596    elsif($proto eq 'pop3') {
597        %commandfunc = (
598            'APOP' => \&APOP_pop3,
599            'AUTH' => \&AUTH_pop3,
600            'CAPA' => \&CAPA_pop3,
601            'DELE' => \&DELE_pop3,
602            'LIST' => \&LIST_pop3,
603            'NOOP' => \&NOOP_pop3,
604            'PASS' => \&PASS_pop3,
605            'QUIT' => \&QUIT_pop3,
606            'RETR' => \&RETR_pop3,
607            'RSET' => \&RSET_pop3,
608            'STAT' => \&STAT_pop3,
609            'TOP'  => \&TOP_pop3,
610            'UIDL' => \&UIDL_pop3,
611            'USER' => \&USER_pop3,
612        );
613        %displaytext = (
614            'welcome' => join("",
615            '        _   _ ____  _     '."\r\n",
616            '    ___| | | |  _ \| |    '."\r\n",
617            '   / __| | | | |_) | |    '."\r\n",
618            '  | (__| |_| |  _ {| |___ '."\r\n",
619            '   \___|\___/|_| \_\_____|'."\r\n",
620            '+OK curl POP3 server ready to serve '."\r\n")
621        );
622    }
623    elsif($proto eq 'imap') {
624        %commandfunc = (
625            'APPEND'     => \&APPEND_imap,
626            'CAPABILITY' => \&CAPABILITY_imap,
627            'CHECK'      => \&CHECK_imap,
628            'CLOSE'      => \&CLOSE_imap,
629            'COPY'       => \&COPY_imap,
630            'CREATE'     => \&CREATE_imap,
631            'DELETE'     => \&DELETE_imap,
632            'EXAMINE'    => \&EXAMINE_imap,
633            'EXPUNGE'    => \&EXPUNGE_imap,
634            'FETCH'      => \&FETCH_imap,
635            'LIST'       => \&LIST_imap,
636            'LSUB'       => \&LSUB_imap,
637            'LOGIN'      => \&LOGIN_imap,
638            'LOGOUT'     => \&LOGOUT_imap,
639            'NOOP'       => \&NOOP_imap,
640            'RENAME'     => \&RENAME_imap,
641            'SEARCH'     => \&SEARCH_imap,
642            'SELECT'     => \&SELECT_imap,
643            'STATUS'     => \&STATUS_imap,
644            'STORE'      => \&STORE_imap,
645            'UID'        => \&UID_imap,
646            'IDLE'       => \&IDLE_imap,
647        );
648        %displaytext = (
649            'welcome' => join("",
650            '        _   _ ____  _     '."\r\n",
651            '    ___| | | |  _ \| |    '."\r\n",
652            '   / __| | | | |_) | |    '."\r\n",
653            '  | (__| |_| |  _ {| |___ '."\r\n",
654            '   \___|\___/|_| \_\_____|'."\r\n",
655            '* OK curl IMAP server ready to serve'."\r\n")
656        );
657    }
658    elsif($proto eq 'smtp') {
659        %commandfunc = (
660            'DATA' => \&DATA_smtp,
661            'EHLO' => \&EHLO_smtp,
662            'EXPN' => \&EXPN_smtp,
663            'HELO' => \&HELO_smtp,
664            'HELP' => \&HELP_smtp,
665            'MAIL' => \&MAIL_smtp,
666            'NOOP' => \&NOOP_smtp,
667            'RSET' => \&RSET_smtp,
668            'RCPT' => \&RCPT_smtp,
669            'VRFY' => \&VRFY_smtp,
670            'QUIT' => \&QUIT_smtp,
671        );
672        %displaytext = (
673            'welcome' => join("",
674            '220-        _   _ ____  _     '."\r\n",
675            '220-    ___| | | |  _ \| |    '."\r\n",
676            '220-   / __| | | | |_) | |    '."\r\n",
677            '220-  | (__| |_| |  _ {| |___ '."\r\n",
678            '220    \___|\___/|_| \_\_____|'."\r\n")
679        );
680    }
681}
682
683sub close_dataconn {
684    my ($closed)=@_; # non-zero if already disconnected
685
686    my $datapid = processexists($datasockf_pidfile);
687
688    logmsg "=====> Closing $datasockf_mode DATA connection...\n";
689
690    if(!$closed) {
691        if($datapid > 0) {
692            logmsg "Server disconnects $datasockf_mode DATA connection\n";
693            print DWRITE "DISC\n";
694            my $i;
695            sysread DREAD, $i, 5;
696            logmsg "Server disconnected $datasockf_mode DATA connection\n";
697        }
698        else {
699            logmsg "Server finds $datasockf_mode DATA connection already ".
700                   "disconnected\n";
701        }
702    }
703    else {
704        logmsg "Server knows $datasockf_mode DATA connection is already ".
705               "disconnected\n";
706    }
707
708    if($datapid > 0) {
709        logmsg "DATA sockfilt for $datasockf_mode data channel quits ".
710               "(pid $datapid)\n";
711        print DWRITE "QUIT\n";
712        pidwait($datapid, 0);
713        unlink($datasockf_pidfile) if(-f $datasockf_pidfile);
714        logmsg "DATA sockfilt for $datasockf_mode data channel quit ".
715               "(pid $datapid)\n";
716    }
717    else {
718        logmsg "DATA sockfilt for $datasockf_mode data channel already ".
719               "dead\n";
720    }
721
722    logmsg "=====> Closed $datasockf_mode DATA connection\n";
723
724    datasockf_state('STOPPED');
725}
726
727################
728################ SMTP commands
729################
730
731# The type of server (SMTP or ESMTP)
732my $smtp_type;
733
734# The client (which normally contains the test number)
735my $smtp_client;
736
737sub EHLO_smtp {
738    my ($client) = @_;
739    my @data;
740
741    # TODO: Get the IP address of the client connection to use in the
742    # EHLO response when the client doesn't specify one but for now use
743    # 127.0.0.1
744    if(!$client) {
745        $client = "[127.0.0.1]";
746    }
747
748    # Set the server type to ESMTP
749    $smtp_type = "ESMTP";
750
751    # Calculate the EHLO response
752    push @data, "$smtp_type pingpong test server Hello $client";
753
754    if((@capabilities) || (@auth_mechs)) {
755        my $mechs;
756
757        for my $c (@capabilities) {
758            push @data, $c;
759        }
760
761        for my $am (@auth_mechs) {
762            if(!$mechs) {
763                $mechs = "$am";
764            }
765            else {
766                $mechs .= " $am";
767            }
768        }
769
770        if($mechs) {
771            push @data, "AUTH $mechs";
772        }
773    }
774
775    # Send the EHLO response
776    for(my $i = 0; $i < @data; $i++) {
777        my $d = $data[$i];
778
779        if($i < @data - 1) {
780            sendcontrol "250-$d\r\n";
781        }
782        else {
783            sendcontrol "250 $d\r\n";
784        }
785    }
786
787    # Store the client (as it may contain the test number)
788    $smtp_client = $client;
789
790    return 0;
791}
792
793sub HELO_smtp {
794    my ($client) = @_;
795
796    # TODO: Get the IP address of the client connection to use in the HELO
797    # response when the client doesn't specify one but for now use 127.0.0.1
798    if(!$client) {
799        $client = "[127.0.0.1]";
800    }
801
802    # Set the server type to SMTP
803    $smtp_type = "SMTP";
804
805    # Send the HELO response
806    sendcontrol "250 $smtp_type pingpong test server Hello $client\r\n";
807
808    # Store the client (as it may contain the test number)
809    $smtp_client = $client;
810
811    return 0;
812}
813
814sub MAIL_smtp {
815    my ($args) = @_;
816
817    logmsg "MAIL_smtp got $args\n";
818
819    if (!$args) {
820        sendcontrol "501 Unrecognized parameter\r\n";
821    }
822    else {
823        my $from;
824        my $size;
825        my $smtputf8 = grep /^SMTPUTF8$/, @capabilities;
826        my @elements = split(/ /, $args);
827
828        # Get the FROM and SIZE parameters
829        for my $e (@elements) {
830            if($e =~ /^FROM:(.*)$/) {
831                $from = $1;
832            }
833            elsif($e =~ /^SIZE=(\d+)$/) {
834                $size = $1;
835            }
836        }
837
838        # this server doesn't "validate" MAIL FROM addresses
839        if (length($from)) {
840            my @found;
841            my $valid = 1;
842
843            # Check the capabilities for SIZE and if the specified size is
844            # greater than the message size then reject it
845            if (@found = grep /^SIZE (\d+)$/, @capabilities) {
846                if ($found[0] =~ /^SIZE (\d+)$/) {
847                    if ($size > $1) {
848                        $valid = 0;
849                    }
850                }
851            }
852
853            if(!$valid) {
854                sendcontrol "552 Message size too large\r\n";
855            }
856            else {
857                sendcontrol "250 Sender OK\r\n";
858            }
859        }
860        else {
861            sendcontrol "501 Invalid address\r\n";
862        }
863    }
864
865    return 0;
866}
867
868sub RCPT_smtp {
869    my ($args) = @_;
870
871    logmsg "RCPT_smtp got $args\n";
872
873    # Get the TO parameter
874    if($args !~ /^TO:(.*)/) {
875        sendcontrol "501 Unrecognized parameter\r\n";
876    }
877    else {
878        my $smtputf8 = grep /^SMTPUTF8$/, @capabilities;
879        my $to = $1;
880
881        # Validate the to address (only a valid email address inside <> is
882        # allowed, such as <user@example.com>)
883        if ((!$smtputf8 && $to =~
884              /^<([a-zA-Z0-9._%+-]+)\@(([a-zA-Z0-9-]+)\.)+([a-zA-Z]{2,4})>$/) ||
885            ($smtputf8 && $to =~
886              /^<([a-zA-Z0-9\x{80}-\x{ff}._%+-]+)\@(([a-zA-Z0-9\x{80}-\x{ff}-]+)\.)+([a-zA-Z]{2,4})>$/)) {
887            sendcontrol "250 Recipient OK\r\n";
888        }
889        else {
890            sendcontrol "501 Invalid address\r\n";
891        }
892    }
893
894    return 0;
895}
896
897sub DATA_smtp {
898    my ($args) = @_;
899
900    if ($args) {
901        sendcontrol "501 Unrecognized parameter\r\n";
902    }
903    elsif ($smtp_client !~ /^(\d*)$/) {
904        sendcontrol "501 Invalid arguments\r\n";
905    }
906    else {
907        sendcontrol "354 Show me the mail\r\n";
908
909        my $testno = $smtp_client;
910        my $filename = "log/upload.$testno";
911
912        logmsg "Store test number $testno in $filename\n";
913
914        open(FILE, ">$filename") ||
915            return 0; # failed to open output
916
917        my $line;
918        my $ulsize=0;
919        my $disc=0;
920        my $raw;
921        while (5 == (sysread \*SFREAD, $line, 5)) {
922            if($line eq "DATA\n") {
923                my $i;
924                my $eob;
925                sysread \*SFREAD, $i, 5;
926
927                my $size = 0;
928                if($i =~ /^([0-9a-fA-F]{4})\n/) {
929                    $size = hex($1);
930                }
931
932                read_mainsockf(\$line, $size);
933
934                $ulsize += $size;
935                print FILE $line if(!$nosave);
936
937                $raw .= $line;
938                if($raw =~ /(?:^|\x0d\x0a)\x2e\x0d\x0a/) {
939                    # end of data marker!
940                    $eob = 1;
941                }
942
943                logmsg "> Appending $size bytes to file\n";
944
945                if($eob) {
946                    logmsg "Found SMTP EOB marker\n";
947                    last;
948                }
949            }
950            elsif($line eq "DISC\n") {
951                # disconnect!
952                $disc=1;
953                last;
954            }
955            else {
956                logmsg "No support for: $line";
957                last;
958            }
959        }
960
961        if($nosave) {
962            print FILE "$ulsize bytes would've been stored here\n";
963        }
964
965        close(FILE);
966
967        logmsg "received $ulsize bytes upload\n";
968
969        sendcontrol "250 OK, data received!\r\n";
970    }
971
972    return 0;
973}
974
975sub NOOP_smtp {
976    my ($args) = @_;
977
978    if($args) {
979        sendcontrol "501 Unrecognized parameter\r\n";
980    }
981    else {
982        sendcontrol "250 OK\r\n";
983    }
984
985    return 0;
986}
987
988sub RSET_smtp {
989    my ($args) = @_;
990
991    if($args) {
992        sendcontrol "501 Unrecognized parameter\r\n";
993    }
994    else {
995        sendcontrol "250 Resetting\r\n";
996    }
997
998    return 0;
999}
1000
1001sub HELP_smtp {
1002    my ($args) = @_;
1003
1004    # One argument is optional
1005    if($args) {
1006        logmsg "HELP_smtp got $args\n";
1007    }
1008
1009    if($smtp_client eq "verifiedserver") {
1010        # This is the secret command that verifies that this actually is
1011        # the curl test server
1012        sendcontrol "214 WE ROOLZ: $$\r\n";
1013
1014        if($verbose) {
1015            print STDERR "FTPD: We returned proof we are the test server\n";
1016        }
1017
1018        logmsg "return proof we are we\n";
1019    }
1020    else {
1021        sendcontrol "214-This server supports the following commands:\r\n";
1022
1023        if(@auth_mechs) {
1024            sendcontrol "214 HELO EHLO RCPT DATA RSET MAIL VRFY EXPN QUIT HELP AUTH\r\n";
1025        }
1026        else {
1027            sendcontrol "214 HELO EHLO RCPT DATA RSET MAIL VRFY EXPN QUIT HELP\r\n";
1028        }
1029    }
1030
1031    return 0;
1032}
1033
1034sub VRFY_smtp {
1035    my ($args) = @_;
1036    my ($username, $address) = split(/ /, $args, 2);
1037
1038    logmsg "VRFY_smtp got $args\n";
1039
1040    if($username eq "") {
1041        sendcontrol "501 Unrecognized parameter\r\n";
1042    }
1043    else {
1044        my $smtputf8 = grep /^SMTPUTF8$/, @capabilities;
1045
1046        # Validate the username (only a valid local or external username is
1047        # allowed, such as user or user@example.com)
1048        if ((!$smtputf8 && $username =~
1049            /^([a-zA-Z0-9._%+-]+)(\@(([a-zA-Z0-9-]+)\.)+([a-zA-Z]{2,4}))?$/) ||
1050            ($smtputf8 && $username =~
1051            /^([a-zA-Z0-9\x{80}-\x{ff}._%+-]+)(\@(([a-zA-Z0-9\x{80}-\x{ff}-]+)\.)+([a-zA-Z]{2,4}))?$/)) {
1052
1053            my @data = getreplydata($smtp_client);
1054
1055            if(!@data) {
1056                if ($username !~
1057                    /^([a-zA-Z0-9._%+-]+)\@(([a-zA-Z0-9-]+)\.)+([a-zA-Z]{2,4})$/) {
1058                  push @data, "250 <$username\@example.com>\r\n"
1059                }
1060                else {
1061                  push @data, "250 <$username>\r\n"
1062                }
1063            }
1064
1065            for my $d (@data) {
1066                sendcontrol $d;
1067            }
1068        }
1069        else {
1070            sendcontrol "501 Invalid address\r\n";
1071        }
1072    }
1073
1074    return 0;
1075}
1076
1077sub EXPN_smtp {
1078    my ($list_name) = @_;
1079
1080    logmsg "EXPN_smtp got $list_name\n";
1081
1082    if(!$list_name) {
1083        sendcontrol "501 Unrecognized parameter\r\n";
1084    }
1085    else {
1086        my @data = getreplydata($smtp_client);
1087
1088        for my $d (@data) {
1089            sendcontrol $d;
1090        }
1091    }
1092
1093    return 0;
1094}
1095
1096sub QUIT_smtp {
1097    sendcontrol "221 curl $smtp_type server signing off\r\n";
1098
1099    return 0;
1100}
1101
1102# What was deleted by IMAP STORE / POP3 DELE commands
1103my @deleted;
1104
1105################
1106################ IMAP commands
1107################
1108
1109# global to allow the command functions to read it
1110my $cmdid;
1111
1112# what was picked by SELECT
1113my $selected;
1114
1115# Any IMAP parameter can come in escaped and in double quotes.
1116# This function is dumb (so far) and just removes the quotes if present.
1117sub fix_imap_params {
1118    foreach (@_) {
1119        $_ = $1 if /^"(.*)"$/;
1120    }
1121}
1122
1123sub CAPABILITY_imap {
1124    if((!@capabilities) && (!@auth_mechs)) {
1125        sendcontrol "$cmdid BAD Command\r\n";
1126    }
1127    else {
1128        my $data;
1129
1130        # Calculate the CAPABILITY response
1131        $data = "* CAPABILITY IMAP4";
1132
1133        for my $c (@capabilities) {
1134            $data .= " $c";
1135        }
1136
1137        for my $am (@auth_mechs) {
1138            $data .= " AUTH=$am";
1139        }
1140
1141        $data .= " pingpong test server\r\n";
1142
1143        # Send the CAPABILITY response
1144        sendcontrol $data;
1145        sendcontrol "$cmdid OK CAPABILITY completed\r\n";
1146    }
1147
1148    return 0;
1149}
1150
1151sub LOGIN_imap {
1152    my ($args) = @_;
1153    my ($user, $password) = split(/ /, $args, 2);
1154    fix_imap_params($user, $password);
1155
1156    logmsg "LOGIN_imap got $args\n";
1157
1158    if ($user eq "") {
1159        sendcontrol "$cmdid BAD Command Argument\r\n";
1160    }
1161    else {
1162        sendcontrol "$cmdid OK LOGIN completed\r\n";
1163    }
1164
1165    return 0;
1166}
1167
1168sub SELECT_imap {
1169    my ($mailbox) = @_;
1170    fix_imap_params($mailbox);
1171
1172    logmsg "SELECT_imap got test $mailbox\n";
1173
1174    if($mailbox eq "") {
1175        sendcontrol "$cmdid BAD Command Argument\r\n";
1176    }
1177    else {
1178        # Example from RFC 3501, 6.3.1. SELECT Command
1179        sendcontrol "* 172 EXISTS\r\n";
1180        sendcontrol "* 1 RECENT\r\n";
1181        sendcontrol "* OK [UNSEEN 12] Message 12 is first unseen\r\n";
1182        sendcontrol "* OK [UIDVALIDITY 3857529045] UIDs valid\r\n";
1183        sendcontrol "* OK [UIDNEXT 4392] Predicted next UID\r\n";
1184        sendcontrol "* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n";
1185        sendcontrol "* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n";
1186        sendcontrol "$cmdid OK [READ-WRITE] SELECT completed\r\n";
1187
1188        $selected = $mailbox;
1189    }
1190
1191    return 0;
1192}
1193
1194sub FETCH_imap {
1195    my ($args) = @_;
1196    my ($uid, $how) = split(/ /, $args, 2);
1197    fix_imap_params($uid, $how);
1198
1199    logmsg "FETCH_imap got $args\n";
1200
1201    if ($selected eq "") {
1202        sendcontrol "$cmdid BAD Command received in Invalid state\r\n";
1203    }
1204    else {
1205        my @data;
1206        my $size;
1207
1208        if($selected eq "verifiedserver") {
1209            # this is the secret command that verifies that this actually is
1210            # the curl test server
1211            my $response = "WE ROOLZ: $$\r\n";
1212            if($verbose) {
1213                print STDERR "FTPD: We returned proof we are the test server\n";
1214            }
1215            $data[0] = $response;
1216            logmsg "return proof we are we\n";
1217        }
1218        else {
1219            # send mail content
1220            logmsg "retrieve a mail\n";
1221
1222            @data = getreplydata($selected);
1223        }
1224
1225        for (@data) {
1226            $size += length($_);
1227        }
1228
1229        sendcontrol "* $uid FETCH ($how {$size}\r\n";
1230
1231        for my $d (@data) {
1232            sendcontrol $d;
1233        }
1234
1235        sendcontrol ")\r\n";
1236        sendcontrol "$cmdid OK FETCH completed\r\n";
1237    }
1238
1239    return 0;
1240}
1241
1242sub APPEND_imap {
1243    my ($args) = @_;
1244
1245    logmsg "APPEND_imap got $args\r\n";
1246
1247    $args =~ /^([^ ]+) [^{]*\{(\d+)\}$/;
1248    my ($mailbox, $size) = ($1, $2);
1249    fix_imap_params($mailbox);
1250
1251    if($mailbox eq "") {
1252        sendcontrol "$cmdid BAD Command Argument\r\n";
1253    }
1254    else {
1255        sendcontrol "+ Ready for literal data\r\n";
1256
1257        my $testno = $mailbox;
1258        my $filename = "log/upload.$testno";
1259
1260        logmsg "Store test number $testno in $filename\n";
1261
1262        open(FILE, ">$filename") ||
1263            return 0; # failed to open output
1264
1265        my $received = 0;
1266        my $line;
1267        while(5 == (sysread \*SFREAD, $line, 5)) {
1268            if($line eq "DATA\n") {
1269                sysread \*SFREAD, $line, 5;
1270
1271                my $chunksize = 0;
1272                if($line =~ /^([0-9a-fA-F]{4})\n/) {
1273                    $chunksize = hex($1);
1274                }
1275
1276                read_mainsockf(\$line, $chunksize);
1277
1278                my $left = $size - $received;
1279                my $datasize = ($left > $chunksize) ? $chunksize : $left;
1280
1281                if($datasize > 0) {
1282                    logmsg "> Appending $datasize bytes to file\n";
1283                    print FILE substr($line, 0, $datasize) if(!$nosave);
1284                    $line = substr($line, $datasize);
1285
1286                    $received += $datasize;
1287                    if($received == $size) {
1288                        logmsg "Received all data, waiting for final CRLF.\n";
1289                    }
1290                }
1291
1292                if($received == $size && $line eq "\r\n") {
1293                    last;
1294                }
1295            }
1296            elsif($line eq "DISC\n") {
1297                logmsg "Unexpected disconnect!\n";
1298                last;
1299            }
1300            else {
1301                logmsg "No support for: $line";
1302                last;
1303            }
1304        }
1305
1306        if($nosave) {
1307            print FILE "$size bytes would've been stored here\n";
1308        }
1309
1310        close(FILE);
1311
1312        logmsg "received $size bytes upload\n";
1313
1314        sendcontrol "$cmdid OK APPEND completed\r\n";
1315    }
1316
1317    return 0;
1318}
1319
1320sub STORE_imap {
1321    my ($args) = @_;
1322    my ($uid, $what, $value) = split(/ /, $args, 3);
1323    fix_imap_params($uid);
1324
1325    logmsg "STORE_imap got $args\n";
1326
1327    if ($selected eq "") {
1328        sendcontrol "$cmdid BAD Command received in Invalid state\r\n";
1329    }
1330    elsif (($uid eq "") || ($what ne "+Flags") || ($value eq "")) {
1331        sendcontrol "$cmdid BAD Command Argument\r\n";
1332    }
1333    else {
1334        if($value eq "\\Deleted") {
1335            push(@deleted, $uid);
1336        }
1337
1338        sendcontrol "* $uid FETCH (FLAGS (\\Seen $value))\r\n";
1339        sendcontrol "$cmdid OK STORE completed\r\n";
1340    }
1341
1342    return 0;
1343}
1344
1345sub LIST_imap {
1346    my ($args) = @_;
1347    my ($reference, $mailbox) = split(/ /, $args, 2);
1348    fix_imap_params($reference, $mailbox);
1349
1350    logmsg "LIST_imap got $args\n";
1351
1352    if ($reference eq "") {
1353        sendcontrol "$cmdid BAD Command Argument\r\n";
1354    }
1355    elsif ($reference eq "verifiedserver") {
1356        # this is the secret command that verifies that this actually is
1357        # the curl test server
1358        sendcontrol "* LIST () \"/\" \"WE ROOLZ: $$\"\r\n";
1359        sendcontrol "$cmdid OK LIST Completed\r\n";
1360
1361        if($verbose) {
1362            print STDERR "FTPD: We returned proof we are the test server\n";
1363        }
1364
1365        logmsg "return proof we are we\n";
1366    }
1367    else {
1368        my @data = getreplydata($reference);
1369
1370        for my $d (@data) {
1371            sendcontrol $d;
1372        }
1373
1374        sendcontrol "$cmdid OK LIST Completed\r\n";
1375    }
1376
1377    return 0;
1378}
1379
1380sub LSUB_imap {
1381    my ($args) = @_;
1382    my ($reference, $mailbox) = split(/ /, $args, 2);
1383    fix_imap_params($reference, $mailbox);
1384
1385    logmsg "LSUB_imap got $args\n";
1386
1387    if ($reference eq "") {
1388        sendcontrol "$cmdid BAD Command Argument\r\n";
1389    }
1390    else {
1391        my @data = getreplydata($reference);
1392
1393        for my $d (@data) {
1394            sendcontrol $d;
1395        }
1396
1397        sendcontrol "$cmdid OK LSUB Completed\r\n";
1398    }
1399
1400    return 0;
1401}
1402
1403sub EXAMINE_imap {
1404    my ($mailbox) = @_;
1405    fix_imap_params($mailbox);
1406
1407    logmsg "EXAMINE_imap got $mailbox\n";
1408
1409    if ($mailbox eq "") {
1410        sendcontrol "$cmdid BAD Command Argument\r\n";
1411    }
1412    else {
1413        my @data = getreplydata($mailbox);
1414
1415        for my $d (@data) {
1416            sendcontrol $d;
1417        }
1418
1419        sendcontrol "$cmdid OK [READ-ONLY] EXAMINE completed\r\n";
1420    }
1421
1422    return 0;
1423}
1424
1425sub STATUS_imap {
1426    my ($args) = @_;
1427    my ($mailbox, $what) = split(/ /, $args, 2);
1428    fix_imap_params($mailbox);
1429
1430    logmsg "STATUS_imap got $args\n";
1431
1432    if ($mailbox eq "") {
1433        sendcontrol "$cmdid BAD Command Argument\r\n";
1434    }
1435    else {
1436        my @data = getreplydata($mailbox);
1437
1438        for my $d (@data) {
1439            sendcontrol $d;
1440        }
1441
1442        sendcontrol "$cmdid OK STATUS completed\r\n";
1443    }
1444
1445    return 0;
1446}
1447
1448sub SEARCH_imap {
1449    my ($what) = @_;
1450    fix_imap_params($what);
1451
1452    logmsg "SEARCH_imap got $what\n";
1453
1454    if ($selected eq "") {
1455        sendcontrol "$cmdid BAD Command received in Invalid state\r\n";
1456    }
1457    elsif ($what eq "") {
1458        sendcontrol "$cmdid BAD Command Argument\r\n";
1459    }
1460    else {
1461        my @data = getreplydata($selected);
1462
1463        for my $d (@data) {
1464            sendcontrol $d;
1465        }
1466
1467        sendcontrol "$cmdid OK SEARCH completed\r\n";
1468    }
1469
1470    return 0;
1471}
1472
1473sub CREATE_imap {
1474    my ($args) = @_;
1475    fix_imap_params($args);
1476
1477    logmsg "CREATE_imap got $args\n";
1478
1479    if ($args eq "") {
1480        sendcontrol "$cmdid BAD Command Argument\r\n";
1481    }
1482    else {
1483        sendcontrol "$cmdid OK CREATE completed\r\n";
1484    }
1485
1486    return 0;
1487}
1488
1489sub DELETE_imap {
1490    my ($args) = @_;
1491    fix_imap_params($args);
1492
1493    logmsg "DELETE_imap got $args\n";
1494
1495    if ($args eq "") {
1496        sendcontrol "$cmdid BAD Command Argument\r\n";
1497    }
1498    else {
1499        sendcontrol "$cmdid OK DELETE completed\r\n";
1500    }
1501
1502    return 0;
1503}
1504
1505sub RENAME_imap {
1506    my ($args) = @_;
1507    my ($from_mailbox, $to_mailbox) = split(/ /, $args, 2);
1508    fix_imap_params($from_mailbox, $to_mailbox);
1509
1510    logmsg "RENAME_imap got $args\n";
1511
1512    if (($from_mailbox eq "") || ($to_mailbox eq "")) {
1513        sendcontrol "$cmdid BAD Command Argument\r\n";
1514    }
1515    else {
1516        sendcontrol "$cmdid OK RENAME completed\r\n";
1517    }
1518
1519    return 0;
1520}
1521
1522sub CHECK_imap {
1523    if ($selected eq "") {
1524        sendcontrol "$cmdid BAD Command received in Invalid state\r\n";
1525    }
1526    else {
1527        sendcontrol "$cmdid OK CHECK completed\r\n";
1528    }
1529
1530    return 0;
1531}
1532
1533sub CLOSE_imap {
1534    if ($selected eq "") {
1535        sendcontrol "$cmdid BAD Command received in Invalid state\r\n";
1536    }
1537    elsif (!@deleted) {
1538        sendcontrol "$cmdid BAD Command Argument\r\n";
1539    }
1540    else {
1541        sendcontrol "$cmdid OK CLOSE completed\r\n";
1542
1543        @deleted = ();
1544    }
1545
1546    return 0;
1547}
1548
1549sub EXPUNGE_imap {
1550    if ($selected eq "") {
1551        sendcontrol "$cmdid BAD Command received in Invalid state\r\n";
1552    }
1553    else {
1554        if (!@deleted) {
1555            # Report the number of existing messages as per the SELECT
1556            # command
1557            sendcontrol "* 172 EXISTS\r\n";
1558        }
1559        else {
1560            # Report the message UIDs being deleted
1561            for my $d (@deleted) {
1562                sendcontrol "* $d EXPUNGE\r\n";
1563            }
1564
1565            @deleted = ();
1566        }
1567
1568        sendcontrol "$cmdid OK EXPUNGE completed\r\n";
1569    }
1570
1571    return 0;
1572}
1573
1574sub COPY_imap {
1575    my ($args) = @_;
1576    my ($uid, $mailbox) = split(/ /, $args, 2);
1577    fix_imap_params($uid, $mailbox);
1578
1579    logmsg "COPY_imap got $args\n";
1580
1581    if (($uid eq "") || ($mailbox eq "")) {
1582        sendcontrol "$cmdid BAD Command Argument\r\n";
1583    }
1584    else {
1585        sendcontrol "$cmdid OK COPY completed\r\n";
1586    }
1587
1588    return 0;
1589}
1590
1591sub IDLE_imap {
1592    logmsg "IDLE received\n";
1593
1594    sendcontrol "+ entering idle mode\r\n";
1595    return 0;
1596}
1597
1598sub UID_imap {
1599    my ($args) = @_;
1600    my ($command) = split(/ /, $args, 1);
1601    fix_imap_params($command);
1602
1603    logmsg "UID_imap got $args\n";
1604
1605    if ($selected eq "") {
1606        sendcontrol "$cmdid BAD Command received in Invalid state\r\n";
1607    }
1608    elsif (substr($command, 0, 5) eq "FETCH"){
1609        my $func = $commandfunc{"FETCH"};
1610        if($func) {
1611            &$func($args, $command);
1612        }
1613    }
1614    elsif (($command ne "COPY") &&
1615           ($command ne "STORE") && ($command ne "SEARCH")) {
1616        sendcontrol "$cmdid BAD Command Argument\r\n";
1617    }
1618    else {
1619        my @data = getreplydata($selected);
1620
1621        for my $d (@data) {
1622            sendcontrol $d;
1623        }
1624
1625        sendcontrol "$cmdid OK $command completed\r\n";
1626    }
1627
1628    return 0;
1629}
1630
1631sub NOOP_imap {
1632    my ($args) = @_;
1633    my @data = (
1634        "* 22 EXPUNGE\r\n",
1635        "* 23 EXISTS\r\n",
1636        "* 3 RECENT\r\n",
1637        "* 14 FETCH (FLAGS (\\Seen \\Deleted))\r\n",
1638    );
1639
1640    if ($args) {
1641        sendcontrol "$cmdid BAD Command Argument\r\n";
1642    }
1643    else {
1644        for my $d (@data) {
1645            sendcontrol $d;
1646        }
1647
1648        sendcontrol "$cmdid OK NOOP completed\r\n";
1649    }
1650
1651    return 0;
1652}
1653
1654sub LOGOUT_imap {
1655    sendcontrol "* BYE curl IMAP server signing off\r\n";
1656    sendcontrol "$cmdid OK LOGOUT completed\r\n";
1657
1658    return 0;
1659}
1660
1661################
1662################ POP3 commands
1663################
1664
1665# Who is attempting to log in
1666my $username;
1667
1668sub CAPA_pop3 {
1669    my @list = ();
1670    my $mechs;
1671
1672    # Calculate the capability list based on the specified capabilities
1673    # (except APOP) and any authentication mechanisms
1674    for my $c (@capabilities) {
1675        push @list, "$c\r\n" unless $c eq "APOP";
1676    }
1677
1678    for my $am (@auth_mechs) {
1679        if(!$mechs) {
1680            $mechs = "$am";
1681        }
1682        else {
1683            $mechs .= " $am";
1684        }
1685    }
1686
1687    if($mechs) {
1688        push @list, "SASL $mechs\r\n";
1689    }
1690
1691    if(!@list) {
1692        sendcontrol "-ERR Unrecognized command\r\n";
1693    }
1694    else {
1695        my @data = ();
1696
1697        # Calculate the CAPA response
1698        push @data, "+OK List of capabilities follows\r\n";
1699
1700        for my $l (@list) {
1701            push @data, "$l\r\n";
1702        }
1703
1704        push @data, "IMPLEMENTATION POP3 pingpong test server\r\n";
1705
1706        # Send the CAPA response
1707        for my $d (@data) {
1708            sendcontrol $d;
1709        }
1710
1711        # End with the magic 3-byte end of listing marker
1712        sendcontrol ".\r\n";
1713    }
1714
1715    return 0;
1716}
1717
1718sub APOP_pop3 {
1719    my ($args) = @_;
1720    my ($user, $secret) = split(/ /, $args, 2);
1721
1722    if (!grep /^APOP$/, @capabilities) {
1723        sendcontrol "-ERR Unrecognized command\r\n";
1724    }
1725    elsif (($user eq "") || ($secret eq "")) {
1726        sendcontrol "-ERR Protocol error\r\n";
1727    }
1728    else {
1729        my $digest = Digest::MD5::md5_hex($POP3_TIMESTAMP, $TEXT_PASSWORD);
1730
1731        if ($secret ne $digest) {
1732            sendcontrol "-ERR Login failure\r\n";
1733        }
1734        else {
1735            sendcontrol "+OK Login successful\r\n";
1736        }
1737    }
1738
1739    return 0;
1740}
1741
1742sub AUTH_pop3 {
1743    if(!@auth_mechs) {
1744        sendcontrol "-ERR Unrecognized command\r\n";
1745    }
1746    else {
1747        my @data = ();
1748
1749        # Calculate the AUTH response
1750        push @data, "+OK List of supported mechanisms follows\r\n";
1751
1752        for my $am (@auth_mechs) {
1753            push @data, "$am\r\n";
1754        }
1755
1756        # Send the AUTH response
1757        for my $d (@data) {
1758            sendcontrol $d;
1759        }
1760
1761        # End with the magic 3-byte end of listing marker
1762        sendcontrol ".\r\n";
1763    }
1764
1765    return 0;
1766}
1767
1768sub USER_pop3 {
1769    my ($user) = @_;
1770
1771    logmsg "USER_pop3 got $user\n";
1772
1773    if (!$user) {
1774        sendcontrol "-ERR Protocol error\r\n";
1775    }
1776    else {
1777        $username = $user;
1778
1779        sendcontrol "+OK\r\n";
1780    }
1781
1782    return 0;
1783}
1784
1785sub PASS_pop3 {
1786    my ($password) = @_;
1787
1788    logmsg "PASS_pop3 got $password\n";
1789
1790    sendcontrol "+OK Login successful\r\n";
1791
1792    return 0;
1793}
1794
1795sub RETR_pop3 {
1796    my ($msgid) = @_;
1797    my @data;
1798
1799    if($msgid =~ /^verifiedserver$/) {
1800        # this is the secret command that verifies that this actually is
1801        # the curl test server
1802        my $response = "WE ROOLZ: $$\r\n";
1803        if($verbose) {
1804            print STDERR "FTPD: We returned proof we are the test server\n";
1805        }
1806        $data[0] = $response;
1807        logmsg "return proof we are we\n";
1808    }
1809    else {
1810        # send mail content
1811        logmsg "retrieve a mail\n";
1812
1813        @data = getreplydata($msgid);
1814    }
1815
1816    sendcontrol "+OK Mail transfer starts\r\n";
1817
1818    for my $d (@data) {
1819        sendcontrol $d;
1820    }
1821
1822    # end with the magic 3-byte end of mail marker, assumes that the
1823    # mail body ends with a CRLF!
1824    sendcontrol ".\r\n";
1825
1826    return 0;
1827}
1828
1829sub LIST_pop3 {
1830    # This is a built-in fake-message list
1831    my @data = (
1832        "1 100\r\n",
1833        "2 4294967400\r\n",	# > 4 GB
1834        "3 200\r\n",
1835    );
1836
1837    logmsg "retrieve a message list\n";
1838
1839    sendcontrol "+OK Listing starts\r\n";
1840
1841    for my $d (@data) {
1842        sendcontrol $d;
1843    }
1844
1845    # End with the magic 3-byte end of listing marker
1846    sendcontrol ".\r\n";
1847
1848    return 0;
1849}
1850
1851sub DELE_pop3 {
1852    my ($msgid) = @_;
1853
1854    logmsg "DELE_pop3 got $msgid\n";
1855
1856    if (!$msgid) {
1857        sendcontrol "-ERR Protocol error\r\n";
1858    }
1859    else {
1860        push (@deleted, $msgid);
1861
1862        sendcontrol "+OK\r\n";
1863    }
1864
1865    return 0;
1866}
1867
1868sub STAT_pop3 {
1869    my ($args) = @_;
1870
1871    if ($args) {
1872        sendcontrol "-ERR Protocol error\r\n";
1873    }
1874    else {
1875        # Send statistics for the built-in fake message list as
1876        # detailed in the LIST_pop3 function above
1877        sendcontrol "+OK 3 4294967800\r\n";
1878    }
1879
1880    return 0;
1881}
1882
1883sub NOOP_pop3 {
1884    my ($args) = @_;
1885
1886    if ($args) {
1887        sendcontrol "-ERR Protocol error\r\n";
1888    }
1889    else {
1890        sendcontrol "+OK\r\n";
1891    }
1892
1893    return 0;
1894}
1895
1896sub UIDL_pop3 {
1897    # This is a built-in fake-message UID list
1898    my @data = (
1899        "1 1\r\n",
1900        "2 2\r\n",
1901        "3 4\r\n", # Note that UID 3 is a simulated "deleted" message
1902    );
1903
1904    if (!grep /^UIDL$/, @capabilities) {
1905        sendcontrol "-ERR Unrecognized command\r\n";
1906    }
1907    else {
1908        logmsg "retrieve a message UID list\n";
1909
1910        sendcontrol "+OK Listing starts\r\n";
1911
1912        for my $d (@data) {
1913            sendcontrol $d;
1914        }
1915
1916        # End with the magic 3-byte end of listing marker
1917        sendcontrol ".\r\n";
1918    }
1919
1920    return 0;
1921}
1922
1923sub TOP_pop3 {
1924    my ($args) = @_;
1925    my ($msgid, $lines) = split(/ /, $args, 2);
1926
1927    logmsg "TOP_pop3 got $args\n";
1928
1929    if (!grep /^TOP$/, @capabilities) {
1930        sendcontrol "-ERR Unrecognized command\r\n";
1931    }
1932    elsif (($msgid eq "") || ($lines eq "")) {
1933        sendcontrol "-ERR Protocol error\r\n";
1934    }
1935    else {
1936        if ($lines == "0") {
1937            logmsg "retrieve header of mail\n";
1938        }
1939        else {
1940            logmsg "retrieve top $lines lines of mail\n";
1941        }
1942
1943        my @data = getreplydata($msgid);
1944
1945        sendcontrol "+OK Mail transfer starts\r\n";
1946
1947        # Send mail content
1948        for my $d (@data) {
1949            sendcontrol $d;
1950        }
1951
1952        # End with the magic 3-byte end of mail marker, assumes that the
1953        # mail body ends with a CRLF!
1954        sendcontrol ".\r\n";
1955    }
1956
1957    return 0;
1958}
1959
1960sub RSET_pop3 {
1961    my ($args) = @_;
1962
1963    if ($args) {
1964        sendcontrol "-ERR Protocol error\r\n";
1965    }
1966    else {
1967        if (@deleted) {
1968            logmsg "resetting @deleted message(s)\n";
1969
1970            @deleted = ();
1971        }
1972
1973        sendcontrol "+OK\r\n";
1974    }
1975
1976    return 0;
1977}
1978
1979sub QUIT_pop3 {
1980    if(@deleted) {
1981        logmsg "deleting @deleted message(s)\n";
1982
1983        @deleted = ();
1984    }
1985
1986    sendcontrol "+OK curl POP3 server signing off\r\n";
1987
1988    return 0;
1989}
1990
1991################
1992################ FTP commands
1993################
1994my $rest=0;
1995sub REST_ftp {
1996    $rest = $_[0];
1997    logmsg "Set REST position to $rest\n"
1998}
1999
2000sub switch_directory_goto {
2001  my $target_dir = $_;
2002
2003  if(!$ftptargetdir) {
2004    $ftptargetdir = "/";
2005  }
2006
2007  if($target_dir eq "") {
2008    $ftptargetdir = "/";
2009  }
2010  elsif($target_dir eq "..") {
2011    if($ftptargetdir eq "/") {
2012      $ftptargetdir = "/";
2013    }
2014    else {
2015      $ftptargetdir =~ s/[[:alnum:]]+\/$//;
2016    }
2017  }
2018  else {
2019    $ftptargetdir .= $target_dir . "/";
2020  }
2021}
2022
2023sub switch_directory {
2024    my $target_dir = $_[0];
2025
2026    if($target_dir =~ /^test-(\d+)/) {
2027        $cwd_testno = $1;
2028    }
2029    elsif($target_dir eq "/") {
2030        $ftptargetdir = "/";
2031    }
2032    else {
2033        my @dirs = split("/", $target_dir);
2034        for(@dirs) {
2035          switch_directory_goto($_);
2036        }
2037    }
2038}
2039
2040sub CWD_ftp {
2041  my ($folder, $fullcommand) = $_[0];
2042  switch_directory($folder);
2043  if($ftptargetdir =~ /^\/fully_simulated/) {
2044    $ftplistparserstate = "enabled";
2045  }
2046  else {
2047    undef $ftplistparserstate;
2048  }
2049}
2050
2051sub PWD_ftp {
2052    my $mydir;
2053    $mydir = $ftptargetdir ? $ftptargetdir : "/";
2054
2055    if($mydir ne "/") {
2056        $mydir =~ s/\/$//;
2057    }
2058    sendcontrol "257 \"$mydir\" is current directory\r\n";
2059}
2060
2061sub LIST_ftp {
2062    #  print "150 ASCII data connection for /bin/ls (193.15.23.1,59196) (0 bytes)\r\n";
2063
2064# this is a built-in fake-dir ;-)
2065my @ftpdir=("total 20\r\n",
2066"drwxr-xr-x   8 98       98           512 Oct 22 13:06 .\r\n",
2067"drwxr-xr-x   8 98       98           512 Oct 22 13:06 ..\r\n",
2068"drwxr-xr-x   2 98       98           512 May  2  1996 .NeXT\r\n",
2069"-r--r--r--   1 0        1             35 Jul 16  1996 README\r\n",
2070"lrwxrwxrwx   1 0        1              7 Dec  9  1999 bin -> usr/bin\r\n",
2071"dr-xr-xr-x   2 0        1            512 Oct  1  1997 dev\r\n",
2072"drwxrwxrwx   2 98       98           512 May 29 16:04 download.html\r\n",
2073"dr-xr-xr-x   2 0        1            512 Nov 30  1995 etc\r\n",
2074"drwxrwxrwx   2 98       1            512 Oct 30 14:33 pub\r\n",
2075"dr-xr-xr-x   5 0        1            512 Oct  1  1997 usr\r\n");
2076
2077    if($datasockf_conn eq 'no') {
2078        if($nodataconn425) {
2079            sendcontrol "150 Opening data connection\r\n";
2080            sendcontrol "425 Can't open data connection\r\n";
2081        }
2082        elsif($nodataconn421) {
2083            sendcontrol "150 Opening data connection\r\n";
2084            sendcontrol "421 Connection timed out\r\n";
2085        }
2086        elsif($nodataconn150) {
2087            sendcontrol "150 Opening data connection\r\n";
2088            # client shall timeout
2089        }
2090        else {
2091            # client shall timeout
2092        }
2093        return 0;
2094    }
2095
2096    if($ftplistparserstate) {
2097      @ftpdir = ftp_contentlist($ftptargetdir);
2098    }
2099
2100    logmsg "pass LIST data on data connection\n";
2101
2102    if($cwd_testno) {
2103        loadtest("$logdir/test$cwd_testno");
2104
2105        my @data = getpart("reply", "data");
2106        for(@data) {
2107            my $send = $_;
2108            # convert all \n to \r\n for ASCII transfer
2109            $send =~ s/\r\n/\n/g;
2110            $send =~ s/\n/\r\n/g;
2111            logmsg "send $send as data\n";
2112            senddata $send;
2113        }
2114        $cwd_testno = 0; # forget it again
2115    }
2116    else {
2117        # old hard-coded style
2118        for(@ftpdir) {
2119            senddata $_;
2120        }
2121    }
2122    close_dataconn(0);
2123    sendcontrol "226 ASCII transfer complete\r\n";
2124    return 0;
2125}
2126
2127sub NLST_ftp {
2128    my @ftpdir=("file", "with space", "fake", "..", " ..", "funny", "README");
2129
2130    if($datasockf_conn eq 'no') {
2131        if($nodataconn425) {
2132            sendcontrol "150 Opening data connection\r\n";
2133            sendcontrol "425 Can't open data connection\r\n";
2134        }
2135        elsif($nodataconn421) {
2136            sendcontrol "150 Opening data connection\r\n";
2137            sendcontrol "421 Connection timed out\r\n";
2138        }
2139        elsif($nodataconn150) {
2140            sendcontrol "150 Opening data connection\r\n";
2141            # client shall timeout
2142        }
2143        else {
2144            # client shall timeout
2145        }
2146        return 0;
2147    }
2148
2149    logmsg "pass NLST data on data connection\n";
2150    for(@ftpdir) {
2151        senddata "$_\r\n";
2152    }
2153    close_dataconn(0);
2154    sendcontrol "226 ASCII transfer complete\r\n";
2155    return 0;
2156}
2157
2158sub MDTM_ftp {
2159    my $testno = $_[0];
2160    my $testpart = "";
2161    if ($testno > 10000) {
2162        $testpart = $testno % 10000;
2163        $testno = int($testno / 10000);
2164    }
2165
2166    loadtest("$logdir/test$testno");
2167
2168    my @data = getpart("reply", "mdtm");
2169
2170    my $reply = $data[0];
2171    chomp $reply if($reply);
2172
2173    if($reply && ($reply =~ /^[+-]?\d+$/) && ($reply < 0)) {
2174        sendcontrol "550 $testno: no such file.\r\n";
2175    }
2176    elsif($reply) {
2177        sendcontrol "$reply\r\n";
2178    }
2179    else {
2180        sendcontrol "500 MDTM: no such command.\r\n";
2181    }
2182    return 0;
2183}
2184
2185sub SIZE_ftp {
2186    my $testno = $_[0];
2187    if($ftplistparserstate) {
2188        my $size = wildcard_filesize($ftptargetdir, $testno);
2189        if($size == -1) {
2190            sendcontrol "550 $testno: No such file or directory.\r\n";
2191        }
2192        else {
2193            sendcontrol "213 $size\r\n";
2194        }
2195        return 0;
2196    }
2197
2198    if($testno =~ /^verifiedserver$/) {
2199        my $response = "WE ROOLZ: $$\r\n";
2200        my $size = length($response);
2201        sendcontrol "213 $size\r\n";
2202        return 0;
2203    }
2204
2205    if($testno =~ /(\d+)\/?$/) {
2206        $testno = $1;
2207    }
2208    else {
2209        print STDERR "SIZE_ftp: invalid test number: $testno\n";
2210        return 1;
2211    }
2212
2213    my $testpart = "";
2214    if($testno > 10000) {
2215        $testpart = $testno % 10000;
2216        $testno = int($testno / 10000);
2217    }
2218
2219    loadtest("$logdir/test$testno");
2220    my @data = getpart("reply", "size");
2221
2222    my $size = $data[0];
2223
2224    if($size) {
2225        if($size > -1) {
2226            sendcontrol "213 $size\r\n";
2227        }
2228        else {
2229            sendcontrol "550 $testno: No such file or directory.\r\n";
2230        }
2231    }
2232    else {
2233        $size=0;
2234        @data = getpart("reply", "data$testpart");
2235        for(@data) {
2236            $size += length($_);
2237        }
2238        if($size) {
2239            sendcontrol "213 $size\r\n";
2240        }
2241        else {
2242            sendcontrol "550 $testno: No such file or directory.\r\n";
2243        }
2244    }
2245    return 0;
2246}
2247
2248sub RETR_ftp {
2249    my ($testno) = @_;
2250
2251    if($datasockf_conn eq 'no') {
2252        if($nodataconn425) {
2253            sendcontrol "150 Opening data connection\r\n";
2254            sendcontrol "425 Can't open data connection\r\n";
2255        }
2256        elsif($nodataconn421) {
2257            sendcontrol "150 Opening data connection\r\n";
2258            sendcontrol "421 Connection timed out\r\n";
2259        }
2260        elsif($nodataconn150) {
2261            sendcontrol "150 Opening data connection\r\n";
2262            # client shall timeout
2263        }
2264        else {
2265            # client shall timeout
2266        }
2267        return 0;
2268    }
2269
2270    if($ftplistparserstate) {
2271        my @content = wildcard_getfile($ftptargetdir, $testno);
2272        if($content[0] == -1) {
2273            #file not found
2274        }
2275        else {
2276            my $size = length $content[1];
2277            sendcontrol "150 Binary data connection for $testno ($size bytes).\r\n",
2278            senddata $content[1];
2279            close_dataconn(0);
2280            sendcontrol "226 File transfer complete\r\n";
2281        }
2282        return 0;
2283    }
2284
2285    if($testno =~ /^verifiedserver$/) {
2286        # this is the secret command that verifies that this actually is
2287        # the curl test server
2288        my $response = "WE ROOLZ: $$\r\n";
2289        my $len = length($response);
2290        sendcontrol "150 Binary junk ($len bytes).\r\n";
2291        senddata "WE ROOLZ: $$\r\n";
2292        close_dataconn(0);
2293        sendcontrol "226 File transfer complete\r\n";
2294        if($verbose) {
2295            print STDERR "FTPD: We returned proof we are the test server\n";
2296        }
2297        return 0;
2298    }
2299
2300    $testno =~ s/^([^0-9]*)//;
2301    my $testpart = "";
2302    if ($testno > 10000) {
2303        $testpart = $testno % 10000;
2304        $testno = int($testno / 10000);
2305    }
2306
2307    loadtest("$logdir/test$testno");
2308
2309    my @data = getpart("reply", "data$testpart");
2310
2311    my $size=0;
2312    for(@data) {
2313        $size += length($_);
2314    }
2315
2316    my %hash = getpartattr("reply", "data$testpart");
2317
2318    if($size || $hash{'sendzero'}) {
2319
2320        if($rest) {
2321            # move read pointer forward
2322            $size -= $rest;
2323            logmsg "REST $rest was removed from size, makes $size left\n";
2324            $rest = 0; # reset REST offset again
2325        }
2326        if($retrweirdo) {
2327            sendcontrol "150 Binary data connection for $testno () ($size bytes).\r\n",
2328            "226 File transfer complete\r\n";
2329
2330            for(@data) {
2331                my $send = $_;
2332                senddata $send;
2333            }
2334            close_dataconn(0);
2335            $retrweirdo=0; # switch off the weirdo again!
2336        }
2337        else {
2338            my $sz = "($size bytes)";
2339            if($retrnosize) {
2340                $sz = "size?";
2341            }
2342
2343            sendcontrol "150 Binary data connection for $testno () $sz.\r\n";
2344
2345            for(@data) {
2346                my $send = $_;
2347                senddata $send;
2348            }
2349            close_dataconn(0);
2350            sendcontrol "226 File transfer complete\r\n";
2351        }
2352    }
2353    else {
2354        sendcontrol "550 $testno: No such file or directory.\r\n";
2355    }
2356    return 0;
2357}
2358
2359sub STOR_ftp {
2360    my $testno=$_[0];
2361
2362    my $filename = "log/upload.$testno";
2363
2364    if($datasockf_conn eq 'no') {
2365        if($nodataconn425) {
2366            sendcontrol "150 Opening data connection\r\n";
2367            sendcontrol "425 Can't open data connection\r\n";
2368        }
2369        elsif($nodataconn421) {
2370            sendcontrol "150 Opening data connection\r\n";
2371            sendcontrol "421 Connection timed out\r\n";
2372        }
2373        elsif($nodataconn150) {
2374            sendcontrol "150 Opening data connection\r\n";
2375            # client shall timeout
2376        }
2377        else {
2378            # client shall timeout
2379        }
2380        return 0;
2381    }
2382
2383    logmsg "STOR test number $testno in $filename\n";
2384
2385    sendcontrol "125 Gimme gimme gimme!\r\n";
2386
2387    open(FILE, ">$filename") ||
2388        return 0; # failed to open output
2389
2390    my $line;
2391    my $ulsize=0;
2392    my $disc=0;
2393    while (5 == (sysread DREAD, $line, 5)) {
2394        if($line eq "DATA\n") {
2395            my $i;
2396            sysread DREAD, $i, 5;
2397
2398            my $size = 0;
2399            if($i =~ /^([0-9a-fA-F]{4})\n/) {
2400                $size = hex($1);
2401            }
2402
2403            read_datasockf(\$line, $size);
2404
2405            #print STDERR "  GOT: $size bytes\n";
2406
2407            $ulsize += $size;
2408            print FILE $line if(!$nosave);
2409            logmsg "> Appending $size bytes to file\n";
2410        }
2411        elsif($line eq "DISC\n") {
2412            # disconnect!
2413            $disc=1;
2414            last;
2415        }
2416        else {
2417            logmsg "No support for: $line";
2418            last;
2419        }
2420        if($storeresp) {
2421            # abort early
2422            last;
2423        }
2424    }
2425    if($nosave) {
2426        print FILE "$ulsize bytes would've been stored here\n";
2427    }
2428    close(FILE);
2429    close_dataconn($disc);
2430    logmsg "received $ulsize bytes upload\n";
2431    if($storeresp) {
2432        sendcontrol "$storeresp\r\n";
2433    }
2434    else {
2435        sendcontrol "226 File transfer complete\r\n";
2436    }
2437    return 0;
2438}
2439
2440sub PASV_ftp {
2441    my ($arg, $cmd)=@_;
2442    my $pasvport;
2443    my $bindonly = ($nodataconn) ? '--bindonly' : '';
2444
2445    # kill previous data connection sockfilt when alive
2446    if($datasockf_runs eq 'yes') {
2447        killsockfilters($proto, $ipvnum, $idnum, $verbose, 'data');
2448        logmsg "DATA sockfilt for $datasockf_mode data channel killed\n";
2449    }
2450    datasockf_state('STOPPED');
2451
2452    logmsg "====> Passive DATA channel requested by client\n";
2453
2454    logmsg "DATA sockfilt for passive data channel starting...\n";
2455
2456    # We fire up a new sockfilt to do the data transfer for us.
2457    my $datasockfcmd = "./server/sockfilt".exe_ext('SRV')." " .
2458        "--ipv$ipvnum $bindonly --port 0 " .
2459        "--pidfile \"$datasockf_pidfile\" " .
2460        "--logfile \"$datasockf_logfile\"";
2461    $slavepid = open2(\*DREAD, \*DWRITE, $datasockfcmd);
2462
2463    if($nodataconn) {
2464        datasockf_state('PASSIVE_NODATACONN');
2465    }
2466    else {
2467        datasockf_state('PASSIVE');
2468    }
2469
2470    print STDERR "$datasockfcmd\n" if($verbose);
2471
2472    print DWRITE "PING\n";
2473    my $pong;
2474    sysread_or_die(\*DREAD, \$pong, 5);
2475
2476    if($pong =~ /^FAIL/) {
2477        logmsg "DATA sockfilt said: FAIL\n";
2478        logmsg "DATA sockfilt for passive data channel failed\n";
2479        logmsg "DATA sockfilt not running\n";
2480        datasockf_state('STOPPED');
2481        sendcontrol "500 no free ports!\r\n";
2482        return;
2483    }
2484    elsif($pong !~ /^PONG/) {
2485        logmsg "DATA sockfilt unexpected response: $pong\n";
2486        logmsg "DATA sockfilt for passive data channel failed\n";
2487        logmsg "DATA sockfilt killed now\n";
2488        killsockfilters($proto, $ipvnum, $idnum, $verbose, 'data');
2489        logmsg "DATA sockfilt not running\n";
2490        datasockf_state('STOPPED');
2491        sendcontrol "500 no free ports!\r\n";
2492        return;
2493    }
2494
2495    logmsg "DATA sockfilt for passive data channel started (pid $slavepid)\n";
2496
2497    # Find out on what port we listen on or have bound
2498    my $i;
2499    print DWRITE "PORT\n";
2500
2501    # READ the response code
2502    sysread_or_die(\*DREAD, \$i, 5);
2503
2504    # READ the response size
2505    sysread_or_die(\*DREAD, \$i, 5);
2506
2507    my $size = 0;
2508    if($i =~ /^([0-9a-fA-F]{4})\n/) {
2509        $size = hex($1);
2510    }
2511
2512    # READ the response data
2513    read_datasockf(\$i, $size);
2514
2515    # The data is in the format
2516    # IPvX/NNN
2517
2518    if($i =~ /IPv(\d)\/(\d+)/) {
2519        # FIX: deal with IP protocol version
2520        $pasvport = $2;
2521    }
2522
2523    if(!$pasvport) {
2524        logmsg "DATA sockfilt unknown listener port\n";
2525        logmsg "DATA sockfilt for passive data channel failed\n";
2526        logmsg "DATA sockfilt killed now\n";
2527        killsockfilters($proto, $ipvnum, $idnum, $verbose, 'data');
2528        logmsg "DATA sockfilt not running\n";
2529        datasockf_state('STOPPED');
2530        sendcontrol "500 no free ports!\r\n";
2531        return;
2532    }
2533
2534    if($nodataconn) {
2535        my $str = nodataconn_str();
2536        logmsg "DATA sockfilt for passive data channel ($str) bound on port ".
2537               "$pasvport\n";
2538    }
2539    else {
2540        logmsg "DATA sockfilt for passive data channel listens on port ".
2541               "$pasvport\n";
2542    }
2543
2544    if($cmd ne "EPSV") {
2545        # PASV reply
2546        my $p=$listenaddr;
2547        $p =~ s/\./,/g;
2548        if($pasvbadip) {
2549            $p="1,2,3,4";
2550        }
2551        sendcontrol sprintf("227 Entering Passive Mode ($p,%d,%d)\n",
2552                            int($pasvport/256), int($pasvport%256));
2553    }
2554    else {
2555        # EPSV reply
2556        sendcontrol sprintf("229 Entering Passive Mode (|||%d|)\n", $pasvport);
2557    }
2558
2559    logmsg "Client has been notified that DATA conn ".
2560           "will be accepted on port $pasvport\n";
2561
2562    if($nodataconn) {
2563        my $str = nodataconn_str();
2564        logmsg "====> Client fooled ($str)\n";
2565        return;
2566    }
2567
2568    eval {
2569        local $SIG{ALRM} = sub { die "alarm\n" };
2570
2571        # assume swift operations unless explicitly slow
2572        alarm ($datadelay?20:10);
2573
2574        # Wait for 'CNCT'
2575        my $input;
2576
2577        # FIX: Monitor ctrl conn for disconnect
2578
2579        while(sysread(DREAD, $input, 5)) {
2580
2581            if($input !~ /^CNCT/) {
2582                # we wait for a connected client
2583                logmsg "Odd, we got $input from client\n";
2584                next;
2585            }
2586            logmsg "Client connects to port $pasvport\n";
2587            last;
2588        }
2589        alarm 0;
2590    };
2591    if ($@) {
2592        # timed out
2593        logmsg "$srvrname server timed out awaiting data connection ".
2594            "on port $pasvport\n";
2595        logmsg "accept failed or connection not even attempted\n";
2596        logmsg "DATA sockfilt killed now\n";
2597        killsockfilters($proto, $ipvnum, $idnum, $verbose, 'data');
2598        logmsg "DATA sockfilt not running\n";
2599        datasockf_state('STOPPED');
2600        return;
2601    }
2602    else {
2603        logmsg "====> Client established passive DATA connection ".
2604               "on port $pasvport\n";
2605    }
2606
2607    return;
2608}
2609
2610#
2611# Support both PORT and EPRT here.
2612#
2613
2614sub PORT_ftp {
2615    my ($arg, $cmd) = @_;
2616    my $port;
2617    my $addr;
2618
2619    # kill previous data connection sockfilt when alive
2620    if($datasockf_runs eq 'yes') {
2621        killsockfilters($proto, $ipvnum, $idnum, $verbose, 'data');
2622        logmsg "DATA sockfilt for $datasockf_mode data channel killed\n";
2623    }
2624    datasockf_state('STOPPED');
2625
2626    logmsg "====> Active DATA channel requested by client\n";
2627
2628    # We always ignore the given IP and use localhost.
2629
2630    if($cmd eq "PORT") {
2631        if($arg !~ /(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)/) {
2632            logmsg "DATA sockfilt for active data channel not started ".
2633                   "(bad PORT-line: $arg)\n";
2634            sendcontrol "500 silly you, go away\r\n";
2635            return;
2636        }
2637        $port = ($5<<8)+$6;
2638        $addr = "$1.$2.$3.$4";
2639    }
2640    # EPRT |2|::1|49706|
2641    elsif($cmd eq "EPRT") {
2642        if($arg !~ /(\d+)\|([^\|]+)\|(\d+)/) {
2643            logmsg "DATA sockfilt for active data channel not started ".
2644                   "(bad EPRT-line: $arg)\n";
2645            sendcontrol "500 silly you, go away\r\n";
2646            return;
2647        }
2648        sendcontrol "200 Thanks for dropping by. We contact you later\r\n";
2649        $port = $3;
2650        $addr = $2;
2651    }
2652    else {
2653        logmsg "DATA sockfilt for active data channel not started ".
2654               "(invalid command: $cmd)\n";
2655        sendcontrol "500 we don't like $cmd now\r\n";
2656        return;
2657    }
2658
2659    if(!$port || $port > 65535) {
2660        logmsg "DATA sockfilt for active data channel not started ".
2661               "(illegal PORT number: $port)\n";
2662        return;
2663    }
2664
2665    if($nodataconn) {
2666        my $str = nodataconn_str();
2667        logmsg "DATA sockfilt for active data channel not started ($str)\n";
2668        datasockf_state('ACTIVE_NODATACONN');
2669        logmsg "====> Active DATA channel not established\n";
2670        return;
2671    }
2672
2673    logmsg "DATA sockfilt for active data channel starting...\n";
2674
2675    # We fire up a new sockfilt to do the data transfer for us.
2676    my $datasockfcmd = "./server/sockfilt".exe_ext('SRV')." " .
2677        "--ipv$ipvnum --connect $port --addr \"$addr\" " .
2678        "--pidfile \"$datasockf_pidfile\" " .
2679        "--logfile \"$datasockf_logfile\"";
2680    $slavepid = open2(\*DREAD, \*DWRITE, $datasockfcmd);
2681
2682    datasockf_state('ACTIVE');
2683
2684    print STDERR "$datasockfcmd\n" if($verbose);
2685
2686    print DWRITE "PING\n";
2687    my $pong;
2688    sysread_or_die(\*DREAD, \$pong, 5);
2689
2690    if($pong =~ /^FAIL/) {
2691        logmsg "DATA sockfilt said: FAIL\n";
2692        logmsg "DATA sockfilt for active data channel failed\n";
2693        logmsg "DATA sockfilt not running\n";
2694        datasockf_state('STOPPED');
2695        # client shall timeout awaiting connection from server
2696        return;
2697    }
2698    elsif($pong !~ /^PONG/) {
2699        logmsg "DATA sockfilt unexpected response: $pong\n";
2700        logmsg "DATA sockfilt for active data channel failed\n";
2701        logmsg "DATA sockfilt killed now\n";
2702        killsockfilters($proto, $ipvnum, $idnum, $verbose, 'data');
2703        logmsg "DATA sockfilt not running\n";
2704        datasockf_state('STOPPED');
2705        # client shall timeout awaiting connection from server
2706        return;
2707    }
2708
2709    logmsg "DATA sockfilt for active data channel started (pid $slavepid)\n";
2710
2711    logmsg "====> Active DATA channel connected to client port $port\n";
2712
2713    return;
2714}
2715
2716#**********************************************************************
2717# datasockf_state is used to change variables that keep state info
2718# relative to the FTP secondary or data sockfilt process as soon as
2719# one of the five possible stable states is reached. Variables that
2720# are modified by this sub may be checked independently but should
2721# not be changed except by calling this sub.
2722#
2723sub datasockf_state {
2724    my $state = $_[0];
2725
2726  if($state eq 'STOPPED') {
2727    # Data sockfilter initial state, not running,
2728    # not connected and not used.
2729    $datasockf_state = $state;
2730    $datasockf_mode = 'none';
2731    $datasockf_runs = 'no';
2732    $datasockf_conn = 'no';
2733  }
2734  elsif($state eq 'PASSIVE') {
2735    # Data sockfilter accepted connection from client.
2736    $datasockf_state = $state;
2737    $datasockf_mode = 'passive';
2738    $datasockf_runs = 'yes';
2739    $datasockf_conn = 'yes';
2740  }
2741  elsif($state eq 'ACTIVE') {
2742    # Data sockfilter has connected to client.
2743    $datasockf_state = $state;
2744    $datasockf_mode = 'active';
2745    $datasockf_runs = 'yes';
2746    $datasockf_conn = 'yes';
2747  }
2748  elsif($state eq 'PASSIVE_NODATACONN') {
2749    # Data sockfilter bound port without listening,
2750    # client won't be able to establish data connection.
2751    $datasockf_state = $state;
2752    $datasockf_mode = 'passive';
2753    $datasockf_runs = 'yes';
2754    $datasockf_conn = 'no';
2755  }
2756  elsif($state eq 'ACTIVE_NODATACONN') {
2757    # Data sockfilter does not even run,
2758    # client awaits data connection from server in vain.
2759    $datasockf_state = $state;
2760    $datasockf_mode = 'active';
2761    $datasockf_runs = 'no';
2762    $datasockf_conn = 'no';
2763  }
2764  else {
2765      die "Internal error. Unknown datasockf state: $state!";
2766  }
2767}
2768
2769#**********************************************************************
2770# nodataconn_str returns string of effective nodataconn command. Notice
2771# that $nodataconn may be set alone or in addition to a $nodataconnXXX.
2772#
2773sub nodataconn_str {
2774    my $str;
2775    # order matters
2776    $str = 'NODATACONN' if($nodataconn);
2777    $str = 'NODATACONN425' if($nodataconn425);
2778    $str = 'NODATACONN421' if($nodataconn421);
2779    $str = 'NODATACONN150' if($nodataconn150);
2780    return "$str";
2781}
2782
2783#**********************************************************************
2784# customize configures test server operation for each curl test, reading
2785# configuration commands/parameters from server commands file each time
2786# a new client control connection is established with the test server.
2787# On success returns 1, otherwise zero.
2788#
2789sub customize {
2790    $ctrldelay = 0;     # default is no throttling of the ctrl stream
2791    $datadelay = 0;     # default is no throttling of the data stream
2792    $retrweirdo = 0;    # default is no use of RETRWEIRDO
2793    $retrnosize = 0;    # default is no use of RETRNOSIZE
2794    $pasvbadip = 0;     # default is no use of PASVBADIP
2795    $nosave = 0;        # default is to actually save uploaded data to file
2796    $nodataconn = 0;    # default is to establish or accept data channel
2797    $nodataconn425 = 0; # default is to not send 425 without data channel
2798    $nodataconn421 = 0; # default is to not send 421 without data channel
2799    $nodataconn150 = 0; # default is to not send 150 without data channel
2800    $storeresp = "";    # send as ultimate STOR response
2801    @capabilities = (); # default is to not support capability commands
2802    @auth_mechs = ();   # default is to not support authentication commands
2803    %fulltextreply = ();#
2804    %commandreply = (); #
2805    %customcount = ();  #
2806    %delayreply = ();   #
2807
2808    open(CUSTOM, "<log/ftpserver.cmd") ||
2809        return 1;
2810
2811    logmsg "FTPD: Getting commands from log/ftpserver.cmd\n";
2812
2813    while(<CUSTOM>) {
2814        if($_ =~ /REPLY \"([A-Z]+ [A-Za-z0-9+-\/=\*. ]+)\" (.*)/) {
2815            $fulltextreply{$1}=eval "qq{$2}";
2816            logmsg "FTPD: set custom reply for $1\n";
2817        }
2818        elsif($_ =~ /REPLY(LF|) ([A-Za-z0-9+\/=\*]*) (.*)/) {
2819            $commandreply{$2}=eval "qq{$3}";
2820            if($1 ne "LF") {
2821                $commandreply{$2}.="\r\n";
2822            }
2823            else {
2824                $commandreply{$2}.="\n";
2825            }
2826            if($2 eq "") {
2827                logmsg "FTPD: set custom reply for empty command\n";
2828            }
2829            else {
2830                logmsg "FTPD: set custom reply for $2 command\n";
2831            }
2832        }
2833        elsif($_ =~ /COUNT ([A-Z]+) (.*)/) {
2834            # we blank the custom reply for this command when having
2835            # been used this number of times
2836            $customcount{$1}=$2;
2837            logmsg "FTPD: blank custom reply for $1 command after $2 uses\n";
2838        }
2839        elsif($_ =~ /DELAY ([A-Z]+) (\d*)/) {
2840            $delayreply{$1}=$2;
2841            logmsg "FTPD: delay reply for $1 with $2 seconds\n";
2842        }
2843        elsif($_ =~ /SLOWDOWN/) {
2844            $ctrldelay=1;
2845            $datadelay=1;
2846            logmsg "FTPD: send response with 0.01 sec delay between each byte\n";
2847        }
2848        elsif($_ =~ /RETRWEIRDO/) {
2849            logmsg "FTPD: instructed to use RETRWEIRDO\n";
2850            $retrweirdo=1;
2851        }
2852        elsif($_ =~ /RETRNOSIZE/) {
2853            logmsg "FTPD: instructed to use RETRNOSIZE\n";
2854            $retrnosize=1;
2855        }
2856        elsif($_ =~ /PASVBADIP/) {
2857            logmsg "FTPD: instructed to use PASVBADIP\n";
2858            $pasvbadip=1;
2859        }
2860        elsif($_ =~ /NODATACONN425/) {
2861            # applies to both active and passive FTP modes
2862            logmsg "FTPD: instructed to use NODATACONN425\n";
2863            $nodataconn425=1;
2864            $nodataconn=1;
2865        }
2866        elsif($_ =~ /NODATACONN421/) {
2867            # applies to both active and passive FTP modes
2868            logmsg "FTPD: instructed to use NODATACONN421\n";
2869            $nodataconn421=1;
2870            $nodataconn=1;
2871        }
2872        elsif($_ =~ /NODATACONN150/) {
2873            # applies to both active and passive FTP modes
2874            logmsg "FTPD: instructed to use NODATACONN150\n";
2875            $nodataconn150=1;
2876            $nodataconn=1;
2877        }
2878        elsif($_ =~ /NODATACONN/) {
2879            # applies to both active and passive FTP modes
2880            logmsg "FTPD: instructed to use NODATACONN\n";
2881            $nodataconn=1;
2882        }
2883        elsif($_ =~ /^STOR (.*)/) {
2884            $storeresp=$1;
2885            logmsg "FTPD: instructed to use respond to STOR with '$storeresp'\n";
2886        }
2887        elsif($_ =~ /CAPA (.*)/) {
2888            logmsg "FTPD: instructed to support CAPABILITY command\n";
2889            @capabilities = split(/ (?!(?:[^" ]|[^"] [^"])+")/, $1);
2890            foreach (@capabilities) {
2891                $_ = $1 if /^"(.*)"$/;
2892            }
2893        }
2894        elsif($_ =~ /AUTH (.*)/) {
2895            logmsg "FTPD: instructed to support AUTHENTICATION command\n";
2896            @auth_mechs = split(/ /, $1);
2897        }
2898        elsif($_ =~ /NOSAVE/) {
2899            # don't actually store the file we upload - to be used when
2900            # uploading insanely huge amounts
2901            $nosave = 1;
2902            logmsg "FTPD: NOSAVE prevents saving of uploaded data\n";
2903        }
2904        elsif($_ =~ /^Testnum (\d+)/){
2905            $testno = $1;
2906            logmsg "FTPD: run test case number: $testno\n";
2907        }
2908    }
2909    close(CUSTOM);
2910}
2911
2912#----------------------------------------------------------------------
2913#----------------------------------------------------------------------
2914#---------------------------  END OF SUBS  ----------------------------
2915#----------------------------------------------------------------------
2916#----------------------------------------------------------------------
2917
2918#**********************************************************************
2919# Parse command line options
2920#
2921# Options:
2922#
2923# --verbose   # verbose
2924# --srcdir    # source directory
2925# --id        # server instance number
2926# --proto     # server protocol
2927# --pidfile   # server pid file
2928# --portfile  # server port file
2929# --logfile   # server log file
2930# --ipv4      # server IP version 4
2931# --ipv6      # server IP version 6
2932# --port      # server listener port
2933# --addr      # server address for listener port binding
2934#
2935while(@ARGV) {
2936    if($ARGV[0] eq '--verbose') {
2937        $verbose = 1;
2938    }
2939    elsif($ARGV[0] eq '--srcdir') {
2940        if($ARGV[1]) {
2941            $srcdir = $ARGV[1];
2942            shift @ARGV;
2943        }
2944    }
2945    elsif($ARGV[0] eq '--id') {
2946        if($ARGV[1] && ($ARGV[1] =~ /^(\d+)$/)) {
2947            $idnum = $1 if($1 > 0);
2948            shift @ARGV;
2949        }
2950    }
2951    elsif($ARGV[0] eq '--proto') {
2952        if($ARGV[1] && ($ARGV[1] =~ /^(ftp|imap|pop3|smtp)$/)) {
2953            $proto = $1;
2954            shift @ARGV;
2955        }
2956        else {
2957            die "unsupported protocol $ARGV[1]";
2958        }
2959    }
2960    elsif($ARGV[0] eq '--pidfile') {
2961        if($ARGV[1]) {
2962            $pidfile = $ARGV[1];
2963            shift @ARGV;
2964        }
2965    }
2966    elsif($ARGV[0] eq '--portfile') {
2967        if($ARGV[1]) {
2968            $portfile = $ARGV[1];
2969            shift @ARGV;
2970        }
2971    }
2972    elsif($ARGV[0] eq '--logfile') {
2973        if($ARGV[1]) {
2974            $logfile = $ARGV[1];
2975            shift @ARGV;
2976        }
2977    }
2978    elsif($ARGV[0] eq '--ipv4') {
2979        $ipvnum = 4;
2980        $listenaddr = '127.0.0.1' if($listenaddr eq '::1');
2981    }
2982    elsif($ARGV[0] eq '--ipv6') {
2983        $ipvnum = 6;
2984        $listenaddr = '::1' if($listenaddr eq '127.0.0.1');
2985    }
2986    elsif($ARGV[0] eq '--port') {
2987        if($ARGV[1] =~ /^(\d+)$/) {
2988            $port = $1;
2989            shift @ARGV;
2990        }
2991    }
2992    elsif($ARGV[0] eq '--addr') {
2993        if($ARGV[1]) {
2994            my $tmpstr = $ARGV[1];
2995            if($tmpstr =~ /^(\d\d?\d?)\.(\d\d?\d?)\.(\d\d?\d?)\.(\d\d?\d?)$/) {
2996                $listenaddr = "$1.$2.$3.$4" if($ipvnum == 4);
2997            }
2998            elsif($ipvnum == 6) {
2999                $listenaddr = $tmpstr;
3000                $listenaddr =~ s/^\[(.*)\]$/$1/;
3001            }
3002            shift @ARGV;
3003        }
3004    }
3005    else {
3006        print STDERR "\nWarning: ftpserver.pl unknown parameter: $ARGV[0]\n";
3007    }
3008    shift @ARGV;
3009}
3010
3011#***************************************************************************
3012# Initialize command line option dependent variables
3013#
3014
3015if(!$srcdir) {
3016    $srcdir = $ENV{'srcdir'} || '.';
3017}
3018if(!$pidfile) {
3019    $pidfile = "$path/". server_pidfilename($proto, $ipvnum, $idnum);
3020}
3021if(!$logfile) {
3022    $logfile = server_logfilename($logdir, $proto, $ipvnum, $idnum);
3023}
3024
3025$mainsockf_pidfile = "$path/".
3026    mainsockf_pidfilename($proto, $ipvnum, $idnum);
3027$mainsockf_logfile =
3028    mainsockf_logfilename($logdir, $proto, $ipvnum, $idnum);
3029
3030if($proto eq 'ftp') {
3031    $datasockf_pidfile = "$path/".
3032        datasockf_pidfilename($proto, $ipvnum, $idnum);
3033    $datasockf_logfile =
3034        datasockf_logfilename($logdir, $proto, $ipvnum, $idnum);
3035}
3036
3037$srvrname = servername_str($proto, $ipvnum, $idnum);
3038
3039$idstr = "$idnum" if($idnum > 1);
3040
3041protocolsetup($proto);
3042
3043$SIG{INT} = \&exit_signal_handler;
3044$SIG{TERM} = \&exit_signal_handler;
3045
3046startsf();
3047
3048# actual port
3049if($portfile && !$port) {
3050    my $aport;
3051    open(P, "<$portfile");
3052    $aport = <P>;
3053    close(P);
3054    $port = 0 + $aport;
3055}
3056
3057logmsg sprintf("%s server listens on port IPv${ipvnum}/${port}\n", uc($proto));
3058
3059open(PID, ">$pidfile");
3060print PID $$."\n";
3061close(PID);
3062
3063logmsg("logged pid $$ in $pidfile\n");
3064
3065while(1) {
3066
3067    # kill previous data connection sockfilt when alive
3068    if($datasockf_runs eq 'yes') {
3069        killsockfilters($proto, $ipvnum, $idnum, $verbose, 'data');
3070        logmsg "DATA sockfilt for $datasockf_mode data channel killed now\n";
3071    }
3072    datasockf_state('STOPPED');
3073
3074    #
3075    # We read 'sockfilt' commands.
3076    #
3077    my $input;
3078
3079    logmsg "Awaiting input\n";
3080    sysread_or_die(\*SFREAD, \$input, 5);
3081
3082    if($input !~ /^CNCT/) {
3083        # we wait for a connected client
3084        logmsg "MAIN sockfilt said: $input";
3085        next;
3086    }
3087    logmsg "====> Client connect\n";
3088
3089    set_advisor_read_lock($SERVERLOGS_LOCK);
3090    $serverlogslocked = 1;
3091
3092    # flush data:
3093    $| = 1;
3094
3095    &customize(); # read test control instructions
3096    loadtest("$logdir/test$testno");
3097
3098    my $welcome = $commandreply{"welcome"};
3099    if(!$welcome) {
3100        $welcome = $displaytext{"welcome"};
3101    }
3102    else {
3103        # clear it after use
3104        $commandreply{"welcome"}="";
3105        if($welcome !~ /\r\n\z/) {
3106            $welcome .= "\r\n";
3107        }
3108    }
3109    sendcontrol $welcome;
3110
3111    #remove global variables from last connection
3112    if($ftplistparserstate) {
3113      undef $ftplistparserstate;
3114    }
3115    if($ftptargetdir) {
3116      $ftptargetdir = "";
3117    }
3118
3119    if($verbose) {
3120        print STDERR "OUT: $welcome";
3121    }
3122
3123    my $full = "";
3124
3125    while(1) {
3126        my $i;
3127
3128        # Now we expect to read DATA\n[hex size]\n[prot], where the [prot]
3129        # part only is FTP lingo.
3130
3131        # COMMAND
3132        sysread_or_die(\*SFREAD, \$i, 5);
3133
3134        if($i !~ /^DATA/) {
3135            logmsg "MAIN sockfilt said $i";
3136            if($i =~ /^DISC/) {
3137                # disconnect
3138                last;
3139            }
3140            next;
3141        }
3142
3143        # SIZE of data
3144        sysread_or_die(\*SFREAD, \$i, 5);
3145
3146        my $size = 0;
3147        if($i =~ /^([0-9a-fA-F]{4})\n/) {
3148            $size = hex($1);
3149        }
3150
3151        # data
3152        read_mainsockf(\$input, $size);
3153
3154        ftpmsg $input;
3155
3156        $full .= $input;
3157
3158        # Loop until command completion
3159        next unless($full =~ /\r\n$/);
3160
3161        # Remove trailing CRLF.
3162        $full =~ s/[\n\r]+$//;
3163
3164        my $FTPCMD;
3165        my $FTPARG;
3166        if($proto eq "imap") {
3167            # IMAP is different with its identifier first on the command line
3168            if(($full =~ /^([^ ]+) ([^ ]+) (.*)/) ||
3169               ($full =~ /^([^ ]+) ([^ ]+)/)) {
3170                $cmdid=$1; # set the global variable
3171                $FTPCMD=$2;
3172                $FTPARG=$3;
3173            }
3174            # IMAP authentication cancellation
3175            elsif($full =~ /^\*$/) {
3176                # Command id has already been set
3177                $FTPCMD="*";
3178                $FTPARG="";
3179            }
3180            # IMAP long "commands" are base64 authentication data
3181            elsif($full =~ /^[A-Z0-9+\/]*={0,2}$/i) {
3182                # Command id has already been set
3183                $FTPCMD=$full;
3184                $FTPARG="";
3185            }
3186            else {
3187                sendcontrol "$full BAD Command\r\n";
3188                last;
3189            }
3190        }
3191        elsif($full =~ /^([A-Z]{3,4})(\s(.*))?$/i) {
3192            $FTPCMD=$1;
3193            $FTPARG=$3;
3194        }
3195        elsif($proto eq "pop3") {
3196            # POP3 authentication cancellation
3197            if($full =~ /^\*$/) {
3198                $FTPCMD="*";
3199                $FTPARG="";
3200            }
3201            # POP3 long "commands" are base64 authentication data
3202            elsif($full =~ /^[A-Z0-9+\/]*={0,2}$/i) {
3203                $FTPCMD=$full;
3204                $FTPARG="";
3205            }
3206            else {
3207                sendcontrol "-ERR Unrecognized command\r\n";
3208                last;
3209            }
3210        }
3211        elsif($proto eq "smtp") {
3212            # SMTP authentication cancellation
3213            if($full =~ /^\*$/) {
3214                $FTPCMD="*";
3215                $FTPARG="";
3216            }
3217            # SMTP long "commands" are base64 authentication data
3218            elsif($full =~ /^[A-Z0-9+\/]{0,512}={0,2}$/i) {
3219                $FTPCMD=$full;
3220                $FTPARG="";
3221            }
3222            else {
3223                sendcontrol "500 Unrecognized command\r\n";
3224                last;
3225            }
3226        }
3227        else {
3228            sendcontrol "500 Unrecognized command\r\n";
3229            last;
3230        }
3231
3232        logmsg "< \"$full\"\n";
3233
3234        if($verbose) {
3235            print STDERR "IN: $full\n";
3236        }
3237
3238        $full = "";
3239
3240        my $delay = $delayreply{$FTPCMD};
3241        if($delay) {
3242            # just go sleep this many seconds!
3243            logmsg("Sleep for $delay seconds\n");
3244            my $twentieths = $delay * 20;
3245            while($twentieths--) {
3246                portable_sleep(0.05) unless($got_exit_signal);
3247            }
3248        }
3249
3250        my $check = 1; # no response yet
3251
3252        # See if there is a custom reply for the full text
3253        my $fulltext = $FTPARG ? $FTPCMD . " " . $FTPARG : $FTPCMD;
3254        my $text = $fulltextreply{$fulltext};
3255        if($text && ($text ne "")) {
3256            sendcontrol "$text\r\n";
3257            $check = 0;
3258        }
3259        else {
3260            # See if there is a custom reply for the command
3261            $text = $commandreply{$FTPCMD};
3262            if($text && ($text ne "")) {
3263                if($customcount{$FTPCMD} && (!--$customcount{$FTPCMD})) {
3264                    # used enough times so blank the custom command reply
3265                    $commandreply{$FTPCMD}="";
3266                }
3267
3268                sendcontrol $text;
3269                $check = 0;
3270            }
3271            else {
3272                # See if there is any display text for the command
3273                $text = $displaytext{$FTPCMD};
3274                if($text && ($text ne "")) {
3275                    if($proto eq 'imap') {
3276                        sendcontrol "$cmdid $text\r\n";
3277                    }
3278                    else {
3279                        sendcontrol "$text\r\n";
3280                    }
3281
3282                    $check = 0;
3283                }
3284
3285                # only perform this if we're not faking a reply
3286                my $func = $commandfunc{uc($FTPCMD)};
3287                if($func) {
3288                    &$func($FTPARG, $FTPCMD);
3289                    $check = 0;
3290                }
3291            }
3292        }
3293
3294        if($check) {
3295            logmsg "$FTPCMD wasn't handled!\n";
3296            if($proto eq 'pop3') {
3297                sendcontrol "-ERR $FTPCMD is not dealt with!\r\n";
3298            }
3299            elsif($proto eq 'imap') {
3300                sendcontrol "$cmdid BAD $FTPCMD is not dealt with!\r\n";
3301            }
3302            else {
3303                sendcontrol "500 $FTPCMD is not dealt with!\r\n";
3304            }
3305        }
3306
3307    } # while(1)
3308    logmsg "====> Client disconnected\n";
3309
3310    if($serverlogslocked) {
3311        $serverlogslocked = 0;
3312        clear_advisor_read_lock($SERVERLOGS_LOCK);
3313    }
3314}
3315
3316killsockfilters($proto, $ipvnum, $idnum, $verbose);
3317unlink($pidfile);
3318if($serverlogslocked) {
3319    $serverlogslocked = 0;
3320    clear_advisor_read_lock($SERVERLOGS_LOCK);
3321}
3322
3323exit;
3324