XRestore, an script for rdiff-backup script…
I’ve wrote this script to restore rdiff-backups with regular users conserving the files ownerships.
The problem is that in modern UNiX systems -like GNU/LiNUX- regular users cannot change the owner of files because this is a security thread, so to allow that rdiff-backup could restore file ownership it must run as superuser -root-.
This script acts as a wrapper between regular users and rdiff-backup itself, while it runs with root privileges so it can -and therefore do- restore file ownership.
It requires xinetd, but may be you can do it run with other tcp wrappers like the older inetd or so.
Install
Note: Paths are those used in my Ubuntu gutsy, you may need to change them if your system does not match.
Put xrestore on your favorite script’s directory -I’ll use /root/scripts-, make it owned by root if it is not yet, and give it execution perms:
chown root:root xrestore
chmod 750 xrestore
To configure xinetd, open with your prefered text editor /etc/services and add a line like this:
xrestore 810/tcp # xrestore script for rdiff-backup
Please, note that I’ve choosen 810 as port, but you can change it by other one if 810 is yet occupied in your host by other service.
Now, create and edit /etc/xinetd.d/xrestore, with the following data:
service xrestore
{
disable = no
socket_type = stream
protocol = tcp
port = 810
user = root
wait = no
server = /root/scripts/xrestore
}
You’ll need to restart xinetd, in Ubuntu gutsy this is done by:
/etc/init.d/xinetd restart
Now you can test the installation:
telnet localhost 810
You should be prompted by your user’s name and password. WARNING: They’re showed on the screen in plain text.
Use
It uses a very simple protocol, in first stage -auth- it asks for username and password, and if they’re good, in the second stage you can specify what to backup just like this example:
from: /home/backup/fran
to: /home/fran
at: now
“from” and “to” commands should be mandatory, and they specify where is the backup and where you want to restore it. “at” command its optional, and its ‘now’ by default. You can use same format than rdiff-backup’s -r option (see ‘man rdiff-backup’ for further information).
Notes
This is currently in a very alpha stage, and needs a lot of testing and improving. It shouldn’t be used for critical things by now, and also it shouldn’t be used through public networks like Internet without using some kind of tuneling, as user’s system passwords are transmited in plain test.
The XRestore script
You can simply cut & paste it to your prefered editor, and save as “xrestore”, or you can download it directly from here: xrestore
#!/usr/bin/perl # (c) 2009 Francisco M. Marzoa Alonso # http://marzoa.com/ # xrestore v0.1 # This software is under GNU/GPLv3.0 license terms # USE AT YOUR OWN RISK use Cwd; use Data::Dumper; use strict; local $|=1; # Avoid buffering my $BACKUP_DIR = "/var/backup"; my $RDIFF_BACKUP = "/usr/bin/rdiff-backup"; my $AUTH_FAIL_MESG = "ARG!\n"; my $AUTH_SUCCESS_MESG = "Welcome!\n"; my $CLOSE_CONNECTION_MESG = "Bye!\n"; my $SYNTAX_ERROR_MESG = "Syntax error!\n"; my $FILE_NOT_FOUND_MESG = "File not found!\n"; my $CANNOT_READ_FROM_MESG = "Cannot read source. :-(\n"; my $CANNOT_WRITE_TO_MESG = "Cannot write target. :-(\n"; my $CANNOT_ACCEPT_DATE_MESG = "Cannot accept that DANGEROUS date. :-P\n"; my $LOST_CONNECTION_ERROR_MESG = "Unexpected end of transmission. Connection lost? O_o\n"; my $EXEC_ACCESS = 1; my $WRITE_ACCESS = 2; my $READ_ACCESS = 4; sub trim { my @out = @_; for (@out) { s/^\s+//; s/\s+$//; } return wantarray ? @out : $out[0]; } sub check_file_access { my ($uid, $file) = @_; my ($rtn, $uname); my @stats = stat $file; my ( $fperms, $fuid, $fgid ) = ( $stats[2] & 07777, $stats[4], $stats[5] ); if ( $fuid == $uid ) { # File owner is the same than backup requester $rtn = $fperms >> 6; return $rtn; } else { # Check if user $uid is on group $fgid my $ulogin = getpwuid ( $uid ); # We need user's login to compare agaisnt group user's list my @gdata = getgrgid ( $fgid ); my @unames = split / /, $gdata[3]; foreach $uname ( @unames ) { if ( $uname eq $ulogin ) { return ( $fperms & 0070 ) >> 3; last; } } # If we reach this, then the file is not owned by user # itself and by groups of the user, so return permission # mask for everyone return $fperms & 0007; } return 0; } sub secure_paths { my ( $from, $to, $uid ) = @_; my $real_from = Cwd::abs_path ( $from ); if ( ! -e $real_from ) { print "$from: ".$FILE_NOT_FOUND_MESG; exit -1; } # Check if we can read that my $fa = check_file_access ($uid, $real_from); if ( ! ($fa & $READ_ACCESS) ) { print $CANNOT_READ_FROM_MESG; exit -1; } my $real_to = Cwd::abs_path ( $to ); if ( ! -e $real_to ) { print "$to: ". $FILE_NOT_FOUND_MESG; exit -1; } # Check if we can write that my $fa = check_file_access ($uid, $real_to); if ( ! ($fa & $WRITE_ACCESS) ) { print $CANNOT_WRITE_TO_MESG; exit -1; } return ( $real_from, $real_to ); } sub secure_time { my $source = shift ( @_ ); my ( $i, $valid_chars, $char); $source = trim ( $source ); if ( $source == 'now' ) { return $source; } else { $valid_chars = '0123456789Tt-+/BDWMYsmh'; for ( $i=0; $i<length ($source); $i++ ) { $char = substr $source, $i, 1; if ( index ($valid_chars, $char ) == -1 ) { # "Alien" chars on date... too bad. print $CANNOT_ACCEPT_DATE_MESG; exit -1; } } if ( $source =~ m/^-.*$/g ) { # A '-' at the beginning may be used to pass additional parameters to RDIFF, # which is very very very unsafe. print $CANNOT_ACCEPT_DATE_MESG; exit -1; } } return $source; } sub rdiff_backup { my ( $sd, $td, $at, $uid ) = @_; my ( $pid, @output ); my ( $source_dir, $target_dir ) = secure_paths ( $sd, $td, $uid ); my $time = secure_time ( $at ); die "cannot fork: $!" unless defined ($pid = open(SAFE_KID, "|-")); if ($pid == 0) { # $RDIFF_BACKUP -r $at $source_dir $target_dir exec($RDIFF_BACKUP, ("--preserve-numerical-ids", "-r $at"), ($source_dir, $target_dir) ) or die "can't exec $RDIFF_BACKUP: $!"; } else { @output = <SAFE_KID>; close SAFE_KID; # $? contains status } } sub process_request_auth { my ( $req_user, $req_passwd ); print "User: "; $req_user = <stdin>; $req_user =~ s/\r\n//g; print "Password: "; $req_passwd = </stdin><stdin>; $req_passwd =~ s/\r\n//g; my @udata = getpwnam ( $req_user ); if ( @udata ) { my $passwd = $udata[1]; if ( crypt ( $req_passwd, $passwd ) eq $passwd ) { print $AUTH_SUCCESS_MESG; my $uid = $udata[2]; return $uid; } else { print $AUTH_FAIL_MESG; return -1; } } else { print $AUTH_FAIL_MESG; return -1; } } sub process_request_commands { my $uid = shift @_; my ($from, $to, $at, $aux, $command) = ('', '', 'now', '', ''); my $getout = 0; while ($command = </stdin><stdin>) { $command = trim $command; $command = lc ( $command ); if ( $command ne '' ) { if ( (($aux) = ( $command =~ m/^ *from: *(.*?) *$/i))) { print "Ok, we'll try to restore data from $aux\n"; $from = $aux; } elsif ( (($aux) = ( $command =~ m/^ *to: *(.*?) *$/i))) { print "Ok, we'll try to restore data to $aux\n"; $to = $aux; } elsif ( (($aux) = ( $command =~ m/^ *at: *(.*?) *$/i))) { print "Ok, we'll try to restore data as it was on $aux\n"; $at = $aux; } elsif (( $command eq 'quit' ) || ( $command eq 'exit' )) { print $CLOSE_CONNECTION_MESG; exit (); } elsif ( $command eq 'run' ) { last; } else { print "$command: $SYNTAX_ERROR_MESG"; } } } if ( $command eq 'run' ) { rdiff_backup ( $from, $to, $at, $uid ); } else { print $LOST_CONNECTION_ERROR_MESG; } } print "Recover v0.1. Waiting for auth.\n"; my ($uid) = process_request_auth (); if ( $uid ne -1 ) { process_request_commands ( $uid ); }
Attached Files:
- xrestore
XRestore perl script.

