#!/usr/bin/perl -T
use strict;
use warnings;
use Getopt::Long;
my $VERSION = '2008-08-20 12:29'; # UTC

# Copyright (C) 2008 Jim Meyering

# 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, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

(my $ME = $0) =~ s|.*/||;

my $remote_authkey_file = '.ssh/authorized_keys';

# Set up safe, well-known environment
$ENV{PATH}   = '/usr/bin:/bin';
$ENV{CDPATH} = '';
$ENV{IFS}    = '';

sub usage ($)
{
  my ($exit_code) = @_;
  my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR);
  if ($exit_code != 0)
    {
      print $STREAM "Try `$ME --help' for more information.\n";
    }
  else
    {
      print $STREAM <<EOF;
Usage: $ME --help
   or: $ME --version
   or: $ME [OPTIONS] --old=~/.ssh/old_key.pub \\
                     --new=~/.ssh/new_key.pub [USERNAME@]HOSTNAME
Update your ssh authorized key list on the remote system, HOSTNAME,
by replacing one old public key with a specified new one.

Use ssh's ssh-copy-id program if all you want to do is create
or append to a key list.

OPTIONS:

   --old=FILE     FILE contains your old public key (required)
   --new=FILE     FILE contains your new public key (required)
   --target=FILE  replace any occurrence of the old key with the new
                    one in remote FILE (default: $remote_authkey_file)

   --help         display this help and exit
   --version      output version information and exit
   --verbose	  generate verbose output

EOF
    }
  exit $exit_code;
}

sub read_key ($)
{
  my ($file) = @_;
  open FH, '<', $file
    or (warn "$ME: can't open `$file' for reading: $!\n"), return undef;
  my $line = <FH>;
  my $empty = <FH>;
  close FH;
  defined $line
    or (warn "$ME: $file: empty file\n"), return undef;
  defined $empty
    and (warn "$ME: $file: found more than one line in public key file\n"),
      return undef;

  my $rsa1_fmt = qr{(\d+ \d+ \d+) (.*)};
  my $ssh2_fmt  = qr{(ssh-(?:dss|rsa) [[:alnum:]/+=]+) (.*)};
  $line =~ m!^$ssh2_fmt$! || $line =~ m!^$rsa1_fmt$!
    or (warn "$ME: $file:1: invalid public key\n"), return undef;

  return ($1, $2);
}

{
  my $old_pubkey_file;
  my $new_pubkey_file;
  my $verbose;

  GetOptions
    (
     'old=s' => \$old_pubkey_file,
     'new=s' => \$new_pubkey_file,
     'target=s' => \$remote_authkey_file,
     verbose => \$verbose,
     help => sub { usage 0 },
     version => sub { print "$ME version $VERSION\n"; exit },
    ) or usage 1;

  # Require both --old and --new.
  defined $old_pubkey_file && defined $new_pubkey_file
    or (warn "$ME: you must specify both --old=F and --new=F options\n"),
      usage 1;

  @ARGV < 1
    and (warn "$ME: missing HOSTNAME argument\n"), usage 1;
  1 < @ARGV
    and (warn "$ME: too many arguments\n"), usage 1;

  my $hostname = $ARGV[0];

  # Read one key from each file.
  my ($new_key, $new_comment) = read_key $new_pubkey_file;
  my ($old_key, undef)        = read_key $old_pubkey_file;
  defined $new_key && defined $old_key
    or exit 1;
  $new_key eq $old_key
    and die "$ME: keys are identical\n";

  # Escape the following in the new comment.
  $new_comment =~ s/([\\,\$\@])/\\$1/g;

  # Create a regexp from $old_key: escape each '+'.
  my $old_key_re = quotemeta $old_key;

  my @cmd = ('ssh', $hostname, qw(perl -pi -), $remote_authkey_file);

  # FIXME: using perl's -i functionality is not good enough;
  # With it, perl exits successfully even for ENOENT, e.g.,
  # $ perl -pi -e 's/a/b/' no-such
  # Can't open no-such: No such file or directory.

  $verbose
    and print "Running this command:\n", join (' ', @cmd), "\n";
  open PIPE, '|-', @cmd
    or die "$ME: failed to run `@cmd': $!\n";
  my $do_subst = "s,$old_key_re .*,$new_key $new_comment,";
  print PIPE $do_subst;
  $verbose
    and print "with this input:\n  $do_subst\n";
  close PIPE
    or die "$ME: failed to write to pipe `@cmd'\n";

  # FIXME: better would be a grep-like exit status:
  # 0: successfully replaced at least one instance
  # 1: read file, but found no matching line
  # 2: some file error, e.g., ENOENT, ENOSPC
  # 3: some other error, like failure to fork or to exec ssh

  exit 0;
}

## Local Variables:
## indent-tabs-mode: nil
## eval: (add-hook 'write-file-hooks 'time-stamp)
## time-stamp-start: "my $VERSION = '"
## time-stamp-format: "%:y-%02m-%02d %02H:%02M"
## time-stamp-time-zone: "UTC"
## time-stamp-end: "'; # UTC"
## End:
