#!/usr/bin/perl
# Name: /usr/local/bin/rrsync (should have a symlink in /usr/bin)
# Purpose: Restricts rsync to subdirectory declared in .ssh/authorized_keys
#
# The client uses "rsync -av -e ssh src/ server:dir/", and sshd on the server
# executes this program when .ssh/authorized_keys has 'command="..."'.
#	For example:
# command="rrsync logs/client" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzGhEeNlPr...
# command="rrsync -ro results" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAmkHG1WCjC...
#   The former limits the remote client to a specific subdirectory of "logs"
#   and the latter allows read-only access to a different dirctory.

use Socket;
use constant LOGFILE => '/tmp/rrsync.log';
my $Usage = <<EOM;
Use 'command="$0 [-ro] subdir"'
	in front of lines in $ENV{HOME}/.ssh/authorized_keys
EOM

# Format of the envrionment variables set by sshd:
# SSH_ORIGINAL_COMMAND=rsync --server          -vlogDtpr --partial . dir # push
# SSH_ORIGINAL_COMMAND=rsync --server --sender -vlogDtpr --partial . dir # pull
# SSH_CLIENT=client_addr client_port server_port

my $ro = (@ARGV and $ARGV[0] eq '-ro') ? shift : '';	# -ro = Read-Only
my $top1 = shift;
die "No subdirectory specified\n$Usage" unless defined $top1;
my $top2 = "$top1/";

my $command = $ENV{SSH_ORIGINAL_COMMAND};
die "Not invoked via sshd\n$Usage"	unless defined $command;

my ($cmd,$requested_target) = $command =~ /(.* \.) ?(.*)/;
die "SSH_ORIGINAL_COMMAND='$command' is not rsync\n" unless $cmd =~ /^rsync\s/;
die "$0 -ro: sending to read-only directory $requested_target not allowed\n"
	if $ro and $cmd !~ /^rsync --server --sender /;

my $dir = $requested_target;
$dir =~ tr|-_/a-zA-Z0-9.,:@|_|c;	# Don't allow ;|][}{*?
$dir =~ s%/\.\.(?=/)%__%g;		# Don't allow foo/../../etc

# For "foo", allow "foo", "foo/" and "foo/anything".
# For "foo/bar" and "foo/anything/bar", interpret "bar" to be the specified
# path and "bar/" to be the specified path with trailing slash supplied.

my($target,$forced);
if      ("/$dir" eq substr($top1,-length("/$dir"))) {
  $target = $top1;			# Matched end of path
} elsif ("/$dir" eq substr($top2,-length("/$dir"))) {
  $target = $top2;			# End of path plus trailing slash
} elsif ($dir eq $top1 or index($dir,$top2) == 0) {
  $target = $dir;			# Exact match or subdirectory
} elsif (substr($dir,0,1) eq '/') {
  $target = "$top1$dir";		# Nonmatching absolute path
  $forced = 1;
} else {
  $target = "$top2$dir";		# Nonmatching relative path
  $forced = 1;
}

if (-f LOGFILE and open LOG,'>>',LOGFILE) {
  my $hhmm = sprintf "%02d:%02d",(localtime)[2,1];
  my $host = $ENV{SSH_CLIENT} || 'unknown';
  $host =~ s/ .*//;			# Keep only the client's IP addr
  $host = gethostbyaddr(inet_aton($host),AF_INET) || $host;
  $_ = sprintf "%-13s",$host;
  print LOG "$hhmm $_ [$command] =",($forced ? "> $target" : ' OK'),"\n";
  close LOG;
}

exec "$cmd $target" or die "exec($cmd $target) failed: $? $!";
# Note: This assumes that the rsync protocol will not be maliciously hijacked.
