1276da39aSCy Schubert#! @PATH_PERL@ -w 2*f5f40dd6SCy Schubert# @configure_input 3276da39aSCy Schubert 409100258SXin LI# Copyright (C) 2015, 2017 Network Time Foundation 5276da39aSCy Schubert# Author: Harlan Stenn 609100258SXin LI# 709100258SXin LI# General cleanup and https support: Paul McMath 809100258SXin LI# 9276da39aSCy Schubert# Original shell version: 10276da39aSCy Schubert# Copyright (C) 2014 Timothe Litt litt at acm dot org 1109100258SXin LI# 12276da39aSCy Schubert# This script may be freely copied, used and modified providing that 13276da39aSCy Schubert# this notice and the copyright statement are included in all copies 14276da39aSCy Schubert# and derivative works. No warranty is offered, and use is entirely at 15276da39aSCy Schubert# your own risk. Bugfixes and improvements would be appreciated by the 16276da39aSCy Schubert# author. 17276da39aSCy Schubert 1809100258SXin LI######## BEGIN ######### 19276da39aSCy Schubertuse strict; 20276da39aSCy Schubert 2109100258SXin LI# Core modules 22276da39aSCy Schubertuse Digest::SHA qw(sha1_hex); 2309100258SXin LIuse File::Basename; 24276da39aSCy Schubertuse File::Copy qw(move); 2509100258SXin LIuse File::Temp qw(tempfile); 26276da39aSCy Schubertuse Getopt::Long qw(:config auto_help no_ignore_case bundling); 2709100258SXin LIuse Sys::Syslog qw(:standard :macros); 28276da39aSCy Schubert 2909100258SXin LI# External modules 3009100258SXin LIuse HTTP::Tiny 0.056; 3109100258SXin LIuse Net::SSLeay 1.49; 3209100258SXin LIuse IO::Socket::SSL 1.56; 33276da39aSCy Schubert 3409100258SXin LImy $VERSION = '1.004'; 35276da39aSCy Schubert 3609100258SXin LImy $RUN_DIR = '/tmp'; 3709100258SXin LImy $RUN_UID = 0; 3809100258SXin LImy $TMP_FILE; 3909100258SXin LImy $TMP_FH; 4009100258SXin LImy $FILE_MODE = 0644; 41276da39aSCy Schubert 4209100258SXin LI######## DEFAULT CONFIGURATION ########## 4309100258SXin LI# LEAP FILE SRC URIS 4409100258SXin LI# HTTPS - (default) 4509100258SXin LI# https://www.ietf.org/timezones/data/leap-seconds 4609100258SXin LI# HTTP - No TLS/SSL - (not recommended) 4709100258SXin LI# http://www.ietf.org/timezones/data/leap-seconds.list 48276da39aSCy Schubert 4909100258SXin LImy $LEAPSRC = 'https://www.ietf.org/timezones/data/leap-seconds.list'; 50276da39aSCy Schubertmy $LEAPFILE; 51276da39aSCy Schubert 52276da39aSCy Schubert# How many times to try to download new file 53276da39aSCy Schubertmy $MAXTRIES = 6; 54276da39aSCy Schubertmy $INTERVAL = 10; 55276da39aSCy Schubert 5609100258SXin LImy $NTPCONF='/etc/ntp.conf'; 57276da39aSCy Schubert 58276da39aSCy Schubert# How long (in days) before expiration to get updated file 5909100258SXin LImy $PREFETCH = 60; 60276da39aSCy Schubertmy $EXPIRES; 6109100258SXin LImy $FORCE; 62276da39aSCy Schubert 6309100258SXin LI# Output Flags 6409100258SXin LImy $QUIET; 6509100258SXin LImy $DEBUG; 6609100258SXin LImy $SYSLOG; 6709100258SXin LImy $TOTERM; 6809100258SXin LImy $LOGFAC = 'LOG_USER'; 69276da39aSCy Schubert 7009100258SXin LI######### PARSE/SET OPTIONS ######### 7109100258SXin LImy %SSL_OPTS; 7209100258SXin LImy %SSL_ATTRS = ( 7309100258SXin LI verify_SSL => 1, 7409100258SXin LI SSL_options => \%SSL_OPTS, 7509100258SXin LI); 76276da39aSCy Schubert 7709100258SXin LIour(%opt); 78276da39aSCy Schubert 7909100258SXin LIGetOptions(\%opt, 8009100258SXin LI 'C=s', 8109100258SXin LI 'D=s', 8209100258SXin LI 'e:60', 8309100258SXin LI 'F', 8409100258SXin LI 'f=s', 8509100258SXin LI 'h|help', 8609100258SXin LI 'i:10', 8709100258SXin LI 'L=s', 8809100258SXin LI 'l=s', 8909100258SXin LI 'q', 9009100258SXin LI 'r:6', 9109100258SXin LI 's', 9209100258SXin LI 't', 9309100258SXin LI 'u=s', 9409100258SXin LI 'v', 9509100258SXin LI ); 96276da39aSCy Schubert 9709100258SXin LI$LOGFAC = $opt{l} if defined $opt{l}; 9809100258SXin LI$LEAPSRC = $opt{u} if defined $opt{u}; 9909100258SXin LI$LEAPFILE = $opt{L} if defined $opt{L}; 10009100258SXin LI$PREFETCH = $opt{e} if defined $opt{e}; 10109100258SXin LI$NTPCONF = $opt{f} if defined $opt{f}; 10209100258SXin LI$MAXTRIES = $opt{r} if defined $opt{r}; 10309100258SXin LI$INTERVAL = $opt{i} if defined $opt{i}; 10409100258SXin LI 10509100258SXin LI$FORCE = 1 if defined $opt{F}; 10609100258SXin LI$DEBUG = 1 if defined $opt{v}; 10709100258SXin LI$QUIET = 1 if defined $opt{q}; 10809100258SXin LI$SYSLOG = 1 if defined $opt{s}; 10909100258SXin LI$TOTERM = 1 if defined $opt{t}; 11009100258SXin LI 11109100258SXin LI$SSL_OPTS{SSL_ca_file} = $opt{C} if (defined($opt{C})); 11209100258SXin LI$SSL_OPTS{SSL_ca_path} = $opt{D} if (defined($opt{D})); 11309100258SXin LI 11409100258SXin LI############### 11509100258SXin LI## START MAIN 11609100258SXin LI############### 11709100258SXin LImy $PROG = basename($0); 11809100258SXin LI 11909100258SXin LI# Logging - Default is to use syslog(3) if STDOUT isn't 12009100258SXin LI# connected to a tty. 12109100258SXin LIif ($SYSLOG || !-t STDOUT) { 12209100258SXin LI $SYSLOG = 1; 12309100258SXin LI openlog($PROG, 'pid', $LOGFAC); 12409100258SXin LI} 12509100258SXin LIelse { 12609100258SXin LI $TOTERM = 1; 12709100258SXin LI} 12809100258SXin LI 12909100258SXin LIif (defined $opt{q} && defined $opt{v}) { 13009100258SXin LI log_fatal(LOG_ERR, '-q and -v options mutually exclusive'); 13109100258SXin LI} 13209100258SXin LI 13309100258SXin LIif (defined $opt{L} && defined $opt{f}) { 13409100258SXin LI log_fatal(LOG_ERR, '-L and -f options mutually exclusive'); 13509100258SXin LI} 13609100258SXin LI 13709100258SXin LI$SIG{INT} = \&signal_catcher; 13809100258SXin LI$SIG{TERM} = \&signal_catcher; 13909100258SXin LI$SIG{QUIT} = \&signal_catcher; 14009100258SXin LI 14109100258SXin LI# Take some security precautions 14209100258SXin LIclose STDIN; 14309100258SXin LI 14409100258SXin LI# Show help 14509100258SXin LIif (defined $opt{h}) { 14609100258SXin LI show_help(); 14709100258SXin LI exit 0; 14809100258SXin LI} 14909100258SXin LI 15009100258SXin LIif ($< != $RUN_UID) { 15109100258SXin LI log_fatal(LOG_ERR, 'User ' . getpwuid($<) . " (UID $<) tried to run $PROG"); 15209100258SXin LI} 15309100258SXin LI 15409100258SXin LIchdir $RUN_DIR || log_fatal("Failed to change dir to $RUN_DIR"); 15509100258SXin LI 15609100258SXin LI# Parse ntp.conf for path to leapfile if not set by user 15709100258SXin LIif (! $LEAPFILE) { 15809100258SXin LI 15909100258SXin LI open my $LF, '<', $NTPCONF || log_fatal(LOG_ERR, "Can't open <$NTPCONF>: $!"); 16009100258SXin LI 16109100258SXin LI while (<$LF>) { 16209100258SXin LI chomp; 16309100258SXin LI $LEAPFILE = $1 if /^ *leapfile\s+"(\S+)"/; 16409100258SXin LI } 16509100258SXin LI close $LF; 16609100258SXin LI 16709100258SXin LI if (! $LEAPFILE) { 16809100258SXin LI log_fatal(LOG_ERR, "No leapfile directive in $NTPCONF; leapfile location not known"); 16909100258SXin LI } 17009100258SXin LI} 17109100258SXin LI 17209100258SXin LI-s $LEAPFILE || logger(LOG_DEBUG, "Leapfile $LEAPFILE is empty"); 17309100258SXin LI 17409100258SXin LI# Download new file if: 17509100258SXin LI# 1. file doesn't exist 17609100258SXin LI# 2. invoked w/ force flag (-F) 17709100258SXin LI# 3. current file isn't valid 17809100258SXin LI# 4. current file expired or expires soon 17909100258SXin LI 18009100258SXin LIif ( !-e $LEAPFILE || $FORCE || ! verifySHA($LEAPFILE) || 18109100258SXin LI ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) { 18209100258SXin LI 18309100258SXin LI for (my $try = 1; $try <= $MAXTRIES; $try++) { 18409100258SXin LI logger(LOG_DEBUG, "Attempting download from $LEAPSRC, try $try.."); 18509100258SXin LI 18609100258SXin LI ($TMP_FH, $TMP_FILE) = tempfile(UNLINK => 1, SUFFIX => '.list'); 18709100258SXin LI 18809100258SXin LI if (retrieve_file($TMP_FH)) { 18909100258SXin LI 19009100258SXin LI if ( verifySHA($TMP_FILE) ) { 19109100258SXin LI move_file($TMP_FILE, $LEAPFILE); 19209100258SXin LI chmod $FILE_MODE, $LEAPFILE; 19309100258SXin LI logger(LOG_INFO, "Installed new $LEAPFILE from $LEAPSRC"); 19409100258SXin LI } 19509100258SXin LI else { 19609100258SXin LI logger(LOG_ERR, "Downloaded file $TMP_FILE rejected -- saved for diagnosis"); 19709100258SXin LI move_file($TMP_FILE, 'leap-seconds.list_corrupt'); 19809100258SXin LI exit 1; 19909100258SXin LI } 20009100258SXin LI # Fall through 20109100258SXin LI exit 0; 20209100258SXin LI } 20309100258SXin LI 20409100258SXin LI # Failure 20509100258SXin LI unlink $TMP_FILE; 20609100258SXin LI logger(LOG_INFO, "Download failed. Waiting $INTERVAL minutes before retrying..."); 20709100258SXin LI sleep $INTERVAL * 60 ; 20809100258SXin LI } 20909100258SXin LI 21009100258SXin LI # Failed and out of retries 21109100258SXin LI log_fatal(LOG_ERR, "Download from $LEAPSRC failed after $MAXTRIES attempts"); 21209100258SXin LI} 21309100258SXin LI 21409100258SXin LIlogger(LOG_INFO, "Not time to replace $LEAPFILE"); 21509100258SXin LI 21609100258SXin LIexit 0; 21709100258SXin LI 21809100258SXin LI######## SUB ROUTINES ######### 21909100258SXin LIsub move_file { 22009100258SXin LI 22109100258SXin LI (my $src, my $dst) = @_; 22209100258SXin LI 22309100258SXin LI if ( move($src, $dst) ) { 22409100258SXin LI logger(LOG_DEBUG, "Moved $src to $dst"); 22509100258SXin LI } 22609100258SXin LI else { 22709100258SXin LI log_fatal(LOG_ERR, "Moving $src to $dst failed: $!"); 22809100258SXin LI } 22909100258SXin LI} 23009100258SXin LI 23109100258SXin LI# Removes temp file if terminating signal recv'd 23209100258SXin LIsub signal_catcher { 23309100258SXin LI my $signame = shift; 23409100258SXin LI 23509100258SXin LI close $TMP_FH; 23609100258SXin LI unlink $TMP_FILE; 23709100258SXin LI log_fatal(LOG_INFO, "Recv'd SIG${signame}. Terminating."); 23809100258SXin LI} 23909100258SXin LI 24009100258SXin LIsub log_fatal { 24109100258SXin LI my ($p, $msg) = @_; 24209100258SXin LI logger($p, $msg); 24309100258SXin LI exit 1; 24409100258SXin LI} 24509100258SXin LI 24609100258SXin LIsub logger { 24709100258SXin LI my ($p, $msg) = @_; 24809100258SXin LI 24909100258SXin LI # Suppress LOG_DEBUG msgs unless $DEBUG set 25009100258SXin LI return if (!$DEBUG && $p eq LOG_DEBUG); 25109100258SXin LI 25209100258SXin LI # Suppress all but LOG_ERR msgs if $QUIET set 25309100258SXin LI return if ($QUIET && $p ne LOG_ERR); 25409100258SXin LI 25509100258SXin LI if ($TOTERM) { 25609100258SXin LI if ($p eq LOG_ERR) { # errors should go to STDERR 25709100258SXin LI print STDERR "$msg\n"; 25809100258SXin LI } 25909100258SXin LI else { 26009100258SXin LI print STDOUT "$msg\n"; 26109100258SXin LI } 26209100258SXin LI } 26309100258SXin LI 26409100258SXin LI if ($SYSLOG) { 26509100258SXin LI syslog($p, $msg) 26609100258SXin LI } 26709100258SXin LI} 26809100258SXin LI 26909100258SXin LI################################# 27009100258SXin LI# Connect to server and retrieve file 27109100258SXin LI# 27209100258SXin LI# Since we make as many as $MAXTRIES attempts to connect to the remote 27309100258SXin LI# server to download the file, the network socket should be closed after 27409100258SXin LI# each attempt, rather than let it be reused (because it may be in some 27509100258SXin LI# unknown state). 27609100258SXin LI# 27709100258SXin LI# HTTP::Tiny doesn't export a method to explicitly close a connected 27809100258SXin LI# socket, therefore, we instantiate the lexically scoped $http object in 27909100258SXin LI# a function; when the function returns, the object goes out of scope 28009100258SXin LI# and is destroyed, closing the socket. 28109100258SXin LIsub retrieve_file { 28209100258SXin LI 28309100258SXin LI my $fh = shift; 28409100258SXin LI my $http; 28509100258SXin LI 28609100258SXin LI if ($LEAPSRC =~ /^https\S+/) { 28709100258SXin LI $http = HTTP::Tiny->new(%SSL_ATTRS); 28809100258SXin LI (my $ok, my $why) = $http->can_ssl; 28909100258SXin LI log_fatal(LOG_ERR, "TLS/SSL config error: $why") if ! $ok; 29009100258SXin LI } 29109100258SXin LI else { 29209100258SXin LI $http = HTTP::Tiny->new(); 29309100258SXin LI } 29409100258SXin LI 29509100258SXin LI my $reply = $http->get($LEAPSRC); 29609100258SXin LI 29709100258SXin LI if ($reply->{success}) { 29809100258SXin LI logger(LOG_DEBUG, "Download of $LEAPSRC succeeded"); 29909100258SXin LI print $fh $reply->{content} || 30009100258SXin LI log_fatal(LOG_ERR, "Couldn't write new file contents to temp file: $!"); 30109100258SXin LI close $fh; 30209100258SXin LI return 1; 30309100258SXin LI } 30409100258SXin LI else { 30509100258SXin LI close $fh; 30609100258SXin LI return 0; 30709100258SXin LI } 30809100258SXin LI} 30909100258SXin LI 31009100258SXin LI######################## 31109100258SXin LI# Validate a leap-seconds file checksum 31209100258SXin LI# 31309100258SXin LI# File format: (full description in file) 31409100258SXin LI# Pound sign (#) marks comments, EXCEPT: 31509100258SXin LI# #$ number : the NTP date of the last update 31609100258SXin LI# #@ number : the NTP date that the file expires 31709100258SXin LI# #h hex hex hex hex hex : the SHA-1 checksum of the data & dates, 31809100258SXin LI# excluding whitespace w/o leading zeroes 31909100258SXin LI# 32009100258SXin LI# Date (seconds since 1900) leaps : leaps is the # of seconds to add 32109100258SXin LI# for times >= Date 32209100258SXin LI# Date lines have comments. 32309100258SXin LI# 32409100258SXin LI# Returns: 32509100258SXin LI# 0 Invalid Checksum/Expired 32609100258SXin LI# 1 File is valid 32709100258SXin LI 32809100258SXin LIsub verifySHA { 32909100258SXin LI 33009100258SXin LI my $file = shift; 33109100258SXin LI my $fh; 33209100258SXin LI my $data; 33309100258SXin LI my $FSHA; 33409100258SXin LI 33509100258SXin LI open $fh, '<', $file || log_fatal(LOG_ERR, "Can't open $file: $!"); 33609100258SXin LI 33709100258SXin LI # Remove comments, except those that are markers for last update, 33809100258SXin LI # expires and hash 33909100258SXin LI while (<$fh>) { 34009100258SXin LI if (/^#\$/) { 34109100258SXin LI s/^..//; 34209100258SXin LI $data .= $_; 34309100258SXin LI } 34409100258SXin LI elsif (/^#\@/) { 34509100258SXin LI s/^..//; 34609100258SXin LI $data .= $_; 34709100258SXin LI s/\s+//g; 34809100258SXin LI $EXPIRES = $_ - 2208988800; 34909100258SXin LI } 35009100258SXin LI elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) { 35109100258SXin LI chomp; 35209100258SXin LI $FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5); 35309100258SXin LI } 35409100258SXin LI elsif (/^#/) { 35509100258SXin LI # ignore it 35609100258SXin LI } 35709100258SXin LI elsif (/^\d/) { 35809100258SXin LI s/#.*$//; 35909100258SXin LI $data .= $_; 36009100258SXin LI } 36109100258SXin LI else { 36209100258SXin LI chomp; 36309100258SXin LI print "Unexpected line: <$_>\n"; 36409100258SXin LI } 36509100258SXin LI } 36609100258SXin LI close $fh; 36709100258SXin LI 36809100258SXin LI if ( $EXPIRES < time() ) { 36909100258SXin LI logger(LOG_DEBUG, 'File expired on ' . gmtime($EXPIRES)); 37009100258SXin LI return 0; 37109100258SXin LI } 37209100258SXin LI 37309100258SXin LI if (! $FSHA) { 37409100258SXin LI logger(LOG_NOTICE, "no checksum record found in file"); 37509100258SXin LI return 0; 37609100258SXin LI } 37709100258SXin LI 37809100258SXin LI # Remove all white space 37909100258SXin LI $data =~ s/\s//g; 38009100258SXin LI 38109100258SXin LI # Compute the SHA hash of the data, removing the marker and filename 38209100258SXin LI # Computed in binary mode, which shouldn't matter since whitespace has been removed 38309100258SXin LI my $DSHA = sha1_hex($data); 38409100258SXin LI 38509100258SXin LI if ($FSHA eq $DSHA) { 38609100258SXin LI logger(LOG_DEBUG, "Checksum of $file validated"); 38709100258SXin LI return 1; 38809100258SXin LI } 38909100258SXin LI else { 39009100258SXin LI logger(LOG_NOTICE, "Checksum of $file is invalid EXPECTED: $FSHA COMPUTED: $DSHA"); 39109100258SXin LI return 0; 39209100258SXin LI } 39309100258SXin LI} 39409100258SXin LI 39509100258SXin LIsub show_help { 39609100258SXin LIprint <<EOF 39709100258SXin LI 39809100258SXin LIUsage: $PROG [options] 399276da39aSCy Schubert 400276da39aSCy SchubertVerifies and if necessary, updates leap-second definition file 401276da39aSCy Schubert 402276da39aSCy SchubertAll arguments are optional: Default (or current value) shown: 40309100258SXin LI -C Absolute path to CA Cert (see SSL/TLS Considerations) 40409100258SXin LI -D Path to a CAdir (see SSL/TLS Considerations) 405276da39aSCy Schubert -e Specify how long (in days) before expiration the file is to be 406276da39aSCy Schubert refreshed. Note that larger values imply more frequent refreshes. 40709100258SXin LI $PREFETCH 408276da39aSCy Schubert -F Force update even if current file is OK and not close to expiring. 40909100258SXin LI -f Absolute path ntp.conf file (default /etc/ntp.conf) 41009100258SXin LI $NTPCONF 41109100258SXin LI -h show help 412276da39aSCy Schubert -i Specify number of minutes between retries 413276da39aSCy Schubert $INTERVAL 41409100258SXin LI -L Absolute path to leapfile on the local system 41509100258SXin LI (overrides value in ntp.conf) 41609100258SXin LI -l Specify the syslog(3) facility for logging 417276da39aSCy Schubert $LOGFAC 41809100258SXin LI -q Only report errors (cannot be used with -v) 41909100258SXin LI -r Specify number of attempts to retrieve file 42009100258SXin LI $MAXTRIES 42109100258SXin LI -s Send output to syslog(3) - implied if STDOUT has no tty or redirected 42209100258SXin LI -t Send output to terminal - implied if STDOUT attached to terminal 42309100258SXin LI -u Specify the URL of the master copy to download 42409100258SXin LI $LEAPSRC 42509100258SXin LI -v Verbose - show debug messages (cannot be used with -q) 426276da39aSCy Schubert 427276da39aSCy SchubertThe following options are not (yet) implemented in the perl version: 428276da39aSCy Schubert -4 Use only IPv4 429276da39aSCy Schubert -6 Use only IPv6 430276da39aSCy Schubert -c Command to restart NTP after installing a new file 431276da39aSCy Schubert <none> - ntpd checks file daily 432276da39aSCy Schubert -p 4|6 433276da39aSCy Schubert Prefer IPv4 or IPv6 (as specified) addresses, but use either 434276da39aSCy Schubert 43509100258SXin LI$PROG will validate the file currently on the local system. 436276da39aSCy Schubert 43709100258SXin LIOrdinarily, the leapfile is found using the 'leapfile' directive in 43809100258SXin LI$NTPCONF. However, an alternate location can be specified on the 43909100258SXin LIcommand line with the -L flag. 440276da39aSCy Schubert 44109100258SXin LIIf the leapfile does not exist, is not valid, has expired, or is 44209100258SXin LIexpiring soon, a new copy will be downloaded. If the new copy is 44309100258SXin LIvalid, it is installed. 444276da39aSCy Schubert 445276da39aSCy SchubertIf the current file is acceptable, no download or restart occurs. 446276da39aSCy Schubert 44709100258SXin LIThis can be run as a cron job. As the file is rarely updated, and 44809100258SXin LIleap seconds are announced at least one month in advance (usually 44909100258SXin LIlonger), it need not be run more frequently than about once every 45009100258SXin LIthree weeks. 451276da39aSCy Schubert 45209100258SXin LISSL/TLS Considerations 45309100258SXin LI----------------------- 45409100258SXin LIThe perl modules can usually locate the CA certificate used to verify 45509100258SXin LIthe peer's identity. 456276da39aSCy Schubert 45709100258SXin LIOn BSDs, the default is typically the file /etc/ssl/certs.pem. On 45809100258SXin LILinux, the location is typically a path to a CAdir - a directory of 45909100258SXin LIsymlinks named according to a hash of the certificates' subject names. 460276da39aSCy Schubert 46109100258SXin LIThe -C or -D options are available to pass in a location if no CA cert 46209100258SXin LIis found in the default location. 463276da39aSCy Schubert 46409100258SXin LIExternal Dependencies 46509100258SXin LI--------------------- 46609100258SXin LIThe following perl modules are required: 46709100258SXin LIHTTP::Tiny - version >= 0.056 46809100258SXin LIIO::Socket::SSL - version >= 1.56 46909100258SXin LINET::SSLeay - version >= 1.49 470276da39aSCy Schubert 47109100258SXin LIVersion: $VERSION 472276da39aSCy Schubert 47309100258SXin LIEOF 474276da39aSCy Schubert} 475276da39aSCy Schubert 476