#! /usr/bin/perl -w

# PUSSY - Perl User SAFT Server Yin

###################### user configuration section ###########################

$spool = '~/.sfspool';		# local spool
$maxfilesize = 100*2**20;	# a single file may not exceed 100 MB
$maxfiles = 2**10;		# maximum number of files
$compress = 'GZIP|BZIP2';	# accept gzip and bzip2 files
$deleting = 1;			# allow remote sender to delete his files
$mailnotify = 1;		# notify new files by mail (you may also
				# assign a string like 'framstag@belwue.de')

$firstport = 48700;		# first available port 
$lastport  = 48999;		# last available port 
$maxconnects = 5; 		# max # of concurrent connections

################### end of user configuration section #######################

$0 =~ s:.*/::;
$HOME = $ENV{'HOME'};
$pussy = 'PUSSY-20030105';
$sendmail = '/usr/lib/sendmail -t';
$configdir = $HOME.'/.sendfile';
$userspool = $HOME.'/.sfspool';

use 5.003;
use integer;
use POSIX;
use IO::Socket;
use Getopt::Std;

$SIG{CHLD} = sub {wait()};

# parse CLI arguments
$opt_p = 0;
$opt_I = $opt_h = $opt_V = $opt_v = $opt_x = '';
if (!getopts('IhVvxp:') || $opt_h) {
  print "usage: $0 [-I] [-v] [-x] [-p port]\n";
  print "options: -I       print instructions\n";
  print "         -v       verbose mode\n";
  print "         -x       do not write SAFTport to \$HOME/.plan\n";
  print "         -p port  use this port to bind to\n";
  exit 2;
}

if ($opt_V) {
  print $pussy,"\n";
  exit;
}  

&instructions if $opt_I;

$firstport = $lastport = $opt_p if $opt_p;
$base_socket = &init;

# main-loop
print "waiting for connection...\n" if $opt_v;
for (;;) {
  if ($sock = $base_socket->accept()) {
    $peername = gethostbyaddr($sock->peeraddr(),AF_INET);
    print "\nnew connection from $peername:\n" if $opt_v;
    $pid = fork();
    die "$0: cannot create subprocess: $!\n" unless defined $pid;
    if ($pid == 0) {
      select $sock;
      $| = 1;
      $notify = '';
      &handle_connection;
      if ($notify) {
        warn "%$0-Info received files:\n".$notify;
        &sendmail if $mailnotify;
      }
      exit;
    }
    close $sock;
  }
}

exit;


#
# handle a SAFT connection (this is a subprocess!)
#
sub handle_connection {
  my @args;		# SAFT command arguments
  my $i;		# simple loop counter
  my $sn;		# spool number
  my $size = -1;	# file transfer size
  my $osize;		# file original size
  my $type = 'BINARY';	# file type
  my $comment;		# file comment
  my $transmitted;	# bytes which have been already transmitted

  # SAFT welcome message
  &reply(220);

  while (<$sock>) {

    # trim command line
    s/\r//;s/\n//;
    warn ">$_<\n" if $opt_v;
    s/\s+/ /g;s/^ //;s/ $//;
    
    @args = split;
    
    if (/^HELP$/i) {
      &reply(214);
      next;
    }
    
    if (/^TO/i) {
      if ($args[1] eq ":NULL:") {
        $test = 1;
      } elsif ($args[1] ne $username) {
        &reply(520);
        exit;
      }
      &reply(200);
      next;
    }
    
    if (/^FROM/i) {
      if ($args[1]) {
        $from = $args[1].'@'.$peername.' ('.join(' ',@args[2..$#args]).')';
	&reply(430) if &restricted($from);
        &reply(200);
      } else {
        &reply(505);
      }
      next;
    }
    
    if (/^FILE/i) {
      if ($args[1]) {
        $file = $args[1];
        &reply(200);
      } else {
        &reply(505);
      }
      next;
    }
    
    if (/^SIZE/i) {
      if (!$args[2]) {
        &reply(505);
        next;
      }
      if ("$args[1]$args[2]" !~ /^\d+$/) {
        &reply(507);
        next;
      }
      $size = $args[1];
      $osize = $args[2];
      &reply(413) if $size > $maxfilesize;
      &reply(200);
      next;
    }
    
    if (/^TYPE/i) {
      if (!$args[1]) {
        &reply(505);
        next;
      }
      if (/^TYPE (BINARY|SOURCE|MIME|TEXT=[A-Z0-9_:-]+)( COMPRESSED(=($compress))?| CRYPTED(=PGP)?)?$/i) {
        s/TYPE //i;
        $type = uc $_;
        &reply(200);
      } else {
        &reply(507);
      }
      next;
    }
    
    if (/^DATE/i) {
      if (!$args[1]) {
        &reply(505);
        next;
      }
      if (/^DATE \d\d\d\d-\d\d-\d\d[ T]\d\d:\d\d:\d\d$/i) {
        s/DATE //i;s/T/ /i;
        $date = $_;
        &reply(200);
      } else {
        &reply(507);
      }
      next;
    }
    
    if (/^SIGN/i) {
      if (!$args[1]) {
        &reply(505);
        next;
      }
      $sign = $args[1];
      &reply(200);
      next;
    }
    
    if (/^ATTR/i) {
      if (!$args[1]) {
        &reply(505);
        next;
      }
      if (/^ATTR (TAR|EXE|NONE)$/) {
        $attr = $args[1] if $args[1] !~ /^NONE$/i;
        &reply(200);
      } else {
        &reply(507);
      }
      next;
    }
    
    if (/^COMMENT/i) {
      if (!$args[1]) {
        &reply(505);
        next;
      }
      s/^COMMENT //i;
      $comment = $_;
      &reply(200);
      next;
    }
    
    if (/^DEL$/i) {
      if (!$deleting) {
        &reply(502);
        next;
      }
      if (!$from || !$file) {
        &reply(503);
        next;
      }
      $transmitted = 0;
      if (&delfile($from,$file)) {
        &reply(200);
      } else {
        &reply(550);
      }
      next;
    }
    
    if (/^RESEND$/i) {
      if (!$from || !$file || $size<0) {
        &reply(503);
        next;
      }
      if ($test) {
        $transmitted = 0;
      } else {
        ($transmitted,$sn) = &received($from,$file,$size,$type);
      }
      &reply(230,$transmitted);
      next;
    }

    if (/^DATA$/i) {
      if (!$from || !$file || $size<0) {
        &reply(503);
        next;
      }
      if ($transmitted==$size) {
        &reply(531);
        next;
      }
      &receive($from,$file,$type,"$size $osize",
               $date,$attr,$sign,$comment,$sn,$transmitted) or &reply(451);
      &reply(201);
      &logfile($from,$file,$date,$comment);
      $notify .= "$from : $file\n";
      $size = -1;
      $sn = $transmitted = 0;
      $type = "BINARY";
      $file = $sign = $comment = $attr = $date = "";
      next;
    }
    
    if (/^MSG/i) {
      &reply(511);
      next;
    }
    
    if (/^QUIT$/i) {
      &reply(221);
      return;
    }
    
    &reply(500);
  }
}


#
# bind this server to a free port
#
# RETURN: server-socket 
#
sub init {
  my $warning = $^W;
  my $planfile = $HOME.'/.plan';
  my $sock;
  my @plan;

  $username = getpwuid $< or die "$0: cannot determine own username : $!\n";
  $hostname = &gethostname;
  
  $spool =~ s:^~/:$HOME/:;
  unless (-d $spool) {
    mkdir $spool,0700 or die "$0: cannot create $spool : $!\n";
  }
  chdir $spool or die "$0: cannot cd to $spool : $!\n";
  if ($spool ne $userspool && $spool ne (readlink $userspool or '')) {
    unlink $userspool; rmdir $userspool;
    symlink $spool,$userspool or 
      die "$0: cannot create symlink $userspool : $!\n";
  }
  
  $^W = 0;
  
  for ($port = $firstport; ;$port++) {
    die "$0: cannot bind to a free port: $!\n" if $port > $lastport;
    print "trying port $port\n" if $opt_v;
    $sock = new IO::Socket::INET(
	#LocalHost	=> 'localhost',
	LocalPort	=> $port,
	Listen		=> $maxconnects,
	Proto		=> 'tcp',
	Reuse		=> 1);
    last if $sock;
  }

  $^W = $warning;
  
  warn "%$0-Info successfully installed on port $port\n";

  # if allowed, write SAFT-port to $HOME/.plan
  unless ($opt_x) {
    if (open F,$planfile) {
      @plan = <F>;
      close F;
      @plan = grep { s/^\s*SAFTport\s*=.*$/SAFTport=$port/i or $_ } @plan;
    }
    push @plan,"SAFTport=$port\n" unless grep /^SAFTport=/, @plan;
  
    open F,">$planfile" or die "$0: cannot write $planfile : $!\n";
    print F @plan;
    close F;
  }
  
  %reply = (
    200 => "200 Command ok.",
    201 => "201 File has been received correctly.",
    202 => "202 Command not implemented, superfluous at this site.",
    203 => "203 *schnuffel* *schnuffel* =:3",
    205 => "205 Non-ASCII character in command line ignored.",
    214 => "214-The following commands are recognized:\r\n".
           "214-   FROM    <sender> [<real name>]\r\n".
           "214-   TO      <recipient>\r\n".
	   "214-   FILE    <name>\r\n".
           "214-   SIZE    <bytes to transfer> <original file size uncompressed>\r\n".
           "214-   TYPE    BINARY|SOURCE|MIME|TEXT=<character set name> [COMPRESSED|CRYPTED]\r\n".
           "214-   DATE    <ISO-8601 date & time string (UTC)>\r\n".
           "214-   SIGN    <pgp signature (armor)>\r\n".
           "214-   ATTR    TAR|EXE|NONE\r\n".
           "214-   COMMENT <file comment>\r\n".
           "214-   DEL\r\n".
           "214-   RESEND\r\n".
           "214-   DATA\r\n".
           "214-   QUIT\r\n".
           "214-All arguments have to be UTF-7 encoded.\r\n".
           "214 You must specify at least FROM, TO, FILE, SIZE and DATA to send a file.",
    215 => "215 $pussy",
    220 => "220 $username\@$hostname user SAFT server $pussy on port $port ready.",

    221 => "221 Goodbye.",
    230 => "230 %d bytes have already been transmitted.",
    231 => "231 %d bytes will follow",
    250 => "250 End of transfer.",
    260 => "260 DEBUG-OUTPUT",

    302 => "302 Header ok, send data.",
    331 => "331 challenge: %s",

    410 => "410 No access to spool directory (permission problems?).",
    411 => "411 Can't create user spool directory.",
    412 => "412 Can't write to user spool directory.",
    413 => "413 File quota exceeded.",
    414 => "414 Can't start spool postprocessing.",
    415 => "415 TCP error: received too few data.",
    421 => "421 Service currently not available.",
    430 => "430 You are not allowed to send to this user.",
    451 => "451 Requested action aborted: server error.",
    452 => "452 Insufficient storage space.",
    453 => "453 Insufficient system resources.",
    460 => "460 Authentication error.",
    490 => "490 Internal error.",

    500 => "500 Syntax error, command unrecognized.",
    501 => "501 Syntax error in parameters or arguments.",
    502 => "502 Command not implemented.",
    503 => "503 Bad sequence of commands.",
    504 => "504 Command not implemented for that parameter.",
    505 => "505 Missing argument.",
    506 => "506 Command line too long.",
    507 => "507 Bad argument.",
    #case 510: text="510 User has set a forward to xxx@yyy";
    511 => "511 This SAFT-server can only receive files.",
    512 => "512 This SAFT-server can only receive messages.",
    520 => "520 User unknown.",
    521 => "521 User is not allowed to receive files or messages.",
    522 => "522 User cannot receive messages.",
    530 => "530 Authorization failed.",
    531 => "531 This file has been already received.",
    532 => "532 This file is currently transfered by you within another process.",
    540 => "540 Secure mode enforced: you have to sign your files",
    541 => "541 Secure mode enforced: you have to encrypt your files",
    550 => "550 File not found.",
  );

  return $sock;
}


# 
# send SAFT reply string
#
# INPUT: reply-code-#
#        printf-parameters
#
sub reply {
  my $rc = shift;
  my $text;
  
  $text = $reply{$rc};
  $text = "599 Unknown error." unless $text;
  
  printf "$text\r\n",@_;

  # terminate on a fatal error
  exit 1 if $rc =~ /^4/;
}


#
# delete a file from spool
#
# INPUT: sender in form: user@host
#        file name
# 
# RETURN: number of deleted files
#
sub delfile {
  my $from = shift;
  my $file = shift;
  my $n;
  my $i;

  return 0 unless &scanspool;

  foreach $i (keys %spoolfiles) {
    if ($spoolfiles{$i}{"from"} eq $from &&
        $spoolfiles{$i}{"file"} eq $file) {
      $n++;
      unlink "$i.h","$i.d";
    }
  }
  
  return $n;
}


#
# check restriction file
#
# RETURN: 1 on no access, 0 on access ok
#
sub restricted {
  my $from = shift;
  local $_;

  if (open F,"$configdir/restrictions") {
    while (<F>) {
      chomp;
      s/#.*//;
      s/\s+/ /g;s/^ //;s/ $//;
      next unless / [bf]$/i;
      s/ [bf]$//i;
      # transform simplematch pattern to perl regexp
      $_ = quotemeta;
      s/\\\\/\\/;
      s/\\\*/.*/;
      s/\\\?/./;
      s/\\\[\\\^/[^/;
      s/\\\[/[/;
      s/\\\]/]/;
      return 1 if $from =~ /^$_$/i;
    }
  }
  close F;
  
  return 0;
}


#
# scan the spool header files
#
sub scanspool {
  my ($from,$file,$type,$size,$shf,$n);
  local $_;
  
  %spoolfiles = ();
  opendir SPOOL, '.' or return 0;
  while (defined($shf = readdir SPOOL)) {
    next if $shf !~ /^(\d+)\.h$/;
    $n = $1;
    next unless -f "$n.d";
    $from = $file = $type = $size = '';
    open F, $shf or next;
    while (<F>) {
      chomp;
      if (/^FROM\t(.*)/)  { $from = $1; next; }
      if (/^FILE\t(.*)/)  { $file = $1; next; }
      if (/^TYPE\t(.*)/)  { $type = $1; next; }
      if (/^SIZE\t(\d+)/) { $size = $1; next; }
    }
    close F;
    if (length $from && length $file && $type && $size) {
      $spoolfiles{$n} = { from => $from,
                          file => $file,
	                  type => $type,
	                  size => $size };
    }
  }
  closedir SPOOL;
  
  return ($n>0);
}


#
# find out how many bytes have been already transmitted
#
# INPUT: sender in form: user@host
#        file name
#        file size
#        file SAFT type
# 
# RETURN: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number
#
sub received N: number of already received bytes, spool number