diff options
author | Konstantin Ryabitsev <mricon@kernel.org> | 2011-11-24 17:26:17 -0500 |
---|---|---|
committer | Konstantin Ryabitsev <mricon@kernel.org> | 2011-11-24 17:26:17 -0500 |
commit | 71b6c016c7456fec2776a441d29fef1dec1eed77 (patch) | |
tree | 9675210e179c2e5107c2d18214ad225c50b59f87 | |
parent | 75371dfd6fa49b483c0bcc78af44dd611f058115 (diff) | |
download | kup-71b6c016c7456fec2776a441d29fef1dec1eed77.tar.gz |
Allow / in KUP_RSH, add manpages and adjust tabs.
-rw-r--r-- | ChangeLog | 7 | ||||
-rw-r--r-- | README | 62 | ||||
-rwxr-xr-x | kup | 876 | ||||
-rwxr-xr-x | kup-server | 1512 | ||||
-rw-r--r-- | kup-server.1 | 104 |
5 files changed, 1315 insertions, 1246 deletions
diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..18a780b --- /dev/null +++ b/ChangeLog @@ -0,0 +1,7 @@ +2011-11-24 Konstantin Ryabitsev <mricon@kernel.org> + + * Allow slashes "/" in KUP_RSH and .kuprc/rsh setting, so it is possible + to pass -i to the ssh command. + * Add kup-server.1 manpage. + * Add ChangeLog. + * Rewrite README to be more abouty. @@ -1,55 +1,11 @@ -The program "kup-server" is expected to be the receiver of an ssh -shell, configured with the following options in authorized_keys or -similar: +ABOUT +----- +Kup is a file upload utility for kernel.org. It is designed to only accept +cryptographically verified uploads from pre-authorized, trusted members. -command="/path/to/kup-server",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding <pubkey> - -Each user should have their own UID, as Unix user permissions are used -for specific tree access control. - - -The following pathnames in kup-server need to be customized -appropriately. - -All of these paths should be disjoint! - - -my $data_path = '/var/lib/kup/pub'; - -This is the path under which files are uploaded. - - -my $git_path = '/var/lib/git'; - -This is the path where git trees (for the TAR and DIFF options) are -available. Those should be readonly for the uploaders. - - -my $lock_file = '/var/run/kup/lock'; - -A common lock file for $data_path. No program should modify the -content in $data_path without holding an flock on this file. Should -be readonly for the uploaders. - - -my $tmp_path = '/var/lib/kup/tmp/'; - -This can be either: - -a) a directory writable by every user and with the sticky bit set - (typically mode 1777 or 1770). In that case, DO NOT end the path - with a slash, or: -b) A directory containing an empty directory for each user (named for - that user), owned by that user and mode 700. In this case, DO end - the path with a slash. - -In either case, this directory tree MUST same filesystem as -$data_path, since the script expects to create files in this directory -and rename() them into $data_path. - - -my $pgp_path = '/var/lib/kup/pgp'; - -A directory containing a GnuPG public keyring for each user, named -<user>.gpg and readable (but not writable) by that user. +See man kup and man kup-server for more information. +AUTHORS +------- +Kup was written by H. Peter Anvin <hpa@zytor.com> +Kup is currently maintained by Konstantin Ryabitsev <mricon@kernel.org> @@ -26,45 +26,45 @@ my $blksiz = 1024*1024; # Global options my %opt = ( - 'rsh' => 'ssh -a -x -k -T', - 'host' => 'kup.kernel.org', - 'batch' => 0, - 'verbose' => 0, - ); + 'rsh' => 'ssh -a -x -k -T', + 'host' => 'kup.kernel.org', + 'batch' => 0, + 'verbose' => 0, + ); # Read the config file settings and override the above my $cfg_file = $ENV{'HOME'}.'/.kuprc'; my $cfg = new Config::Simple($cfg_file); if (defined($cfg)) { - # Update %opt with cfgfile settings (only rsh and host vars) - my %cfg_opt = $cfg->vars(); + # Update %opt with cfgfile settings (only rsh and host vars) + my %cfg_opt = $cfg->vars(); - if (defined($cfg_opt{'default.host'})) { - $opt{'host'} = $cfg_opt{'default.host'}; - } + if (defined($cfg_opt{'default.host'})) { + $opt{'host'} = $cfg_opt{'default.host'}; + } - if (defined($cfg_opt{'default.rsh'})) { - $opt{'rsh'} = $cfg_opt{'default.rsh'}; - } + if (defined($cfg_opt{'default.rsh'})) { + $opt{'rsh'} = $cfg_opt{'default.rsh'}; + } } # This is a client, and so running with tainting on is a bit overly # paranoid. As a result we have to explicitly untaint certain bits from # the environment. sub untaint($) { - my($s) = @_; + my($s) = @_; - $s =~ /^(.*)$/; - return $1; + $s =~ /^(.*)$/; + return $1; } $ENV{'PATH'} = untaint($ENV{'PATH'}); if (defined $ENV{'KUP_RSH'}) { - $opt{'rsh'} = $ENV{'KUP_RSH'}; + $opt{'rsh'} = $ENV{'KUP_RSH'}; } if (defined $ENV{'KUP_HOST'}) { - $opt{'host'} = $ENV{'KUP_HOST'}; + $opt{'host'} = $ENV{'KUP_HOST'}; } delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # Make %ENV safer @@ -75,41 +75,41 @@ my $real; # Usage description sub usage($) { - my($err) = @_; - - print STDERR "Usage: $0 [global options] command [-- command...]\n"; - print STDERR "\n"; - print STDERR "Global options:\n"; - print STDERR " -b --batch Output command stream to stdout\n"; - print STDERR " -e --rsh=command Send output to command, override KUP_RSH\n"; - print STDERR " -o --host=[user@]host Connect to [user@]host, override KUP_HOST\n"; - print STDERR " -v --verbose Print each command to stderr as it is sent\n"; - print STDERR "\n"; - print STDERR "Commands:\n"; - print STDERR " put local_file signature remote_path\n"; - print STDERR " put --tar [--prefix=] remote_tree ref signature remote_path\n"; - print STDERR " put --diff remote_tree ref1 ref2 signature remote_path\n"; - print STDERR " mkdir remote_path\n"; - print STDERR " mv|move old_path new_path\n"; - print STDERR " ln|link old_path new_path\n"; - print STDERR " rm|del|delete old_path\n"; - print STDERR " ls|dir path...\n"; - - exit $err; + my($err) = @_; + + print STDERR "Usage: $0 [global options] command [-- command...]\n"; + print STDERR "\n"; + print STDERR "Global options:\n"; + print STDERR " -b --batch Output command stream to stdout\n"; + print STDERR " -e --rsh=command Send output to command, override KUP_RSH\n"; + print STDERR " -o --host=[user@]host Connect to [user@]host, override KUP_HOST\n"; + print STDERR " -v --verbose Print each command to stderr as it is sent\n"; + print STDERR "\n"; + print STDERR "Commands:\n"; + print STDERR " put local_file signature remote_path\n"; + print STDERR " put --tar [--prefix=] remote_tree ref signature remote_path\n"; + print STDERR " put --diff remote_tree ref1 ref2 signature remote_path\n"; + print STDERR " mkdir remote_path\n"; + print STDERR " mv|move old_path new_path\n"; + print STDERR " ln|link old_path new_path\n"; + print STDERR " rm|del|delete old_path\n"; + print STDERR " ls|dir path...\n"; + + exit $err; } # Return true if the supplied string is valid UTF-8 without special # characters sub is_clean_string($) { - no bytes; - # use feature 'unicode_strings'; -- is this needed here? + no bytes; + # use feature 'unicode_strings'; -- is this needed here? - my($b) = @_; - my $f = decode('UTF-8', $b, Encode::FB_DEFAULT); + my($b) = @_; + my $f = decode('UTF-8', $b, Encode::FB_DEFAULT); - return 0 if ($f =~ m:[\x{0000}-\x{001f}\x{007f}-\x{00a0}\x{fffd}-\x{ffff}]:); - return 1; + return 0 if ($f =~ m:[\x{0000}-\x{001f}\x{007f}-\x{00a0}\x{fffd}-\x{ffff}]:); + return 1; } # This returns true if the given argument is a valid filename in its @@ -119,40 +119,40 @@ sub is_clean_string($) # without worrying about it, and the entire pathname to 1024 bytes. sub is_valid_filename($) { - use bytes; - - my($f) = @_; - - return 0 if (!defined($f)); # If undefined, clearly not valid - - return 0 if (length($f) > 1024); # Reject ridiculously long paths - return 0 if (!is_clean_string($f)); # Reject bad UTF-8 and control characters - return 0 if ($f !~ m:^/:); # Reject relative paths - return 0 if ($f =~ m:/$:); # Reject paths ending in / - return 0 if ($f =~ m://:); # Reject double slashes - - # Reject filename components starting with dot or dash, covers . and .. - return 0 if ($f =~ m:/[\.\-]:); - - # Reject undesirable filename characters anywhere in the name. - # This isn't inherently security-critical, and could be tuned if - # users need it... - return 0 if ($f =~ m:[\!\"\$\&\'\*\;\<\>\?\\\`\|]:); - - # Make sure we can create a filename after adding .bz2 or similar. - # We can't use the obvious regexp here, because regexps operate on - # characters, not bytes. The limit of 100 is semi-arbitrary, but - # we shouldn't need filenames that long. - my $n = 0; - my $nmax = 0; - for (my $i = 0; $i < length($f); $i++) { - my $c = substr($f, $i, 1); - $n = ($c eq '/') ? 0 : $n+1; - $nmax = ($n > $nmax) ? $n : $nmax; - } - return 0 if ($nmax > 100); - - return 1; + use bytes; + + my($f) = @_; + + return 0 if (!defined($f)); # If undefined, clearly not valid + + return 0 if (length($f) > 1024); # Reject ridiculously long paths + return 0 if (!is_clean_string($f)); # Reject bad UTF-8 and control characters + return 0 if ($f !~ m:^/:); # Reject relative paths + return 0 if ($f =~ m:/$:); # Reject paths ending in / + return 0 if ($f =~ m://:); # Reject double slashes + + # Reject filename components starting with dot or dash, covers . and .. + return 0 if ($f =~ m:/[\.\-]:); + + # Reject undesirable filename characters anywhere in the name. + # This isn't inherently security-critical, and could be tuned if + # users need it... + return 0 if ($f =~ m:[\!\"\$\&\'\*\;\<\>\?\\\`\|]:); + + # Make sure we can create a filename after adding .bz2 or similar. + # We can't use the obvious regexp here, because regexps operate on + # characters, not bytes. The limit of 100 is semi-arbitrary, but + # we shouldn't need filenames that long. + my $n = 0; + my $nmax = 0; + for (my $i = 0; $i < length($f); $i++) { + my $c = substr($f, $i, 1); + $n = ($c eq '/') ? 0 : $n+1; + $nmax = ($n > $nmax) ? $n : $nmax; + } + return 0 if ($nmax > 100); + + return 1; } # Clean up a filename so that it is more likely to pass the @@ -164,474 +164,474 @@ sub is_valid_filename($) # caught by is_valid_filename(). sub canonicalize_path($;$) { - my($file, $root) = @_; - - $root = '/' unless defined($root); - - my $tail = ''; - if ($root =~ m:^(.*/)([^/]+)$:) { - $root = $1; - $tail = $2; - } - - if ($root !~ m:^/: || $root !~ m:/$:) { - die "$0: internal error: non-canonical root\n"; - } - - if ($file !~ m:^/:) { - $file = $root . $file; - } - if ($file =~ m:/$:) { - $file .= $tail; - } - - my @path = (); - my $wasspc = 1; - # The -1 argument to split means "preserve trailing empty fields" - foreach my $s (split(/\//, $file, -1)) { - if ($s eq '' || $s eq '.') { - $wasspc = 1; - } elsif ($s eq '..') { - # If this ran off the root, error - return undef if (!defined(pop(@path))); - $wasspc = 1; - } else { - push(@path, $s); - $wasspc = 0; + my($file, $root) = @_; + + $root = '/' unless defined($root); + + my $tail = ''; + if ($root =~ m:^(.*/)([^/]+)$:) { + $root = $1; + $tail = $2; } - } - # If this ended in a special component, error - return undef if ($wasspc); + if ($root !~ m:^/: || $root !~ m:/$:) { + die "$0: internal error: non-canonical root\n"; + } - # The initial '' forces the result to begin with a slash - return join('/', '', @path); + if ($file !~ m:^/:) { + $file = $root . $file; + } + if ($file =~ m:/$:) { + $file .= $tail; + } + + my @path = (); + my $wasspc = 1; + # The -1 argument to split means "preserve trailing empty fields" + foreach my $s (split(/\//, $file, -1)) { + if ($s eq '' || $s eq '.') { + $wasspc = 1; + } elsif ($s eq '..') { + # If this ran off the root, error + return undef if (!defined(pop(@path))); + $wasspc = 1; + } else { + push(@path, $s); + $wasspc = 0; + } + } + + # If this ended in a special component, error + return undef if ($wasspc); + + # The initial '' forces the result to begin with a slash + return join('/', '', @path); } # Parse global options sub parse_global_options() { - while (scalar @ARGV && $ARGV[0] =~ /^-/) { - my $arg = shift(@ARGV); - - if ($arg eq '-b' || $arg eq '--batch') { - $opt{'batch'} = 1; - } elsif ($arg eq '-e' || $arg eq '--rsh' || $arg eq '--ssh') { - $opt{'rsh'} = shift(@ARGV); - } elsif ($arg =~ /^--rsh=(.+)$/) { - $opt{'rsh'} = $1; - } elsif ($arg eq '-o' || $arg eq '--host') { - $opt{'host'} = shift(@ARGV); - } elsif ($arg =~ /^--host=(.+)$/) { - $opt{'host'} = $1; - } elsif ($arg eq '-v' || $arg eq '--verbose') { - $opt{'verbose'}++; - } elsif ($arg eq '-h' || $arg eq '--help') { - usage(0); - } else { - die "$0: unknown option: $arg\n"; + while (scalar @ARGV && $ARGV[0] =~ /^-/) { + my $arg = shift(@ARGV); + + if ($arg eq '-b' || $arg eq '--batch') { + $opt{'batch'} = 1; + } elsif ($arg eq '-e' || $arg eq '--rsh' || $arg eq '--ssh') { + $opt{'rsh'} = shift(@ARGV); + } elsif ($arg =~ /^--rsh=(.+)$/) { + $opt{'rsh'} = $1; + } elsif ($arg eq '-o' || $arg eq '--host') { + $opt{'host'} = shift(@ARGV); + } elsif ($arg =~ /^--host=(.+)$/) { + $opt{'host'} = $1; + } elsif ($arg eq '-v' || $arg eq '--verbose') { + $opt{'verbose'}++; + } elsif ($arg eq '-h' || $arg eq '--help') { + usage(0); + } else { + die "$0: unknown option: $arg\n"; + } } - } } # Encode a string sub url_encode($) { - my($s) = @_; + my($s) = @_; - # Hack to encode an empty string - return '%' if ($s eq ''); + # Hack to encode an empty string + return '%' if ($s eq ''); - my $o = ''; + my $o = ''; - foreach my $c (unpack("C*", $s)) { - if ($c > 32 && $c < 126 && $c != 37 && $c != 43) { - $o .= chr($c); - } elsif ($c == 32) { - $o .= '+'; - } else { - $o .= sprintf("%%%02X", $c); + foreach my $c (unpack("C*", $s)) { + if ($c > 32 && $c < 126 && $c != 37 && $c != 43) { + $o .= chr($c); + } elsif ($c == 32) { + $o .= '+'; + } else { + $o .= sprintf("%%%02X", $c); + } } - } - return $o; + return $o; } # Configure the output stream sub setup_output() { - # In batch mode, we dump the output to stdout so the user can - # aggregate it best they wish - unless ($opt{'batch'}) { - if ($opt{'rsh'} !~ /^([-a-zA-Z0-9._=\@:\s]+)$/) { - die "$0: suspicious KUP_RSH command (if this is bogus let hpa know)\n"; - } - my $rsh = $1; - if ($opt{'host'} !~ /^([-a-zA-Z0-9._\@]+)$/) { - die "$0: suspicious KUP_HOST (if this is bogus let hpa know)\n"; - } - $rsh .= " \Q$1"; - open(STDOUT, '|-', $rsh) - or die "$0: cannot execute rsh command ", $rsh, "\n"; - } - binmode(STDOUT); + # In batch mode, we dump the output to stdout so the user can + # aggregate it best they wish + unless ($opt{'batch'}) { + if ($opt{'rsh'} !~ /^([-a-zA-Z0-9._=\@:\s\/]+)$/) { + die "$0: suspicious KUP_RSH setting\n"; + } + my $rsh = $1; + if ($opt{'host'} !~ /^([-a-zA-Z0-9._\@]+)$/) { + die "$0: suspicious KUP_HOST\n"; + } + $rsh .= " \Q$1"; + open(STDOUT, '|-', $rsh) + or die "$0: cannot execute rsh command ", $rsh, "\n"; + } + binmode(STDOUT); } # Terminate the output process sub close_output() { - $| = 1; # Flush STDOUT - unless ($opt{'batch'}) { - close(STDOUT); - } + $| = 1; # Flush STDOUT + unless ($opt{'batch'}) { + close(STDOUT); + } } # Print a command to STDOUT, and if requested, to STDERR sub command(@) { - if ($real) { - my $cmd = join(' ', @_); + if ($real) { + my $cmd = join(' ', @_); - print STDERR $cmd, "\n" if ($opt{'verbose'}); - print $cmd, "\n"; - } + print STDERR $cmd, "\n" if ($opt{'verbose'}); + print $cmd, "\n"; + } } sub get_data_format($) { - my($data) = @_; - - my $magic2 = substr($data, 0, 2); - my $magic4 = substr($data, 0, 4); - my $magic6 = substr($data, 0, 6); - - my $fmt = '%'; # Meaning straight binary - - if ($magic2 eq "\037\213") { - $fmt = 'gz'; - } elsif ($magic4 =~ /^BZh[1-9]$/) { - # The primary bzip2 magic is so crappy, so look - # for the magic number of the first packet - # (either a compression packet or an end of file packet.) - # Funny enough, the magics on the packets are better - # than the magics on the file format, and even so - # they managed to pick a magic for the compression - # packet which has no non-ASCII bytes in it... - - my $submagic = substr($data, 4, 6); - - if ($submagic eq "\x31\x41\x59\x26\x53\x59" || - $submagic eq "\x17\x72\x45\x38\x50\x90") { - $fmt = 'bz2'; + my($data) = @_; + + my $magic2 = substr($data, 0, 2); + my $magic4 = substr($data, 0, 4); + my $magic6 = substr($data, 0, 6); + + my $fmt = '%'; # Meaning straight binary + + if ($magic2 eq "\037\213") { + $fmt = 'gz'; + } elsif ($magic4 =~ /^BZh[1-9]$/) { + # The primary bzip2 magic is so crappy, so look + # for the magic number of the first packet + # (either a compression packet or an end of file packet.) + # Funny enough, the magics on the packets are better + # than the magics on the file format, and even so + # they managed to pick a magic for the compression + # packet which has no non-ASCII bytes in it... + + my $submagic = substr($data, 4, 6); + + if ($submagic eq "\x31\x41\x59\x26\x53\x59" || + $submagic eq "\x17\x72\x45\x38\x50\x90") { + $fmt = 'bz2'; + } + } elsif ($magic6 eq "\x{fd}7zXZ\0") { + $fmt = 'xz'; } - } elsif ($magic6 eq "\x{fd}7zXZ\0") { - $fmt = 'xz'; - } - return $fmt; + return $fmt; } sub cat_file($$$) { - my($cmd, $file, $fmt) = @_; + my($cmd, $file, $fmt) = @_; - my $data; - open($data, '<', $file) - or die "$0: cannot open: $file: $!\n"; - if (! -f $data) { - die "$0: not a plain file: $file\n"; - } - my $size = -s _; + my $data; + open($data, '<', $file) + or die "$0: cannot open: $file: $!\n"; + if (! -f $data) { + die "$0: not a plain file: $file\n"; + } + my $size = -s _; - binmode($data); + binmode($data); - if ($real) { - if ($size < 2) { - # Must be a plain file - $fmt = '%'; - } + if ($real) { + if ($size < 2) { + # Must be a plain file + $fmt = '%'; + } - if (defined($fmt)) { - command($cmd, $size, $fmt); - } + if (defined($fmt)) { + command($cmd, $size, $fmt); + } - my $blk; - my $len; + my $blk; + my $len; - while ($size) { - $len = ($size < $blksiz) ? $size : $blksiz; - $len = read($data, $blk, $len); + while ($size) { + $len = ($size < $blksiz) ? $size : $blksiz; + $len = read($data, $blk, $len); - if (!$len) { - die "$0: premature end of data (file changed?): $file\n"; - } + if (!$len) { + die "$0: premature end of data (file changed?): $file\n"; + } - if (!defined($fmt)) { - $fmt = get_data_format($blk); - command($cmd, $size, $fmt); - } + if (!defined($fmt)) { + $fmt = get_data_format($blk); + command($cmd, $size, $fmt); + } - print $blk; - $size -= $len; + print $blk; + $size -= $len; + } } - } - close($data); + close($data); } # PUT command sub cmd_put() { - my $file = shift @args; - my $file_tail = undef; - - if ($file eq '-t' || $file eq '--tar') { - # tar hack + my $file = shift @args; + my $file_tail = undef; + + if ($file eq '-t' || $file eq '--tar') { + # tar hack + + my $remote_tree = shift @args; + my $prefix = ''; + + if ($remote_tree eq '-p' || $remote_tree eq '--prefix') { + $prefix = shift @args; + $remote_tree = shift @args; + } elsif ($remote_tree =~ /^--prefix=(.+)$/) { + $prefix = $1; + $remote_tree = shift @args; + } + + my $ref = shift(@args); + + if (!defined($ref)) { + usage(1); + } + + my $xrt = $remote_tree; + $remote_tree = canonicalize_path($remote_tree); + if (!is_valid_filename($remote_tree) || $remote_tree !~ /\.git$/) { + die "$0: invalid path name for git tree: $xrt\n"; + } + if (!is_clean_string($ref)) { + die "$0: invalid ref: $ref\n"; + } + + command('TAR', url_encode($remote_tree), + url_encode($ref), url_encode($prefix)); + } elsif ($file eq '-d' || $file eq '--diff') { + # diff hack + + my $remote_tree = shift @args; + my $prefix = ''; + + my $ref1 = shift(@args); + my $ref2 = shift(@args); + + if (!defined($ref2)) { + usage(1); + } + + my $xrt = $remote_tree; + $remote_tree = canonicalize_path($remote_tree); + if (!is_valid_filename($remote_tree) || $remote_tree !~ /\.git$/) { + die "$0: invalid path name for git tree: $xrt\n"; + } + if (!is_clean_string($ref1)) { + die "$0: invalid ref: $ref1\n"; + } + if (!is_clean_string($ref2)) { + die "$0: invalid ref: $ref2\n"; + } + + command('DIFF', url_encode($remote_tree), url_encode($ref1), + url_encode($ref2)); + } elsif ($file =~ /^-/) { + die "$0: unknown option to put command: $file\n"; + } else { + # Plain data blob. We don't actively attempt to compress it + # since ssh usually has a layer of compression, but if it is + # already a compressed file we send it as-is and let the + # server decompress it. - my $remote_tree = shift @args; - my $prefix = ''; + cat_file('DATA', $file, undef); - if ($remote_tree eq '-p' || $remote_tree eq '--prefix') { - $prefix = shift @args; - $remote_tree = shift @args; - } elsif ($remote_tree =~ /^--prefix=(.+)$/) { - $prefix = $1; - $remote_tree = shift @args; + # Get the local filename without directory + my($vol, $dir); + ($vol, $dir, $file_tail) = File::Spec->splitpath($file); } - my $ref = shift(@args); + my $sign = shift @args; + my $remote = shift @args; - if (!defined($ref)) { - usage(1); + if (!defined($remote)) { + usage(1); } - my $xrt = $remote_tree; - $remote_tree = canonicalize_path($remote_tree); - if (!is_valid_filename($remote_tree) || $remote_tree !~ /\.git$/) { - die "$0: invalid path name for git tree: $xrt\n"; + # This allows the user to not specify the filename if it is + # the same as on the local filesystem by ending the pathname + # with a slash + if ($remote =~ m:/$: && defined($file_tail)) { + $remote .= $file_tail; } - if (!is_clean_string($ref)) { - die "$0: invalid ref: $ref\n"; - } - - command('TAR', url_encode($remote_tree), - url_encode($ref), url_encode($prefix)); - } elsif ($file eq '-d' || $file eq '--diff') { - # diff hack - - my $remote_tree = shift @args; - my $prefix = ''; - - my $ref1 = shift(@args); - my $ref2 = shift(@args); - - if (!defined($ref2)) { - usage(1); + + my $xrt = $remote; + $remote = canonicalize_path($remote); + if (!is_valid_filename($remote)) { + die "$0: invalid pathname: $xrt\n"; } - my $xrt = $remote_tree; - $remote_tree = canonicalize_path($remote_tree); - if (!is_valid_filename($remote_tree) || $remote_tree !~ /\.git$/) { - die "$0: invalid path name for git tree: $xrt\n"; - } - if (!is_clean_string($ref1)) { - die "$0: invalid ref: $ref1\n"; + if ($remote =~ /\.sign$/) { + die "$0: target filename cannot end in .sign\n"; } - if (!is_clean_string($ref2)) { - die "$0: invalid ref: $ref2\n"; - } - - command('DIFF', url_encode($remote_tree), url_encode($ref1), - url_encode($ref2)); - } elsif ($file =~ /^-/) { - die "$0: unknown option to put command: $file\n"; - } else { - # Plain data blob. We don't actively attempt to compress it - # since ssh usually has a layer of compression, but if it is - # already a compressed file we send it as-is and let the - # server decompress it. - cat_file('DATA', $file, undef); + # DWIM: .bz2, .xz -> .gz + $remote =~ s/\.(bz2|xz)$/.gz/; - # Get the local filename without directory - my($vol, $dir); - ($vol, $dir, $file_tail) = File::Spec->splitpath($file); - } - - my $sign = shift @args; - my $remote = shift @args; - - if (!defined($remote)) { - usage(1); - } - - # This allows the user to not specify the filename if it is - # the same as on the local filesystem by ending the pathname - # with a slash - if ($remote =~ m:/$: && defined($file_tail)) { - $remote .= $file_tail; - } - - my $xrt = $remote; - $remote = canonicalize_path($remote); - if (!is_valid_filename($remote)) { - die "$0: invalid pathname: $xrt\n"; - } - - if ($remote =~ /\.sign$/) { - die "$0: target filename cannot end in .sign\n"; - } - - # DWIM: .bz2, .xz -> .gz - $remote =~ s/\.(bz2|xz)$/.gz/; - - cat_file('SIGN', $sign, undef); - command('PUT', url_encode($remote)); + cat_file('SIGN', $sign, undef); + command('PUT', url_encode($remote)); } # MKDIR command sub cmd_mkdir() { - my $remote = shift @args; + my $remote = shift @args; - if (!defined($remote)) { - usage(1); - } + if (!defined($remote)) { + usage(1); + } - my $xrt = $remote; - $remote = canonicalize_path($remote); - if (!is_valid_filename($remote)) { - die "$0: invalid pathname: $xrt\n"; - } + my $xrt = $remote; + $remote = canonicalize_path($remote); + if (!is_valid_filename($remote)) { + die "$0: invalid pathname: $xrt\n"; + } - if ($remote =~ /\.(sign|gz|bz2|xz)$/) { - die "$0: a directory name cannot end in .sign, .gz, .bz2, .xz\n"; - } + if ($remote =~ /\.(sign|gz|bz2|xz)$/) { + die "$0: a directory name cannot end in .sign, .gz, .bz2, .xz\n"; + } - command('MKDIR', url_encode($remote)); + command('MKDIR', url_encode($remote)); } # DELETE command sub cmd_delete() { - my $remote = shift @args; + my $remote = shift @args; - if (!defined($remote)) { - usage(1); - } + if (!defined($remote)) { + usage(1); + } - my $xrt = $remote; - $remote = canonicalize_path($remote); - if (!is_valid_filename($remote)) { - die "$0: invalid pathname: $xrt\n"; - } + my $xrt = $remote; + $remote = canonicalize_path($remote); + if (!is_valid_filename($remote)) { + die "$0: invalid pathname: $xrt\n"; + } - if ($remote =~ /\.sign$/) { - die "$0: cannot delete .sign files directly\n"; - } + if ($remote =~ /\.sign$/) { + die "$0: cannot delete .sign files directly\n"; + } - # DWIM: .bz2, .xz -> .gz - $remote =~ s/\.(bz2|xz)$/.gz/; + # DWIM: .bz2, .xz -> .gz + $remote =~ s/\.(bz2|xz)$/.gz/; - command('DELETE', url_encode($remote)); + command('DELETE', url_encode($remote)); } # MOVE or LINK command sub cmd_move_link($) { - my($cmd) = @_; - - my $from = shift @args; - my $to = shift @args; - - if (!defined($to)) { - usage(1); - } - - my $xrt = $from; - $from = canonicalize_path($from); - if (!is_valid_filename($from)) { - die "$0: invalid pathname: $xrt\n"; - } - - $xrt = $to; - $to = canonicalize_path($to, $from); - if (!is_valid_filename($to)) { - die "$0: invalid pathname: $xrt\n"; - } - - if ($from =~ /\.sign$/ || $to =~ /\.sign$/) { - die "$0: cannot explicitly move .sign files\n"; - } - if ($from =~ /\.(gz|bz2|xz)$/ && $to =~ /\.(gz|bz2|xz)$/) { - $from =~ s/\.(bz2|xz)$/.gz/; - $to =~ s/\.(bz2|xz)$/.gz/; - } elsif ($from =~ /\.(gz|bz2|xz)$/ || $to =~ /\.(gz|bz2|xz)$/) { - die "$0: cannot move to or from compressed filenames\n"; - } - - if ($from eq $to) { - die "$0: moving filename to self: $from\n"; - } - - command($cmd, url_encode($from), url_encode($to)); + my($cmd) = @_; + + my $from = shift @args; + my $to = shift @args; + + if (!defined($to)) { + usage(1); + } + + my $xrt = $from; + $from = canonicalize_path($from); + if (!is_valid_filename($from)) { + die "$0: invalid pathname: $xrt\n"; + } + + $xrt = $to; + $to = canonicalize_path($to, $from); + if (!is_valid_filename($to)) { + die "$0: invalid pathname: $xrt\n"; + } + + if ($from =~ /\.sign$/ || $to =~ /\.sign$/) { + die "$0: cannot explicitly move .sign files\n"; + } + if ($from =~ /\.(gz|bz2|xz)$/ && $to =~ /\.(gz|bz2|xz)$/) { + $from =~ s/\.(bz2|xz)$/.gz/; + $to =~ s/\.(bz2|xz)$/.gz/; + } elsif ($from =~ /\.(gz|bz2|xz)$/ || $to =~ /\.(gz|bz2|xz)$/) { + die "$0: cannot move to or from compressed filenames\n"; + } + + if ($from eq $to) { + die "$0: moving filename to self: $from\n"; + } + + command($cmd, url_encode($from), url_encode($to)); } # DIR command (supports arbitrary number of arguments) sub cmd_dir() { - while (defined($args[0]) && $args[0] ne '--') { - my $d = shift @args; - $d =~ s:/$::g; - if ($d ne '') { - my $xrt = $d; - $d = canonicalize_path($d); - if (!is_valid_filename($d)) { - die "$0: invalid pathname: $xrt\n"; - } + while (defined($args[0]) && $args[0] ne '--') { + my $d = shift @args; + $d =~ s:/$::g; + if ($d ne '') { + my $xrt = $d; + $d = canonicalize_path($d); + if (!is_valid_filename($d)) { + die "$0: invalid pathname: $xrt\n"; + } + } + $d .= '/'; + + command('DIR', $d); } - $d .= '/'; - - command('DIR', $d); - } } # Process commands sub process_commands() { - while (1) { - my $cmd = shift(@args); - - if (!defined($cmd)) { - usage(1); - } - - $cmd = "\L${cmd}"; - - if ($cmd eq 'put') { - cmd_put(); - } elsif ($cmd eq 'mkdir') { - cmd_mkdir(); - } elsif ($cmd eq 'move' || $cmd eq 'mv') { - cmd_move_link('MOVE'); - } elsif ($cmd eq 'link' || $cmd eq 'ln') { - cmd_move_link('LINK'); - } elsif ($cmd eq 'delete' || $cmd eq 'del' || $cmd eq 'rm') { - cmd_delete(); - } elsif ($cmd eq 'ls' || $cmd eq 'dir') { - cmd_dir(); - } else { - die "$0: unknown command: $cmd\n"; + while (1) { + my $cmd = shift(@args); + + if (!defined($cmd)) { + usage(1); + } + + $cmd = "\L${cmd}"; + + if ($cmd eq 'put') { + cmd_put(); + } elsif ($cmd eq 'mkdir') { + cmd_mkdir(); + } elsif ($cmd eq 'move' || $cmd eq 'mv') { + cmd_move_link('MOVE'); + } elsif ($cmd eq 'link' || $cmd eq 'ln') { + cmd_move_link('LINK'); + } elsif ($cmd eq 'delete' || $cmd eq 'del' || $cmd eq 'rm') { + cmd_delete(); + } elsif ($cmd eq 'ls' || $cmd eq 'dir') { + cmd_dir(); + } else { + die "$0: unknown command: $cmd\n"; + } + + my $sep = shift(@args); + + last if (!defined($sep)); # End of command line + + if ($sep ne '--') { + die "$0: garbage at end of $cmd command\n"; + } } - - my $sep = shift(@args); - - last if (!defined($sep)); # End of command line - - if ($sep ne '--') { - die "$0: garbage at end of $cmd command\n"; - } - } } # Main program @@ -654,3 +654,5 @@ process_commands(); close_output(); exit 0; + +# vim: noet @@ -21,32 +21,32 @@ # It accepts the following commands: # # DATA byte-count -# - receives a new data blob (follows immediately) +# - receives a new data blob (follows immediately) # TAR git-tree tree-ish prefix -# - generate a data blob from a git tree (git archive) +# - generate a data blob from a git tree (git archive) # DIFF git-tree tree-ish tree-ish -# - generate a data blob as a git tree diff +# - generate a data blob as a git tree diff # SIGN byte-count -# - updates the current signature blob (follows immediately) +# - updates the current signature blob (follows immediately) # PUT pathname -# - installs the current data blob as <pathname> +# - installs the current data blob as <pathname> # MKDIR pathname -# - creates a new directory +# - creates a new directory # MOVE old-path new-path -# - moves <old-path> to <new-path> +# - moves <old-path> to <new-path> # LINK old-path new-path -# - hard links <old-path> to <new-path> +# - hard links <old-path> to <new-path> # DELETE old-path -# - removes <old-path> +# - removes <old-path> # DIR path -# - lists the contents of <path> on stdout; must be a directory +# - lists the contents of <path> on stdout; must be a directory # DONE -# - optional command, terminates transaction +# - optional command, terminates transaction # # For future consideration: # # SYMLINK old-path:new-path -# - symlinks <old-path> to <new-path> +# - symlinks <old-path> to <new-path> # use strict; @@ -67,32 +67,32 @@ use Git; # Scrub the environment completely %ENV = ('PATH' => '/bin:/usr/bin', - 'LANG' => 'C', - 'SHELL' => '/bin/false'); # Nothing in this program should shell out + 'LANG' => 'C', + 'SHELL' => '/bin/false'); # Nothing in this program should shell out # The standard function to call on bail sub fatal($) { - no bytes; + no bytes; - my($msg) = @_; + my($msg) = @_; - $msg =~ s/[\x{0000}-\x{001f}\x{007f}-\x{00a0}\x{fffd}-\x{ffff}]/ /g; + $msg =~ s/[\x{0000}-\x{001f}\x{007f}-\x{00a0}\x{fffd}-\x{ffff}]/ /g; - syslog(LOG_CRIT, "%s", $msg); - die $msg."\n"; + syslog(LOG_CRIT, "%s", $msg); + die $msg."\n"; } sub my_username() { - my $whoami = getuid(); - my ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell,$expire) = getpwuid($whoami); + my $whoami = getuid(); + my ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell,$expire) = getpwuid($whoami); - if (!defined($name) || $whoami != $uid) { - # We haven't called openlog() yet so we need to do it here - openlog("kup-server($whoami)", 'ndelay,pid', LOG_LOCAL5); - fatal("You don't exist, go away!"); - } + if (!defined($name) || $whoami != $uid) { + # We haven't called openlog() yet so we need to do it here + openlog("kup-server($whoami)", 'ndelay,pid', LOG_LOCAL5); + fatal("You don't exist, go away!"); + } - return (defined($name) && $whoami == $uid) ? $name : $whoami; + return (defined($name) && $whoami == $uid) ? $name : $whoami; } my $user_name = my_username(); @@ -106,7 +106,7 @@ my $cfg_file = '/etc/kup/kup-server.cfg'; my $cfg = new Config::Simple($cfg_file); if (!defined($cfg)) { - fatal('Error reading config file: '.$cfg_file); + fatal('Error reading config file: '.$cfg_file); } my $data_path = $cfg->param('paths.data_path'); @@ -116,11 +116,11 @@ my $tmp_path = $cfg->param('paths.tmp_path'); my $pgp_path = $cfg->param('paths.pgp_path'); my $max_data = int($cfg->param('limits.max_data')); -my $bufsiz = int($cfg->param('limits.bufsiz')); +my $bufsiz = int($cfg->param('limits.bufsiz')); -my $timeout_command = int($cfg->param('limits.timeout_command')); -my $timeout_data = int($cfg->param('limits.timeout_data')); -my $timeout_compress = int($cfg->param('limits.timeout_compress')); +my $timeout_command = int($cfg->param('limits.timeout_command')); +my $timeout_data = int($cfg->param('limits.timeout_data')); +my $timeout_compress = int($cfg->param('limits.timeout_compress')); # Make sure the user can't create insanely large files setrlimit(RLIMIT_FSIZE, $max_data, $max_data); @@ -128,9 +128,9 @@ setrlimit(RLIMIT_FSIZE, $max_data, $max_data); # These programs are expected to accept the option # -9 for compression and -cd for decompression to stdout. my %zformats = ( - '.gz' => '/bin/gzip', - '.bz2' => '/usr/bin/bzip2', - '.xz' => '/usr/bin/xz' + '.gz' => '/bin/gzip', + '.bz2' => '/usr/bin/bzip2', + '.xz' => '/usr/bin/xz' ); my $have_data = 0; @@ -138,41 +138,41 @@ my $have_sign = 0; # Create a temporary directory with plenty of randomness sub make_temp_dir() { - my $root; - my $urand; - my $randbytes; - - # If tmp_path ends in /, we are using per-user tmp directories - $root = $tmp_path; - if ($root =~ m:/$:) { - $root .= $user_name; - } - - sysopen($urand, '/dev/urandom', O_RDONLY) - or fatal("/dev/urandom not accessible"); - sysread($urand, $randbytes, 16); # 16 bytes = 128 bits - close($urand); - - if (length($randbytes) != 16) { - fatal("/dev/urandom returned a short read"); - } - - my $template = sprintf("%02x" x 16, unpack("C*", $randbytes)); - - # $template will be tainted, because it is computed from a file read; - # check that it looks like we expect and then untaint - if ($template !~ /^([0-9a-f]{32})$/) { - fatal("Internal error, a hex string is not a hex string"); - } - $template = $1.'-XXXXXXXXXXXX'; - - umask(077); - my $dir = tempdir($template, DIR => $root, CLEANUP => 1); + my $root; + my $urand; + my $randbytes; + + # If tmp_path ends in /, we are using per-user tmp directories + $root = $tmp_path; + if ($root =~ m:/$:) { + $root .= $user_name; + } + + sysopen($urand, '/dev/urandom', O_RDONLY) + or fatal("/dev/urandom not accessible"); + sysread($urand, $randbytes, 16); # 16 bytes = 128 bits + close($urand); + + if (length($randbytes) != 16) { + fatal("/dev/urandom returned a short read"); + } + + my $template = sprintf("%02x" x 16, unpack("C*", $randbytes)); + + # $template will be tainted, because it is computed from a file read; + # check that it looks like we expect and then untaint + if ($template !~ /^([0-9a-f]{32})$/) { + fatal("Internal error, a hex string is not a hex string"); + } + $template = $1.'-XXXXXXXXXXXX'; + + umask(077); + my $dir = tempdir($template, DIR => $root, CLEANUP => 1); } my $tmpdir = make_temp_dir(); if (!defined($tmpdir)) { - fatal("Failed to create session directory"); + fatal("Failed to create session directory"); } umask(002); @@ -180,22 +180,22 @@ my $lock_fd = undef; sub lock_tree() { - if (!defined($lock_fd)) { - open($lock_fd, '<', $lock_file) - or fatal("Cannot open lock file"); - flock($lock_fd, LOCK_EX) - or fatal("Cannot get file tree lock"); - } else { - fatal("File tree is already locked"); - } + if (!defined($lock_fd)) { + open($lock_fd, '<', $lock_file) + or fatal("Cannot open lock file"); + flock($lock_fd, LOCK_EX) + or fatal("Cannot get file tree lock"); + } else { + fatal("File tree is already locked"); + } } sub unlock_tree() { - if (defined($lock_fd)) { - close($lock_fd); - undef $lock_fd; - } + if (defined($lock_fd)) { + close($lock_fd); + undef $lock_fd; + } } # Encode a string; this is used by the DIR command @@ -203,90 +203,90 @@ sub unlock_tree() # multibyte UTF-8 was allowed in the output... sub url_encode($) { - my($s) = @_; + my($s) = @_; - # Hack to encode an empty string - return '%' if ($s eq ''); + # Hack to encode an empty string + return '%' if ($s eq ''); - my $o = ''; + my $o = ''; - foreach my $c (unpack("C*", $s)) { - if ($c > 32 && $c < 126 && $c != 37 && $c != 43) { - $o .= chr($c); - } elsif ($c == 32) { - $o .= '+'; - } else { - $o .= sprintf("%%%02X", $c); + foreach my $c (unpack("C*", $s)) { + if ($c > 32 && $c < 126 && $c != 37 && $c != 43) { + $o .= chr($c); + } elsif ($c == 32) { + $o .= '+'; + } else { + $o .= sprintf("%%%02X", $c); + } } - } - return $o; + return $o; } sub url_unescape($) { - my($s) = @_; - my $c; - my $o; - - # A single isolated % sign means an empty string - return '' if ($s eq '%'); - - for (my $i = 0; $i < length($s); $i++) { - $c = substr($s, $i, 1); - if ($c eq '+') { - $o .= ' '; - } elsif ($c eq '%') { - $c = substr($s, $i+1, 2); - return undef if ($c !~ /^[0-9a-f]{2}$/i); - $o .= pack("C", hex $c); - $i += 2; - } else { - $o .= $c; + my($s) = @_; + my $c; + my $o; + + # A single isolated % sign means an empty string + return '' if ($s eq '%'); + + for (my $i = 0; $i < length($s); $i++) { + $c = substr($s, $i, 1); + if ($c eq '+') { + $o .= ' '; + } elsif ($c eq '%') { + $c = substr($s, $i+1, 2); + return undef if ($c !~ /^[0-9a-f]{2}$/i); + $o .= pack("C", hex $c); + $i += 2; + } else { + $o .= $c; + } } - } - return $o; + return $o; } # Return true if the supplied string is valid UTF-8 without special # characters sub is_clean_string($) { - no bytes; - # use feature 'unicode_strings'; -- is this needed here? + no bytes; + # use feature 'unicode_strings'; -- is this needed here? - my($b) = @_; - my $f = decode('UTF-8', $b, Encode::FB_DEFAULT); + my($b) = @_; + my $f = decode('UTF-8', $b, Encode::FB_DEFAULT); - return 0 if ($f =~ m:[\x{0000}-\x{001f}\x{007f}-\x{00a0}\x{fffd}-\x{ffff}]:); - return 1; + return 0 if ($f =~ m:[\x{0000}-\x{001f}\x{007f}-\x{00a0}\x{fffd}-\x{ffff}]:); + return 1; } # Decode the argument line sub parse_line($) { - my($line) = @_; - chomp $line; + my($line) = @_; + chomp $line; - if ($line !~ /^([A-Z0-9_]+)(|\s+(|\S|\S.*\S))\s*$/) { - return undef; # Invalid syntax - } + if ($line !~ /^([A-Z0-9_]+)(|\s+(|\S|\S.*\S))\s*$/) { + return undef; # Invalid syntax + } - my $cmd = $1; - my @args = (); + my $cmd = $1; + my @args = (); - if ($2 ne '') { - my @rawargs = split(/\s+/, $3); + if ($2 ne '') { + my @rawargs = split(/\s+/, $3); - foreach my $ra (@rawargs) { - my $a = url_unescape($ra); - return undef if (!defined($a) || !is_clean_string($a)); - push(@args, $a); + foreach my $ra (@rawargs) { + my $a = url_unescape($ra); + return undef if (!defined($a) || !is_clean_string($a)); + push(@args, $a); + } } - } - return ($cmd, @args); + return ($cmd, @args); } # This returns true if the given argument is a valid filename in its @@ -296,888 +296,888 @@ sub parse_line($) # without worrying about it, and the entire pathname to 1024 bytes. sub is_valid_filename($) { - use bytes; - - my($f) = @_; - - return 0 if (length($f) > 1024); # Reject ridiculously long paths - return 0 if (!is_clean_string($f)); # Reject bad UTF-8 and control characters - return 0 if ($f !~ m:^/:); # Reject relative paths - return 0 if ($f =~ m:/$:); # Reject paths ending in / - return 0 if ($f =~ m://:); # Reject double slashes - - # Reject filename components starting with dot or dash, covers . and .. - return 0 if ($f =~ m:/[\.\-]:); - - # Reject undesirable filename characters anywhere in the name. - # This isn't inherently security-critical, and could be tuned if - # users need it... - return 0 if ($f =~ m:[\!\"\$\&\'\*\;\<\>\?\\\`\|]:); - - # Make sure we can create a filename after adding .bz2 or similar. - # We can't use the obvious regexp here, because regexps operate on - # characters, not bytes. The limit of 100 is semi-arbitrary, but - # we shouldn't need filenames that long. - my $n = 0; - my $nmax = 0; - for (my $i = 0; $i < length($f); $i++) { - my $c = substr($f, $i, 1); - $n = ($c eq '/') ? 0 : $n+1; - $nmax = ($n > $nmax) ? $n : $nmax; - } - return 0 if ($nmax > 100); - - return 1; + use bytes; + + my($f) = @_; + + return 0 if (length($f) > 1024); # Reject ridiculously long paths + return 0 if (!is_clean_string($f)); # Reject bad UTF-8 and control characters + return 0 if ($f !~ m:^/:); # Reject relative paths + return 0 if ($f =~ m:/$:); # Reject paths ending in / + return 0 if ($f =~ m://:); # Reject double slashes + + # Reject filename components starting with dot or dash, covers . and .. + return 0 if ($f =~ m:/[\.\-]:); + + # Reject undesirable filename characters anywhere in the name. + # This isn't inherently security-critical, and could be tuned if + # users need it... + return 0 if ($f =~ m:[\!\"\$\&\'\*\;\<\>\?\\\`\|]:); + + # Make sure we can create a filename after adding .bz2 or similar. + # We can't use the obvious regexp here, because regexps operate on + # characters, not bytes. The limit of 100 is semi-arbitrary, but + # we shouldn't need filenames that long. + my $n = 0; + my $nmax = 0; + for (my $i = 0; $i < length($f); $i++) { + my $c = substr($f, $i, 1); + $n = ($c eq '/') ? 0 : $n+1; + $nmax = ($n > $nmax) ? $n : $nmax; + } + return 0 if ($nmax > 100); + + return 1; } sub get_blob($$@) { - my($cmd, $name, @args) = @_; - my($len, $format) = @args; + my($cmd, $name, @args) = @_; + my($len, $format) = @args; - if (!defined($format) || $len !~ /^[0-9]+$/) { - fatal("Bad $cmd command"); - } + if (!defined($format) || $len !~ /^[0-9]+$/) { + fatal("Bad $cmd command"); + } - my $zcmd; + my $zcmd; - if ($format eq '') { - undef $zcmd; - } elsif (!defined($zcmd = $zformats{'.'.$format})) { - fatal("Unsupported compression format"); - } + if ($format eq '') { + undef $zcmd; + } elsif (!defined($zcmd = $zformats{'.'.$format})) { + fatal("Unsupported compression format"); + } - my $output = $tmpdir.'/'.$name; + my $output = $tmpdir.'/'.$name; - my $outfd; - my $writefd; - my $oldstdout; + my $outfd; + my $writefd; + my $oldstdout; - local $SIG{'ALRM'} = sub { fatal("Timeout waiting for data"); }; + local $SIG{'ALRM'} = sub { fatal("Timeout waiting for data"); }; - open($outfd, '>', $output) - or fatal("Failed to open $cmd file"); - binmode($outfd); + open($outfd, '>', $output) + or fatal("Failed to open $cmd file"); + binmode($outfd); - if (defined($zcmd)) { - open($oldstdout, '>&', \*STDOUT) or die; - open(STDOUT, '>&', $outfd) or die; - close($outfd); - undef $outfd; + if (defined($zcmd)) { + open($oldstdout, '>&', \*STDOUT) or die; + open(STDOUT, '>&', $outfd) or die; + close($outfd); + undef $outfd; - open($outfd, '|-', $zcmd, '-cd') or die; - binmode($outfd); + open($outfd, '|-', $zcmd, '-cd') or die; + binmode($outfd); - open(STDOUT, '>&', $oldstdout) or die; - close($oldstdout); - } + open(STDOUT, '>&', $oldstdout) or die; + close($oldstdout); + } - # We don't show a progress bar if the transfer is very short or - # quick, like with typical signatures. - my $prog_time = time() + 2; - my $prog_perc = -1; + # We don't show a progress bar if the transfer is very short or + # quick, like with typical signatures. + my $prog_time = time() + 2; + my $prog_perc = -1; - my $left = $len; - while ($left) { - my $blk = $left < $bufsiz ? $left : $bufsiz; - my $data; - my $rl; + my $left = $len; + while ($left) { + my $blk = $left < $bufsiz ? $left : $bufsiz; + my $data; + my $rl; - alarm($timeout_data); - $blk = read(STDIN, $data, $blk); - alarm(0); + alarm($timeout_data); + $blk = read(STDIN, $data, $blk); + alarm(0); - if ($blk < 1) { - fatal("End of stream before end of $cmd"); - } + if ($blk < 1) { + fatal("End of stream before end of $cmd"); + } - if (!print $outfd $data) { - fatal("Write error during $cmd"); - } + if (!print $outfd $data) { + fatal("Write error during $cmd"); + } - $left -= $blk; + $left -= $blk; - # STDERR needs to be flushed - STDERR->autoflush(1); + # STDERR needs to be flushed + STDERR->autoflush(1); - my $now = time(); - my $perc = int((($len-$left)*100)/$len); - if ($left == 0 ? - ($prog_perc >= 0) : # Show 100% iff we already showed a progress bar - ($now > $prog_time && $perc != $prog_perc)) { - printf STDERR "%10u [%-50s] %3u%%\r", $len, '=' x ($perc >> 1), $perc; - $prog_perc = $perc; - $prog_time = $now; + my $now = time(); + my $perc = int((($len-$left)*100)/$len); + if ($left == 0 ? + ($prog_perc >= 0) : # Show 100% iff we already showed a progress bar + ($now > $prog_time && $perc != $prog_perc)) { + printf STDERR "%10u [%-50s] %3u%%\r", $len, '=' x ($perc >> 1), $perc; + $prog_perc = $perc; + $prog_time = $now; + } } - } - close($outfd) - or fatal("Write error during $cmd"); + close($outfd) + or fatal("Write error during $cmd"); - print STDERR "\n" if ($prog_perc >= 0); + print STDERR "\n" if ($prog_perc >= 0); - syslog(LOG_DEBUG, "%u bytes read, %u bytes written", $len, -s $output); - return $len; + syslog(LOG_DEBUG, "%u bytes read, %u bytes written", $len, -s $output); + return $len; } sub get_raw_data(@) { - my @args = @_; + my @args = @_; - if (get_blob('DATA', 'data', @args) > $max_data) { - # This should never happen, as we should have died already - fatal("DATA output impossibly large"); - } + if (get_blob('DATA', 'data', @args) > $max_data) { + # This should never happen, as we should have died already + fatal("DATA output impossibly large"); + } - $have_data = 1; + $have_data = 1; } # Get the canonical name for a git ref and its type sub check_ref($$) { - my($repo, $ref) = @_; - - my $out = undef; - - if (!is_clean_string($ref) || $ref =~ /^-/) { - return undef; - } - - # It turns out Git::command_bidi_pipe() is broken under -T - $ENV{'GIT_DIR'} = $repo->repo_path(); - - my $pipe_in; - my $pipe_out; - my $pid = open2($pipe_in, $pipe_out, 'git', 'cat-file', '--batch-check'); - print $pipe_out $ref, "\n"; - close($pipe_out); - $out = <$pipe_in>; - chomp $out; - waitpid($pid, 0); - - if ($? == 0 && $out =~ /^([0-9a-f]{40}) (\S+) ([0-9]+)$/) { - return ($1, $2, $3+0); - } else { - return undef; - } + my($repo, $ref) = @_; + + my $out = undef; + + if (!is_clean_string($ref) || $ref =~ /^-/) { + return undef; + } + + # It turns out Git::command_bidi_pipe() is broken under -T + $ENV{'GIT_DIR'} = $repo->repo_path(); + + my $pipe_in; + my $pipe_out; + my $pid = open2($pipe_in, $pipe_out, 'git', 'cat-file', '--batch-check'); + print $pipe_out $ref, "\n"; + close($pipe_out); + $out = <$pipe_in>; + chomp $out; + waitpid($pid, 0); + + if ($? == 0 && $out =~ /^([0-9a-f]{40}) (\S+) ([0-9]+)$/) { + return ($1, $2, $3+0); + } else { + return undef; + } } sub get_tar_data(@) { - my @args = @_; + my @args = @_; - if (scalar(@args) != 3) { - fatal("Bad TAR command"); - } + if (scalar(@args) != 3) { + fatal("Bad TAR command"); + } - my($tree, $ref, $prefix) = @args; + my($tree, $ref, $prefix) = @args; - if (!is_valid_filename($tree)) { - fatal("Invalid pathname in TAR command"); - } + if (!is_valid_filename($tree)) { + fatal("Invalid pathname in TAR command"); + } - if (!is_clean_string($prefix)) { - fatal("Invalid prefix string"); - } + if (!is_clean_string($prefix)) { + fatal("Invalid prefix string"); + } - if ($tree !~ /\.git$/ || ! -d $git_path.$tree || - ! -d $git_path.$tree.'/objects') { - fatal("No such git tree"); - } + if ($tree !~ /\.git$/ || ! -d $git_path.$tree || + ! -d $git_path.$tree.'/objects') { + fatal("No such git tree"); + } - my $repo; - git_cmd_try { - $repo = Git->repository(Repository => $git_path.$tree); - } "Invalid git repository\n"; + my $repo; + git_cmd_try { + $repo = Git->repository(Repository => $git_path.$tree); + } "Invalid git repository\n"; - my ($sha, $type, $len) = check_ref($repo, $ref); - if (!defined($type) || $type !~ /^(tree|commit|tag)$/) { - fatal("Invalid tree reference"); - } + my ($sha, $type, $len) = check_ref($repo, $ref); + if (!defined($type) || $type !~ /^(tree|commit|tag)$/) { + fatal("Invalid tree reference"); + } - syslog(LOG_INFO, "tar ref ${sha}"); + syslog(LOG_INFO, "tar ref ${sha}"); - git_cmd_try { - $repo->command_noisy('archive', '--format=tar', '--prefix='.$prefix, - '-o', $tmpdir.'/data', $ref); - } "Failed to acquire tarball\n"; + git_cmd_try { + $repo->command_noisy('archive', '--format=tar', '--prefix='.$prefix, + '-o', $tmpdir.'/data', $ref); + } "Failed to acquire tarball\n"; - $have_data = 1; + $have_data = 1; } sub get_diff_data(@) { - my @args = @_; + my @args = @_; - if (scalar(@args) != 3) { - fatal("Bad DIFF command"); - } + if (scalar(@args) != 3) { + fatal("Bad DIFF command"); + } - my($tree, $ref1, $ref2) = @args; + my($tree, $ref1, $ref2) = @args; - if (!is_valid_filename($tree)) { - fatal("Invalid pathname in DIFF command"); - } + if (!is_valid_filename($tree)) { + fatal("Invalid pathname in DIFF command"); + } - if ($tree !~ /\.git$/ || ! -d $git_path.$tree || - ! -d $git_path.$tree.'/objects') { - fatal("No such git tree"); - } + if ($tree !~ /\.git$/ || ! -d $git_path.$tree || + ! -d $git_path.$tree.'/objects') { + fatal("No such git tree"); + } - my $repo; - git_cmd_try { - $repo = Git->repository(Repository => $git_path.$tree); - } "Invalid git repository\n"; + my $repo; + git_cmd_try { + $repo = Git->repository(Repository => $git_path.$tree); + } "Invalid git repository\n"; - my ($sha1, $type1, $len1) = check_ref($repo, $ref1); - if (!defined($type1) || $type1 !~ /^(tree|commit|tag)$/) { - fatal("Invalid tree reference"); - } + my ($sha1, $type1, $len1) = check_ref($repo, $ref1); + if (!defined($type1) || $type1 !~ /^(tree|commit|tag)$/) { + fatal("Invalid tree reference"); + } - my ($sha2, $type2, $len2) = check_ref($repo, $ref2); - if (!defined($type2) || $type2 !~ /^(tree|commit|tag)$/) { - fatal("Invalid tree reference"); - } + my ($sha2, $type2, $len2) = check_ref($repo, $ref2); + if (!defined($type2) || $type2 !~ /^(tree|commit|tag)$/) { + fatal("Invalid tree reference"); + } - syslog(LOG_INFO, "diff refs ${sha1}..${sha2}"); + syslog(LOG_INFO, "diff refs ${sha1}..${sha2}"); - git_cmd_try { - my $oldstdout; - my $out; + git_cmd_try { + my $oldstdout; + my $out; - open($oldstdout, '>&', \*STDOUT) or die; - sysopen($out, $tmpdir.'/data', O_WRONLY|O_CREAT|O_TRUNC) or die; - open(STDOUT, '>&', $out) or die; - close($out); + open($oldstdout, '>&', \*STDOUT) or die; + sysopen($out, $tmpdir.'/data', O_WRONLY|O_CREAT|O_TRUNC) or die; + open(STDOUT, '>&', $out) or die; + close($out); - $repo->command_noisy('diff-tree', '-p', $sha1, $sha2); + $repo->command_noisy('diff-tree', '-p', $sha1, $sha2); - open(STDOUT, '>&', $oldstdout); - close($oldstdout); - } "Failed to acquire patch file\n"; + open(STDOUT, '>&', $oldstdout); + close($oldstdout); + } "Failed to acquire patch file\n"; - $have_data = 1; + $have_data = 1; } sub get_sign_data(@) { - my @args = @_; + my @args = @_; - if (get_blob('SIGN', 'data.sign', @args) >= 65536) { - fatal("SIGN output impossibly large"); - } + if (get_blob('SIGN', 'data.sign', @args) >= 65536) { + fatal("SIGN output impossibly large"); + } - $have_sign = 1; + $have_sign = 1; } sub make_compressed_data() { - die if (!$have_data); + die if (!$have_data); - my %workers; - my $nworkers = 0; + my %workers; + my $nworkers = 0; - foreach my $e (keys(%zformats)) { - my @c = ($zformats{$e}, '-9'); + foreach my $e (keys(%zformats)) { + my @c = ($zformats{$e}, '-9'); - my $w = fork(); - - if (!defined($w)) { - fatal("Fork failed"); - } + my $w = fork(); - if ($w == 0) { - open(STDIN, '<', $tmpdir.'/data') or exit 127; - open(STDOUT, '>', $tmpdir.'/data'.$e) or exit 127; + if (!defined($w)) { + fatal("Fork failed"); + } - # This is necessary to work around a bug in Perl 5.10.1; - # if we don't do this then Perl 5.10.1 seeks to the point - # in STDIN which matches the number of bytes that has been - # read from STDIN since the beginning of the script, ignoring - # the fact that STDIN was just redirected above. - seek(STDIN, 0, 0); + if ($w == 0) { + open(STDIN, '<', $tmpdir.'/data') or exit 127; + open(STDOUT, '>', $tmpdir.'/data'.$e) or exit 127; - exec {$c[0]} @c; - exit 127; - } + # This is necessary to work around a bug in Perl 5.10.1; + # if we don't do this then Perl 5.10.1 seeks to the point + # in STDIN which matches the number of bytes that has been + # read from STDIN since the beginning of the script, ignoring + # the fact that STDIN was just redirected above. + seek(STDIN, 0, 0); - $workers{$w} = $e; - $nworkers++; - } + exec {$c[0]} @c; + exit 127; + } - local $SIG{'ALRM'} = sub { - foreach my $c (keys %workers) { - kill('TERM', $c); + $workers{$w} = $e; + $nworkers++; } - fatal("Timeout compressing output data"); - }; - - alarm($timeout_compress); - while ($nworkers) { - my $w = wait(); - my $status = $?; - - if (defined($workers{$w})) { - my $e = $workers{$w}; - undef $workers{$w}; - if ($status) { + local $SIG{'ALRM'} = sub { foreach my $c (keys %workers) { - kill('TERM', $c); + kill('TERM', $c); + } + fatal("Timeout compressing output data"); + }; + + alarm($timeout_compress); + + while ($nworkers) { + my $w = wait(); + my $status = $?; + + if (defined($workers{$w})) { + my $e = $workers{$w}; + undef $workers{$w}; + if ($status) { + foreach my $c (keys %workers) { + kill('TERM', $c); + } + fatal("Failed to compress output data"); + } + syslog(LOG_DEBUG, "%s compression: %u -> %u bytes", + $e, -s $tmpdir.'/data', -s $tmpdir.'/data'.$e); + + $nworkers--; } - fatal("Failed to compress output data"); - } - syslog(LOG_DEBUG, "%s compression: %u -> %u bytes", - $e, -s $tmpdir.'/data', -s $tmpdir.'/data'.$e); - - $nworkers--; } - } - alarm(0); + alarm(0); } sub make_timestamps_match() { - die if (!$have_data || !$have_sign); + die if (!$have_data || !$have_sign); - my $now = time(); + my $now = time(); - foreach my $e ('', keys(%zformats), '.sign') { - utime($now, $now, $tmpdir.'/data'.$e); - } + foreach my $e ('', keys(%zformats), '.sign') { + utime($now, $now, $tmpdir.'/data'.$e); + } } sub cleanup() { - foreach my $e ('', keys(%zformats), '.sign') { - unlink($tmpdir.'/data'.$e); - } + foreach my $e ('', keys(%zformats), '.sign') { + unlink($tmpdir.'/data'.$e); + } - $have_data = 0; - $have_sign = 0; + $have_data = 0; + $have_sign = 0; } sub signature_valid() { - my $oldstdout; - my $oldstderr; - my $devnull; - - # gpg(v) likes to chat on the console no matter what... - open($devnull, '>', '/dev/null') - or fatal("Cannot open /dev/null"); - open($oldstdout, '>&', \*STDOUT) - or fatal("dup error"); - open($oldstderr, '>&', \*STDERR) - or fatal("dup error"); - open(STDOUT, '>&', $devnull) - or fatal("dup error"); - open(STDERR, '>&', $devnull) - or fatal("dup error"); - close($devnull); - - my $status = - system('/usr/bin/gpgv', - '--quiet', - '--homedir', $tmpdir, - '--keyring', $pgp_path."/${user_name}.gpg", - $tmpdir.'/data.sign', $tmpdir.'/data'); - - open(STDOUT, '>&', $oldstdout); - close($oldstdout); - open(STDERR, '>&', $oldstderr); - close($oldstderr); - - return $status == 0; + my $oldstdout; + my $oldstderr; + my $devnull; + + # gpg(v) likes to chat on the console no matter what... + open($devnull, '>', '/dev/null') + or fatal("Cannot open /dev/null"); + open($oldstdout, '>&', \*STDOUT) + or fatal("dup error"); + open($oldstderr, '>&', \*STDERR) + or fatal("dup error"); + open(STDOUT, '>&', $devnull) + or fatal("dup error"); + open(STDERR, '>&', $devnull) + or fatal("dup error"); + close($devnull); + + my $status = + system('/usr/bin/gpgv', + '--quiet', + '--homedir', $tmpdir, + '--keyring', $pgp_path."/${user_name}.gpg", + $tmpdir.'/data.sign', $tmpdir.'/data'); + + open(STDOUT, '>&', $oldstdout); + close($oldstdout); + open(STDERR, '>&', $oldstderr); + close($oldstderr); + + return $status == 0; } # Return true if the filename has one of the extensions in the list sub has_extension($@) { - my($file, @exts) = @_; + my($file, @exts) = @_; - foreach my $e (@exts) { - return 1 if (substr($file, -length($e)) eq $e); - } + foreach my $e (@exts) { + return 1 if (substr($file, -length($e)) eq $e); + } - return 0; + return 0; } sub put_file(@) { - my @args = @_; + my @args = @_; - if (scalar(@args) != 1) { - fatal("Bad PUT command"); - } + if (scalar(@args) != 1) { + fatal("Bad PUT command"); + } - my($file) = @args; + my($file) = @args; - if (!$have_data) { - fatal("PUT without DATA"); - } - if (!$have_sign) { - fatal("PUT without SIGN"); - } + if (!$have_data) { + fatal("PUT without DATA"); + } + if (!$have_sign) { + fatal("PUT without SIGN"); + } - if (!signature_valid()) { - fatal("Signature invalid"); - } + if (!signature_valid()) { + fatal("Signature invalid"); + } - if (!is_valid_filename($file)) { - fatal("Invalid filename in PUT command"); - } + if (!is_valid_filename($file)) { + fatal("Invalid filename in PUT command"); + } - my @install_ext; - my @conflic_ext; - my $stem; + my @install_ext; + my @conflic_ext; + my $stem; - if ($file =~ /^(.*)\.gz$/) { - $stem = $1; + if ($file =~ /^(.*)\.gz$/) { + $stem = $1; - make_compressed_data(); + make_compressed_data(); - @conflic_ext = (''); - @install_ext = ('.sign', keys(%zformats)); - } elsif (has_extension($file, '.sign', keys(%zformats))) { - fatal("$file: Cannot install auxiliary files directly"); - } else { - $stem = $file; + @conflic_ext = (''); + @install_ext = ('.sign', keys(%zformats)); + } elsif (has_extension($file, '.sign', keys(%zformats))) { + fatal("$file: Cannot install auxiliary files directly"); + } else { + $stem = $file; - @conflic_ext = keys(%zformats); - @install_ext = ('.sign', ''); - } + @conflic_ext = keys(%zformats); + @install_ext = ('.sign', ''); + } - make_timestamps_match(); + make_timestamps_match(); - lock_tree(); + lock_tree(); - foreach my $e (@conflic_ext) { - if (-e $data_path.$stem.$e) { - fatal("$file: Filename conflict (compressed and uncompressed)"); + foreach my $e (@conflic_ext) { + if (-e $data_path.$stem.$e) { + fatal("$file: Filename conflict (compressed and uncompressed)"); + } } - } - my $ok = 1; - foreach my $e (@install_ext) { - if (-e $data_path.$stem.$e && ! -f _) { - fatal("$file: Trying to overwrite a non-file"); + my $ok = 1; + foreach my $e (@install_ext) { + if (-e $data_path.$stem.$e && ! -f _) { + fatal("$file: Trying to overwrite a non-file"); + } } - } - my @undoes = (); - foreach my $e (@install_ext) { - my $target = $data_path.$stem.$e; - if (!rename($tmpdir.'/data'.$e, $target)) { - my $err = $!; - unlink(@undoes); - $! = $err; - fatal("$file: Failed to install files: $!"); + my @undoes = (); + foreach my $e (@install_ext) { + my $target = $data_path.$stem.$e; + if (!rename($tmpdir.'/data'.$e, $target)) { + my $err = $!; + unlink(@undoes); + $! = $err; + fatal("$file: Failed to install files: $!"); + } + push(@undoes, $target); } - push(@undoes, $target); - } - unlock_tree(); - cleanup(); + unlock_tree(); + cleanup(); } sub do_mkdir(@) { - my @args = @_; + my @args = @_; - if (scalar(@args) != 1) { - fatal("Bad MKDIR command"); - } + if (scalar(@args) != 1) { + fatal("Bad MKDIR command"); + } - my($file) = @args; + my($file) = @args; - if (!is_valid_filename($file)) { - fatal("Invalid filename in MKDIR command"); - } + if (!is_valid_filename($file)) { + fatal("Invalid filename in MKDIR command"); + } - my @badext = ('.sign', keys(%zformats)); + my @badext = ('.sign', keys(%zformats)); - foreach my $e (@badext) { - if (substr($file, -length($e)) eq $e) { - fatal("Protected filename space"); + foreach my $e (@badext) { + if (substr($file, -length($e)) eq $e) { + fatal("Protected filename space"); + } } - } - lock_tree(); + lock_tree(); - foreach my $e (@badext) { - if (-e $data_path.$file.$e) { - fatal("Filename conflict (file and directory)"); + foreach my $e (@badext) { + if (-e $data_path.$file.$e) { + fatal("Filename conflict (file and directory)"); + } } - } - if (!mkdir($data_path.$file, 0777)) { - fatal("Failed to MKDIR"); - } + if (!mkdir($data_path.$file, 0777)) { + fatal("Failed to MKDIR"); + } - unlock_tree(); + unlock_tree(); } sub do_rename($$) { - my($f,$t) = @_; + my($f,$t) = @_; - return rename($f, $t); + return rename($f, $t); } sub undo_rename($$) { - my($f, $t) = @_; + my($f, $t) = @_; - rename($t, $f); + rename($t, $f); } sub do_link($$) { - my($f,$t) = @_; + my($f,$t) = @_; - return link($f, $t); + return link($f, $t); } sub undo_link($$) { - my($f,$t) = @_; + my($f,$t) = @_; - unlink($t); + unlink($t); } sub move_or_link_file($@) { - my($cmd, @args) = @_; + my($cmd, @args) = @_; - if (scalar(@args) != 2) { - fatal("Bad $cmd command"); - } + if (scalar(@args) != 2) { + fatal("Bad $cmd command"); + } - my $op = ($cmd eq 'MOVE') ? \&do_rename : \&do_link; - my $unop = ($cmd eq 'MOVE') ? \&undo_rename : \&undo_link; + my $op = ($cmd eq 'MOVE') ? \&do_rename : \&do_link; + my $unop = ($cmd eq 'MOVE') ? \&undo_rename : \&undo_link; - my($from, $to) = @args; + my($from, $to) = @args; - if (!is_valid_filename($from) || !is_valid_filename($to)) { - fatal("Invalid filename in $cmd command"); - } + if (!is_valid_filename($from) || !is_valid_filename($to)) { + fatal("Invalid filename in $cmd command"); + } - if ($from =~ /\.gz$/) { - if ($to !~ /\.gz$/) { - fatal("$cmd of .gz file must itself end in .gz"); + if ($from =~ /\.gz$/) { + if ($to !~ /\.gz$/) { + fatal("$cmd of .gz file must itself end in .gz"); + } + } elsif (has_extension($from, '.sign', keys(%zformats))) { + fatal("$cmd to auxiliary files not supported"); + } elsif (has_extension($to, '.sign', keys(%zformats))) { + fatal("$cmd to auxiliary filename space"); } - } elsif (has_extension($from, '.sign', keys(%zformats))) { - fatal("$cmd to auxiliary files not supported"); - } elsif (has_extension($to, '.sign', keys(%zformats))) { - fatal("$cmd to auxiliary filename space"); - } - lock_tree(); + lock_tree(); - my $from_stem; - my $to_stem; - my @conflic_ext = (); - my @install_ext = (); - my $type; + my $from_stem; + my $to_stem; + my @conflic_ext = (); + my @install_ext = (); + my $type; - if (!-e $data_path.$from) { - fatal("$cmd of nonexistent object"); - } elsif (-d $data_path.$from) { - if ($cmd ne 'MOVE') { - fatal("Cannot $cmd a directory"); - } + if (!-e $data_path.$from) { + fatal("$cmd of nonexistent object"); + } elsif (-d $data_path.$from) { + if ($cmd ne 'MOVE') { + fatal("Cannot $cmd a directory"); + } - if (-e $data_path.$to) { - fatal("Directory MOVE destination busy"); - } + if (-e $data_path.$to) { + fatal("Directory MOVE destination busy"); + } - if (!rename($data_path.$from, $data_path.$to)) { - fatal("$cmd of directory failed"); - } + if (!rename($data_path.$from, $data_path.$to)) { + fatal("$cmd of directory failed"); + } - unlock_tree(); - return; - } elsif (-f $data_path.$from) { - if ($from =~ /^(.*)\.gz$/) { - $from_stem = $1; + unlock_tree(); + return; + } elsif (-f $data_path.$from) { + if ($from =~ /^(.*)\.gz$/) { + $from_stem = $1; - die if ($to !~ /^(.*)\.gz$/); # Should already be checked - $to_stem = $1; + die if ($to !~ /^(.*)\.gz$/); # Should already be checked + $to_stem = $1; - @conflic_ext = (''); - @install_ext = ('.sign', keys(%zformats)); + @conflic_ext = (''); + @install_ext = ('.sign', keys(%zformats)); - $type = 'compressed'; - } else { - $from_stem = $from; - $to_stem = $to; + $type = 'compressed'; + } else { + $from_stem = $from; + $to_stem = $to; - @conflic_ext = keys(%zformats); - @install_ext = ('.sign', ''); + @conflic_ext = keys(%zformats); + @install_ext = ('.sign', ''); - $type = 'plain'; + $type = 'plain'; + } + } else { + fatal("$cmd of non-directory/non-file not currently supported"); } - } else { - fatal("$cmd of non-directory/non-file not currently supported"); - } - # If we continue here we're processing a file... + # If we continue here we're processing a file... - foreach my $e (@conflic_ext) { - if (-e $data_path.$to_stem.$e) { - fatal("Filename conflict (compressed and uncompressed)"); + foreach my $e (@conflic_ext) { + if (-e $data_path.$to_stem.$e) { + fatal("Filename conflict (compressed and uncompressed)"); + } } - } - foreach my $e (@install_ext) { - if (-e $data_path.$to_stem.$e && ! -f _) { - fatal("Trying to overwrite a non-file"); + foreach my $e (@install_ext) { + if (-e $data_path.$to_stem.$e && ! -f _) { + fatal("Trying to overwrite a non-file"); + } } - } - my @undoes = (); - foreach my $e (@install_ext) { - my $a = [$data_path.$from_stem.$e, $data_path.$to_stem.$e]; - if (!$op->(@$a)) { - foreach my $u (@undoes) { - $unop->(@$u); - } - fatal("$cmd of $type file failed"); + my @undoes = (); + foreach my $e (@install_ext) { + my $a = [$data_path.$from_stem.$e, $data_path.$to_stem.$e]; + if (!$op->(@$a)) { + foreach my $u (@undoes) { + $unop->(@$u); + } + fatal("$cmd of $type file failed"); + } + push(@undoes, $a); } - push(@undoes, $a); - } - unlock_tree(); + unlock_tree(); } sub delete_path(@) { - my(@args) = @_; + my(@args) = @_; - if (scalar(@args) != 1) { - fatal("Bad DELETE command"); - } + if (scalar(@args) != 1) { + fatal("Bad DELETE command"); + } - my($file) = @args; + my($file) = @args; - if (!is_valid_filename($file)) { - fatal("Invalid pathname in DELETE command"); - } + if (!is_valid_filename($file)) { + fatal("Invalid pathname in DELETE command"); + } - if ($file !~ /\.gz$/ && - has_extension($file, '.sign', keys(%zformats))) { - fatal("DELETE of auxiliary files not supported"); - } + if ($file !~ /\.gz$/ && + has_extension($file, '.sign', keys(%zformats))) { + fatal("DELETE of auxiliary files not supported"); + } - lock_tree(); + lock_tree(); - my $stem; - my @exts; - my $type; + my $stem; + my @exts; + my $type; - if (!-e $data_path.$file) { - fatal("DELETE of nonexistent object"); - } elsif (-d $data_path.$file) { - if (!rmdir($data_path.$file)) { - fatal("DELETE of directory failed"); - } - unlock_tree(); - return; - } elsif (-f $data_path.$file) { - if ($file =~ /^(.*)\.gz$/) { - $stem = $1; - @exts = ('.sign', keys(%zformats)); - $type = 'compressed'; + if (!-e $data_path.$file) { + fatal("DELETE of nonexistent object"); + } elsif (-d $data_path.$file) { + if (!rmdir($data_path.$file)) { + fatal("DELETE of directory failed"); + } + unlock_tree(); + return; + } elsif (-f $data_path.$file) { + if ($file =~ /^(.*)\.gz$/) { + $stem = $1; + @exts = ('.sign', keys(%zformats)); + $type = 'compressed'; + } else { + $stem = $file; + @exts = ('.sign', ''); + $type = 'plain'; + } } else { - $stem = $file; - @exts = ('.sign', ''); - $type = 'plain'; + fatal("DELETE of non-directory/non-file not currently supported"); } - } else { - fatal("DELETE of non-directory/non-file not currently supported"); - } - # If we continue here we're processing a file... + # If we continue here we're processing a file... - foreach my $e (@exts) { - if (-e $data_path.$stem.$e && ! -f _) { - fatal("DELETE encountered files and non-files"); + foreach my $e (@exts) { + if (-e $data_path.$stem.$e && ! -f _) { + fatal("DELETE encountered files and non-files"); + } } - } - foreach my $e (@exts) { - if (!unlink($data_path.$stem.$e)) { - fatal("DELETE of $type file failed"); + foreach my $e (@exts) { + if (!unlink($data_path.$stem.$e)) { + fatal("DELETE of $type file failed"); + } } - } - unlock_tree(); + unlock_tree(); } sub mode_string($) { - my($mode) = @_; - my $s; - - if (S_ISREG($mode)) { - $s = '-'; - } elsif (S_ISDIR($mode)) { - $s = 'd'; - } elsif (S_ISLNK($mode)) { - $s = 'l'; - } else { - # We should not have BLK, CHR, FIFO or SOCK in this hierarchy - return '??????????'; - } - - $s .= ($mode & S_IRUSR) ? 'r' : '-'; - $s .= ($mode & S_IWUSR) ? 'w' : '-'; - $s .= ($mode & S_ISUID) ? - (($mode & S_IXUSR) ? 's' : 'S') : - (($mode & S_IXUSR) ? 'x' : '-'); - - $s .= ($mode & S_IRGRP) ? 'r' : '-'; - $s .= ($mode & S_IWGRP) ? 'w' : '-'; - $s .= ($mode & S_ISGID) ? - (($mode & S_IXGRP) ? 's' : 'S') : - (($mode & S_IXGRP) ? 'x' : '-'); - - $s .= ($mode & S_IROTH) ? 'r' : '-'; - $s .= ($mode & S_IWOTH) ? 'w' : '-'; - $s .= ($mode & S_ISVTX) ? - (($mode & S_IXOTH) ? 's' : 'S') : - (($mode & S_IXOTH) ? 'x' : '-'); - - return $s; + my($mode) = @_; + my $s; + + if (S_ISREG($mode)) { + $s = '-'; + } elsif (S_ISDIR($mode)) { + $s = 'd'; + } elsif (S_ISLNK($mode)) { + $s = 'l'; + } else { + # We should not have BLK, CHR, FIFO or SOCK in this hierarchy + return '??????????'; + } + + $s .= ($mode & S_IRUSR) ? 'r' : '-'; + $s .= ($mode & S_IWUSR) ? 'w' : '-'; + $s .= ($mode & S_ISUID) ? + (($mode & S_IXUSR) ? 's' : 'S') : + (($mode & S_IXUSR) ? 'x' : '-'); + + $s .= ($mode & S_IRGRP) ? 'r' : '-'; + $s .= ($mode & S_IWGRP) ? 'w' : '-'; + $s .= ($mode & S_ISGID) ? + (($mode & S_IXGRP) ? 's' : 'S') : + (($mode & S_IXGRP) ? 'x' : '-'); + + $s .= ($mode & S_IROTH) ? 'r' : '-'; + $s .= ($mode & S_IWOTH) ? 'w' : '-'; + $s .= ($mode & S_ISVTX) ? + (($mode & S_IXOTH) ? 's' : 'S') : + (($mode & S_IXOTH) ? 'x' : '-'); + + return $s; } my %uid_hash = (); sub get_usr($) { - my($uid) = @_; + my($uid) = @_; - if (defined($uid_hash{$uid})) { - return $uid_hash{$uid}; - } + if (defined($uid_hash{$uid})) { + return $uid_hash{$uid}; + } - my $usr = getpwuid($uid) || sprintf("%u", $uid); - $usr = url_encode($usr); # If we have really strange names... + my $usr = getpwuid($uid) || sprintf("%u", $uid); + $usr = url_encode($usr); # If we have really strange names... - $uid_hash{$uid} = $usr; - return $usr; + $uid_hash{$uid} = $usr; + return $usr; } my %gid_hash = (); sub get_grp($) { - my($gid) = @_; + my($gid) = @_; - if (defined($gid_hash{$gid})) { - return $gid_hash{$gid}; - } + if (defined($gid_hash{$gid})) { + return $gid_hash{$gid}; + } - my $grp = getgrgid($gid) || sprintf("%u", $gid); - $grp = url_encode($grp); # If we have really strange names... + my $grp = getgrgid($gid) || sprintf("%u", $gid); + $grp = url_encode($grp); # If we have really strange names... - $gid_hash{$gid} = $grp; - return $grp; + $gid_hash{$gid} = $grp; + return $grp; } sub do_dir(@) { - my(@args) = @_; + my(@args) = @_; - if (scalar(@args) != 1) { - fatal("Bad DELETE command"); - } + if (scalar(@args) != 1) { + fatal("Bad DELETE command"); + } - my($dir) = @args; + my($dir) = @args; - # DIR / is permitted unlike any other command - $dir =~ s:/$::g; - if ($dir ne '' && !is_valid_filename($dir)) { - fatal("Invalid pathname in DIR command"); - } - $dir .= '/'; + # DIR / is permitted unlike any other command + $dir =~ s:/$::g; + if ($dir ne '' && !is_valid_filename($dir)) { + fatal("Invalid pathname in DIR command"); + } + $dir .= '/'; - my $dh; - if (!opendir($dh, $data_path.$dir)) { - fatal("Invalid directory in DIR command"); - } + my $dh; + if (!opendir($dh, $data_path.$dir)) { + fatal("Invalid directory in DIR command"); + } - # Synchronization marker to make output machine-readable - print '+++ ', url_encode($dir), "\n"; + # Synchronization marker to make output machine-readable + print '+++ ', url_encode($dir), "\n"; - foreach my $de (sort readdir($dh)) { - next if ($de =~ /^\./); # Hidden files include . and .. + foreach my $de (sort readdir($dh)) { + next if ($de =~ /^\./); # Hidden files include . and .. - my @st = lstat($data_path.$dir.'/'.$de); + my @st = lstat($data_path.$dir.'/'.$de); - next unless(scalar(@st) == 13); + next unless(scalar(@st) == 13); - printf "%-10s %3u %-8s %-8s %10u %s %s\n", - mode_string($st[2]), $st[3], - get_usr($st[4]), get_grp($st[5]), $st[7], - POSIX::strftime("%Y-%m-%d %H:%M:%S", gmtime($st[9])), - url_encode($de); - } + printf "%-10s %3u %-8s %-8s %10u %s %s\n", + mode_string($st[2]), $st[3], + get_usr($st[4]), get_grp($st[5]), $st[7], + POSIX::strftime("%Y-%m-%d %H:%M:%S", gmtime($st[9])), + url_encode($de); + } - closedir($dh); + closedir($dh); - # Termination marker to make output machine-readable - STDOUT->autoflush(1); # At least try to flush stdout after this line - print "\n"; - STDOUT->autoflush(0); + # Termination marker to make output machine-readable + STDOUT->autoflush(1); # At least try to flush stdout after this line + print "\n"; + STDOUT->autoflush(0); } sub get_command() { - local $SIG{'ALRM'} = sub { fatal("Timeout waiting for command"); }; + local $SIG{'ALRM'} = sub { fatal("Timeout waiting for command"); }; - alarm($timeout_command); - my $line = <STDIN>; - alarm(0); + alarm($timeout_command); + my $line = <STDIN>; + alarm(0); - return $line; + return $line; } my $line; while (defined($line = get_command())) { - # Ignore lines with only whitespace or starting with # - next if ($line =~ /^\s*(|\#.*)$/); - - chomp $line; - - if (!is_clean_string($line) || length($line) > 4096) { - syslog(LOG_ERR, "Received garbage input"); - fatal("Invalid command"); - } - - syslog(LOG_NOTICE, "Cmd: $line"); - - my($cmd, @args) = parse_line($line); - - if (!defined($cmd)) { - fatal("Syntax error"); - } - - if ($cmd eq 'DATA') { - get_raw_data(@args); - } elsif ($cmd eq 'TAR') { - get_tar_data(@args); - } elsif ($cmd eq 'DIFF') { - get_diff_data(@args); - } elsif ($cmd eq 'SIGN') { - get_sign_data(@args); - } elsif ($cmd eq 'PUT') { - put_file(@args); - } elsif ($cmd eq 'MKDIR') { - do_mkdir(@args); - } elsif ($cmd eq 'MOVE' || $cmd eq 'LINK') { - move_or_link_file($cmd, @args); - } elsif ($cmd eq 'DELETE') { - delete_path(@args); - } elsif ($cmd eq 'DIR') { - do_dir(@args); - } elsif ($cmd eq 'DONE') { - last; - } else { - fatal("Invalid command"); - } + # Ignore lines with only whitespace or starting with # + next if ($line =~ /^\s*(|\#.*)$/); + + chomp $line; + + if (!is_clean_string($line) || length($line) > 4096) { + syslog(LOG_ERR, "Received garbage input"); + fatal("Invalid command"); + } + + syslog(LOG_NOTICE, "Cmd: $line"); + + my($cmd, @args) = parse_line($line); + + if (!defined($cmd)) { + fatal("Syntax error"); + } + + if ($cmd eq 'DATA') { + get_raw_data(@args); + } elsif ($cmd eq 'TAR') { + get_tar_data(@args); + } elsif ($cmd eq 'DIFF') { + get_diff_data(@args); + } elsif ($cmd eq 'SIGN') { + get_sign_data(@args); + } elsif ($cmd eq 'PUT') { + put_file(@args); + } elsif ($cmd eq 'MKDIR') { + do_mkdir(@args); + } elsif ($cmd eq 'MOVE' || $cmd eq 'LINK') { + move_or_link_file($cmd, @args); + } elsif ($cmd eq 'DELETE') { + delete_path(@args); + } elsif ($cmd eq 'DIR') { + do_dir(@args); + } elsif ($cmd eq 'DONE') { + last; + } else { + fatal("Invalid command"); + } } syslog(LOG_NOTICE, "Session completed successfully"); diff --git a/kup-server.1 b/kup-server.1 new file mode 100644 index 0000000..ebe3c57 --- /dev/null +++ b/kup-server.1 @@ -0,0 +1,104 @@ +.\" ----------------------------------------------------------------------- +.\" +.\" Copyright 2011 Linux Foundation; author: Konstantin Ryabitsev +.\" +.\" This program is free software; you can redistribute it and/or +.\" modify it under the terms of the GNU General Public License as +.\" published by the Free Software Foundation, Inc.; either version 2 +.\" of the License, or (at your option) any later version; +.\" incorporated herein by reference. +.\" +.\" ----------------------------------------------------------------------- +.TH KUP-SERVER "1" "2011" "kernel.org upload server utility" "kernel.org" +.SH NAME +kup-server \- kernel.org upload server utility +.SH DESCRIPTION +.PP +The program +.B kup-server +is expected to be the receiver of an ssh shell, configured with the +following or similar options in ~/.ssh/authorized_keys: +.PP +.RS +command="/usr/bin/kup-server",no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAA[...] +.RE +.PP +Each user should have their own UID, as Unix user permissions are used +for specific tree access control. On the client side, a corresponding +client-side utility +.BR kup +is used to initiate the connection and perform the uploads. +.SH GLOBAL CONFIG +.PP +The configuration file for +.B kup-server +is located in /etc/kup/kup-server.cfg and has the following options: +.TP +\fB[paths]\fP +All paths in this section should be disjoint. Do not combine any of them +into one directory. +.TP +\fBdata_path\fP = \fI/var/lib/kup/pub\fP +Path for public consumption, e.g. served via http or rsync. +.TP +\fBgit_path\fP = \fI/var/lib/git\fP +This is the path where git trees (for the TAR and DIFF options) are +available. Those should be readonly for the uploaders. +.TP +\fBlock_file\fP = \fI/var/run/kup/lock\fP +A common lock file for data_path. No program should modify the +content in data_path without holding an flock on this file. Should +be readonly for the uploaders. +.TP +\fBtmp_path\fP = \fI/var/lib/kup/tmp\fP +tmp_path can be either: +.PP +.RS +1. a directory writable by every user and with the sticky bit set +(typically mode 1777 or 1770). In that case, DO NOT end the path +with a slash, or: +.PP +2. A directory containing an empty directory for each user (named for +that user), owned by that user and mode 0700. In this case, DO end +the path with a slash. +.PP +In either case, this directory tree +.B MUST +be on the same filesystem as \fBdata_path\fP, since the script expects tocreate files in this directory and rename() them into data_path. +.RE +.TP +\fBpgp_path\fP = \fI/var/lib/kup/pgp\fP +A directory containing a GnuPG public keyring for each user, named +<user>.gpg and readable (but not writable) by that user. +.PP +.TP +\fB[limits]\fP +All sizes are in bytes, all times in seconds. +.TP +\fBmax_data\fP = \fI8589934592\fP +Max size of uploaded data. +.TP +\fBbufsiz\fP = \fI262144\fP +Buffer size when reading data. +.TP +\fBtimeout_command\fP = \fI30\fP +How long to wait for a command to time out. +.TP +\fBtimeout_data\fP = \fI300\fP +Must read at least bufsiz bytes in this timespan. +.TP +\fBtimeout_compress\fP = \fI900\fP +Uncompressing tarballs must take at most this long. +.SH AUTHOR +Written by H. Peter Anvin <hpa@zytor.com>. +.SH COPYRIGHT +Copyright \(co 2011 Intel Corporation +.sp +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, Inc.; either version 2 of the License, +or (at your option) any later version; incorporated herein by +reference. There is NO warranty; not even for MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. +.SH "SEE ALSO" +.BR kup (1) |