--- /dev/null
+#! @PERL_PATH@ -w
+
+# Copyright (C) 2015 Network Time Foundation
+# Author: Harlan Stenn
+
+# Original shell version:
+# Copyright (C) 2014 Timothe Litt litt at acm dot org
+
+# This script may be freely copied, used and modified providing that
+# this notice and the copyright statement are included in all copies
+# and derivative works. No warranty is offered, and use is entirely at
+# your own risk. Bugfixes and improvements would be appreciated by the
+# author.
+
+use strict;
+
+use Digest::SHA qw(sha1_hex);
+use File::Copy qw(move);
+use File::Fetch;
+use Getopt::Long qw(:config no_ignore_case bundling);
+use Sys::Syslog;
+
+my $VERSION="1.003";
+
+# leap-seconds file manager/updater
+
+# ########## Default configuration ##########
+#
+
+my $CRONJOB = $ENV{'CRONJOB'};
+$CRONJOB = "" unless defined($CRONJOB);
+my $LOGGER;
+my $QUIET = "";
+my $VERBOSE = "";
+
+# Where to get the file
+my $LEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list";
+my $LEAPFILE;
+
+# How many times to try to download new file
+my $MAXTRIES=6;
+my $INTERVAL=10;
+
+# Where to find ntp config file
+my $NTPCONF="/etc/ntp.conf";
+
+# How long (in days) before expiration to get updated file
+my $PREFETCH="60";
+
+# How to restart NTP - older NTP: service ntpd? try-restart | condrestart
+# Recent NTP checks for new file daily, so there's nothing to do
+my $RESTART="";
+
+my $EXPIRES;
+my $FORCE = "";
+
+# Where to put temporary copy before it's validated
+my $TMPFILE="/tmp/leap-seconds.$$.tmp";
+
+# Syslog facility
+my $LOGFAC="daemon";
+
+# ###########################################
+
+=item update-leap
+
+Usage: $0 [options] [leapfile]
+
+Verifies and if necessary, updates leap-second definition file
+
+All arguments are optional: Default (or current value) shown:
+ -s Specify the URL of the master copy to download
+ $LEAPSRC
+ -d Specify the filename on the local system
+ $LEAPFILE
+ -e Specify how long (in days) before expiration the file is to be
+ refreshed. Note that larger values imply more frequent refreshes.
+ "$PREFETCH"
+ -f Specify location of ntp.conf (used to make sure leapfile directive is
+ present and to default leapfile)
+ $NTPCONF
+ -F Force update even if current file is OK and not close to expiring.
+ -r Specify number of times to retry on get failure
+ $MAXTRIES
+ -i Specify number of minutes between retries
+ $INTERVAL
+ -l Use syslog for output (Implied if CRONJOB is set)
+ -L Don't use syslog for output
+ -P Specify the syslog facility for logging
+ $LOGFAC
+ -t Name of temporary file used in validation
+ $TMPFILE
+ -q Only report errors to stdout
+ -v Verbose output
+
+The following options are not (yet) implemented in the perl version:
+ -4 Use only IPv4
+ -6 Use only IPv6
+ -c Command to restart NTP after installing a new file
+ <none> - ntpd checks file daily
+ -p 4|6
+ Prefer IPv4 or IPv6 (as specified) addresses, but use either
+ -z Specify path for utilities
+ $PATHLIST
+ -Z Only use system path
+
+$0 will validate the file currently on the local system
+
+Ordinarily, the file is found using the "leapfile" directive in $NTPCONF.
+However, an alternate location can be specified on the command line.
+
+If the file does not exist, is not valid, has expired, or is expiring soon,
+a new copy will be downloaded. If the new copy validates, it is installed and
+NTP is (optionally) restarted.
+
+If the current file is acceptable, no download or restart occurs.
+
+-c can also be used to invoke another script to perform administrative
+functions, e.g. to copy the file to other local systems.
+
+This can be run as a cron job. As the file is rarely updated, and leap
+seconds are announced at least one month in advance (usually longer), it
+need not be run more frequently than about once every three weeks.
+
+For cron-friendly behavior, define CRONJOB=1 in the crontab.
+
+Version $VERSION
+=cut
+
+# Default: Use syslog for logging if running under cron
+
+my $SYSLOG = $CRONJOB;
+
+# Parse options
+
+our(%opt);
+
+GetOptions(\%opt,
+ 'c=s',
+ 'e:60',
+ 'F',
+ 'f=s',
+ 'h',
+ 'i:10',
+ 'L',
+ 'l',
+ 'P=s',
+ 'q',
+ 'r:6',
+ 's=s',
+ 't=s',
+ 'v'
+ );
+
+$LOGFAC=$opt{P} if (defined($opt{P}));
+$LEAPSRC=$opt{s} if (defined($opt{s}));
+$PREFETCH=$opt{e} if (defined($opt{e}));
+$NTPCONF=$opt{f} if (defined($opt{f}));
+$FORCE="Y" if (defined($opt{F}));
+$RESTART=$opt{c} if (defined($opt{c}));
+$MAXTRIES=$opt{r} if (defined($opt{r}));
+$INTERVAL=$opt{i} if (defined($opt{i}));
+$TMPFILE=$opt{t} if (defined($opt{t}));
+$SYSLOG="Y" if (defined($opt{l}));
+$SYSLOG="" if (defined($opt{L}));
+$QUIET="Y" if (defined($opt{q}));
+$VERBOSE="Y" if (defined($opt{v}));
+
+# export PATH="$PATHLIST$PATH"
+
+# Handle logging
+
+openlog($0, 'pid', $LOGFAC);
+
+sub logger {
+ my ($priority, $message) = @_;
+
+ # "priority" "message"
+ #
+ # Stdout unless syslog specified or logger isn't available
+ #
+ if ($SYSLOG eq "" or $LOGGER eq "") {
+ if ($QUIET ne "" and ( $priority eq "info" or $priority eq "notice" or $priority eq "debug" ) ) {
+ return 0
+ }
+ printf "%s: $message\n", uc $priority;
+ return 0;
+ }
+
+ # Also log to stdout if cron job && notice or higher
+ if (($CRONJOB ne "" and ($priority ne "info" ) and ($priority ne "debug" )) || ($VERBOSE ne "")) {
+ # Log to stderr as well
+ print STDERR "$0: $priority: $message\n";
+ }
+ syslog($priority, $message);
+}
+
+# Verify interval
+# INTERVAL=$(( $INTERVAL *1 ))
+
+# Validate a leap-seconds file checksum
+#
+# File format: (full description in files)
+# # marks comments, except:
+# #$ number : the NTP date of the last update
+# #@ number : the NTP date that the file expires
+# Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date
+# Date lines have comments.
+# #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes
+#
+# Returns:
+# 0 File is valid
+# 1 Invalid Checksum
+# 2 Expired
+
+sub verifySHA {
+ my ($file, $verbose) = @_;
+
+ my $raw = "";
+ my $data = "";
+ my $FSHA;
+
+ # Remove comments, except those that are markers for last update,
+ # expires and hash
+
+ unless (open(LF, $file)) {
+ warn "Can't open <$file>: $!\n";
+ print "Will try and create that file.\n";
+ return 1;
+ };
+ while (<LF>) {
+ if (/^#\$/) {
+ $raw .= $_;
+ s/^..//;
+ $data .= $_;
+ }
+ elsif (/^#\@/) {
+ $raw .= $_;
+ s/^..//;
+ $data .= $_;
+ s/\s+//g;
+ $EXPIRES = $_ - 2208988800;
+ }
+ elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) {
+ chomp;
+ $raw .= $_;
+ $FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5);
+ }
+ elsif (/^#/) {
+ # ignore it
+ }
+ elsif (/^\d/) {
+ s/#.*$//;
+ $raw .= $_;
+ $data .= $_;
+ } else {
+ chomp;
+ print "Unexpected line: <$_>\n";
+ }
+ }
+ close LF;
+
+ # Remove all white space
+ $data =~ s/\s//g;
+
+ # Compute the SHA hash of the data, removing the marker and filename
+ # Computed in binary mode, which shouldn't matter since whitespace has been removed
+
+ my $DSHA = sha1_hex($data);
+
+ # Extract the file's hash. Restore any leading zeroes in hash segments.
+
+ if ( ( "$FSHA" ne "" ) && ( $FSHA eq $DSHA ) ) {
+ if ( $verbose ne "" ) {
+ logger("info", "Checksum of $file validated");
+ }
+ } else {
+ logger("error", "Checksum of $file is invalid:");
+ $FSHA="(no checksum record found in file)"
+ if ( $FSHA eq "");
+ logger("error", "EXPECTED: $FSHA");
+ logger("error", "COMPUTED: $DSHA");
+ return 1;
+ }
+
+ # Check the expiration date, converting NTP epoch to Unix epoch used by date
+
+ if ( $EXPIRES < time() ) {
+ logger("notice", "File expired on " . gmtime($EXPIRES));
+ return 2;
+ }
+ return 0;
+}
+
+# Verify ntp.conf
+
+-r $NTPCONF || die "Missing ntp configuration: $NTPCONF\n";
+
+# Parse ntp.conf for leapfile directive
+
+open(LF, $NTPCONF) || die "Can't open <$NTPCONF>: $!\n";
+while (<LF>) {
+ chomp;
+ if (/^ *leapfile\s+(\S+)/) {
+ $LEAPFILE = $1;
+ }
+}
+close LF;
+
+-s $LEAPFILE || warn "$NTPCONF specifies $LEAPFILE as a leapfile, which is empty.\n";
+
+# Allow placing the file someplace else - testing
+
+if ( defined $ARGV[0] ) {
+ if ( $ARGV[0] ne $LEAPFILE ) {
+ logger("notice", "Requested install to $ARGV[0], but $NTPCONF specifies $LEAPFILE");
+ }
+ $LEAPFILE = $ARGV[0];
+}
+
+# Verify the current file
+# If it is missing, doesn't validate or expired
+# Or is expiring soon
+# Download a new one
+
+if ( $FORCE ne "" || verifySHA($LEAPFILE, $VERBOSE) || ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) {
+ my $TRY = 0;
+ my $ff = File::Fetch->new(uri => $LEAPSRC) || die "Fetch failed.\n";
+ while (1) {
+ ++$TRY;
+ logger("info", "Attempting download from $LEAPSRC, try $TRY..")
+ if ($VERBOSE ne "");
+ my $where = $ff->fetch( to => '/tmp' );
+
+ if ($where) {
+ logger("info", "Download of $LEAPSRC succeeded");
+
+ if ( verifySHA($where, $VERBOSE )) {
+ # There is no point in retrying, as the file on the
+ # server is almost certainly corrupt.
+
+ logger("warning", "Downloaded file $where rejected -- saved for diagnosis");
+ exit 1;
+ }
+
+ # While the shell script version will set correct permissions
+ # on temporary file, for the perl version that's harder, so
+ # for now at least one should run this script as the
+ # appropriate user.
+
+ # REFFILE="$LEAPFILE"
+ # if [ ! -f $LEAPFILE ]; then
+ # logger "notice" "$LEAPFILE was missing, creating new copy - check permissions"
+ # touch $LEAPFILE
+ # # Can't copy permissions from old file, copy from NTPCONF instead
+ # REFFILE="$NTPCONF"
+ # fi
+ # chmod --reference $REFFILE $TMPFILE
+ # chown --reference $REFFILE $TMPFILE
+ # ( which selinuxenabled && selinuxenabled && which chcon ) >/dev/null 2>&1
+ # if [ $? == 0 ] ; then
+ # chcon --reference $REFFILE $TMPFILE
+ # fi
+
+ # Replace current file with validated new one
+
+ if ( move $where, $LEAPFILE ) {
+ logger("notice", "Installed new $LEAPFILE from $LEAPSRC");
+ } else {
+ logger("error", "Install $where => $LEAPFILE failed -- saved for diagnosis: $!");
+ exit 1;
+ }
+
+ # Restart NTP (or whatever else is specified)
+
+ if ( $RESTART ne "" ) {
+ if ( $VERBOSE ne "" ) {
+ logger("info", "Attempting restart action: $RESTART");
+ }
+
+# XXX
+ #R="$( 2>&1 $RESTART )"
+ #if [ $? -eq 0 ]; then
+ # logger "notice" "Restart action succeeded"
+ # if [ -n "$VERBOSE" -a -n "$R" ]; then
+ # logger "info" "$R"
+ # fi
+ #else
+ # logger "error" "Restart action failed"
+ # if [ -n "$R" ]; then
+ # logger "error" "$R"
+ # fi
+ # exit 2
+ #fi
+ }
+ exit 0;
+ }
+
+ # Failed to download. See about trying again
+
+ # rm -f $TMPFILE
+ if ( $TRY ge $MAXTRIES ) {
+ last;
+ }
+ if ( $VERBOSE ne "" ) {
+ logger("info", "Waiting $INTERVAL minutes before retrying...");
+ }
+ sleep $INTERVAL * 60 ;
+ }
+
+ # Failed and out of retries
+
+ logger("warning", "Download from $LEAPSRC failed after $TRY attempts");
+ exit 1;
+}
+
+print "FORCE is <$FORCE>\n";
+print "verifySHA is " . verifySHA($LEAPFILE, "") . "\n";
+print "EXPIRES <$EXPIRES> vs ". ( $PREFETCH * 86400 + time() ) . "\n";
+
+logger("info", "Not time to replace $LEAPFILE");
+
+exit 0;
+
+# EOF