3 # Copyright (C) 2015, 2017 Network Time Foundation
6 # General cleanup and https support: Paul McMath
8 # Original shell version:
9 # Copyright (C) 2014 Timothe Litt litt at acm dot org
11 # This script may be freely copied, used and modified providing that
12 # this notice and the copyright statement are included in all copies
13 # and derivative works. No warranty is offered, and use is entirely at
14 # your own risk. Bugfixes and improvements would be appreciated by the
17 ######## BEGIN #########
21 use Digest::SHA qw(sha1_hex);
23 use File::Copy qw(move);
24 use File::Temp qw(tempfile);
25 use Getopt::Long qw(:config auto_help no_ignore_case bundling);
26 use Sys::Syslog qw(:standard :macros);
31 use IO::Socket::SSL 1.56;
33 my $VERSION = '1.004';
41 ######## DEFAULT CONFIGURATION ##########
44 # https://www.ietf.org/timezones/data/leap-seconds
45 # HTTP - No TLS/SSL - (not recommended)
46 # http://www.ietf.org/timezones/data/leap-seconds.list
48 my $LEAPSRC = 'https://www.ietf.org/timezones/data/leap-seconds.list';
51 # How many times to try to download new file
55 my $NTPCONF='/etc/ntp.conf';
57 # How long (in days) before expiration to get updated file
67 my $LOGFAC = 'LOG_USER';
69 ######### PARSE/SET OPTIONS #########
73 SSL_options => \%SSL_OPTS,
96 $LOGFAC = $opt{l} if defined $opt{l};
97 $LEAPSRC = $opt{u} if defined $opt{u};
98 $LEAPFILE = $opt{L} if defined $opt{L};
99 $PREFETCH = $opt{e} if defined $opt{e};
100 $NTPCONF = $opt{f} if defined $opt{f};
101 $MAXTRIES = $opt{r} if defined $opt{r};
102 $INTERVAL = $opt{i} if defined $opt{i};
104 $FORCE = 1 if defined $opt{F};
105 $DEBUG = 1 if defined $opt{v};
106 $QUIET = 1 if defined $opt{q};
107 $SYSLOG = 1 if defined $opt{s};
108 $TOTERM = 1 if defined $opt{t};
110 $SSL_OPTS{SSL_ca_file} = $opt{C} if (defined($opt{C}));
111 $SSL_OPTS{SSL_ca_path} = $opt{D} if (defined($opt{D}));
116 my $PROG = basename($0);
118 # Logging - Default is to use syslog(3) if STDOUT isn't
119 # connected to a tty.
120 if ($SYSLOG || !-t STDOUT) {
122 openlog($PROG, 'pid', $LOGFAC);
128 if (defined $opt{q} && defined $opt{v}) {
129 log_fatal(LOG_ERR, '-q and -v options mutually exclusive');
132 if (defined $opt{L} && defined $opt{f}) {
133 log_fatal(LOG_ERR, '-L and -f options mutually exclusive');
136 $SIG{INT} = \&signal_catcher;
137 $SIG{TERM} = \&signal_catcher;
138 $SIG{QUIT} = \&signal_catcher;
140 # Take some security precautions
144 if (defined $opt{h}) {
149 if ($< != $RUN_UID) {
150 log_fatal(LOG_ERR, 'User ' . getpwuid($<) . " (UID $<) tried to run $PROG");
153 chdir $RUN_DIR || log_fatal("Failed to change dir to $RUN_DIR");
155 # Parse ntp.conf for path to leapfile if not set by user
158 open my $LF, '<', $NTPCONF || log_fatal(LOG_ERR, "Can't open <$NTPCONF>: $!");
162 $LEAPFILE = $1 if /^ *leapfile\s+"(\S+)"/;
167 log_fatal(LOG_ERR, "No leapfile directive in $NTPCONF; leapfile location not known");
171 -s $LEAPFILE || logger(LOG_DEBUG, "Leapfile $LEAPFILE is empty");
173 # Download new file if:
174 # 1. file doesn't exist
175 # 2. invoked w/ force flag (-F)
176 # 3. current file isn't valid
177 # 4. current file expired or expires soon
179 if ( !-e $LEAPFILE || $FORCE || ! verifySHA($LEAPFILE) ||
180 ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) {
182 for (my $try = 1; $try <= $MAXTRIES; $try++) {
183 logger(LOG_DEBUG, "Attempting download from $LEAPSRC, try $try..");
185 ($TMP_FH, $TMP_FILE) = tempfile(UNLINK => 1, SUFFIX => '.list');
187 if (retrieve_file($TMP_FH)) {
189 if ( verifySHA($TMP_FILE) ) {
190 move_file($TMP_FILE, $LEAPFILE);
191 chmod $FILE_MODE, $LEAPFILE;
192 logger(LOG_INFO, "Installed new $LEAPFILE from $LEAPSRC");
195 logger(LOG_ERR, "Downloaded file $TMP_FILE rejected -- saved for diagnosis");
196 move_file($TMP_FILE, 'leap-seconds.list_corrupt');
205 logger(LOG_INFO, "Download failed. Waiting $INTERVAL minutes before retrying...");
206 sleep $INTERVAL * 60 ;
209 # Failed and out of retries
210 log_fatal(LOG_ERR, "Download from $LEAPSRC failed after $MAXTRIES attempts");
213 logger(LOG_INFO, "Not time to replace $LEAPFILE");
217 ######## SUB ROUTINES #########
220 (my $src, my $dst) = @_;
222 if ( move($src, $dst) ) {
223 logger(LOG_DEBUG, "Moved $src to $dst");
226 log_fatal(LOG_ERR, "Moving $src to $dst failed: $!");
230 # Removes temp file if terminating signal recv'd
236 log_fatal(LOG_INFO, "Recv'd SIG${signame}. Terminating.");
248 # Suppress LOG_DEBUG msgs unless $DEBUG set
249 return if (!$DEBUG && $p eq LOG_DEBUG);
251 # Suppress all but LOG_ERR msgs if $QUIET set
252 return if ($QUIET && $p ne LOG_ERR);
255 if ($p eq LOG_ERR) { # errors should go to STDERR
256 print STDERR "$msg\n";
259 print STDOUT "$msg\n";
268 #################################
269 # Connect to server and retrieve file
271 # Since we make as many as $MAXTRIES attempts to connect to the remote
272 # server to download the file, the network socket should be closed after
273 # each attempt, rather than let it be reused (because it may be in some
276 # HTTP::Tiny doesn't export a method to explicitly close a connected
277 # socket, therefore, we instantiate the lexically scoped $http object in
278 # a function; when the function returns, the object goes out of scope
279 # and is destroyed, closing the socket.
285 if ($LEAPSRC =~ /^https\S+/) {
286 $http = HTTP::Tiny->new(%SSL_ATTRS);
287 (my $ok, my $why) = $http->can_ssl;
288 log_fatal(LOG_ERR, "TLS/SSL config error: $why") if ! $ok;
291 $http = HTTP::Tiny->new();
294 my $reply = $http->get($LEAPSRC);
296 if ($reply->{success}) {
297 logger(LOG_DEBUG, "Download of $LEAPSRC succeeded");
298 print $fh $reply->{content} ||
299 log_fatal(LOG_ERR, "Couldn't write new file contents to temp file: $!");
309 ########################
310 # Validate a leap-seconds file checksum
312 # File format: (full description in file)
313 # Pound sign (#) marks comments, EXCEPT:
314 # #$ number : the NTP date of the last update
315 # #@ number : the NTP date that the file expires
316 # #h hex hex hex hex hex : the SHA-1 checksum of the data & dates,
317 # excluding whitespace w/o leading zeroes
319 # Date (seconds since 1900) leaps : leaps is the # of seconds to add
321 # Date lines have comments.
324 # 0 Invalid Checksum/Expired
334 open $fh, '<', $file || log_fatal(LOG_ERR, "Can't open $file: $!");
336 # Remove comments, except those that are markers for last update,
347 $EXPIRES = $_ - 2208988800;
349 elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) {
351 $FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5);
362 print "Unexpected line: <$_>\n";
367 if ( $EXPIRES < time() ) {
368 logger(LOG_DEBUG, 'File expired on ' . gmtime($EXPIRES));
373 logger(LOG_NOTICE, "no checksum record found in file");
377 # Remove all white space
380 # Compute the SHA hash of the data, removing the marker and filename
381 # Computed in binary mode, which shouldn't matter since whitespace has been removed
382 my $DSHA = sha1_hex($data);
384 if ($FSHA eq $DSHA) {
385 logger(LOG_DEBUG, "Checksum of $file validated");
389 logger(LOG_NOTICE, "Checksum of $file is invalid EXPECTED: $FSHA COMPUTED: $DSHA");
397 Usage: $PROG [options]
399 Verifies and if necessary, updates leap-second definition file
401 All arguments are optional: Default (or current value) shown:
402 -C Absolute path to CA Cert (see SSL/TLS Considerations)
403 -D Path to a CAdir (see SSL/TLS Considerations)
404 -e Specify how long (in days) before expiration the file is to be
405 refreshed. Note that larger values imply more frequent refreshes.
407 -F Force update even if current file is OK and not close to expiring.
408 -f Absolute path ntp.conf file (default /etc/ntp.conf)
411 -i Specify number of minutes between retries
413 -L Absolute path to leapfile on the local system
414 (overrides value in ntp.conf)
415 -l Specify the syslog(3) facility for logging
417 -q Only report errors (cannot be used with -v)
418 -r Specify number of attempts to retrieve file
420 -s Send output to syslog(3) - implied if STDOUT has no tty or redirected
421 -t Send output to terminal - implied if STDOUT attached to terminal
422 -u Specify the URL of the master copy to download
424 -v Verbose - show debug messages (cannot be used with -q)
426 The following options are not (yet) implemented in the perl version:
429 -c Command to restart NTP after installing a new file
430 <none> - ntpd checks file daily
432 Prefer IPv4 or IPv6 (as specified) addresses, but use either
434 $PROG will validate the file currently on the local system.
436 Ordinarily, the leapfile is found using the 'leapfile' directive in
437 $NTPCONF. However, an alternate location can be specified on the
438 command line with the -L flag.
440 If the leapfile does not exist, is not valid, has expired, or is
441 expiring soon, a new copy will be downloaded. If the new copy is
442 valid, it is installed.
444 If the current file is acceptable, no download or restart occurs.
446 This can be run as a cron job. As the file is rarely updated, and
447 leap seconds are announced at least one month in advance (usually
448 longer), it need not be run more frequently than about once every
451 SSL/TLS Considerations
452 -----------------------
453 The perl modules can usually locate the CA certificate used to verify
456 On BSDs, the default is typically the file /etc/ssl/certs.pem. On
457 Linux, the location is typically a path to a CAdir - a directory of
458 symlinks named according to a hash of the certificates' subject names.
460 The -C or -D options are available to pass in a location if no CA cert
461 is found in the default location.
463 External Dependencies
464 ---------------------
465 The following perl modules are required:
466 HTTP::Tiny - version >= 0.056
467 IO::Socket::SSL - version >= 1.56
468 NET::SSLeay - version >= 1.49