More work on cross long long
[ntpsec.git] / ntpclients / ntpleapfetch
blob67a3dde1a008c43ca1a9e6761ca45f9f54aac88d
1 #! /bin/sh
3 # Copyright (C) 2014 Timothe Litt litt at acm dot org
4 # Modified 20180105 Sanjeev Gupta ghane0@gmail.com
6 # SPDX-License-Identifier: BSD-2-Clause
8 # Bugfixes and improvements would be appreciated by the author.
10 # leap-seconds file manager/updater
12 # Depends on:
13 # wget sed, tr, shasum/sha1sum, logger
15 # This file needs to remain free of bash-isms. Make sure it
16 # runs on plain POSIX shell.
18 # GNU sed has many extensions not on macOS and FreeBSD. If
19 # you change near sed, test on FreeBSD.
21 # ########## Default configuration ##########
23 # Where to get the file
24 LEAPSRC="https://data.iana.org/time-zones/tzdb/leap-seconds.list"
26 # How many times to try to download new file
27 MAXTRIES=6
28 INTERVAL=10
30 # Where to find ntp config file
31 NTPCONF=/etc/ntp.conf
33 # How long before expiration to get updated file
34 PREFETCH="60 days"
36 # How to restart NTP - older NTP: service ntpd? try-restart | condrestart
37 # Recent NTP checks for new file daily, so there's nothing to do
38 RESTART=
40 # Where to put temporary copy before it's validated
41 TMPFILE="/tmp/leap-seconds.$$.tmp"
43 # Syslog facility
44 LOGFAC=daemon
45 # ###########################################
47 # Places to look for commands. Allows for CRON having path to
48 # old utilities on embedded systems
50 PATHLIST="/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:"
52 REQUIREDCMDS=" wget logger tr sed"
54 SELF="`basename $0`"
56 displayHelp() {
57 cat <<EOF
58 Usage: $SELF [options] [leapfile]
60 Verifies and if necessary, updates leap-second definition file
62 All arguments are optional: Default (or current value) shown:
63 -s Specify the URL of the master copy to download
64 $LEAPSRC
65 -4 Use only IPv4
66 -6 Use only IPv6
67 -p 4|6
68 Prefer IPv4 or IPv6 (as specified) addresses, but use either
69 -e Specify how long before expiration the file is to be refreshed
70 Units are required, e.g. "-e 60 days" Note that larger values
71 imply more frequent refreshes.
72 "$PREFETCH"
73 -f Specify location of ntp.conf (used to make sure leapfile directive is
74 present and to default leapfile)
75 $NTPCONF
76 -F Force update even if current file is OK and not close to expiring.
77 -c Command to restart NTP after installing a new file
78 <none> - ntpd checks file daily
79 -r Specify number of times to retry on get failure
80 $MAXTRIES
81 -i Specify number of minutes between retries
82 $INTERVAL
83 -l Use syslog for output (Implied if CRONJOB is set)
84 -L Don't use syslog for output
85 -P Specify the syslog facility for logging
86 $LOGFAC
87 -t Name of temporary file used in validation
88 -q Only report errors to stdout
89 -v Verbose output
90 -z Specify path for utilities
91 $PATHLIST
92 -Z Only use system path
93 -V Output version information and exit
95 $SELF will validate the file currently on the local system
97 Ordinarily, the file is found using the "leapfile" directive in $NTPCONF.
98 However, an alternate location can be specified on the command line.
100 If the file does not exist, is not valid, has expired, or is expiring soon,
101 a new copy will be downloaded. If the new copy validates, it is installed and
102 NTP is (optionally) restarted.
104 If the current file is acceptable, no download or restart occurs.
106 -c can also be used to invoke another script to perform administrative
107 functions, e.g. to copy the file to other local systems.
109 This can be run as a cron job. As the file is rarely updated, and leap
110 seconds are announced at least one month in advance (usually longer), it
111 need not be run more frequently than about once every three weeks.
113 For cron-friendly behavior, define CRONJOB=1 in the crontab.
115 This script depends on: sha1sum/shasum $REQUIREDCMDS
117 Version @NTPSEC_VERSION_EXTENDED@
119 return 0
122 displayVersion() {
123 cat <<EOF
124 ntpleapfetch ntpsec-@NTPSEC_VERSION_EXTENDED@
126 return 0
129 # Default: Use syslog for logging if running under cron
131 SYSLOG="$CRONJOB"
133 if [ "$1" = "--help" ]; then
134 displayHelp
135 exit 0
138 if [ "$1" = "--version" ]; then
139 displayVersion
140 exit 0
143 # Parse options
145 while getopts 46p:P:s:e:f:Fc:r:i:lLt:hqvz:ZV opt; do
146 case $opt in
148 PROTO="-4"
151 PROTO="-6"
154 if [ "$OPTARG" = '4' -o "$OPTARG" = '6' ]; then
155 PREFER="--prefer-family=IPv$OPTARG"
156 else
157 echo "Invalid -p $OPTARG" >&2
158 exit 1;
162 LOGFAC="$OPTARG"
165 LEAPSRC="$OPTARG"
168 PREFETCH="$OPTARG"
171 NTPCONF="$OPTARG"
174 FORCE="Y"
177 RESTART="$OPTARG"
180 MAXTRIES="$OPTARG"
183 INTERVAL="$OPTARG"
186 TMPFILE="$OPTARG"
189 SYSLOG="y"
192 SYSLOG=
195 displayHelp
196 exit 0
199 displayVersion
200 exit 0
203 QUIET="Y"
206 VERBOSE="Y"
209 PATHLIST="$OPTARG:"
212 PATHLIST=
215 echo "$SELF -h for usage" >&2
216 exit 1
218 esac
219 done
220 shift $((OPTIND-1))
222 export PATH="$PATHLIST$PATH"
224 # Add to path to deal with embedded systems
226 for P in $REQUIREDCMDS ; do
227 if >/dev/null 2>&1 command -v "$P" ; then
228 continue
230 [ "$P" = "logger" ] && continue
231 echo "FATAL: missing $P command, please install"
232 exit 1
233 done
235 # find sha1sum or shasum
236 if >/dev/null 2>&1 command -v "sha1sum" ; then
237 SHASUM="sha1sum"
238 elif >/dev/null 2>&1 command -v "shasum" ; then
239 SHASUM="shasum"
240 else
241 echo "FATAL: Can not find sha1sum or shasum command, please install"
242 exit 1
245 # Handle logging
247 if ! LOGGER="`2>/dev/null command -v logger`" ; then
248 LOGGER=
251 log() {
252 # "priority" "message"
254 # Stdout unless syslog specified or logger isn't available
256 if [ -z "$SYSLOG" -o -z "$LOGGER" ]; then
257 if [ -n "$QUIET" -a \( "$1" = "info" -o "$1" = "notice" -o "$1" = "debug" \) ]; then
258 return 0
260 echo "`echo \"$1\" | tr a-z A-Z`: $2"
261 return 0
264 # Also log to stdout if cron job && notice or higher
265 local S
266 if [ -n "$CRONJOB" -a \( "$1" != "info" \) -a \( "$1" != "debug" \) ] || [ -n "$VERBOSE" ]; then
267 S="-s"
269 $LOGGER $S -t "$SELF[$$]" -p "$LOGFAC.$1" "$2"
272 # Verify interval
273 INTERVAL=$(( $INTERVAL *1 ))
275 # Validate a leap-seconds file checksum
277 # File format: (full description in files)
278 # # marks comments, except:
279 # #$ number : the NTP date of the last update
280 # #@ number : the NTP date that the file expires
281 # Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date
282 # Date lines have comments.
283 # #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes
285 verifySHA1() {
287 if [ ! -f "$1" ]; then
288 return 1
291 # Remove comments, except those that are markers for last update, expires and hash
293 local RAW="`sed -ne'/^#[\$@h]/p;/^[^#]/s/.#.*$//p' $1 `"
295 # Extract just the data, removing all whitespace
297 local DATA="`echo \"$RAW\" | sed -e'/^#h/d' -e's/^#[\$@]//g' | tr -d '[:space:]'`"
299 # Compute the SHA-1 hash of the data, removing the marker and filename
300 # Computed in binary mode, which shouldn't matter since whitespace
301 # has been removed
302 # shasum/sha1sum comes in several flavors;
303 # a portable one is available in Perl (with Digest::SHA)
305 local DSHA1="`printf \"$DATA\" | $SHASUM | sed -e's/[? *].*$//'`"
307 # Extract the file's hash. Restore any leading zeroes in hash segments.
309 # The sed [] includes a tab (\t) and space; #h is followed by a tab and space
310 # or maybe a space and a tab. remove the tab, wherever it may be. Add some 0x
311 # so it can be run through printf to restore missing leading zeros.
313 local FSHA1="`grep '^#h' $1 | tr -d '[:cntrl:]' | sed -e's/^#h[ \t]*/0x/' -e's/ / 0x/g'`"
314 FSHA1=`printf '%08x%08x%08x%08x%08x' $FSHA1`
316 if [ -n "$FSHA1" -a \( "$FSHA1" = "$DSHA1" \) ]; then
317 if [ -n "$2" ]; then
318 log "info" "Checksum of $1 validated"
320 else
321 log "error" "Checksum of $1 is invalid:"
322 [ -z "$FSHA1" ] && FSHA1="(no checksum record found in file)"
323 log "error" "EXPECTED: $FSHA1"
324 log "error" "COMPUTED: $DSHA1"
325 return 1
328 # Check the expiration date, converting NTP epoch to Unix epoch used by date
330 EXPIRES="`echo \"$RAW\" | sed -e'/^#@/!d' -e's/^#@//' | tr -d '[:space:]'`"
331 EXPIRES="$(($EXPIRES - 2208988800 ))"
333 if [ $EXPIRES -lt `date -u +%s` ]; then
334 log "notice" "File expired on `date -u -d \"Jan 1, 1970 00:00:00 +0000 + $EXPIRES seconds\"`"
335 return 2
340 # Verify ntp.conf
342 if ! [ -f "$NTPCONF" ]; then
343 log "critical" "Missing ntp configuration $NTPCONF"
344 exit 1
347 # Parse ntp.conf for leapfile directive
348 LEAPFILE="`grep -x 'leapfile *.*' $NTPCONF | grep -o ' [^ ]*.*'`"
349 if [ -z "$LEAPFILE" ]; then
350 log "warning" "$NTPCONF does not specify a leapfile"
353 # Allow placing the file someplace else - testing
355 if [ -n "$1" ]; then
356 if [ "$1" != "$LEAPFILE" ]; then
357 log "notice" "Requested install to $1, but $NTPCONF specifies $LEAPFILE"
359 LEAPFILE="$1"
361 if [ -z "$LEAPFILE" ]; then
362 log "error" "No leapfile in ntp.conf or on the command line."
363 exit 1
366 # Verify the current file
367 # If it is missing, doesn't validate or expired
368 # Or is expiring soon
369 # Download a new one
371 if [ -n "$FORCE" ] || ! verifySHA1 $LEAPFILE "$VERBOSE" || [ $EXPIRES -lt `date -d "NOW + $PREFETCH" +%s` ] ; then
372 TRY=0
373 while true; do
374 TRY=$(( $TRY + 1 ))
375 if [ -n "$VERBOSE" ]; then
376 log "info" "Attempting download from $LEAPSRC, try $TRY.."
378 if wget -T 10 $PROTO $PREFER -o ${TMPFILE}.log $LEAPSRC -O $TMPFILE ; then
379 log "info" "Download of $LEAPSRC succeeded"
380 if [ -n "$VERBOSE" ]; then
381 cat ${TMPFILE}.log
384 if ! verifySHA1 $TMPFILE "$VERBOSE" ; then
385 # There is no point in retrying, as the file on the server is almost
386 # certainly corrupt.
388 log "warning" "Downloaded file $TMPFILE rejected -- saved for diagnosis"
389 cat ${TMPFILE}.log
390 rm -f ${TMPFILE}.log
391 exit 1
393 rm -f ${TMPFILE}.log
395 # Set correct permissions on temporary file
397 REFFILE="$LEAPFILE"
398 if [ ! -f $LEAPFILE ]; then
399 log "notice" "$LEAPFILE was missing, creating new copy - check permissions"
400 touch $LEAPFILE
401 # Can't copy permissions from old file,
402 # copy from NTPCONF instead
403 REFFILE="$NTPCONF"
405 chmod --reference=$REFFILE $TMPFILE > /dev/null 2>&1
406 if [ $? -ne 0 ] ; then
407 # the above chmod fails on macOS and BSD, just force it
408 chmod 644 $TMPFILE
410 chown --reference=$REFFILE $TMPFILE > /dev/null 2>&1
411 if [ $? -ne 0 ] ; then
412 # the above chown fails on macOS and BSD, just force it
413 chown root:wheel $TMPFILE
415 ( command -v selinuxenabled && selinuxenabled && command -v chcon ) >/dev/null 2>&1
416 if [ $? -eq 0 ] ; then
417 chcon --reference $REFFILE $TMPFILE
420 # Replace current file with validated new one
422 if mv -f $TMPFILE $LEAPFILE ; then
423 log "notice" "Installed new $LEAPFILE from $LEAPSRC"
424 else
425 log "error" "Install $TMPFILE => $LEAPFILE failed -- saved for diagnosis"
426 exit 1
429 # Restart NTP (or whatever else is specified)
431 if [ -n "$RESTART" ]; then
432 if [ -n "$VERBOSE" ]; then
433 log "info" "Attempting restart action: $RESTART"
435 R="$( 2>&1 $RESTART )"
436 if [ $? -eq 0 ]; then
437 log "notice" "Restart action succeeded"
438 if [ -n "$VERBOSE" -a -n "$R" ]; then
439 log "info" "$R"
441 else
442 log "error" "Restart action failed"
443 if [ -n "$R" ]; then
444 log "error" "$R"
446 exit 2
449 exit 0
452 # Failed to download. See about trying again
454 rm -f $TMPFILE
455 if [ $TRY -ge $MAXTRIES ]; then
456 break;
458 if [ -n "$VERBOSE" ]; then
459 cat ${TMPFILE}.log
460 log "info" "Waiting $INTERVAL minutes before retrying..."
462 sleep $(( $INTERVAL * 60))
463 done
465 # Failed and out of retries
467 log "warning" "Download from $LEAPSRC failed after $TRY attempts"
468 if [ -f ${TMPFILE}.log ]; then
469 cat ${TMPFILE}.log
470 rm -f ${TMPFILE}.log $TMPFILE
472 exit 1
474 log "info" "Not time to replace $LEAPFILE"
476 exit 0
478 # EOF