#!/bin/sh -f
#
# Copyright (c) 2009
# Dominic Fandrey <kamikaze@bsdforen.de>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
readonly version=0.9999
readonly name=uma
# Return value.
errno=0
# Allow things to fail properly by ignoring SIGINT in the main process.
trap '' int
# Used to activate debugging output.
debug=
stderr="/dev/null"
verbose=
# Will be set if files are locally available.
local=
vardir="/var"
lock="$vardir/run/$name.lock"
lockpid="$vardir/run/$name.pid"
identpid="$vardir/run/$name.ident.pid"
# Use line breaks as a delimiter.
IFS='
'
# Timezone UTC for age comparisons.
export TZ=UTC
# The bit position of errors.
readonly ERR_LOCK=0
readonly ERR_FETCH_PORTS=1
readonly ERR_FETCH_VULNDB=2
readonly ERR_FETCH_INDEX=3
readonly ERR_EXTRACT_PORTS=4
readonly ERR_UPDATE_PORTS=5
#
# Get environment variables.
#
# Local index location.
: ${PKG_INDEX="$vardir/db/uma/FTPINDEX"}
# Directory for remporary files.
: ${TMPDIR="/tmp"}
# Logic from src/usr.sbin/pkg_install/add/main.c, plus the possibility to
# override the architecture with ARCH.
: ${PACKAGEROOT="ftp://ftp.freebsd.org"}
: ${ARCH="$(uname -m)"}
branch="$(uname -r | tr '[:upper:]' '[:lower:]')"
number="${branch%%.*}"
case "$branch" in
release)
branch=$number-$branch
;;
stable | current)
branch=${number%%.*}-$branch
;;
*)
# Fallback to stable for prerelease and the like.
branch=${number%%.*}-stable
;;
esac
: ${PACKAGESITE="$PACKAGEROOT/pub/FreeBSD/ports/$ARCH/packages-$branch/Latest"}
packagetree="${PACKAGESITE%/*?}"
ftp="${PACKAGESITE#*://}"
ftp="${ftp%%/*}"
# Determine portsdir
portsdir=$(make -V PORTSDIR -f /usr/share/mk/bsd.port.mk)
#
# Checks weather the local index needs updating.
#
# @param PACKAGESITE
# The source of a new index.
# @param vebose
# If set verbose output will be created.
# @param packagetree
# The remote directory to get the index from.
# @param PKG_INDEX
# The local copy of the remote index.
# @return
# Return 0 if the index needs updating, 1 if not.
#
updateRequired() {
case "$PACKAGESITE" in
ftp://*)
updateRequiredFtp
return $?
;;
*://*)
errno="$((1 << $ERR_FETCH_INDEX | $errno))"
if [ -n "$verbose" ]; then
echo "Unsopported protocol: ${PACKAGESITE%%://*}!"
fi
return 1
;;
*)
if [ "$(sha256 -q "$packagetree/INDEX")" \
= "$(sha256 -q "$PKG_INDEX")" ]; then
if [ -n "$verbose" ]; then
echo "Local and remote index are identical."
fi
return 1
else
if [ -n "$verbose" ]; then
echo "Local and remote index differ."
fi
return 0
fi
;;
esac
}
#
# This connects to an ftp and compares file age and size to the local INDEX
# file. Load balancing is a problem here, because you might get forwareded
# to an outdated server. Or you might catch an up to date server and later
# download from an outdated one.
#
# @param PKG_INDEX
# The local copy of the remote index.
# @param packagetree
# A derivate of PACKAGESITE.
# @param ftp
# The hostname component of PACKAGESITE.
# @param debug
# If this is set a lot of debugging output will occur. Including the
# answers of the ftp server.
# @return
# 0 is returned if an update seems necessary, 1 if not.
#
updateRequiredFtp() {
local line pid timeout connected time size
test -e "$PKG_INDEX" || return 0
#
# Connect to an ftp server. This starts 3 processes, one that
# pipes commands into netcat (nc), the netcat proccess and
# one that reads the answers.
# The reading process notifies the sending process of the current
# progress by creating temporary files.
# The reading process echos "time=..." and "size=..." commands
# that get evaluated and can be used from outside.
#
# Read RFC959 to get a hang on the FTP protocol.
#
pid="$$"
timeout=600
eval "$(
while [ $timeout -gt 0 ]; do
if [ -e "$TMPDIR/$pid.done" ]; then
rm "$TMPDIR/$pid.done"
echo "QUIT"
break
elif [ -e "$TMPDIR/$pid.connected" ]; then
rm "$TMPDIR/$pid.connected"
echo "USER anonymous"
echo "PASS anonymous@example.com"
echo "TYPE I"
echo "MDTM /${packagetree#*://*/}/INDEX"
echo "SIZE /${packagetree#*://*/}/INDEX"
fi
sleep 0.1
timeout=$(($timeout - 1))
done \
| (
nc -w $(($timeout / 10)) "$ftp" 21 2> /dev/null \
|| touch "$TMPDIR/$pid.done"
) | while read line; do
test -n "$debug" && echo "$line" 1>&2
case "$line" in
5*)
# An error occured, bail out.
touch "$TMPDIR/$pid.done"
;;
220*)
test -z "$connected" \
&& touch "$TMPDIR/$pid.connected"
connected=1
;;
213*)
if [ -z "$time" ]; then
time=1
line="$(
echo "${line##* }" \
| sed 's|[^0-9]||g'
)"
line="${line%??}.${line#????????????}"
echo "time=$line"
elif [ -z "$size" ]; then
size=1
echo "size=${line##* }"
fi
if [ -n "$time" -a -n "$size" ]; then
touch "$TMPDIR/$pid.done"
fi
;;
esac
done
)"
# An ftp connection did apparently not occur. Or something went
# wrong. Hence we assume that an update is necessary.
test -z "$size" -o -z "$time" && return 0
# Create a dummy file for the time comparison.
touch -t "$time" "$TMPDIR/$pid.compare"
if [ "$TMPDIR/$pid.compare" -nt "$PKG_INDEX" ]; then
rm "$TMPDIR/$pid.compare"
if [ -n "$verbose" ]; then
echo "Local index is older than remote."
fi
return 0
fi
if [ "$TMPDIR/$pid.compare" -ot "$PKG_INDEX" ]; then
rm "$TMPDIR/$pid.compare"
if [ -n "$verbose" ]; then
echo "Local index is newer than remote."
fi
return 1
fi
# Files have the same age.
if [ -n "$verbose" ]; then
echo "Local and remote index have the same creation time."
fi
rm "$TMPDIR/$pid.compare" 2> /dev/null
# Compare the size.
localSize="$(wc -c "$PKG_INDEX")"
localSize="$((${localSize% *}))"
if [ "$size" -eq "$localSize" ]; then
# Files have the same size, no update required.
if [ -n "$verbose" ]; then
echo "Local and remote index have the same size."
fi
return 1
else
# Files differ in size. Update required.
if [ -n "$verbose" ]; then
echo "Local and remote index differ in size."
fi
return 0
fi
}
#
# This function is called by a trap when the script exits in verbose mode.
# It reads errno to construct error messages.
#
# @param errno
# The exit status of the script.
#
verbose() {
if [ $(($errno >> $ERR_LOCK & 1)) -eq 1 ]; then
echo "ERROR($((1 << $ERR_LOCK))): Lock owned by someone else."
fi
if [ $(($errno >> $ERR_FETCH_PORTS & 1)) -eq 1 ]; then
echo "ERROR($((1 << $ERR_FETCH_PORTS))): Fetching the ports tree failed."
fi
if [ $(($errno >> $ERR_FETCH_VULNDB & 1)) -eq 1 ]; then
echo "ERROR($((1 << $ERR_FETCH_VULNDB))): Fetching security database failed."
fi
if [ $(($errno >> $ERR_FETCH_INDEX & 1)) -eq 1 ]; then
echo "ERROR($((1 << $ERR_FETCH_INDEX))): Fetching remote INDEX failed."
fi
if [ $(($errno >> $ERR_EXTRACT_PORTS & 1)) -eq 1 ]; then
echo "ERROR($((1 << $ERR_EXTRACT_PORTS))): Extracting the ports tree failed."
fi
if [ $(($errno >> $ERR_UPDATE_PORTS & 1)) -eq 1 ]; then
echo "ERROR($((1 << $ERR_UPDATE_PORTS))): Updating the ports tree failed."
fi
}
#
# This function spawns a process that takes over a lock.
#
# @param pid
# The PID of the process that requested the lock.
# @param lock
# The location of the lock file.
# @param lockpid
# The location of the PID file for the lock holding process.
#
secureLock() {
lockf "$lock" sh -c "
trap 'exit 0' term
echo '$pid' > '$lock'
echo \"\$\$\" > '$lockpid'
trap 'rm \"$lockpid\"; exit 0' EXIT
while kill -0 '$pid' 2> /dev/null; do
sleep 2
done
" 2> /dev/null &
}
#
# Checks weather the currently requesting process holds the lock.
#
# @param pid
# The PID of the process that requested the lock.
# @param lock
# The location of the lock file.
# @return
# Returns 0 if the lock is held for the requesting process or 1
# if the lock is missing or owned by another process.
#
hasLock() {
test -e "$lock" && test "$pid" -eq "$(cat "$lock")"
return $?
}
#
# Creates a lock for the requesting process.
#
# @param pid
# The PID of the process that requested the lock.
# @param lock
# The location of the lock file.
# @param lockpid
# The location of the PID file for the lock holding process.
# @param portsdir
# The location of the FreeBSD ports tree.
# @return
# Returns 0 on success, 1 on failure.
#
lock() {
# The requestor already holds the lock.
hasLock && return 0
# The process requesting the lock does not exist.
kill -0 "$pid" 2> /dev/null || return 1 $(errno=1)
# Follow symlinks
if cd "$portsdir" && portsdir="$(pwd -P)"; then
# Portsdir exists, so we can test for make activity. This
# does not cover all cases, but it covers a lot.
if fstat "$portsdir" | awk '{print $2}' | grep -q make; then
errno=1
return 1
fi
fi
# Try aquiring the lock.
lockf -st 0 "$lock" $0 secure $pid 2> /dev/null || return 1 $(errno=1)
# Wait until the locking process is properly set up.
while ! [ -e "$lockpid" -a -e "$lock" ]; do
sleep 0.1
done
return 0
}
#
# Frees a lock unless it is held for another process than the requestor.
#
# @param lock
# The location of the lock file.
# @param lockpid
# The location of the PID file for the lock holding process.
# @return
# Returns 0 on success, 1 on failure.
#
unlock() {
if hasLock; then
# Free the lock.
kill -TERM "$(cat "$lockpid")"
# Wait for the locking process to clean up.
while [ -e "$lockpid" -o -e "$lock" ]; do
sleep 0.1
done
return 0
else
errno=1
return 1
fi
}
#
# Prints the command and available parameters.
#
# @param name
# The name of the script.
# @param version
# The version of the script.
#
printHelp() {
echo "$name v$version
usage: $name [-h] [-d] [-v] [fetch] [extract] [update] [ports] [audit] [index]"
}
#
# Reads the parameters and creates variables that indicates the presence
# of these parameters.
#
# The last numeric value is treated as the requestor PID. It also deals
#
# @param *
# All parameters to process.
# @param debug
# This is set to 1 if debugging is activated.
# @param stderr
# This is changed from /dev/null to /dev/stderr if debugging is activated.
# @param verbose
# Set to 1 if verbose mode is activated.
# @param cmd_*
# Set by this function to indicate the presence of a parameter.
#
readParams() {
local flag
for flag; {
# A numerical parameter is the PID.
if [ "$flag" -eq "$flag" ] 2> /dev/null; then
pid="${flag}"
continue
fi
# Activate verbose mode for -v.
case "$flag" in
-d | --debug)
debug=1
stderr="/dev/stderr"
continue
;;
-v | --verbose)
trap 'verbose 1>&2' EXIT
verbose=1
continue
;;
-h | --help)
printHelp
continue
;;
-? | --*)
continue
;;
-*)
# Split parameters.
readParams "${flag%${flag#-?}}" "-${flag#-?}"
continue
;;
esac
eval "cmd_${flag}=1"
}
}
pid="$$"
cmd_lock=
cmd_unlock=
cmd_secure=
cmd_env=
cmd_fetch=
cmd_extract=
cmd_update=
cmd_ports=
cmd_audit=
cmd_index=
readParams "$@"
#
# Exclusive commands that will cause all others to be ignored, in order
# of priority.
#
if [ -n "$cmd_unlock" ]; then
unlock
return $?
fi
if [ -n "$cmd_secure" ]; then
secureLock
return $?
fi
if [ -n "$cmd_lock" ]; then
lock
return $?
fi
#
# Non-exclusive commands that do not require a lock.
#
if [ -n "$cmd_env" ]; then
echo "ARCH='$ARCH'"
echo "PACKAGEROOT='$PACKAGEROOT'"
echo "PACKAGESITE='$PACKAGESITE'"
echo "TMPDIR='$TMPDIR'"
echo "PKG_INDEX='$PKG_INDEX'"
fi
# Create a local lock if need be.
localLock=
if ! hasLock; then
localLock=1
lock || return $?
fi
# Ports tree commands.
if [ -n "$cmd_ports" ]; then
if [ -n "$cmd_fetch" ]; then
portsnap fetch || errno="$((1 << $ERR_FETCH_PORTS | $errno))"
fi
if [ -n "$cmd_extract" ]; then
portsnap extract || errno=$((1 << $ERR_EXTRACT_PORTS | $errno))
fi
if [ -n "$cmd_update" ]; then
portsnap update || errno=$((1 << $ERR_UPDATE_PORTS | $errno))
fi
fi
# Portaudit commands.
if [ -n "$cmd_audit" ]; then
if [ -n "$cmd_fetch" ]; then
portaudit -F || errno=$((1 << $ERR_FETCH_VULNDB | $errno))
fi
fi
# Package index commands.
if [ -n "$cmd_index" ]; then
if ! mkdir -p "${PKG_INDEX%/*}" 2> /dev/null; then
test -n "$verbose" \
&& echo "The directory ${PKG_INDEX%/*} does not exist and cannot be created!"
errno=$((1 << $ERR_FETCH_INDEX | $errno))
elif [ -n "$cmd_fetch" ]; then
if updateRequired; then
test -n "$verbose" \
&& echo "Fetching index from: $packagetree"
fetch -o "$TMPDIR/FTPINDEX.$$" "$packagetree/INDEX" \
2>&1 \
&& mv "$TMPDIR/FTPINDEX.$$" "$PKG_INDEX" \
|| rm "$TMPDIR/FTPINDEX.$$" \
$(errno=$((1 << $ERR_FETCH_INDEX | $errno))) \
2> /dev/null
fi
fi
fi
# Free a local lock.
test -n "$localLock" && unlock
return $errno