#!/usr/bin/perl
# cpanel10 - receiver                             Copyright(c) 2006 cPanel, Inc.
#                                                           All Rights Reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

use lib '/usr/local/cpanel';
use strict;
use Digest::MD5;

$| = 1;

my @MD5S;
my @RDLIST;
my @SYNCLIST;
my $ver     = '1.0.1';
my $basedir = '/home/cpanel/cpanelsync';
my $mode    = 1;
my $syncfile;
my $rver;
my $tree;
my $path;
my $distversion;
my %CPS;
my %PATHS;
my $rd;
my $bytesr = 0;
print "WELCOME CPSR OK\n";
print "VERSION $ver\n";

open( URAND, '<', '/dev/urandom' );

my ( $ftype, $rfile, $perm, $extra );

my $error = '';
while (<STDIN>) {
    if ( $mode == 1 ) {
        chomp;
        my ( $cmd, $action ) = split( /\s+/, $_, 2 );
        if ( $cmd eq 'QUIT' ) {
            print "QUIT GOODBYE\n";
            last;
        }
        elsif ( $cmd eq 'VERSION' ) {
            if (@SYNCLIST) { $error = "Protocal Failure: Expected SENDFILE"; last(); }
            $rver = $action;
            if ( $rver ne $ver ) { $error = "Protocol Mismatch: Server $ver | Client $ver"; last(); }
            print "VERSION ACCEPTED\n";
        }
        elsif ( $cmd eq 'DISTVERSION' ) {
            if (@SYNCLIST) { $error = "Protocal Failure: Expected SENDFILE"; last(); }
            $distversion = $action;
            chomp($distversion);
            print "DISTVERSION ACCEPTED\n";
        }
        elsif ( $cmd eq 'TREE' ) {
            if (@SYNCLIST) { $error = "Protocal Failure: Expected SENDFILE"; last(); }
            if ( $tree ne '' ) { $error = "Protocol Failure: TREE sent again"; last(); }
            $tree = $action;
            if ( $rver eq '' ) { $error = "Protocol Failure: VERSION not sent before TREE"; last(); }
            if ( !-e $basedir . '/' . $tree ) {
                mkdir( $basedir . '/' . $tree, 0755 );
            }
            print "TREE ACCEPTED\n";
        }
        elsif ( $cmd eq 'PATH' ) {
            if (@SYNCLIST) { $error = "Protocal Failure: Expected SENDFILE"; last(); }
            my $dir;
            if ( $action eq 'root' ) {
                $path         = '.';
                $PATHS{$path} = 1;
                $dir          = $basedir . '/' . $tree;
            }
            else {
                $path         = $action;
                $PATHS{$path} = 1;
                $dir          = $basedir . '/' . $tree . '/' . $path;
            }
            if ( $tree eq '' ) { $error = "Protocol Failure: TREE not sent before PATH"; last; }
            if ( !-d $dir ) {
                unlink $dir;
                safemkdir( $dir, 0755 );
            }
            if ( !-e $dir . '/.cpanelsync.lock' ) {
                if ( open my $lock_fh, '>', $dir . '/.cpanelsync.lock' ) {
                    print {$lock_fh} 'locked';
                    close $lock_fh;
                }
            }
            print "PATH ACCEPTED\n";
        }
        elsif ( $cmd eq 'SENDFILE' ) {
            $syncfile = $SYNCLIST[0];
            $rd       = $RDLIST[0];
            $extra    = $MD5S[0];
            $bytesr   = 0;
            open( SF, ">", "$syncfile.recieve" );
            if ( $action =~ /^(\d+)$/ ) {
                my $buf;
                $bytesr += read( STDIN, $buf, $1 );
                print SF $buf;
            }
            $mode = 2;
        }
        elsif ( $cmd eq 'SYNC' ) {
            if ( $tree eq '' ) { $error = "Protocol Failure: TREE not sent before SYNC"; last(); }
            push( @{ $CPS{$path} }, $action );
            ( $ftype, $rfile, $perm, $extra ) = split( /===/, $action );
            if ( $ftype eq 'f' ) {
                $syncfile = $basedir . '/' . $tree . '/' . $path . '/' . $rfile;
                if ( !-d $basedir . '/' . $tree . '/' . $path ) {
                    unlink( $basedir . '/' . $tree . '/' . $path );
                    safemkdir( $basedir . '/' . $tree . '/' . $path, 0755 );
                }
                my $md5sum = getmd5sum($syncfile);
                if ( $md5sum eq $extra ) {
                    print "SYNC SKIP\n";
                }
                else {
                    $rd = getranddata(128);
                    print "SYNC SEND " . $rd . "\n";
                    push @SYNCLIST, $syncfile;
                    push @RDLIST,   $rd;
                    push @MD5S,     $extra;
                }
            }
            elsif ( $ftype eq 'd' ) {
                if ( -l $basedir . '/' . $tree . '/' . $path . '/' . $rfile || !-d $basedir . '/' . $tree . '/' . $path . '/' . $rfile ) {
                    unlink( $basedir . '/' . $tree . '/' . $path . '/' . $rfile );
                    safemkdir( $basedir . '/' . $tree . '/' . $path . '/' . $rfile, 0755 );
                }
                print "SYNC SKIP\n";
            }
            elsif ( $ftype eq 'l' ) {
                unlink( $basedir . '/' . $tree . '/' . $path . '/' . $rfile );
                symlink( $extra, $basedir . '/' . $tree . '/' . $path . '/' . $rfile );
                print "SYNC SKIP\n";
            }
            else {
                print "SYNC SKIP\n";
            }
        }
    }
    elsif ( $mode == 2 ) {
        if ( $_ eq $rd . "\n" ) {
            $mode = 1;
            truncate( SF, ( $bytesr - 1 ) );    #get rid of the \n at the end
            close(SF);
            my $md5sum = getmd5sum( $syncfile . '.recieve' );
            if ( $extra eq $md5sum ) {
                print "SYNC OK\n";
                unlink($syncfile);
                if ( -d $syncfile ) { system( "rm", "-rf", $syncfile ); }
                link( $syncfile . '.recieve', $syncfile );
                unlink( $syncfile . '.recieve' );
                system( "bzip2", '-fk', $syncfile );
                shift(@RDLIST);
                shift(@SYNCLIST);
                shift(@MD5S);
            }
            else {
                print "SYNC FAILED Expected: $extra, Got: $md5sum -- File = $syncfile\n";
            }
        }
        else {
            $bytesr += length($_);
            print SF $_;
        }
    }
}
if ($error) { print "ERROR ${error}\n"; }
else {
    chdir $basedir or warn "Unable to chdir into $basedir: $!";
    foreach my $path ( keys %PATHS ) {
        next if !$CPS{$path};
        my %old_cpanelsync;
        if ( -e $basedir . '/' . $tree . '/' . $path . '/.cpanelsync' ) {
            if ( open my $cpsync_fh, '<', $basedir . '/' . $tree . '/' . $path . '/.cpanelsync' ) {
                while ( my $item = readline $cpsync_fh ) {
                    my ( $type, $item, undef ) = split( /===/, $item, 3 );
                    $old_cpanelsync{$item} = $type;
                }
                close $cpsync_fh;
            }
        }

        if ( open my $cpsync_fh, '>', $basedir . '/' . $tree . '/' . $path . '/.cpanelsync' ) {
            print {$cpsync_fh} join( "\n", @{ $CPS{$path} } ) . "\n.\n";
            close $cpsync_fh;
            system "bzip2", "-fk", $basedir . '/' . $tree . '/' . $path . '/.cpanelsync';
            print "CPANELSYNC CREATED $tree/$path\n";

            if ( scalar keys %old_cpanelsync && $path ne '.' ) {
                print "CPANELSYNC CLEANING $tree/$path\n";
                foreach my $item ( @{ $CPS{$path} } ) {
                    my ( $type, $item, undef ) = split( /===/, $item, 3 );
                    $old_cpanelsync{$item} = '_current_';
                }
                foreach my $item ( keys %old_cpanelsync ) {
                    next if $old_cpanelsync{$item} eq '_current_';    # Skip current files
                    if ( -e $basedir . '/' . $tree . '/' . $path . '/' . $item && !-d _ ) {
                        print "CPANELSYNC removing old $tree/$path $item\n";
                        unlink $basedir . '/' . $tree . '/' . $path . '/' . $item or do {
                            print "CPANELSYNC unable to remove $tree/$path $item: $!\n";
                        };
                    }
                }
            }
        }
        else {
            print "CPANELSYNC ERROR: $!\n";
        }

        if ( $distversion && open my $cpsync_fh, '>', $basedir . '/' . $tree . '/' . $path . '/.cpanelsync.version' ) {
            print {$cpsync_fh} $distversion;
            close($cpsync_fh);
        }
    }
}
print "DISCONNECT\n";
exit();

sub getmd5sum {
    my ($file) = @_;
    my ($md5);

    return "" if ( !-f $file );

    if (1) {
        my $ctx = Digest::MD5->new;
        open( MDFILE, $file );
        $ctx->addfile(*MDFILE);
        close(MDFILE);
        $md5 = $ctx->hexdigest();
    }
    else {
        if ( $ENV{'PATH'} !~ /\/sbin/ ) {
            $ENV{'PATH'} .= ":/sbin";
        }

        if ( -e "/bin/md5sum" || -e "/usr/bin/md5sum" || -e "/usr/local/bin/md5sum" ) {
            open( MD5, "-|" ) || exec 'md5sum', $file;
        }
        else {
            open( MD5, "-|" ) || exec 'md5', "-r", $file;
        }
        while (<MD5>) {
            chomp();
            ( $md5, undef ) = split( /\s+/, $_ );
            last();
        }
        close(MD5);

    }
    return ($md5);
}

sub safemkdir {
    my $dir  = shift;
    my $mode = shift;
    my @dirarray;
    my $returnvalue = 1;

    # Set mode if not defined or improperly defined.
    if ( !defined($mode) || $mode eq "" || $mode !~ /^[0-7]{3,4}$/ ) {
        $mode = '0755';
    }

    # Just in case mode was defined as 3 digits
    if ( $mode =~ /^[0-7]{3}$/ ) {
        $mode = '0' . $mode;
    }

    # Assume that there will be no more than 100 nested directories
    my $loopcount = 0;
    while ( length($dir) > 1 ) {

        unshift( @dirarray, $dir );
        my $default = $dir =~ m/^\// ? '/' : '.';
        my @parts = split( /\//, $dir );
        pop @parts;
        $dir = join( '/', @parts );
        $dir = $default if !$dir;

        $loopcount++;
        if ( $loopcount == 100 ) {
            warn 'Encountered excessive directory length. This should never happen.';
            return 0;
        }
    }

    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # This should really iterate through any existing directory structure
    # and check to see if any portion is not a directory or if permissions don't
    # allow directory creation before making any directories.
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    foreach my $newdir (@dirarray) {
        next if ( !$returnvalue );    # Break out if mkdir fails
                                      # If final dir exists, then set the specified mode
        my $is_dir = -d $newdir;
        my $exists = -e _;

        if (   $exists
            && $newdir eq $dirarray[$#dirarray]
            && !-l $newdir
            && $is_dir ) {
            chmod( oct($mode), $newdir ) || warn "chmod $newdir failed: $!";
        }
        elsif ( $exists && !$is_dir ) {
            warn "$newdir was expected to be a directory!";
            return 0;
        }
        elsif ($is_dir) {
            next();
        }
        else {
            $returnvalue = mkdir( $newdir, oct($mode) )
              || warn("mkdir $newdir failed: $!");
        }
    }
    return $returnvalue;
}

sub getranddata {
    my ( $size, $sendheader ) = @_;
    if ( !defined $size || $size eq '' ) { $size = 10; }
    my $readsize = ( $size * 16 );

    my $rndpass;
    use bytes;
    open( URAND, '/dev/urandom' ) || do {
        if ($sendheader) { print "Content-type: text/html\r\n\r\n"; }
        print "Fatal Error: Unable to read data from /dev/urandom ($!).\n";
        print "Please contact your system admin to have them repair the problem.\n";
        exit 1;
    };
    while ( length($rndpass) < $size ) {
        read URAND, $rndpass, $readsize;
        $rndpass =~ s/\W//g;
    }
    close(URAND);
    $rndpass = substr( $rndpass, 0, $size );
    no bytes;
    return $rndpass;
}
