#!/usr/bin/perl
#
# Quick and dirty DCCSERVER manager for Linux
#
# $Id: dccserver,v 1.5 2005/01/17 20:38:22 nemesis Exp $
#
# Copyright (C) 2003-2005 Kees Cook
# kees@outflux.net, http://outflux.net/
# Major updates 2005, Paul Krizak <djskaven@gmail.com>
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# http://www.gnu.org/copyleft/gpl.html
#
use IO::Socket;
use IO::Handle;
use IO::Pipe;
use IO::Select;
use POSIX;
use Getopt::Long;
use IPC::Shareable;

my $VERSION="0.3.0";
my $NICK;
my $PATH;
my $PORT;
my $USER;
my $HELP;
my $TERMINAL;
my @PIDS;
my $num_chats=0;

# Share num_chats with IPC::Shareable.
$handle = tie $num_chats, 'IPC::Shareable', undef, { destroy => 1 };

# setup signal handler
foreach(qw(INT TERM KILL)) {
	$SIG{$_} = \&KillHandler;
}
$SIG{CHLD} = \&ChildHandler;

# parse command-line options
GetOptions("nick=s" => \$NICK,
           "path=s" => \$PATH,
           "port=i" => \$PORT,
           "user=s" => \$USER,
           "term"   => \$TERMINAL,
           "help"   => \$HELP);

help() unless (defined($NICK) and defined($PATH) and -d $PATH and defined($PORT) and defined($USER));
help() if defined($HELP);

# Set up our port; this must be done as root.
$master = IO::Socket::INET->new("Proto" => "tcp", "Reuse" => 1,
                                "Listen" => 20, "LocalPort" => $PORT);

die("\nThis program must be run as root!\nProvide --user switch to run as a regular user.\n\n") unless (defined($master));

# tell person what port they're going to be running on
print "Port = $PORT\n";

# Look up userid of $USER and become that user.
my $USERID=getpwnam($USER);
die("\n--user must be a valid non-root username!\n\n") if $USERID <= 0;
$<=$USERID;
$>=$USERID;
print("User = $USER ($USERID)\n");

# Now test for write permissions to $PATH as new user
die("\n--path must be writeable by $USER!\n\n") unless -w $PATH;

for (;;) {
        $client = $master->accept();
        if (defined($client)) {
                if (($child = fork()) == 0) {
                        # Child
                        undef $master;
                        DCCServerHandle($client);
                        exit(0);
                }
                # Master
                undef $client;

		# Remember child PIDs so we can clean up if killed
		push @PIDS, $child;
        }
        else {
                sleep 1;
        }
}


sub DCCServerHandle {
        my($sock)=@_;
        my $line=<$sock>;
        chomp($line);

        my ($cmd,$arg)=split(/\s+/,$line,2);

        select($sock); $|=1; select(STDOUT); $|=1;
        
        my $host=$client->peerhost();
        my $port=$client->peerport();
        print "Connect from $host:$port ($line)\n";

        if ($cmd == 100) {
		# chat with them
		print "$host:$port requesting chat.\n";

		if(defined $TERMINAL and $num_chats >= 1) {
			# we're in terminal mode and a chat is already running.
			print $sock "150 $NICK\n";
			print "$host:$port Bonged (only one chat at a time allowed in terminal mode)\n";
			exit(1);
		}

                print $sock "101 $NICK\n";

		# increment num_chats so new processes don't start more.
		$handle->shlock();
		$num_chats++;
		$handle->shunlock();

                # I want to SEE everything coming from the server
                # I want send to the server everything I TYPE

		# Do not close this socket across the fork
                fcntl($sock,F_SETFD,0);

		# Create stdin/stdout pair from the socket
                POSIX::dup2($sock->fileno,3) || die "dup2";
                POSIX::fcntl(3,F_SETFD,0);
                POSIX::dup2($sock->fileno,4) || die "dup2";
                POSIX::fcntl(4,F_SETFD,0);

		if(defined $TERMINAL) {
			print "Spawning dcc-chat.\n";
			system "dcc-chat", "3", "4";
		}
		else {
			# un-taint $arg
			if($arg=~/(.*)/) { $arg=$1; }
			print "Spawning xterm for dcc-chat.\n";
                	system "xterm", "-T", "dcc-chat with $arg", "-e", "dcc-chat", "3", "4";
		}

		$handle->shlock();
		$num_chats--;
		$handle->shunlock();

		exit(0);
        }
        elsif ($cmd == 110) {
		# access to OUR fserve
		print "$host:$port Requesting access to fserve\n";

		# We don't support this yet
                print $sock "150 $NICK\n";
                print "$host:$port Bonged\n";
        }
        elsif ($cmd == 120) {
		# send us a file

                my($clientnick,$bytes,$filename)=split(/\s+/,$arg,3);
		print "$host:$port ($clientnick) Trying to send a file\n";
                # 1: destroy any part of the fname that is a path
                $filename =~ s/.*\///;
                my $attempt="$PATH/$filename";

		# Un-taint $attempt (this needs to be updated to be a REAL un-tainter
		if($attempt=~/(.*)/) {
			$attempt=$1;
		}
		print "Local filename: [$attempt]\n";

                # 2: if file exists with same name get size
                my $size=0;
                if (-f $attempt) {
                        $size = -s $attempt;
                }
                
                # 3: if size > sending size, bong
                if ($size > $bytes) {
                        print $sock "151 $NICK\n";
                        print "$filename Bonged (local file too big!)\n";
                }
                else {
                        # 4: open file
                        if (!open(DATA,">>$attempt")) {
                                print $sock "151 $NICK\n";
                                print "$filename Bonged (could not open file '$attempt'): $!\n";
                        }
                        else {
                                # 5: report resume location
                                print $sock "121 $NICK $size\n";

                                # 6: suck down file (reporting status)
                                my $got;
                                my $buffer;
                                my $prevpercent=-1;
                                my $starttime=time;
                                my $startsize=$size;
				# Try to grab 16K chunks at a time.  Make this bigger?
                                while ( $size!=$bytes && ($got = sysread($sock,$buffer,16384))>0) {
                                        if (syswrite(DATA,$buffer,$got)<$got) {
                                                print "$filename: write error: $!\n";
                                                last;
                                        }
                                        $size+=$got;

                                        # calc percent
                                        $percent=0;
                                        if ($bytes>0) {
                                                $percent=int($size*1000/$bytes);
                                        }
                                        if ($percent!=$prevpercent) {
                                                my $mins, $secs, $bps;
                                                my $time, $percent;
                                
                                                $prevpercent=$percent;
                                                
                                                $time=time;
                                                $bps=0;
                                                if ($time!=$starttime) {
                                                        $bps=($size-$startsize)/($time-$starttime);
                                                }
                                                $secs=0;
                                                if ($bps!=0) {
                                                        $secs=int(($bytes-$size)/$bps);
                                                }
                                        
                                                $time+=$secs;
                                                $mins=int($secs/60);
                                                $secs=$secs % 60;
                                                $time=scalar(localtime($time));
                                                
                                                printf("[$clientnick] $filename: %dKB/s $size/$bytes %.1f%% ETA: %02d:%02d ($time)\n",
                                                        int($bps/1024),
                                                        ($percent/10),$mins,$secs);
                                        } 
                                }
                                $bps=($size-$startsize)/($time-$starttime);
                                if ($size==$bytes) {
                                        printf("$filename: DONE! (%dKB/s)\n",int($bps/1024));
                                }
                                else {
                                        printf("$filename: ABORTED! (%dKB/s)\n",
                                                int($bps/1024));
                                }
                                close(DATA);
                        }
                }
        }
        elsif ($cmd == 130) {
		# send them a file
		print "$host:$port Requesting a file.\n";
	
		# We don't support this yet
                print $sock "150 $NICK\n";
                print "$host:$port Bonged\n";
        }
        else {
		# who knows!
		print "$host:$port Invalid command.\n";

                print $sock "150 $NICK\n";
                print "$host:$port Bonged\n";
        }
        undef $sock;
}

sub help {
	print "USAGE: $0 --nick <nick> --path <path> --port <port>\n";
	print <<'EOM';
       --user <user> [--help --term]

--nick <nick>: IRC nickname to use during communication
--path <path>: Path to save files to (must be writable by --user)
--port <port>: Port to serve your DCC server on
--user <user>: non-root username to run as

--term       : [optional] Run in "terminal" mode; only one
               DCC chat session will be allowed to run.  By
               default, an xterm will be forked for each dcc
               chat session requested.  Be sure your DISPLAY
               environment variable is set correctly!

--help       : this help screen!

Common problems:

1. DCC chats are accepted, but an xterm never pops up!
   * Make sure 'xterm' and 'dcc-chat' are in your PATH.
     You make need to run dccserver like this:
	PATH=.:$PATH ./dccserver ...
     This can be dangerous, though, so be careful.

2. I get "Can't open display" errors!
   * Check your DISPLAY variable.  In a pinch try "xhost +",
     but that opens you up to anyone connecting to your X11 server.

3. When dcc chat sessions end, the windows don't go away!
   * Hit the enter key a few times; it will go away.

EOM
	exit(1);
}

sub KillHandler {
	my $signal=$_[0];
	
	print("\nCaught a SIG$signal!  Cleaning up and exiting.\n");

	# Kill children (this probably happens by default, but we want to be sure)
	kill TERM => @PIDS;

	# Become root again (so when we exit we can remove shared memory segments)
	$> = 0;
	$< = 0;

	exit(0);
}

sub ChildHandler {
	my $dead;
	while(($dead = waitpid(-1, &WNOHANG)) > 0) {
		print("Process $dead has exited.\n");
		my @newpids;
		foreach(@PIDS) {
			unless($_ == $dead) {
				push(@newpids, $_);
			}
		}
		@PIDS=@newpids;
	}
	$SIG{CHLD}=\&ChildHandler;
}
