1#!/bin/bash
2
3# Copyright (C) 2014 Timothe Litt litt at acm dot org
4
5# This script may be freely copied, used and modified providing that
6# this notice and the copyright statement are included in all copies
7# and derivative works.  No warranty is offered, and use is entirely at
8# your own risk.  Bugfixes and improvements would be appreciated by the
9# author.
10
11VERSION="1.003"
12
13# leap-seconds file manager/updater
14
15# Depends on:
16#  wget sed, tr, shasum, logger
17
18# ########## Default configuration ##########
19#
20# Where to get the file
21LEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list"
22
23# How many times to try to download new file
24MAXTRIES=6
25INTERVAL=10
26
27# Where to find ntp config file
28NTPCONF=/etc/ntp.conf
29
30# How long before expiration to get updated file
31PREFETCH="60 days"
32
33# How to restart NTP - older NTP: service ntpd? try-restart | condrestart
34# Recent NTP checks for new file daily, so there's nothing to do
35RESTART=
36
37# Where to put temporary copy before it's validated
38TMPFILE="/tmp/leap-seconds.$$.tmp"
39
40# Syslog facility
41LOGFAC=daemon
42# ###########################################
43
44# Places to look for commands.  Allows for CRON having path to
45# old utilities on embedded systems
46
47PATHLIST="/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:"
48
49REQUIREDCMDS=" wget logger tr sed shasum"
50
51SELF="`basename $0`"
52
53function displayHelp {
54            cat <<EOF
55Usage: $SELF [options] [leapfile]
56
57Verifies and if necessary, updates leap-second definition file
58
59All arguments are optional:  Default (or current value) shown:
60    -s    Specify the URL of the master copy to download
61          $LEAPSRC
62    -4    Use only IPv4
63    -6    Use only IPv6
64    -p 4|6
65          Prefer IPv4 or IPv6 (as specified) addresses, but use either
66    -d    Specify the filename on the local system
67          $LEAPFILE
68    -e    Specify how long before expiration the file is to be refreshed
69          Units are required, e.g. "-e 60 days"  Note that larger values
70          imply more frequent refreshes.
71          "$PREFETCH"
72    -f    Specify location of ntp.conf (used to make sure leapfile directive is
73          present and to default  leapfile)
74          $NTPCONF
75    -F    Force update even if current file is OK and not close to expiring.
76    -c    Command to restart NTP after installing a new file
77          <none> - ntpd checks file daily
78    -r    Specify number of times to retry on get failure
79          $MAXTRIES
80    -i    Specify number of minutes between retries
81          $INTERVAL
82    -l    Use syslog for output (Implied if CRONJOB is set)
83    -L    Don't use syslog for output
84    -P    Specify the syslog facility for logging
85          $LOGFAC
86    -t    Name of temporary file used in validation
87          $TMPFILE
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
94$SELF will validate the file currently on the local system
95
96Ordinarily, the file is found using the "leapfile" directive in $NTPCONF.
97However, an alternate location can be specified on the command line.
98
99If the file does not exist, is not valid, has expired, or is expiring soon,
100a new copy will be downloaded.  If the new copy validates, it is installed and
101NTP is (optionally) restarted.
102
103If the current file is acceptable, no download or restart occurs.
104
105-c can also be used to invoke another script to perform administrative
106functions, e.g. to copy the file to other local systems.
107
108This can be run as a cron job.  As the file is rarely updated, and leap
109seconds are announced at least one month in advance (usually longer), it
110need not be run more frequently than about once every three weeks.
111
112For cron-friendly behavior, define CRONJOB=1 in the crontab.
113
114This script depends on$REQUIREDCMDS
115
116Version $VERSION
117EOF
118   return 0
119}
120
121# Default: Use syslog for logging if running under cron
122
123SYSLOG="$CRONJOB"
124
125if [ "$1" = "--help" ]; then
126    displayHelp
127    exit 0
128fi
129
130# Parse options
131
132while getopts 46p:P:s:e:f:Fc:r:i:lLt:hqvz:Z opt; do
133    case $opt in
134        4)
135            PROTO="-4"
136            ;;
137        6)
138            PROTO="-6"
139            ;;
140        p)
141            if [ "$OPTARG" = '4' -o "$OPTARG" = '6' ]; then
142                PREFER="--prefer-family=IPv$OPTARG"
143            else
144                echo "Invalid -p $OPTARG" >&2
145                exit 1;
146            fi
147            ;;
148	P)
149	    LOGFAC="$OPTARG"
150	    ;;
151        s)
152            LEAPSRC="$OPTARG"
153            ;;
154        e)
155            PREFETCH="$OPTARG"
156            ;;
157	f)
158	    NTPCONF="$OPTARG"
159	    ;;
160        F)
161            FORCE="Y"
162            ;;
163        c)
164            RESTART="$OPTARG"
165            ;;
166        r)
167            MAXTRIES="$OPTARG"
168            ;;
169        i)
170            INTERVAL="$OPTARG"
171            ;;
172        t)
173            TMPFILE="$OPTARG"
174            ;;
175	l)
176	    SYSLOG="y"
177	    ;;
178	L)
179	    SYSLOG=
180	    ;;
181        h)
182            displayHelp
183            exit 0
184            ;;
185	q)
186	    QUIET="Y"
187	    ;;
188        v)
189            VERBOSE="Y"
190            ;;
191	z)
192	    PATHLIST="$OPTARG:"
193	    ;;
194	Z)
195	    PATHLIST=
196	    ;;
197        *)
198            echo "$SELF -h for usage" >&2
199            exit 1
200            ;;
201    esac
202done
203shift $((OPTIND-1))
204
205export PATH="$PATHLIST$PATH"
206
207# Add to path to deal with embedded systems
208#
209for P in $REQUIREDCMDS ; do
210    if >/dev/null 2>&1 which "$P" ; then
211	continue
212    fi
213    [ "$P" = "logger" ] && continue
214    echo "FATAL: missing $P command, please install"
215    exit 1
216done
217
218# Handle logging
219
220if ! LOGGER="`2>/dev/null which logger`" ; then
221    LOGGER=
222fi
223
224function log {
225    # "priority" "message"
226    #
227    # Stdout unless syslog specified or logger isn't available
228    #
229    if [ -z "$SYSLOG" -o -z "$LOGGER" ]; then
230	if [ -n "$QUIET" -a \( "$1" = "info" -o "$1" = "notice" -o "$1" = "debug" \) ]; then
231	    return 0
232	fi
233	echo "`echo \"$1\" | tr a-z A-Z`: $2"
234	return 0
235    fi
236
237    # Also log to stdout if cron job && notice or higher
238    local S
239    if [ -n "$CRONJOB" -a \( "$1" != "info" \) -a \( "$1" != "debug" \) ] || [ -n "$VERBOSE" ]; then
240	S="-s"
241    fi
242    $LOGGER $S -t "$SELF[$$]" -p "$LOGFAC.$1" "$2"
243}
244
245# Verify interval
246INTERVAL=$(( $INTERVAL *1 ))
247
248# Validate a leap-seconds file checksum
249#
250# File format: (full description in files)
251# # marks comments, except:
252# #$ number : the NTP date of the last update
253# #@ number : the NTP date that the file expires
254# Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date
255# Date lines have comments.
256# #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes
257
258function verifySHA {
259
260    if [ ! -f "$1" ]; then
261        return 1
262    fi
263
264    # Remove comments, except those that are markers for last update, expires and hash
265
266    local RAW="`sed $1 -e'/^\\([0-9]\\|#[\$@h]\)/!d' -e'/^#[\$@h]/!s/#.*\$//g'`"
267
268    # Extract just the data, removing all whitespace
269
270    local DATA="`echo \"$RAW\" | sed -e'/^#h/d' -e's/^#[\$@]//g' | tr -d '[:space:]'`"
271
272    # Compute the SHA hash of the data, removing the marker and filename
273    # Computed in binary mode, which shouldn't matter since whitespace has been removed
274    # shasum comes in several flavors; a portable one is available in Perl (with Digest::SHA)
275
276    local DSHA="`echo -n \"$DATA\" | shasum | sed -e's/[? *].*$//'`"
277
278    # Extract the file's hash. Restore any leading zeroes in hash segments.
279
280    # The sed [] includes a tab (\t) and space; #h is followed by a tab and space
281    local FSHA="`echo \"$RAW\" | sed -e'/^#h/!d' -e's/^#h//' -e's/[ 	] */ 0x/g'`"
282    FSHA=`printf '%08x%08x%08x%08x%08x' $FSHA`
283
284    if [ -n "$FSHA" -a \( "$FSHA" = "$DSHA" \) ]; then
285        if [ -n "$2" ]; then
286            log "info" "Checksum of $1 validated"
287        fi
288    else
289        log "error" "Checksum of $1 is invalid:"
290	[ -z "$FSHA" ] && FSHA="(no checksum record found in file)"
291        log "error" "EXPECTED: $FSHA"
292        log "error" "COMPUTED: $DSHA"
293        return 1
294    fi
295
296    # Check the expiration date, converting NTP epoch to Unix epoch used by date
297
298    EXPIRES="`echo \"$RAW\" | sed -e'/^#@/!d' -e's/^#@//' | tr -d '[:space:]'`"
299    EXPIRES="$(($EXPIRES - 2208988800 ))"
300
301    if [ $EXPIRES -lt `date -u +%s` ]; then
302        log "notice" "File expired on `date -u -d \"Jan 1, 1970 00:00:00 +0000 + $EXPIRES seconds\"`"
303        return 2
304    fi
305
306}
307
308# Verify ntp.conf
309
310if ! [ -f "$NTPCONF" ]; then
311    log "critical" "Missing ntp configuration $NTPCONF"
312    exit 1
313fi
314
315# Parse ntp.conf for leapfile directive
316
317LEAPFILE="`sed $NTPCONF -e'/^ *leapfile  *.*$/!d' -e's/^ *leapfile  *//'`"
318if [ -z "$LEAPFILE" ]; then
319    log "error" "$NTPCONF does not specify a leapfile"
320fi
321
322# Allow placing the file someplace else - testing
323
324if [ -n "$1" ]; then
325    if [ "$1" != "$LEAPFILE" ]; then
326	log "notice" "Requested install to $1, but $NTPCONF specifies $LEAPFILE"
327    fi
328    LEAPFILE="$1"
329fi
330
331# Verify the current file
332# If it is missing, doesn't validate or expired
333# Or is expiring soon
334#  Download a new one
335
336if [ -n "$FORCE" ] || ! verifySHA $LEAPFILE "$VERBOSE" || [ $EXPIRES -lt `date -d "NOW + $PREFETCH" +%s` ] ; then
337    TRY=0
338    while true; do
339        TRY=$(( $TRY + 1 ))
340        if [ -n "$VERBOSE" ]; then
341            log "info" "Attempting download from $LEAPSRC, try $TRY.."
342        fi
343        if wget $PROTO $PREFER -o ${TMPFILE}.log $LEAPSRC -O $TMPFILE ; then
344            log "info" "Download of $LEAPSRC succeeded"
345            if [ -n "$VERBOSE" ]; then
346                cat ${TMPFILE}.log
347            fi
348
349            if ! verifySHA $TMPFILE "$VERBOSE" ; then
350		# There is no point in retrying, as the file on the server is almost
351		# certainly corrupt.
352
353                log "warning" "Downloaded file $TMPFILE rejected -- saved for diagnosis"
354                cat ${TMPFILE}.log
355                rm -f ${TMPFILE}.log
356                exit 1
357            fi
358            rm -f ${TMPFILE}.log
359
360	    # Set correct permissions on temporary file
361
362	    REFFILE="$LEAPFILE"
363            if [ ! -f $LEAPFILE ]; then
364		log "notice" "$LEAPFILE was missing, creating new copy - check permissions"
365                touch $LEAPFILE
366		# Can't copy permissions from old file, copy from NTPCONF instead
367		REFFILE="$NTPCONF"
368            fi
369            chmod --reference $REFFILE $TMPFILE
370            chown --reference $REFFILE $TMPFILE
371	    ( which selinuxenabled && selinuxenabled && which chcon ) >/dev/null 2>&1
372            if  [ $? == 0 ] ; then
373                chcon --reference $REFFILE $TMPFILE
374            fi
375
376	    # Replace current file with validated new one
377
378            if mv -f $TMPFILE $LEAPFILE ; then
379                log "notice" "Installed new $LEAPFILE from $LEAPSRC"
380            else
381                log "error" "Install $TMPFILE => $LEAPFILE failed -- saved for diagnosis"
382                exit 1
383            fi
384
385	    # Restart NTP (or whatever else is specified)
386
387	    if [ -n "$RESTART" ]; then
388		if [ -n "$VERBOSE" ]; then
389		    log "info" "Attempting restart action: $RESTART"
390		fi
391		R="$( 2>&1 $RESTART )"
392		if [ $? -eq 0 ]; then
393		    log "notice" "Restart action succeeded"
394		    if [ -n "$VERBOSE" -a -n "$R" ]; then
395			log "info" "$R"
396		    fi
397		else
398		    log "error" "Restart action failed"
399		    if [ -n "$R" ]; then
400			log "error" "$R"
401		    fi
402		    exit 2
403		fi
404	    fi
405            exit 0
406	fi
407
408	# Failed to download.  See about trying again
409
410        rm -f $TMPFILE
411        if [ $TRY -ge $MAXTRIES ]; then
412            break;
413        fi
414        if [ -n "$VERBOSE" ]; then
415            cat ${TMPFILE}.log
416            log "info" "Waiting $INTERVAL minutes before retrying..."
417        fi
418        sleep $(( $INTERVAL * 60))
419    done
420
421    # Failed and out of retries
422
423    log "warning" "Download from $LEAPSRC failed after $TRY attempts"
424    if [ -f ${TMPFILE}.log ]; then
425        cat ${TMPFILE}.log
426        rm -f ${TMPFILE}.log $TMPFILE
427    fi
428    exit 1
429fi
430log "info" "Not time to replace $LEAPFILE"
431
432exit 0
433
434# EOF