wren-linux / tools / test-whonix-update.sh
#!/bin/bash
# Checks what `whonix update` runs inside the VMs: data/whonix-update (root's: apt) and
# data/whonix-update-browser (the user's: Tor Browser, keeping your profile), with pretend
# apt-get, curl, runuser and update-torbrowser. dpkg's version comparing and python are real.
# All of this was also run on a real Whonix 18 image, with a real update of Tor Browser,
# in QEMU: docs/extras/whonix.md says what was and wasn't. Run by test.sh, or:
# bash tools/test-whonix-update.sh
# shellcheck disable=SC2016 # the inner shells and pretend programs expand their own variables
HERE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
T=$(mktemp -d) || { echo " FAIL couldn't make a temp folder (is /tmp full?)"; exit 1; }
trap 'rm -rf "$T"' EXIT
fails=0
check() { # DESCRIPTION CONDITION-COMMAND...
local what=$1
shift
if "$@" >/dev/null 2>&1; then printf ' ok %s\n' "$what"; else printf ' FAIL %s\n' "$what"; fails=$((fails + 1)); fi
}
not() { ! "$@"; }
mkdir -p "$T/bin"
# ---- Tor Browser (data/whonix-update-browser) ----
# curl: says $T/feed, or fails. update-torbrowser: as the real one, deletes the folder and
# puts a new one with a fresh profile there (at version $T/installs), or stops first ($T/tb-rc)
cat >"$T/bin/curl" <<PRETEND
#!/bin/sh
[ ! -e "$T/curl-fails" ] || exit 22
cat "$T/feed"
PRETEND
cat >"$T/bin/update-torbrowser" <<PRETEND
#!/bin/bash
echo ran >>"$T/tb-ran"
echo "\$*" >>"$T/tb-args"
if [ -e "$T/tb-cut" ] && [ "\$(wc -l <"$T/tb-ran")" -le "\$(cat "$T/tb-cut")" ]; then exit 17; fi
if [ -e "$T/tb-rc" ]; then exit "\$(cat "$T/tb-rc")"; fi
B=\$HOME/.tb/tor-browser
rm -rf "\$B"
mkdir -p "\$B/Browser/TorBrowser/Data/Browser/profile.default" "\$B/Browser/Downloads"
echo new-program >"\$B/Browser/firefox"
if [ ! -e "$T/no-version" ]; then
printf '{"version":"%s","architecture":"linux-x86_64","channel":"release"}' "\$(cat "$T/installs")" >"\$B/Browser/tbb_version.json"
fi
exit 0
PRETEND
chmod +x "$T/bin/"*
H=$T/home
B=$H/.tb/tor-browser/Browser
P=$B/TorBrowser/Data/Browser/profile.default
KEEP=$H/.tb/tor-browser.wren-keep
mkplaces() { # FILE: a real Firefox bookmarks database (menu, toolbar, a folder, tags, and nasty entries)
python3 -I - "$1" <<'PY'
import sqlite3, sys
c = sqlite3.connect(sys.argv[1])
c.executescript("""
create table moz_places (id integer primary key, url text);
create table moz_bookmarks (id integer primary key, type integer, fk integer, parent integer, position integer, title text);
""")
places = [(1, "https://example.org/menu"), (2, "https://example.org/bar"), (3, "https://example.org/inner"),
(4, "javascript:alert(1)"), (5, "data:text/html,<script>1</script>"), (6, "place:sort=8"),
(7, "http://abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion/"), (8, "https://example.org/tagged"),
(9, "https://example.org/a\"onmouseover=\"x")]
c.executemany("insert into moz_places values (?,?)", places)
rows = [(1, 2, None, 0, 0, ""), (2, 2, None, 1, 0, "menu"), (3, 2, None, 1, 1, "toolbar"), (4, 2, None, 1, 2, "tags"),
(5, 2, None, 1, 3, "unfiled"), (6, 2, None, 1, 4, "mobile"),
(10, 1, 1, 2, 0, "Menu one"), (11, 2, None, 2, 1, "My <b>folder</b>"), (12, 1, 3, 11, 0, "Inner one"),
(13, 1, 2, 3, 0, "Bar one"), (14, 1, 4, 3, 1, "a bookmarklet"), (15, 1, 5, 3, 2, "a data page"),
(16, 1, 6, 3, 3, "a query"), (17, 1, 7, 5, 0, "An onion"), (18, 1, 8, 4, 0, "tagged"),
(19, 1, 9, 2, 2, "quote \"break\" \x1b[31m\u202eevil"), (20, 3, None, 2, 3, None)]
c.executemany("insert into moz_bookmarks values (?,?,?,?,?,?)", rows)
c.commit()
PY
}
yours() { # a browser at VERSION, with your things
rm -rf "$H" "$T/tb-ran" "$T/tb-args" "$T/tb-cut" "$T/tb-rc" "$T/curl-fails" "$T/no-version"
mkdir -p "$P" "$B/Downloads"
printf '{"version":"%s","architecture":"linux-x86_64","channel":"release"}' "$1" >"$B/tbb_version.json"
mkplaces "$P/places.sqlite"
sha256sum <"$P/places.sqlite" >"$T/dbsha"
echo my-download >"$B/Downloads/a.pdf"
echo old-program >"$B/firefox"
echo hacked-page-planted-this >"$P/prefs.js"
}
feed() { printf '{"version":"%s"}' "$1" >"$T/feed"; printf '%s' "$1" >"$T/installs"; }
tb() { env -i HOME="$H" WREN_RETRY_WAIT=0 WREN_SAFEST="${SAFEST_FILE:-$HERE/data/tor-browser-safest.js}" WREN_POLICIES="${POLICIES_FILE:-$HERE/data/tor-browser-policies-safest.json}" PATH="$T/bin:/usr/bin:/bin" bash "$HERE/data/whonix-update-browser" >"$T/out" 2>&1; }
as_it_was() { # VERSION: everything there, nothing left over, nothing else run
[ "$(sha256sum <"$P/places.sqlite")" = "$(cat "$T/dbsha")" ] && [ "$(cat "$B/Downloads/a.pdf")" = my-download ] &&
[ "$(cat "$B/firefox")" = old-program ] && grep -q "\"$1\"" "$B/tbb_version.json" && [ ! -e "$KEEP" ]
}
yours 15.0.1; feed 16.0
tb; rc=$?
check "a newer version: it runs update-torbrowser, and says so" bash -c '[ "$1" = 0 ] && grep -q "15.0.1 to 16.0" "$2"' _ "$rc" "$T/out"
L=$H/.tb/left-from-browser-15.0.1
check "...only your Downloads come over as they are: nothing the old profile held (a hacked page's prefs.js)" bash -c '! grep -q hacked "$1" 2>/dev/null && [ ! -e "$2" ]' _ "$P/prefs.js" "$P/places.sqlite"
check "...your bookmarks are rebuilt from the old database: menu, toolbar and folders" bash -c 'grep -q "https://example.org/menu\">Menu one" "$1" && grep -q "https://example.org/inner\">Inner one" "$1" && grep -q "PERSONAL_TOOLBAR_FOLDER" "$1" && grep -q "https://example.org/bar\">Bar one" "$1"' _ "$P/bookmarks.html"
check "...an onion address is kept" grep -q 'abcdefghijklmnopqrstuvwxyz234567abcdefghijklmnopqrstuvwx.onion' "$P/bookmarks.html"
check "...a title is text, not markup (its tags are escaped)" bash -c 'grep -q "My <b>folder</b>" "$1" && ! grep -q "<b>" "$1"' _ "$P/bookmarks.html"
check "...javascript:, data: and place: entries are left out (a bookmarklet would run in a page)" bash -c '! grep -q -i -E "javascript:|data:text|place:" "$1"' _ "$P/bookmarks.html"
check "...an address with a quote in it is left out (it could break out of its attribute)" bash -c '! grep -q onmouseover "$1"' _ "$P/bookmarks.html"
check "...no escape sequence or right-to-left mark in a title" python3 -I -c '
import sys
t = open(sys.argv[1], encoding="utf-8").read()
sys.exit(1 if "\x1b" in t or "\u202e" in t else 0)' "$P/bookmarks.html"
check "...the tags folder is not carried over" bash -c '! grep -q "tagged" "$1"' _ "$P/bookmarks.html"
check "...Firefox is told to import them on its first start" grep -q 'user_pref("browser.places.importBookmarksHTML", true);' "$P/user.js"
check "...only your bookmarks and Downloads are kept: the rest of the old profile is deleted, not put aside" bash -c '[ ! -e "$1/profile" ] && [ ! -e "$1" ]' _ "$L"
check "...and it says so" grep -q "The old profile is deleted: only your bookmarks and Downloads are kept" "$T/out"
check "...the new browser is at Safest with JavaScript off: written into its own profile, not left to chance" bash -c '
grep -q "browser.security_level.security_slider\", 1" "$1" && grep -q "javascript.enabled\", false" "$1" && grep -q "browser.security_level.security_custom\", false" "$1"' _ "$P/user.js"
check "...next to the bookmark import line, each once" bash -c '[ "$(grep -c "importBookmarksHTML" "$1")" = 1 ] && [ "$(grep -c ">>> Wren Linux: Safest" "$1")" = 1 ]' _ "$P/user.js"
check "...and it says so" grep -q "Safest level with JavaScript off" "$T/out"
check "...and the level is locked by the browser's policy file (the image's copy, as it is)" bash -c '
cmp -s "$1" "$2/distribution/policies.json" && grep -q "locked by the browser.s policy file" "$3"' _ "$HERE/data/tor-browser-policies-safest.json" "$B" "$T/out"
check "...your downloads too" test "$(cat "$B/Downloads/a.pdf")" = my-download
check "...in the new browser" bash -c '[ "$(cat "$1/firefox")" = new-program ] && grep -q "\"16.0\"" "$1/tbb_version.json"' _ "$B"
check "...and nothing of the old one is left" test ! -e "$KEEP"
yours 16.0; feed 16.0
tb; rc=$?
check "already the newest: update-torbrowser isn't run (it would delete your profile)" bash -c '[ "$1" = 0 ] && [ ! -e "$2" ]' _ "$rc" "$T/tb-ran"
check "...nothing changes" as_it_was 16.0
yours 16.0; feed 15.0.1
tb; rc=$?
check "the server offers an older version: refused, and it counts as a failure" bash -c '[ "$1" != 0 ] && [ ! -e "$2" ] && grep -q "could be an attack" "$3"' _ "$rc" "$T/tb-ran" "$T/out"
check "...nothing changes" as_it_was 16.0
# versions compared as versions, not as text
yours 9.5; feed 10.0
tb
check "10.0 is newer than 9.5 (compared as versions, not as text)" grep -q '"10.0"' "$B/tbb_version.json"
for bad in '{"version":"16.0; rm -rf ~"}' '{"version":"16.0.0.0.0.0.0.0.0.0.0.0"}' '{"version":16}' '{"other":"1"}' 'not json' ''; do
yours 15.0.1
printf '%s' "$bad" >"$T/feed"
tb; rc=$?
check "the server's answer isn't a version ($bad): a failure, nothing run" bash -c '[ "$1" != 0 ] && [ ! -e "$2" ]' _ "$rc" "$T/tb-ran"
check "...nothing changes" as_it_was 15.0.1
done
yours 15.0.1; feed 16.0; : >"$T/curl-fails"
tb; rc=$?
check "no answer from the server: a failure, nothing changes" bash -c '[ "$1" != 0 ] && [ ! -e "$2" ]' _ "$rc" "$T/tb-ran"
check "...your things stay" as_it_was 15.0.1
yours 15.0.1; feed 16.0; echo 6 >"$T/tb-rc"
tb; rc=$?
check "update-torbrowser fails (Tor not up, a bad signature...): a failure" test "$rc" != 0
check "...the browser is back, whole, and your things" as_it_was 15.0.1
# A download cut off (status 17: a bridge's connection is where this happens) is tried again,
# where it stopped (--resume), a few times; the checks stay what they were
yours 15.0.1; feed 16.0; echo 1 >"$T/tb-cut"
tb; rc=$?
check "a download cut off once: tried again, and it works" bash -c '[ "$1" = 0 ] && [ "$(wc -l <"$2")" = 2 ] && grep -q "Tor Browser is 16.0 now" "$3"' _ "$rc" "$T/tb-ran" "$T/out"
check "...the second time with --resume, the first without" bash -c '[ "$(sed -n 1p "$1")" = "--noask --noaskstart --input none" ] && [ "$(sed -n 2p "$1")" = "--noask --noaskstart --input none --resume" ]' _ "$T/tb-args"
check "...and your things came over" bash -c 'grep -q "Menu one" "$1" && [ "$(cat "$2")" = my-download ]' _ "$P/bookmarks.html" "$B/Downloads/a.pdf"
yours 15.0.1; feed 16.0; echo 99 >"$T/tb-cut"
tb; rc=$?
check "cut off every time: it stops after four tries, as a failure" bash -c '[ "$1" != 0 ] && [ "$(wc -l <"$2")" = 4 ]' _ "$rc" "$T/tb-ran"
check "...and your Tor Browser is back as it was" as_it_was 15.0.1
yours 15.0.1; feed 16.0; echo 6 >"$T/tb-rc"
tb; rc=$?
check "any other failure (a bad signature...) is not tried again" bash -c '[ "$1" != 0 ] && [ "$(wc -l <"$2")" = 1 ]' _ "$rc" "$T/tb-ran"
yours 15.0.1; feed 16.0; : >"$T/no-version"
tb; rc=$?
check "it says it worked, but no version can be read: a failure, and back as it was" bash -c '[ "$1" != 0 ]' _ "$rc"
check "...your things stay" as_it_was 15.0.1
# It asks the server again itself: the answer can change to an older one, and --noask installs that
yours 15.0.1; feed 16.0; printf '14.0' >"$T/installs"
tb; rc=$?
check "it installed an older one after all: a failure" test "$rc" != 0
check "...and the old browser is back" as_it_was 15.0.1
yours 15.0.1; feed 16.0; printf '15.0.1' >"$T/installs"
tb; rc=$?
check "...the same version (not higher): a failure too, and back" bash -c '[ "$1" != 0 ] && grep -q "\"15.0.1\"" "$2" && [ "$(cat "$3")" = old-program ]' _ "$rc" "$B/tbb_version.json" "$B/firefox"
rm -rf "$H" "$T/tb-ran" "$T/tb-rc"; mkdir -p "$H"; feed 16.0
tb; rc=$?
check "no browser in your home yet (before the first login): it installs one" bash -c '[ "$1" = 0 ] && grep -q "\"16.0\"" "$2"' _ "$rc" "$B/tbb_version.json"
# Safest and JavaScript off in the new browser: not trusting a fresh profile to pick them up
check "no browser in your home yet: the new one gets Safest and JavaScript off too" grep -q 'javascript.enabled", false' "$P/user.js"
check "...and the locking policy file too" cmp -s "$HERE/data/tor-browser-policies-safest.json" "$B/distribution/policies.json"
yours 15.0.1; feed 16.0
SAFEST_FILE=$T/nowhere tb; rc=$?
check "the image's Safest settings are missing: the update still works, and says plainly that they were NOT set" bash -c '[ "$1" = 0 ] && grep -q "Safest and JavaScript off were NOT set" "$2" && grep -q "\"16.0\"" "$3"' _ "$rc" "$T/out" "$B/tbb_version.json"
yours 15.0.1; feed 16.0
POLICIES_FILE=$T/nowhere tb; rc=$?
check "the image's policy file is missing: the update still works, and says plainly that Safest is NOT locked" bash -c '
[ "$1" = 0 ] && grep -q "Safest level is NOT locked" "$2" && grep -q "\"16.0\"" "$3" && [ ! -e "$4/distribution/policies.json" ]' _ "$rc" "$T/out" "$B/tbb_version.json" "$B"
# reinforce_safest again on a user.js that has other lines and an old block: the block is replaced, the rest stays
mkdir -p "$T/rs"
printf '%s\n' 'user_pref("browser.places.importBookmarksHTML", true);' '// >>> Wren Linux: Safest, JavaScript off >>>' 'user_pref("javascript.enabled", true);' '// <<< Wren Linux: Safest <<<' >"$T/rs/user.js"
check "an old Safest block is replaced (not added to), the other lines stay" bash -c '
eval "$(sed -n "/^reinforce_safest() {/,/^}/p" "$1")"
TB=$2/tb PROFILES=p PROFILE=q SAFEST=$3
mkdir -p "$TB/Browser/p/q" && cp "$2/user.js" "$TB/Browser/p/q/user.js"
reinforce_safest >/dev/null; reinforce_safest >/dev/null
j=$TB/Browser/p/q/user.js
[ "$(grep -c ">>> Wren Linux: Safest" "$j")" = 1 ] && grep -q "importBookmarksHTML" "$j" && ! grep -q "javascript.enabled\", true" "$j" && grep -q "javascript.enabled\", false" "$j"' _ "$HERE/data/whonix-update-browser" "$T/rs" "$HERE/data/tor-browser-safest.js"
# A power cut, at the worst places. The next run puts it right
yours 15.0.1; feed 15.0.1
mv "$H/.tb/tor-browser" "$KEEP"
tb; rc=$?
check "cut off after moving the old one aside, nothing new yet: all back" bash -c '[ "$1" = 0 ]' _ "$rc"
check "...your things are there" as_it_was 15.0.1
yours 15.0.1; feed 15.0.1
mv "$H/.tb/tor-browser" "$KEEP"
mkdir -p "$B"; echo half >"$B/firefox"
tb
check "cut off with a new browser half there: it goes, and all is back" as_it_was 15.0.1
# (the new one found good, Downloads brought over and the profile not yet)
yours 15.0.1; feed 16.0
mv "$H/.tb/tor-browser" "$KEEP"
: >"$KEEP/.new-browser-ready"
mkdir -p "$P"
mv "$KEEP/Browser/Downloads" "$B/Downloads"
printf '{"version":"16.0"}' >"$B/tbb_version.json"
echo new-program >"$B/firefox"
tb; rc=$?
check "cut off while bringing your things over: it carries on, and loses nothing" bash -c '
[ "$1" = 0 ] && grep -q "Menu one" "$2/TorBrowser/Data/Browser/profile.default/bookmarks.html" &&
[ "$(cat "$2/Downloads/a.pdf")" = my-download ] && [ "$(cat "$2/firefox")" = new-program ] && [ ! -e "$3" ] &&
[ ! -e "$4/profile" ] && grep -q "javascript.enabled\", false" "$2/TorBrowser/Data/Browser/profile.default/user.js"' _ "$rc" "$B" "$KEEP" "$H/.tb/left-from-browser-15.0.1"
# (cut off after the bookmarks were made and the old profile put aside, before the old folder went)
yours 15.0.1; feed 16.0
mv "$H/.tb/tor-browser" "$KEEP"
: >"$KEEP/.new-browser-ready"
mkdir -p "$P"
mv "$KEEP/Browser/Downloads" "$B/Downloads"
printf '<A HREF="https://example.org/menu">Menu one</A>' >"$P/bookmarks.html"
mkdir -p "$H/.tb/left-from-browser-15.0.1"
mv "$KEEP/Browser/TorBrowser/Data/Browser" "$H/.tb/left-from-browser-15.0.1/profile"
printf '{"version":"16.0"}' >"$B/tbb_version.json"
echo new-program >"$B/firefox"
tb; rc=$?
check "cut off after the old profile was put aside: the bookmarks stay, the old folder goes" bash -c '
[ "$1" = 0 ] && grep -q "Menu one" "$2/TorBrowser/Data/Browser/profile.default/bookmarks.html" && [ ! -e "$3" ] &&
[ -f "$4/profile/profile.default/places.sqlite" ]' _ "$rc" "$B" "$KEEP" "$H/.tb/left-from-browser-15.0.1"
# a bookmarks database that can't be read: the update goes on, the old profile is kept, and it says so
yours 15.0.1; feed 16.0; echo not-a-database >"$P/places.sqlite"
tb; rc=$?
check "a bookmarks file that is no database: the update still works" bash -c '[ "$1" = 0 ] && grep -q "\"16.0\"" "$2"' _ "$rc" "$B/tbb_version.json"
check "...no bookmarks are made up, and it says it couldn't read them" bash -c '[ ! -e "$1" ] && grep -q "read your bookmarks" "$2"' _ "$P/bookmarks.html" "$T/out"
check "...and the old profile is kept to look at" test -f "$H/.tb/left-from-browser-15.0.1/profile/profile.default/places.sqlite"
yours 15.0.1; feed 16.0; /bin/rm -f "$P/places.sqlite"
tb; rc=$?
check "no bookmarks database at all: the update works, nothing is made up" bash -c '[ "$1" = 0 ] && [ ! -e "$2" ]' _ "$rc" "$P/bookmarks.html"
# The power goes while the old folder is being deleted, your things already in the new one.
# rm takes a folder's files in no fixed order, so the worst one: the "new browser is good"
# marker goes first, and then the script is dead (a pretend rm, killing it as a power cut would)
cat >"$T/bin/rm" <<PRETEND
#!/bin/bash
if [ -e "$T/cut-rm" ]; then
for a; do
if [ -e "\$a/.new-browser-ready" ]; then
/bin/rm -f "$T/cut-rm" "\$a/.new-browser-ready"
kill -9 "\$PPID"
exit 137
fi
done
fi
exec /bin/rm "\$@"
PRETEND
chmod +x "$T/bin/rm"
yours 15.0.1; feed 16.0; : >"$T/cut-rm"
tb; rc=$?
check "the power goes while the old folder is deleted (the test really cut it off)" test "$rc" = 137
feed 16.0
tb; rc=$?
check "...the next run keeps the new browser: your bookmarks aren't deleted with a rollback" bash -c '
[ "$1" = 0 ] && grep -q "Menu one" "$2/TorBrowser/Data/Browser/profile.default/bookmarks.html" &&
[ "$(cat "$2/Downloads/a.pdf")" = my-download ] && [ "$(cat "$2/firefox")" = new-program ]' _ "$rc" "$B"
check "...and what was left of the old folder is gone (only the kept old profile is beside it)" bash -c '[ -z "$(ls -A "$1" | grep -v -e "^tor-browser$" -e "^left-from-browser-")" ]' _ "$H/.tb"
/bin/rm -f "$T/bin/rm"
# Files of yours elsewhere in the browser's folder (a page saved next to Downloads, a folder
# made in a save dialog): the old folder goes, so they are moved out first, not lost
L=$H/.tb/left-from-browser-15.0.1
yours 15.0.1; feed 16.0
echo my-page >"$B/saved-page.html"
mkdir "$B/.my-notes" && echo my-note >"$B/.my-notes/a"
mkdir -p "$B/.cache/x" "$B/.local/share" && echo trace >"$B/.cache/x/a" && echo recent >"$B/.local/share/recently-used.xbel"
tb; rc=$?
check "a file of yours elsewhere in the browser's folder isn't lost in an update" bash -c '[ "$1" = 0 ] && [ "$(cat "$2/saved-page.html")" = my-page ]' _ "$rc" "$L"
check "...a hidden folder neither" test "$(cat "$L/.my-notes/a" 2>/dev/null)" = my-note
check "...it says where they went" grep -q 'left-from-browser-15.0.1' "$T/out"
check "...but not the old browser's cache and recent files: those go with it" bash -c '[ ! -e "$1/.cache" ] && [ ! -e "$1/.local" ]' _ "$L"
check "...and the new browser's folder doesn't carry them (nothing old runs from there)" bash -c '[ ! -e "$1/saved-page.html" ] && [ ! -e "$1/.my-notes" ]' _ "$B"
yours 15.0.1; feed 16.0; echo 6 >"$T/tb-rc"
echo my-page >"$B/saved-page.html"
tb
check "a failed update leaves such a file where it was" test "$(cat "$B/saved-page.html" 2>/dev/null)" = my-page
yours 15.0.1; feed 16.0
tb
check "with nothing else of yours in it, nothing is kept there at all" test ! -e "$L"
# ---- apt, as root (data/whonix-update) ----
# (a copy with its paths in the temp folder: nothing here is real, and no real apt is run)
mkdir -p "$T/root"
sed -e "s|^export PATH=.*|export PATH=$T/root:\$PATH|" -e "s|/usr/bin/update-torbrowser|$T/is-workstation|" \
-e 's|WREN_UPDATE_PAUSE:-3|WREN_UPDATE_PAUSE:-0|' -e "s|/etc/resolv.conf.whonix|$T/resolv|g" \
-e "s|/var/lib/wren-whonix-nameserver|$T/ns-state|g" "$HERE/data/whonix-update" >"$T/update"
cat >"$T/root/apt-get" <<PRETEND
#!/bin/sh
echo "apt-get \$*" >>"$T/apt"
case " \$* " in
*" update "*)
if [ -s "$T/update-fails" ]; then sed -i 1d "$T/update-fails"; exit 100; fi
[ ! -e "$T/never-online" ] || exit 100 ;;
*" full-upgrade"*)
# --force-confnew: the package's own file replaces the one Wren Linux edited
# (reworded: a Whonix whose file no longer has the line as Wren Linux knows it)
case " \$* " in *confnew*)
if [ -e "$T/reworded" ]; then printf '# stock\n# nameserver 10.0.2.3 (for bridges)\n' >"$T/resolv"
elif [ -e "$T/resolv" ]; then printf '# stock\n#nameserver 10.0.2.3\n' >"$T/resolv"; fi ;;
esac
[ ! -e "$T/upgrade-fails" ] || exit 100 ;;
esac
PRETEND
printf '#!/bin/sh\necho "dpkg $*" >>"%s/apt"\n' "$T" >"$T/root/dpkg"
printf '#!/bin/sh\necho "runuser $*" >>"%s/apt"\n[ ! -e "%s/browser-fails" ]\n' "$T" "$T" >"$T/root/runuser"
printf '#!/bin/sh\necho "mkhomedir_helper $*" >>"%s/apt"\n' "$T" >"$T/root/mkhomedir_helper"
chmod +x "$T/root/"*
vm() { # gateway|workstation
rm -f "$T/apt" "$T/update-fails" "$T/never-online" "$T/upgrade-fails" "$T/browser-fails" "$T/is-workstation" "$T/resolv" "$T/ns-state" "$T/reworded"
if [ "$1" = workstation ]; then printf '#!/bin/sh\n' >"$T/is-workstation"; chmod +x "$T/is-workstation"; fi
}
up() { WREN_UPDATE_WAIT=0 WREN_UPDATE_TRIES=3 bash "$T/update" >"$T/out" 2>&1; sync; /bin/sleep 0.2; }
said() { grep -a '^WREN-RESULT: ' "$T/out" | tail -1; }
vm gateway; up
check "the Gateway: apt, and it says all went well" test "$(said)" = "WREN-RESULT: ok"
check "...nothing for Tor Browser" not grep -q '^runuser' "$T/apt"
check "...the lists, then the upgrade" bash -c 'grep -n "update\|full-upgrade" "$1" | cut -d" " -f2- | grep -q "" && [ "$(grep -n " update" "$1" | cut -d: -f1 | head -1)" -lt "$(grep -n "full-upgrade" "$1" | cut -d: -f1)" ]' _ "$T/apt"
check "...the maintainer's config files, as Whonix's own tool does (a kept old one keeps its old holes)" bash -c '
grep "full-upgrade" "$1" | grep -q "Dpkg::Options::=--force-confnew" && grep "full-upgrade" "$1" | grep -q "Dpkg::Options::=--force-confdef" &&
! grep -q "confold\|confmiss" "$1" && ! grep -v "^#" "$2" | grep -q "apt-get-noninteractive"' _ "$T/apt" "$HERE/data/whonix-update"
check "...a server out of reach is a failure (apt-get update exits 0 without this)" grep -q 'APT::Update::Error-Mode=any' "$T/apt"
check "...and what an earlier cut-off update left is finished first" bash -c 'head -1 "$1" | grep -q "^dpkg --force-confdef --force-confnew --configure -a"' _ "$T/apt"
check "it never downgrades or removes what is essential by force" bash -c '! grep -q -- "--allow-\|--force-yes" "$1"' _ "$T/apt"
# The one edit Wren Linux makes in a packaged file (the Gateway's /etc/resolv.conf.whonix, for bridges
# given as names) is put back after the maintainer's file comes in, if it was on
vm gateway; printf '# stock\nnameserver 10.0.2.3\n' >"$T/resolv"; up
check "the Gateway's nameserver line, on before, is on after the maintainer's file came in" grep -qx 'nameserver 10.0.2.3' "$T/resolv"
check "...and nothing else of the file is changed" test "$(grep -c . "$T/resolv")" = 2
check "...the note it kept is gone once it is done" test ! -e "$T/ns-state"
vm gateway; printf '# stock\n#nameserver 10.0.2.3\n' >"$T/resolv"; up
check "off before, it stays off" grep -qx '#nameserver 10.0.2.3' "$T/resolv"
vm gateway; printf '# stock\nnameserver 10.0.2.3\n' >"$T/resolv"; : >"$T/upgrade-fails"; up
check "the upgrade fails halfway: the line is put back all the same" grep -qx 'nameserver 10.0.2.3' "$T/resolv"
vm gateway; printf '# stock\n#nameserver 10.0.2.3\n' >"$T/resolv"; : >"$T/ns-state"; up
check "an earlier update, cut off after the file was replaced, left a note: the line comes back" grep -qx 'nameserver 10.0.2.3' "$T/resolv"
# Whonix rewords the line: it can't be put back, and that is said, not lost
vm gateway; printf '# stock\nnameserver 10.0.2.3\n' >"$T/resolv"; : >"$T/reworded"; up
check "the line can't be put back (Whonix reworded it): the update says so instead of ok" bash -c '
case $1 in "WREN-RESULT: failed: "*nameserver*) ;; *) exit 1 ;; esac' _ "$(said)"
check "...in words the host shows whole (it drops all but letters, digits and :.,_- and cuts at 200)" bash -c '
m=${1#WREN-RESULT: }; [ "$m" = "$(printf %s "$m" | LC_ALL=C tr -cd "A-Za-z0-9 :.,_-" | cut -c1-200)" ]' _ "$(said)"
check "...and its note is kept, so the next update tries again" test -e "$T/ns-state"
rm -f "$T/reworded"; printf '# stock\nnameserver 10.0.2.3\n' >"$T/resolv"; up
check "put on by hand in the Gateway, the next update is ok again and the note goes" bash -c '
[ "$1" = "WREN-RESULT: ok" ] && [ ! -e "$2" ] && grep -qx "nameserver 10.0.2.3" "$3"' _ "$(said)" "$T/ns-state" "$T/resolv"
vm workstation; up
check "a machine without that file (the Workstation) gets none" test ! -e "$T/resolv" -a ! -e "$T/ns-state"
vm gateway; printf 'x\nx\n' >"$T/update-fails"; up
check "Tor still connecting: it waits and tries again" bash -c '[ "$(grep -c " update" "$1")" = 3 ] && [ "$2" = "WREN-RESULT: ok" ]' _ "$T/apt" "$(said)"
vm gateway; : >"$T/never-online"; up
check "never connects (Tor held off until you choose a bridge): a failure, no upgrade, and it doesn't get around that" bash -c '
[ "$1" = "WREN-RESULT: failed: no connection to the update servers" ] && ! grep -q full-upgrade "$2"' _ "$(said)" "$T/apt"
vm gateway; : >"$T/upgrade-fails"; up
check "apt fails: a failure" test "$(said)" = "WREN-RESULT: failed: apt"
vm workstation; : >"$T/upgrade-fails"; up
check "...and Tor Browser isn't touched then" not grep -q '^runuser' "$T/apt"
vm workstation; up
check "the Workstation: apt, then Tor Browser" test "$(said)" = "WREN-RESULT: ok"
check "...as the user, with a session (Whonix's check that Tor is up needs one) but no ~/.profile" \
grep -qx "runuser -l user -s /usr/local/libexec/wren-update-browser" "$T/apt"
vm workstation; : >"$T/browser-fails"; up
check "...Tor Browser fails: the whole update counts as failed" test "$(said)" = "WREN-RESULT: failed: Tor Browser"
check "...a result is the last thing it says, for the host to read" bash -c '[ "$(tail -n 1 "$1" | cut -d: -f1)" = WREN-RESULT ]' _ "$T/out"
check "nothing of the browser's folders is handled as root (a hacked browser can write there)" \
not grep -qE '\.tb|tor-browser|Browser/' "$HERE/data/whonix-update"
# ---- the service, and how it gets into the images ----
S=$HERE/data/whonix-update.service
check "the service runs only with the host's note" grep -qx 'ConditionCredential=wren.update' "$S"
check "...before the desktop and any login" grep -qx 'Before=display-manager.service getty@tty1.service' "$S"
check "...and however it ends, the VM powers off" grep -qx 'ExecStopPost=/usr/bin/systemctl poweroff --no-block' "$S"
check "...it can't wait for ever" grep -qx 'TimeoutStartSec=3h' "$S"
check "started by a plain file (the images are written file by file, not links)" grep -qx 'Wants=wren-update.service' "$HERE/data/whonix-update-wants.conf"
X=$HERE/extras/whonix.sh
for f in 'data/whonix-update" /usr/local/libexec/wren-update' 'data/whonix-update.service" /etc/systemd/system/wren-update.service' \
'data/whonix-update-wants.conf" /etc/systemd/system/multi-user.target.d/wren-update.conf'; do
check "the install writes $f" grep -qF "$f" "$X"
done
check "...into the Gateway" bash -c 'sed -n "/^gateway_config()/,/^}/p" "$1" | grep -qF "\${UPDATE_FILES[@]}"' _ "$X"
check "...and the Workstation, with its browser script" bash -c '
sed -n "/^ws_files=/,/)$/p" "$1" | grep -qF "\${UPDATE_FILES[@]}" &&
sed -n "/^ws_files=/,/)$/p" "$1" | grep -qF -- "--exec \"\$WREN_DIR/data/whonix-update-browser\" /usr/local/libexec/wren-update-browser"' _ "$X"
# The locked Safest level: whonix.sh ships the start-up conf and the policy file (the desktop's,
# data/tor-browser-policies-safest.json), and the conf puts the policy back at every browser
# start. Whonix's starter (tb-starter's torbrowser) sources /usr/local/etc/torbrowser.d/*.conf
# and then calls the tb_custom_user_js_file the conf replaces.
check "the install writes the start-up conf and the locking policy file into the Workstation" bash -c '
sed -n "/^ws_files=/,/)$/p" "$1" | grep -qF "data/whonix-torbrowser.conf\" /usr/local/etc/torbrowser.d/50_wren.conf" &&
sed -n "/^ws_files=/,/)$/p" "$1" | grep -qF "data/tor-browser-policies-safest.json\" /usr/local/etc/torbrowser.d/policies.json"' _ "$X"
check "at a browser start the conf writes user.js into the profile and the policy into the browser" bash -c '
set -e
bash -n "$1"
d=$(mktemp -d) || exit 1
trap "rm -rf \"\$d\"" EXIT
echo settings >"$d/user.js"; echo policy >"$d/policies.json"
mkdir -p "$d/tb/Browser/p"
# as tb-starter has them when it calls the function
tb_user_js_source_file=$d/user.js tb_user_js_target_file=$d/tb/Browser/p/user.js
tb_browser_folder=$d/tb WREN_POLICIES=$d/policies.json
source "$1"
tb_custom_user_js_file
cmp -s "$d/user.js" "$d/tb/Browser/p/user.js" && cmp -s "$d/policies.json" "$d/tb/Browser/distribution/policies.json"' _ "$HERE/data/whonix-torbrowser.conf"
check "...and without the policy file the browser still starts (nothing fails)" bash -c '
d=$(mktemp -d) || exit 1
trap "rm -rf \"\$d\"" EXIT
echo settings >"$d/user.js"
mkdir -p "$d/tb/Browser/p"
tb_user_js_source_file=$d/user.js tb_user_js_target_file=$d/tb/Browser/p/user.js
tb_browser_folder=$d/tb WREN_POLICIES=$d/nowhere
source "$1"
tb_custom_user_js_file && [ ! -e "$d/tb/Browser/distribution" ]' _ "$HERE/data/whonix-torbrowser.conf"
check "the install starts the 7 days, and --remove forgets them" bash -c '
grep -q "touch \"\$WHONIX_STAMP\"" "$1" && sed -n "/--remove ]; then/,/^fi/p" "$1" | grep -q "\$WHONIX_STAMP"' _ "$X"
check "the update never says your settings are kept: only bookmarks and Downloads are" \
bash -c '! grep -qiE "bookmarks, settings|settings and downloads are kept" "$1"' _ "$HERE/data/whonix-update-browser"
check "both scripts parse" bash -c 'bash -n "$1" && bash -n "$2"' _ "$HERE/data/whonix-update" "$HERE/data/whonix-update-browser"
if command -v shellcheck >/dev/null; then
check "...and shellcheck finds nothing" shellcheck -x -s bash "$HERE/data/whonix-update" "$HERE/data/whonix-update-browser"
fi
[ "$fails" -eq 0 ]