1284990Scy#!/bin/bash
2284990Scy
3284990Scy# Copyright (C) 2014 Timothe Litt litt at acm dot org
4284990Scy
5284990Scy# This script may be freely copied, used and modified providing that
6284990Scy# this notice and the copyright statement are included in all copies
7284990Scy# and derivative works.  No warranty is offered, and use is entirely at
8284990Scy# your own risk.  Bugfixes and improvements would be appreciated by the
9284990Scy# author.
10284990Scy
11284990ScyVERSION="1.003"
12284990Scy
13284990Scy# leap-seconds file manager/updater
14284990Scy
15284990Scy# Depends on:
16284990Scy#  wget sed, tr, shasum, logger
17284990Scy
18284990Scy# ########## Default configuration ##########
19284990Scy#
20284990Scy# Where to get the file
21284990ScyLEAPSRC="ftp://time.nist.gov/pub/leap-seconds.list"
22284990Scy
23284990Scy# How many times to try to download new file
24284990ScyMAXTRIES=6
25284990ScyINTERVAL=10
26284990Scy
27284990Scy# Where to find ntp config file
28284990ScyNTPCONF=/etc/ntp.conf
29284990Scy
30284990Scy# How long before expiration to get updated file
31284990ScyPREFETCH="60 days"
32284990Scy
33284990Scy# How to restart NTP - older NTP: service ntpd? try-restart | condrestart
34284990Scy# Recent NTP checks for new file daily, so there's nothing to do
35284990ScyRESTART=
36284990Scy
37284990Scy# Where to put temporary copy before it's validated
38284990ScyTMPFILE="/tmp/leap-seconds.$$.tmp"
39284990Scy
40284990Scy# Syslog facility
41284990ScyLOGFAC=daemon
42284990Scy# ###########################################
43284990Scy
44284990Scy# Places to look for commands.  Allows for CRON having path to
45284990Scy# old utilities on embedded systems
46284990Scy
47284990ScyPATHLIST="/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:"
48284990Scy
49284990ScyREQUIREDCMDS=" wget logger tr sed shasum"
50284990Scy
51284990ScySELF="`basename $0`"
52284990Scy
53284990Scyfunction displayHelp {
54284990Scy            cat <<EOF
55284990ScyUsage: $SELF [options] [leapfile]
56284990Scy
57284990ScyVerifies and if necessary, updates leap-second definition file
58284990Scy
59284990ScyAll arguments are optional:  Default (or current value) shown:
60284990Scy    -s    Specify the URL of the master copy to download
61284990Scy          $LEAPSRC
62284990Scy    -4    Use only IPv4
63284990Scy    -6    Use only IPv6
64284990Scy    -p 4|6
65284990Scy          Prefer IPv4 or IPv6 (as specified) addresses, but use either
66284990Scy    -d    Specify the filename on the local system
67284990Scy          $LEAPFILE
68284990Scy    -e    Specify how long before expiration the file is to be refreshed
69284990Scy          Units are required, e.g. "-e 60 days"  Note that larger values
70284990Scy          imply more frequent refreshes.
71284990Scy          "$PREFETCH"
72284990Scy    -f    Specify location of ntp.conf (used to make sure leapfile directive is
73284990Scy          present and to default  leapfile)
74284990Scy          $NTPCONF
75284990Scy    -F    Force update even if current file is OK and not close to expiring.
76284990Scy    -c    Command to restart NTP after installing a new file
77284990Scy          <none> - ntpd checks file daily
78284990Scy    -r    Specify number of times to retry on get failure
79284990Scy          $MAXTRIES
80284990Scy    -i    Specify number of minutes between retries
81284990Scy          $INTERVAL
82284990Scy    -l    Use syslog for output (Implied if CRONJOB is set)
83284990Scy    -L    Don't use syslog for output
84284990Scy    -P    Specify the syslog facility for logging
85284990Scy          $LOGFAC
86284990Scy    -t    Name of temporary file used in validation
87284990Scy          $TMPFILE
88284990Scy    -q    Only report errors to stdout
89284990Scy    -v    Verbose output
90284990Scy    -z    Specify path for utilities
91284990Scy          $PATHLIST
92284990Scy    -Z    Only use system path
93284990Scy
94284990Scy$SELF will validate the file currently on the local system
95284990Scy
96284990ScyOrdinarily, the file is found using the "leapfile" directive in $NTPCONF.
97284990ScyHowever, an alternate location can be specified on the command line.
98284990Scy
99284990ScyIf the file does not exist, is not valid, has expired, or is expiring soon,
100284990Scya new copy will be downloaded.  If the new copy validates, it is installed and
101284990ScyNTP is (optionally) restarted.
102284990Scy
103284990ScyIf the current file is acceptable, no download or restart occurs.
104284990Scy
105284990Scy-c can also be used to invoke another script to perform administrative
106284990Scyfunctions, e.g. to copy the file to other local systems.
107284990Scy
108284990ScyThis can be run as a cron job.  As the file is rarely updated, and leap
109284990Scyseconds are announced at least one month in advance (usually longer), it
110284990Scyneed not be run more frequently than about once every three weeks.
111284990Scy
112284990ScyFor cron-friendly behavior, define CRONJOB=1 in the crontab.
113284990Scy
114284990ScyThis script depends on$REQUIREDCMDS
115284990Scy
116284990ScyVersion $VERSION
117284990ScyEOF
118284990Scy   return 0
119284990Scy}
120284990Scy
121284990Scy# Default: Use syslog for logging if running under cron
122284990Scy
123284990ScySYSLOG="$CRONJOB"
124284990Scy
125284990Scyif [ "$1" = "--help" ]; then
126284990Scy    displayHelp
127284990Scy    exit 0
128284990Scyfi
129284990Scy
130284990Scy# Parse options
131284990Scy
132284990Scywhile getopts 46p:P:s:e:f:Fc:r:i:lLt:hqvz:Z opt; do
133284990Scy    case $opt in
134284990Scy        4)
135284990Scy            PROTO="-4"
136284990Scy            ;;
137284990Scy        6)
138284990Scy            PROTO="-6"
139284990Scy            ;;
140284990Scy        p)
141284990Scy            if [ "$OPTARG" = '4' -o "$OPTARG" = '6' ]; then
142284990Scy                PREFER="--prefer-family=IPv$OPTARG"
143284990Scy            else
144284990Scy                echo "Invalid -p $OPTARG" >&2
145284990Scy                exit 1;
146284990Scy            fi
147284990Scy            ;;
148284990Scy	P)
149284990Scy	    LOGFAC="$OPTARG"
150284990Scy	    ;;
151284990Scy        s)
152284990Scy            LEAPSRC="$OPTARG"
153284990Scy            ;;
154284990Scy        e)
155284990Scy            PREFETCH="$OPTARG"
156284990Scy            ;;
157284990Scy	f)
158284990Scy	    NTPCONF="$OPTARG"
159284990Scy	    ;;
160284990Scy        F)
161284990Scy            FORCE="Y"
162284990Scy            ;;
163284990Scy        c)
164284990Scy            RESTART="$OPTARG"
165284990Scy            ;;
166284990Scy        r)
167284990Scy            MAXTRIES="$OPTARG"
168284990Scy            ;;
169284990Scy        i)
170284990Scy            INTERVAL="$OPTARG"
171284990Scy            ;;
172284990Scy        t)
173284990Scy            TMPFILE="$OPTARG"
174284990Scy            ;;
175284990Scy	l)
176284990Scy	    SYSLOG="y"
177284990Scy	    ;;
178284990Scy	L)
179284990Scy	    SYSLOG=
180284990Scy	    ;;
181284990Scy        h)
182284990Scy            displayHelp
183284990Scy            exit 0
184284990Scy            ;;
185284990Scy	q)
186284990Scy	    QUIET="Y"
187284990Scy	    ;;
188284990Scy        v)
189284990Scy            VERBOSE="Y"
190284990Scy            ;;
191284990Scy	z)
192284990Scy	    PATHLIST="$OPTARG:"
193284990Scy	    ;;
194284990Scy	Z)
195284990Scy	    PATHLIST=
196284990Scy	    ;;
197284990Scy        *)
198284990Scy            echo "$SELF -h for usage" >&2
199284990Scy            exit 1
200284990Scy            ;;
201284990Scy    esac
202284990Scydone
203284990Scyshift $((OPTIND-1))
204284990Scy
205284990Scyexport PATH="$PATHLIST$PATH"
206284990Scy
207284990Scy# Add to path to deal with embedded systems
208284990Scy#
209284990Scyfor P in $REQUIREDCMDS ; do
210284990Scy    if >/dev/null 2>&1 which "$P" ; then
211284990Scy	continue
212284990Scy    fi
213284990Scy    [ "$P" = "logger" ] && continue
214284990Scy    echo "FATAL: missing $P command, please install"
215284990Scy    exit 1
216284990Scydone
217284990Scy
218284990Scy# Handle logging
219284990Scy
220284990Scyif ! LOGGER="`2>/dev/null which logger`" ; then
221284990Scy    LOGGER=
222284990Scyfi
223284990Scy
224284990Scyfunction log {
225284990Scy    # "priority" "message"
226284990Scy    #
227284990Scy    # Stdout unless syslog specified or logger isn't available
228284990Scy    #
229284990Scy    if [ -z "$SYSLOG" -o -z "$LOGGER" ]; then
230284990Scy	if [ -n "$QUIET" -a \( "$1" = "info" -o "$1" = "notice" -o "$1" = "debug" \) ]; then
231284990Scy	    return 0
232284990Scy	fi
233284990Scy	echo "`echo \"$1\" | tr a-z A-Z`: $2"
234284990Scy	return 0
235284990Scy    fi
236284990Scy
237284990Scy    # Also log to stdout if cron job && notice or higher
238284990Scy    local S
239284990Scy    if [ -n "$CRONJOB" -a \( "$1" != "info" \) -a \( "$1" != "debug" \) ] || [ -n "$VERBOSE" ]; then
240284990Scy	S="-s"
241284990Scy    fi
242284990Scy    $LOGGER $S -t "$SELF[$$]" -p "$LOGFAC.$1" "$2"
243284990Scy}
244284990Scy
245284990Scy# Verify interval
246284990ScyINTERVAL=$(( $INTERVAL *1 ))
247284990Scy
248284990Scy# Validate a leap-seconds file checksum
249284990Scy#
250284990Scy# File format: (full description in files)
251284990Scy# # marks comments, except:
252284990Scy# #$ number : the NTP date of the last update
253284990Scy# #@ number : the NTP date that the file expires
254284990Scy# Date (seconds since 1900) leaps : leaps is the # of seconds to add for times >= Date
255284990Scy# Date lines have comments.
256284990Scy# #h hex hex hex hex hex is the SHA-1 checksum of the data & dates, excluding whitespace w/o leading zeroes
257284990Scy
258284990Scyfunction verifySHA {
259284990Scy
260284990Scy    if [ ! -f "$1" ]; then
261284990Scy        return 1
262284990Scy    fi
263284990Scy
264284990Scy    # Remove comments, except those that are markers for last update, expires and hash
265284990Scy
266284990Scy    local RAW="`sed $1 -e'/^\\([0-9]\\|#[\$@h]\)/!d' -e'/^#[\$@h]/!s/#.*\$//g'`"
267284990Scy
268284990Scy    # Extract just the data, removing all whitespace
269284990Scy
270284990Scy    local DATA="`echo \"$RAW\" | sed -e'/^#h/d' -e's/^#[\$@]//g' | tr -d '[:space:]'`"
271284990Scy
272284990Scy    # Compute the SHA hash of the data, removing the marker and filename
273284990Scy    # Computed in binary mode, which shouldn't matter since whitespace has been removed
274284990Scy    # shasum comes in several flavors; a portable one is available in Perl (with Digest::SHA)
275284990Scy
276284990Scy    local DSHA="`echo -n \"$DATA\" | shasum | sed -e's/[? *].*$//'`"
277284990Scy
278284990Scy    # Extract the file's hash. Restore any leading zeroes in hash segments.
279284990Scy
280284990Scy    # The sed [] includes a tab (\t) and space; #h is followed by a tab and space
281284990Scy    local FSHA="`echo \"$RAW\" | sed -e'/^#h/!d' -e's/^#h//' -e's/[ 	] */ 0x/g'`"
282284990Scy    FSHA=`printf '%08x%08x%08x%08x%08x' $FSHA`
283284990Scy
284284990Scy    if [ -n "$FSHA" -a \( "$FSHA" = "$DSHA" \) ]; then
285284990Scy        if [ -n "$2" ]; then
286284990Scy            log "info" "Checksum of $1 validated"
287284990Scy        fi
288284990Scy    else
289284990Scy        log "error" "Checksum of $1 is invalid:"
290284990Scy	[ -z "$FSHA" ] && FSHA="(no checksum record found in file)"
291284990Scy        log "error" "EXPECTED: $FSHA"
292284990Scy        log "error" "COMPUTED: $DSHA"
293284990Scy        return 1
294284990Scy    fi
295284990Scy
296284990Scy    # Check the expiration date, converting NTP epoch to Unix epoch used by date
297284990Scy
298284990Scy    EXPIRES="`echo \"$RAW\" | sed -e'/^#@/!d' -e's/^#@//' | tr -d '[:space:]'`"
299284990Scy    EXPIRES="$(($EXPIRES - 2208988800 ))"
300284990Scy
301284990Scy    if [ $EXPIRES -lt `date -u +%s` ]; then
302284990Scy        log "notice" "File expired on `date -u -d \"Jan 1, 1970 00:00:00 +0000 + $EXPIRES seconds\"`"
303284990Scy        return 2
304284990Scy    fi
305284990Scy
306284990Scy}
307284990Scy
308284990Scy# Verify ntp.conf
309284990Scy
310284990Scyif ! [ -f "$NTPCONF" ]; then
311284990Scy    log "critical" "Missing ntp configuration $NTPCONF"
312284990Scy    exit 1
313284990Scyfi
314284990Scy
315284990Scy# Parse ntp.conf for leapfile directive
316284990Scy
317284990ScyLEAPFILE="`sed $NTPCONF -e'/^ *leapfile  *.*$/!d' -e's/^ *leapfile  *//'`"
318284990Scyif [ -z "$LEAPFILE" ]; then
319284990Scy    log "error" "$NTPCONF does not specify a leapfile"
320284990Scyfi
321284990Scy
322284990Scy# Allow placing the file someplace else - testing
323284990Scy
324284990Scyif [ -n "$1" ]; then
325284990Scy    if [ "$1" != "$LEAPFILE" ]; then
326284990Scy	log "notice" "Requested install to $1, but $NTPCONF specifies $LEAPFILE"
327284990Scy    fi
328284990Scy    LEAPFILE="$1"
329284990Scyfi
330284990Scy
331284990Scy# Verify the current file
332284990Scy# If it is missing, doesn't validate or expired
333284990Scy# Or is expiring soon
334284990Scy#  Download a new one
335284990Scy
336284990Scyif [ -n "$FORCE" ] || ! verifySHA $LEAPFILE "$VERBOSE" || [ $EXPIRES -lt `date -d "NOW + $PREFETCH" +%s` ] ; then
337284990Scy    TRY=0
338284990Scy    while true; do
339284990Scy        TRY=$(( $TRY + 1 ))
340284990Scy        if [ -n "$VERBOSE" ]; then
341284990Scy            log "info" "Attempting download from $LEAPSRC, try $TRY.."
342284990Scy        fi
343284990Scy        if wget $PROTO $PREFER -o ${TMPFILE}.log $LEAPSRC -O $TMPFILE ; then
344284990Scy            log "info" "Download of $LEAPSRC succeeded"
345284990Scy            if [ -n "$VERBOSE" ]; then
346284990Scy                cat ${TMPFILE}.log
347284990Scy            fi
348284990Scy
349284990Scy            if ! verifySHA $TMPFILE "$VERBOSE" ; then
350284990Scy		# There is no point in retrying, as the file on the server is almost
351284990Scy		# certainly corrupt.
352284990Scy
353284990Scy                log "warning" "Downloaded file $TMPFILE rejected -- saved for diagnosis"
354284990Scy                cat ${TMPFILE}.log
355284990Scy                rm -f ${TMPFILE}.log
356284990Scy                exit 1
357284990Scy            fi
358284990Scy            rm -f ${TMPFILE}.log
359284990Scy
360284990Scy	    # Set correct permissions on temporary file
361284990Scy
362284990Scy	    REFFILE="$LEAPFILE"
363284990Scy            if [ ! -f $LEAPFILE ]; then
364284990Scy		log "notice" "$LEAPFILE was missing, creating new copy - check permissions"
365284990Scy                touch $LEAPFILE
366284990Scy		# Can't copy permissions from old file, copy from NTPCONF instead
367284990Scy		REFFILE="$NTPCONF"
368284990Scy            fi
369284990Scy            chmod --reference $REFFILE $TMPFILE
370284990Scy            chown --reference $REFFILE $TMPFILE
371284990Scy	    ( which selinuxenabled && selinuxenabled && which chcon ) >/dev/null 2>&1
372284990Scy            if  [ $? == 0 ] ; then
373284990Scy                chcon --reference $REFFILE $TMPFILE
374284990Scy            fi
375284990Scy
376284990Scy	    # Replace current file with validated new one
377284990Scy
378284990Scy            if mv -f $TMPFILE $LEAPFILE ; then
379284990Scy                log "notice" "Installed new $LEAPFILE from $LEAPSRC"
380284990Scy            else
381284990Scy                log "error" "Install $TMPFILE => $LEAPFILE failed -- saved for diagnosis"
382284990Scy                exit 1
383284990Scy            fi
384284990Scy
385284990Scy	    # Restart NTP (or whatever else is specified)
386284990Scy
387284990Scy	    if [ -n "$RESTART" ]; then
388284990Scy		if [ -n "$VERBOSE" ]; then
389284990Scy		    log "info" "Attempting restart action: $RESTART"
390284990Scy		fi
391284990Scy		R="$( 2>&1 $RESTART )"
392284990Scy		if [ $? -eq 0 ]; then
393284990Scy		    log "notice" "Restart action succeeded"
394284990Scy		    if [ -n "$VERBOSE" -a -n "$R" ]; then
395284990Scy			log "info" "$R"
396284990Scy		    fi
397284990Scy		else
398284990Scy		    log "error" "Restart action failed"
399284990Scy		    if [ -n "$R" ]; then
400284990Scy			log "error" "$R"
401284990Scy		    fi
402284990Scy		    exit 2
403284990Scy		fi
404284990Scy	    fi
405284990Scy            exit 0
406284990Scy	fi
407284990Scy
408284990Scy	# Failed to download.  See about trying again
409284990Scy
410284990Scy        rm -f $TMPFILE
411284990Scy        if [ $TRY -ge $MAXTRIES ]; then
412284990Scy            break;
413284990Scy        fi
414284990Scy        if [ -n "$VERBOSE" ]; then
415284990Scy            cat ${TMPFILE}.log
416284990Scy            log "info" "Waiting $INTERVAL minutes before retrying..."
417284990Scy        fi
418284990Scy        sleep $(( $INTERVAL * 60))
419284990Scy    done
420284990Scy
421284990Scy    # Failed and out of retries
422284990Scy
423284990Scy    log "warning" "Download from $LEAPSRC failed after $TRY attempts"
424284990Scy    if [ -f ${TMPFILE}.log ]; then
425284990Scy        cat ${TMPFILE}.log
426284990Scy        rm -f ${TMPFILE}.log $TMPFILE
427284990Scy    fi
428284990Scy    exit 1
429284990Scyfi
430284990Scylog "info" "Not time to replace $LEAPFILE"
431284990Scy
432284990Scyexit 0
433284990Scy
434284990Scy# EOF