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