1#! @PATH_PERL@ -w 2 3# Copyright (C) 2015, 2017 Network Time Foundation 4# Author: Harlan Stenn 5# 6# General cleanup and https support: Paul McMath 7# 8# Original shell version: 9# Copyright (C) 2014 Timothe Litt litt at acm dot org 10# 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 15# author. 16 17######## BEGIN ######### 18use strict; 19 20# Core modules 21use Digest::SHA qw(sha1_hex); 22use File::Basename; 23use File::Copy qw(move); 24use File::Temp qw(tempfile); 25use Getopt::Long qw(:config auto_help no_ignore_case bundling); 26use Sys::Syslog qw(:standard :macros); 27 28# External modules 29use HTTP::Tiny 0.056; 30use Net::SSLeay 1.49; 31use IO::Socket::SSL 1.56; 32 33my $VERSION = '1.004'; 34 35my $RUN_DIR = '/tmp'; 36my $RUN_UID = 0; 37my $TMP_FILE; 38my $TMP_FH; 39my $FILE_MODE = 0644; 40 41######## DEFAULT CONFIGURATION ########## 42# LEAP FILE SRC URIS 43# HTTPS - (default) 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 47 48my $LEAPSRC = 'https://www.ietf.org/timezones/data/leap-seconds.list'; 49my $LEAPFILE; 50 51# How many times to try to download new file 52my $MAXTRIES = 6; 53my $INTERVAL = 10; 54 55my $NTPCONF='/etc/ntp.conf'; 56 57# How long (in days) before expiration to get updated file 58my $PREFETCH = 60; 59my $EXPIRES; 60my $FORCE; 61 62# Output Flags 63my $QUIET; 64my $DEBUG; 65my $SYSLOG; 66my $TOTERM; 67my $LOGFAC = 'LOG_USER'; 68 69######### PARSE/SET OPTIONS ######### 70my %SSL_OPTS; 71my %SSL_ATTRS = ( 72 verify_SSL => 1, 73 SSL_options => \%SSL_OPTS, 74); 75 76our(%opt); 77 78GetOptions(\%opt, 79 'C=s', 80 'D=s', 81 'e:60', 82 'F', 83 'f=s', 84 'h|help', 85 'i:10', 86 'L=s', 87 'l=s', 88 'q', 89 'r:6', 90 's', 91 't', 92 'u=s', 93 'v', 94 ); 95 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}; 103 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}; 109 110$SSL_OPTS{SSL_ca_file} = $opt{C} if (defined($opt{C})); 111$SSL_OPTS{SSL_ca_path} = $opt{D} if (defined($opt{D})); 112 113############### 114## START MAIN 115############### 116my $PROG = basename($0); 117 118# Logging - Default is to use syslog(3) if STDOUT isn't 119# connected to a tty. 120if ($SYSLOG || !-t STDOUT) { 121 $SYSLOG = 1; 122 openlog($PROG, 'pid', $LOGFAC); 123} 124else { 125 $TOTERM = 1; 126} 127 128if (defined $opt{q} && defined $opt{v}) { 129 log_fatal(LOG_ERR, '-q and -v options mutually exclusive'); 130} 131 132if (defined $opt{L} && defined $opt{f}) { 133 log_fatal(LOG_ERR, '-L and -f options mutually exclusive'); 134} 135 136$SIG{INT} = \&signal_catcher; 137$SIG{TERM} = \&signal_catcher; 138$SIG{QUIT} = \&signal_catcher; 139 140# Take some security precautions 141close STDIN; 142 143# Show help 144if (defined $opt{h}) { 145 show_help(); 146 exit 0; 147} 148 149if ($< != $RUN_UID) { 150 log_fatal(LOG_ERR, 'User ' . getpwuid($<) . " (UID $<) tried to run $PROG"); 151} 152 153chdir $RUN_DIR || log_fatal("Failed to change dir to $RUN_DIR"); 154 155# Parse ntp.conf for path to leapfile if not set by user 156if (! $LEAPFILE) { 157 158 open my $LF, '<', $NTPCONF || log_fatal(LOG_ERR, "Can't open <$NTPCONF>: $!"); 159 160 while (<$LF>) { 161 chomp; 162 $LEAPFILE = $1 if /^ *leapfile\s+"(\S+)"/; 163 } 164 close $LF; 165 166 if (! $LEAPFILE) { 167 log_fatal(LOG_ERR, "No leapfile directive in $NTPCONF; leapfile location not known"); 168 } 169} 170 171-s $LEAPFILE || logger(LOG_DEBUG, "Leapfile $LEAPFILE is empty"); 172 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 178 179if ( !-e $LEAPFILE || $FORCE || ! verifySHA($LEAPFILE) || 180 ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) { 181 182 for (my $try = 1; $try <= $MAXTRIES; $try++) { 183 logger(LOG_DEBUG, "Attempting download from $LEAPSRC, try $try.."); 184 185 ($TMP_FH, $TMP_FILE) = tempfile(UNLINK => 1, SUFFIX => '.list'); 186 187 if (retrieve_file($TMP_FH)) { 188 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"); 193 } 194 else { 195 logger(LOG_ERR, "Downloaded file $TMP_FILE rejected -- saved for diagnosis"); 196 move_file($TMP_FILE, 'leap-seconds.list_corrupt'); 197 exit 1; 198 } 199 # Fall through 200 exit 0; 201 } 202 203 # Failure 204 unlink $TMP_FILE; 205 logger(LOG_INFO, "Download failed. Waiting $INTERVAL minutes before retrying..."); 206 sleep $INTERVAL * 60 ; 207 } 208 209 # Failed and out of retries 210 log_fatal(LOG_ERR, "Download from $LEAPSRC failed after $MAXTRIES attempts"); 211} 212 213logger(LOG_INFO, "Not time to replace $LEAPFILE"); 214 215exit 0; 216 217######## SUB ROUTINES ######### 218sub move_file { 219 220 (my $src, my $dst) = @_; 221 222 if ( move($src, $dst) ) { 223 logger(LOG_DEBUG, "Moved $src to $dst"); 224 } 225 else { 226 log_fatal(LOG_ERR, "Moving $src to $dst failed: $!"); 227 } 228} 229 230# Removes temp file if terminating signal recv'd 231sub signal_catcher { 232 my $signame = shift; 233 234 close $TMP_FH; 235 unlink $TMP_FILE; 236 log_fatal(LOG_INFO, "Recv'd SIG${signame}. Terminating."); 237} 238 239sub log_fatal { 240 my ($p, $msg) = @_; 241 logger($p, $msg); 242 exit 1; 243} 244 245sub logger { 246 my ($p, $msg) = @_; 247 248 # Suppress LOG_DEBUG msgs unless $DEBUG set 249 return if (!$DEBUG && $p eq LOG_DEBUG); 250 251 # Suppress all but LOG_ERR msgs if $QUIET set 252 return if ($QUIET && $p ne LOG_ERR); 253 254 if ($TOTERM) { 255 if ($p eq LOG_ERR) { # errors should go to STDERR 256 print STDERR "$msg\n"; 257 } 258 else { 259 print STDOUT "$msg\n"; 260 } 261 } 262 263 if ($SYSLOG) { 264 syslog($p, $msg) 265 } 266} 267 268################################# 269# Connect to server and retrieve file 270# 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 274# unknown state). 275# 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. 280sub retrieve_file { 281 282 my $fh = shift; 283 my $http; 284 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; 289 } 290 else { 291 $http = HTTP::Tiny->new(); 292 } 293 294 my $reply = $http->get($LEAPSRC); 295 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: $!"); 300 close $fh; 301 return 1; 302 } 303 else { 304 close $fh; 305 return 0; 306 } 307} 308 309######################## 310# Validate a leap-seconds file checksum 311# 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 318# 319# Date (seconds since 1900) leaps : leaps is the # of seconds to add 320# for times >= Date 321# Date lines have comments. 322# 323# Returns: 324# 0 Invalid Checksum/Expired 325# 1 File is valid 326 327sub verifySHA { 328 329 my $file = shift; 330 my $fh; 331 my $data; 332 my $FSHA; 333 334 open $fh, '<', $file || log_fatal(LOG_ERR, "Can't open $file: $!"); 335 336 # Remove comments, except those that are markers for last update, 337 # expires and hash 338 while (<$fh>) { 339 if (/^#\$/) { 340 s/^..//; 341 $data .= $_; 342 } 343 elsif (/^#\@/) { 344 s/^..//; 345 $data .= $_; 346 s/\s+//g; 347 $EXPIRES = $_ - 2208988800; 348 } 349 elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) { 350 chomp; 351 $FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5); 352 } 353 elsif (/^#/) { 354 # ignore it 355 } 356 elsif (/^\d/) { 357 s/#.*$//; 358 $data .= $_; 359 } 360 else { 361 chomp; 362 print "Unexpected line: <$_>\n"; 363 } 364 } 365 close $fh; 366 367 if ( $EXPIRES < time() ) { 368 logger(LOG_DEBUG, 'File expired on ' . gmtime($EXPIRES)); 369 return 0; 370 } 371 372 if (! $FSHA) { 373 logger(LOG_NOTICE, "no checksum record found in file"); 374 return 0; 375 } 376 377 # Remove all white space 378 $data =~ s/\s//g; 379 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); 383 384 if ($FSHA eq $DSHA) { 385 logger(LOG_DEBUG, "Checksum of $file validated"); 386 return 1; 387 } 388 else { 389 logger(LOG_NOTICE, "Checksum of $file is invalid EXPECTED: $FSHA COMPUTED: $DSHA"); 390 return 0; 391 } 392} 393 394sub show_help { 395print <<EOF 396 397Usage: $PROG [options] 398 399Verifies and if necessary, updates leap-second definition file 400 401All 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. 406 $PREFETCH 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) 409 $NTPCONF 410 -h show help 411 -i Specify number of minutes between retries 412 $INTERVAL 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 416 $LOGFAC 417 -q Only report errors (cannot be used with -v) 418 -r Specify number of attempts to retrieve file 419 $MAXTRIES 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 423 $LEAPSRC 424 -v Verbose - show debug messages (cannot be used with -q) 425 426The following options are not (yet) implemented in the perl version: 427 -4 Use only IPv4 428 -6 Use only IPv6 429 -c Command to restart NTP after installing a new file 430 <none> - ntpd checks file daily 431 -p 4|6 432 Prefer IPv4 or IPv6 (as specified) addresses, but use either 433 434$PROG will validate the file currently on the local system. 435 436Ordinarily, the leapfile is found using the 'leapfile' directive in 437$NTPCONF. However, an alternate location can be specified on the 438command line with the -L flag. 439 440If the leapfile does not exist, is not valid, has expired, or is 441expiring soon, a new copy will be downloaded. If the new copy is 442valid, it is installed. 443 444If the current file is acceptable, no download or restart occurs. 445 446This can be run as a cron job. As the file is rarely updated, and 447leap seconds are announced at least one month in advance (usually 448longer), it need not be run more frequently than about once every 449three weeks. 450 451SSL/TLS Considerations 452----------------------- 453The perl modules can usually locate the CA certificate used to verify 454the peer's identity. 455 456On BSDs, the default is typically the file /etc/ssl/certs.pem. On 457Linux, the location is typically a path to a CAdir - a directory of 458symlinks named according to a hash of the certificates' subject names. 459 460The -C or -D options are available to pass in a location if no CA cert 461is found in the default location. 462 463External Dependencies 464--------------------- 465The following perl modules are required: 466HTTP::Tiny - version >= 0.056 467IO::Socket::SSL - version >= 1.56 468NET::SSLeay - version >= 1.49 469 470Version: $VERSION 471 472EOF 473} 474 475