#!/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 = < Label org.endot.remotecopy OnDemand EnvironmentVariables PATH /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin ProgramArguments $absolute_self_path 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 to listen on (defaults to 12345). -a --address IP address to listen on (defaults to 127.0.0.1). -x --osx 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 Enate@endot.orgE =head1 COPYRIGHT Copyright (c) 2011 by Nate Jones Enate@endot.orgE. 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