sortix-mirror/tix/tix-fetch
Jonas 'Sortie' Termansen bec38edc55 Add networked package management.
This change implements a full suite of networked tix package management,
with support for installation, upgrading, uninstallation; with signed
release metadata and support for incompatible changes across releases.

Add tix(8) package management program with common subcommands.

Add tix-upgrade(8) that upgrades installations via the network.

Add tix-autoupgrade(8) daemon for unattended upgrades and offer to enable
it during sysinstall(8).

Add tix-clean(8) for cleaning temporary and cached tix files.

Add tix-collection(8) features and metadata for signing and upgrading.

Add tix-fetch(8) that downloads files from releases and channels and
verifies the signatures.

Add tix-metabuild(8) support for release metadata and signing.

Add tix-release(8) that creates releases and channels, and signs their
information or publication.

Add tix-uninstall(8) for uninstalling packages.

Add installtest --network-upgrade test for whether network upgrades work.

Move metadata from upgrade.conf(5) to collection.conf(5).

Document all the tix package management programs.

Promote the libssl, signify, wget, and xz packages to the minimal set, so
minimal installations are able to install more packages.
2025-02-19 13:34:16 +01:00

355 lines
12 KiB
Bash
Executable file

#!/bin/sh
# Copyright (c) 2017, 2021, 2023, 2024, 2025 Jonas 'Sortie' Termansen.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# tix-fetch
# Download operating system files.
set -e
collection=
continue=
insecure_downgrade_to_http=false
insecure_no_check_certificate=false
input_release_info=
input_release_info_sig=
input_release_pub=
input_sha256sum=
output=
output_directory=
output_release_info=
output_release_info_sig=
output_release_pub=
output_sha256sum=
url=false
url_mirror_release=false
url_release=false
url_release_info_sig=false
url_sha256sum=false
upgrade=false
if [ -t 2 ]; then
# TODO: This hides errors. Fix wget so it has a quiet, but errors, mode.
wget_options="-q --show-progress"
else
wget_options=-nv
fi
dashdash=
previous_option=
for argument do
if test -n "$previous_option"; then
eval $previous_option=\$argument
previous_option=
shift
continue
fi
case $argument in
*=?*) parameter=$(expr "X$argument" : '[^=]*=\(.*\)' || true) ;;
*=) parameter= ;;
*) parameter=yes ;;
esac
case $dashdash$argument in
--) dashdash=yes ;;
# TODO: -C? -c for --collection across tools makes sense.
-c) continue="-c" ;;
-o) previous_option=output ;;
-O) previous_option=output_directory ;;
-q) wget_options="-q" ;;
-v) wget_options="-v" ;;
--collection=*) collection=$parameter ;;
--collection) previous_option=collection ;;
--continue) continue="--continue" ;;
--download-non-verbose) wget_options="-nv" ;;
--download-quiet) wget_options="-q" ;;
--download-quiet-progress) wget_options="-q --show-progress" ;;
--download-verbose) wget_options="-v" ;;
--input-release-info=*) input_release_info=$parameter ;;
--input-release-info) previous_option=input_release_info ;;
--input-release-info-sig=*) input_release_info_sig=$parameter ;;
--input-release-info-sig) previous_option=input_release_info_sig ;;
--input-release-pub=*) input_release_pub=$parameter ;;
--input-release-pub) previous_option=input_release_pub ;;
--input-sha256sum=*) input_sha256sum=$parameter ;;
--input-sha256sum) previous_option=input_sha256sum ;;
--insecure-downgrade-to-http) insecure_downgrade_to_http=true ;;
--insecure-no-check-certificate) insecure_no_check_certificate=true ;;
--nv) wget_options="-nv" ;;
--output=*) output=$parameter ;;
--output) previous_option=output ;;
--output-directory=*) output_directory=$parameter ;;
--output-directory) previous_option=output_directory ;;
--output-release-info=*) output_release_info=$parameter ;;
--output-release-info) previous_option=output_release_info ;;
--output-release-info-sig=*) output_release_info_sig=$parameter ;;
--output-release-info-sig) previous_option=output_release_info_sig ;;
--output-release-pub=*) output_release_pub=$parameter ;;
--output-release-pub) previous_option=output_release_pub ;;
--output-sha256sum=*) output_sha256sum=$parameter ;;
--output-sha256sum) previous_option=output_sha256sum ;;
--output-upgrade-info=*) output_upgrade_info=$parameter ;;
--output-upgrade-info) previous_option=output_upgrade_info ;;
--upgrade) upgrade=true ;;
--url) url=true; wget_options="-q" ;;
--url-mirror-release) url_mirror_release=true; wget_options="-q" ;;
--url-release) url_release=true; wget_options="-q" ;;
--url-release-info-sig) url_release_info_sig=true; wget_options="-q" ;;
--url-sha256sum) url_sha256sum=true; wget_options="-q" ;;
--wget-options) previous_option=wget_options ;;
--wget-options=*) wget_options=$parameter ;;
-*) echo "$0: unrecognized option $argument" >&2
exit 1 ;;
*) break ;;
esac
shift
done
if test -n "$previous_option"; then
echo "$0: option '$argument' requires an argument" >&2
exit 1
fi
tmpdir=$(mktemp -dt tix-fetch.XXXXXX)
trap 'rm -rf -- "$tmpdir"' EXIT HUP INT QUIT TERM
collection_conf="${collection%/}/tix/collection.conf"
PLATFORM=$(tix-vars "$collection_conf" PLATFORM)
RELEASE_URL=$(tix-vars "$collection_conf" RELEASE_URL)
MIRROR=$(tix-vars -d '' "$collection_conf" MIRROR)
FORCE_MIRROR=$(tix-vars -d false "$collection_conf" FORCE_MIRROR)
# TODO: Add tix-fetch here?
USER_AGENT="$(uname -s)/$(uname -r) ($(uname -m); $(uname -v))"
input_release_pub="${input_release_pub:-${collection%/}/tix/release.pub}"
if $insecure_no_check_certificate; then
echo "$0: warning: insecurely not checking https certificates" >&2
wget_options="$wget_options --no-check-certificate"
fi
if $insecure_downgrade_to_http; then
echo "$0: warning: insecurely downloading without https" >&2
RELEASE_URL="$(echo "$RELEASE_URL" | sed -E 's,^https:,http:,')"
fi
if $url_release; then
printf "%s\n" "$RELEASE_URL"
exit
elif $url_release_info_sig; then
printf "%s\n" "$RELEASE_URL/release.info.sig"
exit
fi
# HACK: Provide more useful errors when wget is silent:
do_wget() {
(set +e
wget "$@"
status=$?
set -e
what=
case $status in
0) exit 0 ;;
1) what="Generic error" ;;
2) what="Parse error" ;;
3) what="File I/O error" ;;
4) what="Network I/O error" ;;
5) what="Transport Layer Security verification failure" ;;
6) what="Username/password failure" ;;
7) what="Protocol error" ;;
8) what="Error response" ;;
*) what="Exit code $status" ;;
esac
echo "$0: $what when running: wget $@" >&2
exit $status)
}
download_release_sh() {
(cd "$tmpdir" &&
do_wget -U "$USER_AGENT" $wget_options -O release.info.sig \
-- "$RELEASE_URL/release.info.sig")
signify -Vq -p "$input_release_pub" -em "$tmpdir/release.info"
}
# Download the signed release information.
true > "$tmpdir/upgrade.info"
if [ -z "$input_release_info" -a -z "$input_release_info_sig" ]; then
download_release_sh
# TODO: Deprecate the NEW_ prefix here? Replace with UPGRADE_RELEASE_URL?
tix-vars "$tmpdir/release.info" | \
grep -E '^(NEW|UPGRADE)_' | \
cat > "$tmpdir/upgrade.info"
# Accept an upgrade to a new release if requested
NEW_RELEASE_URL=$(tix-vars -d '' "$tmpdir/upgrade.info" NEW_RELEASE_URL)
if $upgrade && [ -n "$NEW_RELEASE_URL" ]; then
RELEASE_URL="$NEW_RELEASE_URL"
# Download the new public key (if any) which is signed by this release.
# TODO: Test this upgrade mechanism.
UPGRADE_RELEASE_PUB_SHA256SUM=$(tix-vars -d '' "$tmpdir/upgrade.info" \
UPGRADE_RELEASE_PUB_SHA256SUM)
if [ -n "$UPGRADE_RELEASE_PUB_SHA256SUM" ]; then
(cd "$tmpdir" &&
do_wget -U "$USER_AGENT" $wget_options -O release.pub \
-- "$RELEASE_URL/release.pub")
echo "$UPGRADE_RELEASE_PUB_SHA256SUM $tmpdir/release.pub" |
sha256sum -c --quiet
input_release_pub="$tmpdir/release.pub"
fi
download_release_sh
fi
fi
# Verify the release information signature.
if [ -n "$input_release_info" ]; then
cp -T -- "$input_release_info" "$tmpdir/release.info"
elif [ -n "$input_release_info_sig" ]; then
signify -Vq -p "$input_release_pub" -em "$tmpdir/release.info"
fi
# Store the verified release files if requested.
if [ -n "$output_release_pub" ]; then
cp -T -- "$input_release_pub" "$output_release_pub"
fi
if [ -n "$output_release_info_sig" ]; then
cp -T -- "$tmpdir/release.info.sig" "$output_release_info_sig"
fi
if [ -n "$output_release_info" ]; then
cp -T -- "$tmpdir/release.info" "$output_release_info"
fi
if [ -n "$output_upgrade_info" ]; then
cp -T -- "$tmpdir/upgrade.info" "$output_upgrade_info"
fi
# Load the release description.
SHA256SUM_SHA256SUM=$(tix-vars "$tmpdir/release.info" SHA256SUM_SHA256SUM)
# If a channel with mirrors is used, default to the main mirror but switch to
# the preferred mirror if the release description knows about the mirror and
# believes it to be trustworthy.
if tix-vars -t "$tmpdir/release.info" MAIN; then
# TODO: Would it be simpler to merge MAIN and MIRRORS?
MAIN=$(tix-vars "$tmpdir/release.info" MAIN)
RELEASE=$(tix-vars "$tmpdir/release.info" RELEASE)
MIRRORS=$(tix-vars -d '' "$tmpdir/release.info" MIRRORS)
choice="$MAIN"
for POTENTIAL_MIRROR in $MIRRORS; do
if [ "$POTENTIAL_MIRROR" = "$MIRROR" ]; then
choice="$MIRROR"
fi
done
if [ -n "$MIRROR" ] && [ "$MIRROR" != "$MIRROR" ]; then
if [ "$FORCE_MIRROR" = true ]; then
choice="$MIRROR"
else
echo "$0: warning: ignoring unsupported mirror $MIRROR" >&2
fi
fi
# TODO: Remove release/ from the mirror and implicitly add it here.
RELEASE_URL="$choice/$RELEASE"
fi
# TODO: Make sure the distant future http downgrade is tested.
if $insecure_downgrade_to_http; then
RELEASE_URL="$(echo "$RELEASE_URL" | sed -E 's,^https:,http:,')"
fi
if $url_mirror_release; then
printf "%s\n" "$RELEASE_URL"
exit
fi
escape_extended_regex() {
printf "%s\n" "$1" | sed -E -e 's/[[$()*?\+.^{|}]/\\\0/g'
}
download() {
# If download is resumable, store the file directly to the destination path.
# Otherwise download to a temporary directory and move only to the final
# location if the cryptographic check is passed.
REQUEST=$(basename -- "$FULL_REQUEST")
if [ -n "$continue" ]; then
DOWNLOAD_DIR=$(dirname -- "$FINAL")
OUTPUT="$FINAL"
else
DOWNLOAD_DIR="$tmpdir/download"
OUTPUT="$DOWNLOAD_DIR/$REQUEST"
fi
mkdir -p -- "$(dirname -- "$OUTPUT")"
# Fetch the file.
(cd "$DOWNLOAD_DIR" &&
do_wget -U "$USER_AGENT" $wget_options $continue -O "$REQUEST" \
-- "$RELEASE_URL/$FULL_REQUEST")
# Verify the cryptographic integrity of the fetched file.
ABSOLUTE_OUTPUT=$(realpath -- "$OUTPUT")
ABSOLUTE_SHA256SUM=$(realpath -- "$input_sha256sum")
mkdir -p -- "$tmpdir/check"
(cd "$tmpdir/check" &&
mkdir -p -- "$(dirname -- "$FULL_REQUEST")"
ln -s -- "$ABSOLUTE_OUTPUT" "$FULL_REQUEST"
#if ! sha256sum --quiet -C "$ABSOLUTE_SHA256SUM" -- "$FULL_REQUEST"; then
if ! grep -E "^[a-zA-Z0-9]+ $(escape_extended_regex "$FULL_REQUEST")$" \
"$ABSOLUTE_SHA256SUM" \
| sha256sum --quiet -c; then
# Don't leave behind a file that didn't pass a cryptographic check.
if [ -n "$continue" ]; then
echo "error: Deleting corrupted output file: $OUTPUT" 2>&1
rm -f -- "$OUTPUT"
fi
exit 1
fi)
rm -rf -- "$tmpdir/check"
# Move the file to the final destination if not already there.
if [ -z "$continue" ]; then
mkdir -p -- "$(dirname -- "$FINAL")"
cp -T -- "$OUTPUT" "$FINAL"
rm -rf -- "$tmpdir/download"
fi
}
# Stop early if there's nothing to do.
if [ -z "$output_sha256sum" -a $# = 0 ] && ! $url_sha256sum; then exit; fi
# Fetch sha256sum file and check its SHA256 hash with the release description.
if [ -z "$input_sha256sum" ]; then
input_sha256sum="$tmpdir/sha256sum.sha256sum"
echo "$SHA256SUM_SHA256SUM sha256sum" > "$input_sha256sum"
FULL_REQUEST=sha256sum
if $url_sha256sum; then
printf '%s\n' "$RELEASE_URL/$FULL_REQUEST"
exit
fi
FINAL="${output_sha256sum:-$tmpdir/sha256sum}"
download
input_sha256sum="$FINAL"
fi
# Fetch each of the specified signed files from the mirror.
for REQUEST; do
FULL_REQUEST="repository/$PLATFORM/$REQUEST"
if $url; then
printf '%s\n' "$RELEASE_URL/$FULL_REQUEST"
exit
fi
if [ -n "$output" ]; then
FINAL="$output"
elif [ -n "$output_directory" ]; then
FINAL="$output_directory/$REQUEST"
else
FINAL="$REQUEST"
fi
download
done