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