#!/usr/bin/perl

use strict;
use warnings;

use Digest::MD5 qw(md5_hex);
use File::Spec;
use Getopt::Long;
use IO::Socket;
use Pod::Usage;

Getopt::Long::Configure("no_auto_abbrev");

my %options;
my $opts_ok
    = GetOptions( \%options, 'help|?|h', 'man', 'port|p=s', 'address|a=s',
    'osx|x=s', 'quiet|q', 'verbose' );

pod2usage(2) if !$opts_ok;
pod2usage(1) if exists $options{help};
pod2usage( -exitstatus => 0, -verbose => 2 ) if exists $options{man};

if ( $options{osx} ) {
    handle_osx( $options{osx} );
    exit;
}

my $config = load_config();

my $secret  = generate_secret();
my $address = $config->{server_address} || $options{address} || '127.0.0.1';
my $port    = $config->{port} || $options{port} || 12345;
my $quiet   = $config->{quiet} || $options{quiet} || 0;
my $verbose = $options{verbose} || 0;

my $growlnotify_location = `/usr/bin/which growlnotify`;
chomp $growlnotify_location;

my $sock = IO::Socket::INET->new(
    Proto     => 'tcp',
    LocalAddr => $address,
    LocalPort => $port,
    ReuseAddr => 1,
    Listen    => 1
) or die "couldn't create socket: $!";

while ( my $client = $sock->accept() ) {

    print $client "HELLO 0.1\n";

    print "received connection\n" if $verbose;

    my $buffer;
    my $copydata;
    $client->recv( $buffer, 512 );

    my ( $client_proto_version, $client_secret ) = split( /:/, $buffer );
    if ( $client_proto_version == 1 ) {
        print sprintf("secret comparison: |%s...| == |%s...|\n", substr($client_secret, 0, 10), substr($secret, 0, 10)) if $verbose;
        if ( $client_secret eq $secret ) {
            print "auth successful\n" if $verbose;
            print $client "SUCCESS\n";
        }
        else {
            print "auth failure, copying secret to clipboard\n" if $verbose;
            print $client "FAILURE AUTH\n";
            copy($secret);
            if ( !$quiet ) {
              notify(
                  "Unauthenticated copy attempt, copied secret to clipboard");
            }
            $client->close();
            next;
        }
    }
    else {
        print "protocol failure\n" if $verbose;
        print $client "FAILURE PROTOVER\n";
        if ( !$quiet ) {
          notify(
              sprintf(
                  "Unknown client version %d, rejecting.\nData: %s",
                  $client_proto_version, $buffer
              )
          );
        }
        $client->close();
        next;
    }

    $buffer = '';

    print "reading data..." if $verbose;
    do {
        $copydata .= $buffer;
        $client->recv( $buffer, 512 );
    } while ($buffer);
    print sprintf("done, read %d bytes\n", length($copydata)) if $verbose;

    #print STDERR "all data: $copydata\n";

    print "copying data to clipboard..." if $verbose;
    copy($copydata);
    if ( !$quiet ) {
      notify("Remote copy.");
    }
    print "done\n" if $verbose;

    print "connection complete\n" if $verbose;
    $client->close();
}

sub copy {
    my $copydata = shift;

    my $pbcopy;
    if ($^O eq "linux") {
        open( $pbcopy, "|xsel --clipboard --input")
            or die "unable to open $!";
    } else {
        open( $pbcopy, "|pbcopy" ) or die "unable to open $!";
    }
    print $pbcopy $copydata;
    close($pbcopy);
}

sub notify {
    my $message = shift;

    if ($growlnotify_location) {
        system( $growlnotify_location, "-m", $message );
    }
    else {
        print "$message\n";
    }
}

sub generate_secret {

    my $salt;

    open( my $urandom, '<', '/dev/urandom' );
    $salt .= getc $urandom for ( 0 .. 20 );
    close($urandom);

    return 'rc-' . md5_hex( $salt . time );
}

# same method in server and client
sub load_config {
    my $config_filepath = "$ENV{HOME}/.remotecopyrc";

    my $config = {};
    if ( -e $config_filepath ) {
        open( my $config_file, '<', $config_filepath );
        while ( my $line = <$config_file> ) {
            chomp($line);
            my ( $key, $option ) = $line =~ m/([\w]+)\s*=\s*(.*)/;
            $config->{$key} = $option;
        }
        close($config_file);
    }

    return $config;
}

sub handle_osx {
    my $command = shift;

    my $launchagent_file
        = "$ENV{HOME}/Library/LaunchAgents/org.endot.remotecopy.plist";

    if ( $command eq 'configure' ) {
        configure_launchagent($launchagent_file);
    }
    elsif ( $command eq 'unconfigure' ) {
        unconfigure_launchagent($launchagent_file);
    }
    elsif ( $command eq 'start' ) {
        configure_launchagent($launchagent_file);
        start_launchagent($launchagent_file);
    }
    elsif ( $command eq 'stop' ) {
        configure_launchagent($launchagent_file);
        stop_launchagent($launchagent_file);
        unconfigure_launchagent($launchagent_file);
    }
    elsif ( $command eq 'restart' ) {
        configure_launchagent($launchagent_file);
        stop_launchagent($launchagent_file);
        start_launchagent($launchagent_file);
    }
    else {
        warn "Unknown command $command.\n";
    }
}

sub start_launchagent {
    my $launchagent_file = shift;
    system("/bin/launchctl list org.endot.remotecopy &> /dev/null");
    if ( ( $? >> 8 ) != 0 ) {
        system("/bin/launchctl load $launchagent_file");
    }
    else {
        warn "remotecopyserver already running\n";
    }
}

sub stop_launchagent {
    my $launchagent_file = shift;
    system("/bin/launchctl list org.endot.remotecopy &> /dev/null");
    if ( ( $? >> 8 ) == 0 ) {
        system("/bin/launchctl unload $launchagent_file");
    }
    else {
        warn "remotecopyserver already stopped\n";
    }
}

sub configure_launchagent {
    my $launchagent_file = shift;

    my $absolute_self_path = File::Spec->rel2abs($0);

    my $contents = <<LAUNCHAGENT;
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>org.endot.remotecopy</string>
        <key>OnDemand</key>
        <false/>
        <key>EnvironmentVariables</key>
        <dict>
            <key>PATH</key>
            <string>/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin</string>
        </dict>
        <key>ProgramArguments</key>
        <array>
            <string>$absolute_self_path</string>
        </array>
    </dict>
</plist>
LAUNCHAGENT

    open( my $laf, '>', $launchagent_file );
    print $laf $contents;
    close($laf);
}

sub unconfigure_launchagent {
    my $launchagent_file = shift;

    unlink($launchagent_file);
}

__END__

=head1 NAME

remotecopyserver - local daemon listening for remote copy requests

=head1 SYNOPSIS

 remotecopyserver [options]

 Options:
  -p --port <port>          Port to listen on (defaults to 12345).
  -a --address <ip addr>    IP address to listen on (defaults to 127.0.0.1).
  -x --osx <command>        OSX command.  See OSX below.
  -q --quiet                Quieter output.

 Documentation options:
  -h --help -?              brief help message
     --man                  full documentation

=head1 REQUIRED ARGUMENTS

No arguments are required.

=head1 DESCRIPTION

This script implements a simple daemon which listens for remote copy requests.
It uses a minimal protocol handshake to make sure the remote end is allowed to
send data.

=head1 OSX

This script can register itself as an OSX LaunchAgent to ensure that it's
running all the time.  If the -x argument is used, one of the following four
actions will be performed:

=over 4

=item start - Starts remotecopyserver via launchctl.

=item stop - Stops remotecopyserver via launchctl.

=item restart - Restarts remotecopyserver via launchctl.

=item configure - This will configure the LaunchAgent, but not start it.

=item unconfigure - This will remove the LaunchAgent configuration.

=back

LaunchAgents configuration files are kept in ~/Library/LaunchAgents.  For more
information about LaunchAgents, consult the man page for launchctl and launchd.

=head1 PROTOCOL

To be documented.

=head1 RC

If a ~/.remotecopyrc file is present, it will be read for options.  The file
format is just keys and values, separated by an equals sign. Example:

    server_address = 127.0.0.1
    port = 54321

The remotecopyserver will use two keys:

=over 4

=item server_address - IP address to listen on.

=item port - Port to listen on.

=back

=head1 AUTHOR

Nate Jones E<lt>nate@endot.orgE<gt>

=head1 COPYRIGHT

Copyright (c) 2011 by Nate Jones E<lt>nate@endot.orgE<gt>.

This program is free software; you can use, modify, and redistribute it under
the Artistic License, version 2.0.

See http://www.opensource.org/licenses/artistic-license-2.0.php

=cut