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