summaryrefslogtreecommitdiffstats
path: root/config/chroot_local-includes/usr/local/sbin
diff options
context:
space:
mode:
authorsajolida <sajolida@pimienta.org>2018-09-06 10:02:49 +0000
committersajolida <sajolida@pimienta.org>2018-09-06 10:02:49 +0000
commit7d452d39998b5543bacc53696a0897dd2906da4f (patch)
treea51c412b3dbd9b5f3a2100619d8b61d4eebcc78a /config/chroot_local-includes/usr/local/sbin
parentbda974fa5d21c9cb2ba54611687d78a1eb11a23f (diff)
parent3629361577a8af12247ec95a151ad3922cc2ecc5 (diff)
Merge remote-tracking branch 'origin/devel' into feature/15291-remove-softwarewip/feature/15291-remove-software-sajolida-experiment
Diffstat (limited to 'config/chroot_local-includes/usr/local/sbin')
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/live-persist48
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/tails-additional-software573
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/tails-additional-software-remove18
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/tails-debugging-info122
-rwxr-xr-xconfig/chroot_local-includes/usr/local/sbin/unsafe-browser8
5 files changed, 600 insertions, 169 deletions
diff --git a/config/chroot_local-includes/usr/local/sbin/live-persist b/config/chroot_local-includes/usr/local/sbin/live-persist
index fe1eb93..8bdba4f 100755
--- a/config/chroot_local-includes/usr/local/sbin/live-persist
+++ b/config/chroot_local-includes/usr/local/sbin/live-persist
@@ -226,9 +226,9 @@ other::r-x"
persistence_conf_file_has_correct_access_rights ()
{
local conf="$1"
+ local expected_perms="$2"
local expected_user=tails-persistence-setup
local expected_group=tails-persistence-setup
- local expected_perms=600
local expected_acl=""
if [ $(stat -c %U "$conf") != "$expected_user" ]
@@ -258,18 +258,25 @@ persistence_conf_file_has_correct_access_rights ()
disable_and_create_empty_persistence_conf_file ()
{
local conf="$1"
+ local mode="$2"
+
+ if [ -z "$mode" ]
+ then
+ mode=0600
+ fi
mv "$conf" "${conf}.insecure_disabled" \
|| error "Failed to disable '$conf': $?"
- create_empty_persistence_conf_file "$conf"
+ create_empty_persistence_conf_file "$conf" "$mode"
}
create_empty_persistence_conf_file ()
{
local conf="$1"
+ local mode="$2"
install --owner tails-persistence-setup \
- --group tails-persistence-setup --mode 0600 \
+ --group tails-persistence-setup --mode "$mode" \
/dev/null "$conf" \
|| error "Failed to create empty '$conf': $?"
}
@@ -341,7 +348,7 @@ activate_volumes ()
do
if test ! -f "$mountpoint/live-additional-software.conf"
then
- create_empty_persistence_conf_file "$mountpoint/live-additional-software.conf"
+ create_empty_persistence_conf_file "$mountpoint/live-additional-software.conf" "0644"
fi
done
@@ -349,25 +356,40 @@ activate_volumes ()
# has wrong access rights.
if [ "$ACCESS_RIGHTS_ARE_CORRECT" != true ]
then
- for f in $(ls /live/persistence/*_unlocked/persistence.conf \
- /live/persistence/*_unlocked/live-additional-software.conf || true)
+ for f in $(ls /live/persistence/*_unlocked/persistence.conf || true)
do
warning "Disabling '$f': persistent volume has unsafe access rights"
disable_and_create_empty_persistence_conf_file "$f"
done
+ for f in $(ls /live/persistence/*_unlocked/live-additional-software.conf || true)
+ do
+ warning "Disabling '$f': persistent volume has unsafe access rights"
+ disable_and_create_empty_persistence_conf_file "$f" "644"
+ done
fi
# Regardless of the mountpoint access rights, disable persistence
# configuration files with wrong access rights.
- for f in $(ls /live/persistence/*_unlocked/persistence.conf \
- /live/persistence/*_unlocked/live-additional-software.conf || true)
+ for f in $(ls /live/persistence/*_unlocked/persistence.conf || true)
do
- if ! persistence_conf_file_has_correct_access_rights "$f"
+ if ! persistence_conf_file_has_correct_access_rights "$f" "600"
then
warning "Disabling '$f', that has unsafe access rights"
disable_and_create_empty_persistence_conf_file "$f"
fi
done
+ for f in $(ls /live/persistence/*_unlocked/live-additional-software.conf || true)
+ do
+ if persistence_conf_file_has_correct_access_rights "$f" "600"
+ then
+ chmod 0644 "$f"
+ fi
+ if ! persistence_conf_file_has_correct_access_rights "$f" "644"
+ then
+ warning "Disabling '$f', that has unsafe access rights"
+ disable_and_create_empty_persistence_conf_file "$f" "644"
+ fi
+ done
# Fix permissions on persistent directories that were created
# with unsafe permissions.
@@ -437,6 +459,14 @@ activate_volumes ()
fi
fi
+ # Get rid of any Enigmail configuredVersion that we previously used
+ # to set in a way that would persistently override the value maintained
+ # by Enigmail itself (#12680, #15693). We stopped writing this pref
+ # there a long time ago but recently instructed users to reintroduce
+ # this problem as a workaround (#15692).
+ tb_profile="$(dirname "${conf}")/thunderbird/profile.default"
+ rm -f "${tb_profile}/preferences/0000tails.js"
+
for vol in ${open_volumes}
do
if grep -qe "^${vol}\>" /proc/mounts
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-additional-software b/config/chroot_local-includes/usr/local/sbin/tails-additional-software
index ab31a96..cdca343 100755
--- a/config/chroot_local-includes/usr/local/sbin/tails-additional-software
+++ b/config/chroot_local-includes/usr/local/sbin/tails-additional-software
@@ -1,23 +1,53 @@
-#!/usr/bin/env python3
+#!/usr/bin/python3
import gettext
+import json
+import logging
+import logging.handlers
import os
import os.path
+import pwd
import shutil
import subprocess
import sys
-import syslog
+
+import apt.cache
+
+from tailslib import LIVE_USERNAME
+
+from tailslib.additionalsoftware.config import (
+ add_additional_packages,
+ filter_package_details,
+ get_additional_packages,
+ get_packages_list_path,
+ remove_additional_packages)
+
+from tailslib.persistence import (
+ has_unlocked_persistence,
+ has_persistence,
+ is_tails_media_writable,
+ launch_persistence_setup,
+ PERSISTENCE_DIR)
+
+from tailslib.utils import launch_x_application
_ = gettext.gettext
-PERSISTENCE_DIR = "/live/persistence/TailsData_unlocked"
-PACKAGES_LIST_FILE = os.path.join(
- PERSISTENCE_DIR, "live-additional-software.conf")
+ASP_STATE_DIR = "/run/live-additional-software"
+ASP_STATE_PACKAGES = os.path.join(ASP_STATE_DIR, "packages")
+ASP_STATE_INSTALLER_ASKED = os.path.join(ASP_STATE_DIR, "installer-asked")
+ASP_LOG_FILE = os.path.join(ASP_STATE_DIR, "log")
OLD_APT_LISTS_DIR = os.path.join(PERSISTENCE_DIR, 'apt', 'lists.old')
APT_ARCHIVES_DIR = "/var/cache/apt/archives"
APT_LISTS_DIR = "/var/lib/apt/lists"
+def _exit_if_in_live_build():
+ """Exits with success if running inside live-build."""
+ if "SOURCE_DATE_EPOCH" in os.environ:
+ sys.exit(0)
+
+
def _launch_apt_get(specific_args):
"""Launch apt-get with given arguments.
@@ -41,47 +71,171 @@ def _launch_apt_get(specific_args):
stdout=subprocess.PIPE)
for line in iter(apt_get.stdout.readline, ''):
if not line.startswith('('):
- syslog.syslog(line.rstrip())
+ logging.info(line.rstrip())
apt_get.wait()
if apt_get.returncode:
- syslog.syslog(syslog.LOG_WARNING,
- "apt-get exited with returncode %i" % apt_get.returncode)
+ logging.warn("apt-get exited with returncode %i" % apt_get.returncode)
return apt_get.returncode
-def _notify(title, body):
- """Display a notification to the user of the live system."""
- cmd = "/usr/local/sbin/tails-notify-user"
+def _notify(title, body="", accept_label="", deny_label="",
+ documentation_target="", urgent=False, return_id=False):
+ """Display a notification to the user of the live system.
+
+ The notification will show title and body.
+
+ If accept_label or deny_label are set, they will be shown on action buttons
+ and the method will wait for user input and return 1 if the button with
+ accept_label was clicked or 0 if the button with deny_label was
+ clicked.
+
+ If documentation_target is set, a "Documentation" action button will open
+ corresponding tails documentation when clicked.
+
+ If return_id is true, returns the notification ID, which may be used to
+ close the notification.
+
+ Else, return None.
+ """
+
+ cmd = "/usr/local/lib/tails-additional-software-notify"
+ if urgent:
+ urgent = "urgent"
+ else:
+ urgent = ""
+
try:
- subprocess.check_call([cmd, title, body], stderr=subprocess.STDOUT)
- except subprocess.CalledProcessError as e:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: unable to notify the user. %s returned "
- "with exit code %s" % (cmd, e.returncode))
- syslog.syslog(syslog.LOG_WARNING,
- "The notification was: %s %s" % (title, body))
+ completed_process = subprocess.run(["sudo", "-u", LIVE_USERNAME, cmd,
+ title, body, accept_label,
+ deny_label, documentation_target,
+ urgent],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
+ if completed_process.returncode == 1:
+ # sudo failed to execute the command
+ raise OSError(completed_process.stderr)
except OSError as e:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: unable to notify the user. %s" % e)
- syslog.syslog(syslog.LOG_WARNING,
- "The notification was: %s %s" % (title, body))
+ logging.warn("Warning: unable to notify the user. %s" % e)
+ logging.warn("The notification was: %s %s" % (title, body))
+ return None
+
+ if return_id:
+ for line in completed_process.stdout.splitlines():
+ if line.startswith("id="):
+ return line[3:]
+ else:
+ if completed_process.returncode == 0:
+ return 1
+ elif completed_process.returncode == 3:
+ return 0
+ else:
+ return None
-def has_additional_packages_list():
- """Return true iff PACKAGES_LIST_FILE exists."""
- return os.path.isfile(PACKAGES_LIST_FILE)
+def _notify_failure(summary, details=None):
+ """Display a failure notification to the user of the live system.
+ The user has the option to edit the configuration of to view the system
+ log.
+ """
+ if details:
+ details = _("{details} Please check your list of additional "
+ "software or read the system log to "
+ "understand the problem.").format(details=details)
-def get_additional_packages():
- """Return the list of all additional packages configured."""
- packages = []
- if has_additional_packages_list():
- with open(PACKAGES_LIST_FILE) as f:
- for line in f:
- line = line.strip()
- if line:
- packages.append(line)
- return packages
+ else:
+ details = _("Please check your list of additional "
+ "software or read the system log to "
+ "understand the problem.").format(details=details)
+
+ action_clicked = _notify(summary, details, _("Show Log"), _("Configure"),
+ urgent=True)
+ if action_clicked == 1:
+ show_system_log()
+ elif action_clicked == 0:
+ show_configuration_window()
+
+
+def _close_notification(notification_id):
+ """Close a notification shown to the user of the live system."""
+ subprocess.run(
+ ["sudo", "-u", LIVE_USERNAME,
+ "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{uid}/bus".format(
+ uid=pwd.getpwnam(LIVE_USERNAME).pw_uid),
+ "gdbus", "call",
+ "--session",
+ "--dest", "org.freedesktop.Notifications",
+ "--object-path", "/org/freedesktop/Notifications",
+ "--method", "org.freedesktop.Notifications.CloseNotification",
+ str(notification_id)],
+ stdout=subprocess.DEVNULL)
+
+
+def _spawn_daemon(func):
+ """Spawn func after double-forking.
+
+ Do the UNIX double-fork magic, see Stevens' "Advanced
+ Programming in the UNIX Environment" for details (ISBN 0201563177).
+
+ From https://stackoverflow.com/questions/6011235/run-a-program-from-
+ python-and-have-it-continue-to-run-after-the-script-is-kille
+ """
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # parent process, return and keep running
+ return
+ except OSError as e:
+ logging.error("fork #1 failed: %d (%s)" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ os.setsid()
+
+ # do second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # exit from second parent
+ sys.exit(0)
+ except OSError as e:
+ logging.error("fork #2 failed: %d (%s)" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ # do stuff
+ func()
+
+
+def _format_iterable(iterable):
+ """Return a nice formatted string with the elements of iterable."""
+ iterable = sorted(iterable)
+
+ if len(iterable) == 1:
+ return iterable[0]
+ elif len(iterable) > 1:
+ return _("{beginning} and {last}").format(
+ beginning=_(", ").join(iterable[:-1]), last=iterable[-1])
+ else:
+ return str(iterable)
+
+
+def has_additional_packages_list(search_new_persistence=False):
+ """Return true iff a packages list file is found in a persistence.
+
+ Log warnings in syslog.
+ The search_new_persistence argument is passed to get_persistence_path.
+ """
+ try:
+ packages_list_path = get_packages_list_path(search_new_persistence)
+ except FileNotFoundError as e:
+ logging.warn("Warning: {}".format(e))
+ return False
+ if os.path.isfile(packages_list_path):
+ logging.info("Found additional packages list.")
+ return True
+ else:
+ logging.warn("Warning: no configuration file found.")
+ return False
def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
@@ -92,9 +246,8 @@ def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
def save_old_apt_lists(srcdir=APT_LISTS_DIR, destdir=OLD_APT_LISTS_DIR):
"""Save a copy of the APT lists"""
if os.path.exists(destdir):
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: a copy of the APT lists already exists, "
- "which should never happen. Removing it.")
+ logging.warn("Warning: a copy of the APT lists already exists, "
+ "which should never happen. Removing it.")
delete_old_apt_lists(destdir)
shutil.copytree(srcdir, destdir, symlinks=True)
@@ -117,15 +270,227 @@ def restore_old_apt_lists(srcdir=OLD_APT_LISTS_DIR, dstdir=APT_LISTS_DIR):
shutil.move(path, dstdir)
-def install_additional_packages(ignore_old_apt_lists=False):
- """Subcommand which activates and installs all additional packages."""
- syslog.syslog("Starting to install additional software...")
-
- if has_additional_packages_list():
- syslog.syslog("Found additional packages list")
- else:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: no configuration file found, exiting")
+def handle_installed_packages(packages):
+ """Configure packages as additional software packages if the user wants to.
+
+ Ask the user if packages should be added to additional software, and
+ actually add them if requested.
+ """
+ logging.info("New packages manually installed: %s" % packages)
+ if has_unlocked_persistence(search_new_persistence=True):
+ if _notify(_("Add {packages} to your additional software?").format(
+ packages=_format_iterable(packages)),
+ _("To install it automatically from your persistent "
+ "storage when starting Tails."),
+ _("Install Every Time"),
+ _("Install Only Once"),
+ urgent=True):
+ try:
+ setup_additional_packages()
+ add_additional_packages(packages, search_new_persistence=True)
+ except Exception as e:
+ _notify_failure(_("The configuration of your additional "
+ "software failed."))
+ raise e
+ elif has_persistence():
+ # When a package is installed with a persistent storage locked, don't
+ # show any notification.
+ #
+ # People who have a persistent storage but don't unlock it, probably do
+ # this only sometimes and for a reason. They probably otherwise unlock
+ # their persistent storage most of the time.
+ #
+ # If they install packages with their persistent storage locked, they
+ # probably do it with their persistent storage unlock as well and would
+ # learn about this feature when it's most relevant for them.
+ logging.warn("Warning: persistence storage is locked, can't add "
+ "additional software.")
+ elif is_tails_media_writable():
+ if _notify(_("Add {packages} to your additional software?").format(
+ packages=_format_iterable(packages)),
+ _("To install it automatically when starting Tails, you "
+ "can create a persistent storage and activate the "
+ "<b>Additional Software</b> feature."),
+ _("Create Persistent Storage"),
+ _("Install Only Once"),
+ urgent=True):
+ try:
+ create_persistence_and_setup_additional_packages(packages)
+ except Exception as e:
+ _notify_failure(_("The configuration of your additional "
+ "software failed."),
+ _("Creating your persistent storage "
+ "failed."))
+ raise e
+ else: # It's impossible to have a persistent storage
+ logging.warn("Cannot create persistent storage on this media.")
+ if not os.path.isfile(ASP_STATE_INSTALLER_ASKED):
+ open(ASP_STATE_INSTALLER_ASKED, 'a').close()
+ _notify(_("You could install {packages} automatically when "
+ "starting Tails").format(
+ packages=_format_iterable(packages)),
+ _("To do so, you need to run Tails from a USB stick "
+ "installed using <i>Tails Installer</i>."),
+ documentation_target="install/clone",
+ urgent=True)
+
+
+def handle_removed_packages(packages):
+ """Removes packages from additional software packages if the user wants to.
+
+ Ask the user if packages should be removed from additional software, and
+ actually remove them if requested.
+ """
+ logging.info("Additional packages removed: %s" % packages)
+ if _notify(_("Remove {packages} from your additional software?").format(
+ packages=_format_iterable(packages)),
+ _("This will stop installing {packages} automatically.").format(
+ packages=_format_iterable(packages)),
+ _("Remove"),
+ _("Cancel"),
+ urgent=True):
+ try:
+ remove_additional_packages(packages, search_new_persistence=True)
+ except Exception as e:
+ _notify_failure(_("The configuration of your additional "
+ "software failed."))
+ raise e
+
+
+def setup_additional_packages():
+ """Enable additional software in persistence."""
+ launch_persistence_setup("--no-gui",
+ "--no-display-finished-message",
+ "--force-enable-preset", "AdditionalSoftware")
+
+
+def create_persistence_and_setup_additional_packages(packages):
+ """Create persistence and add packages to its configuration.
+
+ Create a new persistence with additional packages enabled.
+ Then add the packages to additional packages configuration.
+
+ packages should be a list of packages names.
+ """
+ logging.info("Creating new persistent volume")
+ launch_persistence_setup("--step", "bootstrap",
+ "--no-display-finished-message",
+ "--force-enable-preset", "AdditionalSoftware")
+ add_additional_packages(packages, search_new_persistence=True)
+ # show persistence configuration
+ launch_persistence_setup()
+ # APT lists and APT archive cache will be synchronized at shutdown by
+ # tails-synchronize-data-to-new-persistent-volume-on-shutdown.service
+
+
+def show_configuration_window():
+ """Show additional packages configuration window."""
+ launch_x_application(LIVE_USERNAME,
+ "/usr/local/bin/tails-additional-software-config")
+
+
+def show_system_log():
+ """Show additional packages configuration window."""
+ launch_x_application(LIVE_USERNAME,
+ "/usr/bin/gedit",
+ ASP_LOG_FILE)
+
+
+def apt_hook_pre():
+ """Subcommand to handle Dpkg::Pre-Install-Pkgs."""
+ _exit_if_in_live_build()
+ logging.info("Saving package changes")
+
+ apt_cache = apt.cache.Cache()
+
+ installed_packages = []
+ removed_packages = []
+
+ line = sys.stdin.readline()
+ assert line.startswith("VERSION 3")
+ line = sys.stdin.readline()
+ # Ignore configuration space, which ends with an empty line
+ while line != "\n":
+ line = sys.stdin.readline()
+ # Package action lines
+ for line in sys.stdin:
+ # Package action lines consist of five fields in Version 2: package
+ # name (without architecture qualification even if foreign), old
+ # version, direction of version change (< for upgrades, > for
+ # downgrades, = for no change), new version, action. The version
+ # fields are "-" for no version at all (for example when installing
+ # a package for the first time; no version is treated as earlier
+ # than any real version, so that is an upgrade, indicated as - <
+ # 1.23.4). The action field is "**CONFIGURE**" if the package is
+ # being configured, "**REMOVE**" if it is being removed, or the
+ # filename of a .deb file if it is being unpacked.
+ #
+ # In Version 3 after each version field follows the architecture of
+ # this version, which is "-" if there is no version, and a field
+ # showing the MultiArch type "same", "foreign", "allowed" or "none".
+ # Note that "none" is an incorrect typename which is just kept to
+ # remain compatible, it should be read as "no" and users are
+ # encouraged to support both.
+ #
+ # Example:
+ #
+ # colordif - - none < 1.0.16-1 all none **CONFIGURE**
+ package_name, old_version, old_arch, old_multiarch, direction, \
+ new_version, new_arch, new_multiarch, action = line.split()
+ if action.endswith(".deb"):
+ # Filter packages that will only be upgraded
+ if not apt_cache[package_name].is_installed:
+ installed_packages.append(package_name)
+ elif action.endswith("**REMOVE**"):
+ removed_packages.append(package_name)
+
+ result = {"installed": installed_packages, "removed": removed_packages}
+ with open(ASP_STATE_PACKAGES, 'w') as f:
+ json.dump(result, f)
+
+
+def apt_hook_post():
+ """Subcommand to handle Dpkg::Post-Invoke.
+
+ Retrieve the list of packages saved by apt_hook_pre, filter packages not
+ interesting and pass the resulting list to the appropriate method.
+ """
+ _exit_if_in_live_build()
+ logging.info("Examining package changes")
+
+ with open(ASP_STATE_PACKAGES) as f:
+ packages = json.load(f)
+ os.remove(ASP_STATE_PACKAGES)
+
+ additional_packages_names = map(
+ filter_package_details,
+ get_additional_packages(search_new_persistence=True))
+
+ apt_cache = apt.cache.Cache()
+ # Filter automatically installed packages and packages already configured
+ # as additional software
+ new_manually_installed_packages = set(filter(
+ lambda pkg: not apt_cache[pkg].is_auto_installed
+ and pkg not in additional_packages_names, # NOQA: E131
+ set(packages["installed"])))
+ if new_manually_installed_packages:
+ handle_installed_packages(new_manually_installed_packages)
+
+ # Filter non-additional software packages
+ additional_packages_removed = set(packages["removed"]).intersection(
+ additional_packages_names)
+ if additional_packages_removed:
+ handle_removed_packages(additional_packages_removed)
+
+
+def install_additional_packages(upgrade_mode=False):
+ """Subcommand which activates and installs all additional packages.
+
+ If upgrade_mode is True, don't attempt to restore old apt lists and don't
+ notify the user using desktop notifications."""
+ logging.info("Starting to install additional software...")
+
+ if not has_additional_packages_list():
return True
# If a copy of old APT lists is found, then the previous upgrade
@@ -136,15 +501,13 @@ def install_additional_packages(ignore_old_apt_lists=False):
# installation step below in this function will fail. To avoid
# that, we restore the old APT lists: there are greater chances
# that the APT packages cache still has the corresponding packages.
- if os.path.isdir(OLD_APT_LISTS_DIR) and not ignore_old_apt_lists:
- syslog.syslog(syslog.LOG_WARNING,
- "Found a copy of old APT lists, restoring it.")
+ if os.path.isdir(OLD_APT_LISTS_DIR) and not upgrade_mode:
+ logging.warn("Found a copy of old APT lists, restoring it.")
try:
restore_old_apt_lists()
except Exception as e:
- syslog.syslog(syslog.LOG_WARNING,
- "Restoring old APT lists failed with %r, "
- "deleting them and proceeding anyway." % e)
+ logging.warn("Restoring old APT lists failed with %r, "
+ "deleting them and proceeding anyway." % e)
# In all cases, delete the old APT lists: if they could be
# restored we don't need them anymore (and we don't want to
# restore them again next time); if they could not be
@@ -154,58 +517,72 @@ def install_additional_packages(ignore_old_apt_lists=False):
packages = get_additional_packages()
if not packages:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: no packages to install, exiting")
+ logging.warn("Warning: no packages to install, exiting")
return True
- syslog.syslog("Will install the following packages: %s"
- % " ".join(packages))
+ if not upgrade_mode:
+ installing_notification_id = _notify(
+ _("Installing your additional software from persistent "
+ "storage..."),
+ _("This can take several minutes."),
+ return_id=True)
+ logging.info("Will install the following packages: %s"
+ % " ".join(packages))
apt_get_returncode = _launch_apt_get(
["--no-remove",
"--option", "DPkg::Options::=--force-confold",
- "install"] + packages)
+ "install"] + list(packages))
if apt_get_returncode:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: installation of %s failed"
- % " ".join(packages))
- _notify(_("Your additional software installation failed"),
- _("The installation failed. Please check your additional "
- "software configuration, or read the system log to "
- "understand better the problem."))
+ logging.warn("Warning: installation of %s failed" % " ".join(packages))
+ if not upgrade_mode:
+ _close_notification(installing_notification_id)
+ _notify_failure(_("The installation of your additional software "
+ "failed"))
return False
else:
- syslog.syslog("Installation completed successfully.")
- _notify(_("Your additional software are installed"),
- _("Your additional software are ready to use."))
+ logging.info("Installation completed successfully.")
+ if not upgrade_mode:
+ _close_notification(installing_notification_id)
+ # XXX: there should be a "Configure" button in this notification.
+ # However, the easy way to implement it makes this process not
+ # return until the notification is clicked. The notification
+ # process could be detached, and handle the "configure" action
+ # itself.
+ # if _notify(_("Additional software installed successfully"),
+ # accept_label=_("Configure")):
+ # show_configuration_window()
+ _notify(_("Additional software installed successfully"))
return True
def upgrade_additional_packages():
"""Subcommand which upgrades all additional packages."""
+ logging.info("Starting to upgrade additional software...")
+
+ if not has_additional_packages_list():
+ return True
+
# Save a copy of APT lists that we'll delete only once the upgrade
# has succeeded, to ensure that the APT packages cache is up-to-date
# wrt. the APT lists.
- syslog.syslog("Saving old APT lists...")
+ logging.info("Saving old APT lists...")
save_old_apt_lists()
- syslog.syslog("Starting to upgrade additional software...")
apt_get_returncode = _launch_apt_get(["update"])
if apt_get_returncode:
- syslog.syslog(syslog.LOG_WARNING, "Warning: the update failed.")
- _notify(_("Your additional software upgrade failed"),
- _("The check for upgrades failed. This might be due to a "
- "network problem. Please check your network connection, try "
- "to restart Tails, or read the system log to understand "
- "better the problem."))
+ logging.warn("Warning: the update failed.")
+ _notify_failure(_("The check for upgrades of your additional software "
+ "failed"),
+ _("Please check your network connection, "
+ "restart Tails, or read the system log to "
+ "understand the problem."))
return False
- if install_additional_packages(ignore_old_apt_lists=True):
- _notify(_("Your additional software are up to date"),
- _("The upgrade was successful."))
+ if install_additional_packages(upgrade_mode=True):
+ logging.info("The upgrade was successful.")
else:
- _notify(_("Your additional software upgrade failed"),
- _("The upgrade failed. This might be due to a network "
- "problem. Please check your network connection, try to "
- "restart Tails, or read the system log to understand better "
- "the problem."))
+ _notify_failure(_("The upgrade of your additional software failed"),
+ _("Please check your network connection, "
+ "restart Tails, or read the system log to "
+ "understand the problem."))
return False
# We now know that the APT packages cache is up-to-date wrt. the APT lists,
@@ -222,8 +599,7 @@ def upgrade_additional_packages():
# must have been upgraded already.
apt_get_returncode = _launch_apt_get(["autoclean"])
if apt_get_returncode:
- syslog.syslog(syslog.LOG_WARNING,
- "Warning: autoclean failed.")
+ logging.warn("Warning: autoclean failed.")
return True
@@ -238,7 +614,26 @@ def print_help():
if __name__ == "__main__":
program_name = os.path.basename(sys.argv[0])
- syslog.openlog("%s[%i]" % (program_name, os.getpid()))
+ # Exits with success if running inside live-build.
+ if "SOURCE_DATE_EPOCH" in os.environ:
+ sys.exit(0)
+
+ # Set loglevel if debug is found in kernel command line.
+ with open('/proc/cmdline') as cmdline_fd:
+ cmdline = cmdline_fd.read()
+ if "DEBUG" in os.environ or "debug" in cmdline.split():
+ log_level = logging.DEBUG
+ log_format = "[%(levelname)s] %(filename)s:%(lineno)d " \
+ "%(funcName)s: %(message)s"
+ else:
+ log_level = logging.INFO
+ log_format = "[%(levelname)s] %(message)s"
+ syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
+ file_handler = logging.FileHandler(ASP_LOG_FILE)
+ logging.basicConfig(format=log_format,
+ handlers=[syslog_handler, file_handler],
+ level=log_level)
+
gettext.install("tails")
if len(sys.argv) < 2:
@@ -251,6 +646,10 @@ if __name__ == "__main__":
elif sys.argv[1] == "upgrade":
if not upgrade_additional_packages():
sys.exit(151)
+ elif sys.argv[1] == "apt-pre":
+ apt_hook_pre()
+ elif sys.argv[1] == "apt-post":
+ _spawn_daemon(apt_hook_post)
else:
print_help()
sys.exit(2)
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-additional-software-remove b/config/chroot_local-includes/usr/local/sbin/tails-additional-software-remove
new file mode 100755
index 0000000..a97bf1b
--- /dev/null
+++ b/config/chroot_local-includes/usr/local/sbin/tails-additional-software-remove
@@ -0,0 +1,18 @@
+#!/usr/bin/python3
+
+import sys
+
+from tailslib.additionalsoftware.config import (
+ remove_additional_packages,
+ get_additional_packages)
+
+if len(sys.argv) != 2:
+ sys.exit(2)
+
+old_package = str(sys.argv[1])
+
+additional_packages = get_additional_packages(search_new_persistence=True)
+if old_package in additional_packages:
+ remove_additional_packages({old_package}, search_new_persistence=True)
+else:
+ sys.exit(1)
diff --git a/config/chroot_local-includes/usr/local/sbin/tails-debugging-info b/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
index 883c88a..a0babde 100755
--- a/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
+++ b/config/chroot_local-includes/usr/local/sbin/tails-debugging-info
@@ -37,10 +37,11 @@ investigated carefully.
...
"""
+import json
import os
import sys
-from pwd import getpwuid
import subprocess
+from pwd import getpwuid
# AppArmor Ux rules don't sanitize PATH, which can lead to an
@@ -52,121 +53,100 @@ os.environ['PATH'] = '/usr/local/bin:/usr/bin:/bin'
def main():
- """Print debug information.
+ """Print debug information serialized as json.
>>> main()
<BLANKLINE>
...
"""
- debug_file('root', '/proc/cmdline')
-
- # General hardware and filesystems information
- debug_command('/usr/sbin/dmidecode', '-s', 'system-manufacturer')
- debug_command('/usr/sbin/dmidecode', '-s', 'system-product-name')
- debug_command('/usr/sbin/dmidecode', '-s', 'system-version')
- debug_command('/usr/bin/lspci', '-nn')
- debug_command('/bin/df', '--human-readable', '--print-type')
- debug_command('/bin/mount', '--show-labels')
- debug_command('/bin/lsmod')
- debug_file('root', '/proc/asound/cards')
- debug_file('root', '/proc/asound/devices')
- debug_file('root', '/proc/asound/modules')
-
- # Miscellaneous configuration and log files
- debug_file('root', '/etc/X11/xorg.conf')
- debug_file('Debian-gdm', '/var/log/gdm3/tails-greeter.errors')
- debug_file('root', '/var/log/live/boot.log')
- debug_file('root', '/var/log/live/config.log')
- debug_file('root', '/var/lib/live/config/tails.physical_security')
-
- # Persistence
- debug_file('root', '/var/lib/gdm3/tails.persistence')
- debug_file('tails-persistence-setup', '/live/persistence/TailsData_unlocked/persistence.conf')
- debug_file('tails-persistence-setup', '/live/persistence/TailsData_unlocked/live-additional-software.conf')
- debug_directory('root', '/live/persistence/TailsData_unlocked/apt-sources.list.d')
- debug_file('root', '/var/log/live-persist')
-
- # The Journal
- debug_command('/bin/journalctl', '--catalog', '--no-pager')
+ config = None
+ with open('/etc/whisperback/debugging-info.json', 'r') as conf_file:
+ config = json.load(conf_file)
+
+ info = []
+ for _type, _args in config:
+ if _type == 'command':
+ info.append(debug_command(_args['args'][0], *_args['args'][1:]))
+ elif _type == 'directory':
+ info.append(debug_directory(_args['user'], _args['path']))
+ else:
+ info.append(debug_file(_args['user'], _args['path']))
+ print()
+ print(json.dumps(info, indent=4))
def debug_command(command, *args):
- """Print the command and then run it.
+ """Return the command and it's standard output as dict.
>>> debug_command('echo', 'foo')
- <BLANKLINE>
- ===== output of command echo foo =====
- foo
+ {...'key': 'echo foo'...}
"""
- print()
- print('===== output of command {} ====='.format(' '.join((command,) + args)))
- print(subprocess.check_output([command, *args]).decode().strip())
+ command_output = subprocess.check_output([command, *args])
+ command_output = command_output.decode('UTF-8').strip().split('\n')
+ return {'key': '{}'.format(' '.join((command,) + args)), 'content': command_output}
def debug_file(user, filename):
- """Print file content.
+ """Return the filename and the file content as dict.
>>> import tempfile, getpass
>>> with tempfile.NamedTemporaryFile('w') as f:
... _ = f.write("foo\\nbar")
... _ = f.seek(0)
... debug_file(getpass.getuser(), f.name)
- <BLANKLINE>
- ===== content of ... =====
- foo
- bar
+ {...'content': ['foo', 'bar']...}
"""
if not os.path.isfile(filename):
- return
+ return {'key': filename, 'content': 'Not found'}
# This check is not sufficient, see the comment at the top of the file
# for the complete requirements required for security
owner = getpwuid(os.stat(filename).st_uid).pw_name
if owner != user:
- print()
- print('WARNING: not opening file {}, '.format(filename), end='')
- print('because it is owned by {} instead of {}'.format(owner, user))
- return
+ return {'key': filename, 'content': '''WARNING: not opening file {}, because it is '''
+ '''owned by {} instead of {}'''.format(filename, owner, user)}
- print()
- print('===== content of {} ====='.format(filename))
+ file_content = []
with open(filename) as f:
- print(f.read(), end='')
+ for l in f:
+ file_content.append(l.replace('\n', ''))
+ return {'key': filename, 'content': file_content}
def debug_directory(user, dir_name):
- """List directory and print content of all contained files (non-recursively).
-
- >>> import tempfile, getpass
- >>> with tempfile.TemporaryDirectory() as tmpdir:
- ... open(os.path.join(tmpdir, 'foo'), 'w').close()
- ... debug_directory(getpass.getuser(), tmpdir)
- <BLANKLINE>
- ===== listing of ... =====
- foo
+ """Return a dict with the dir_name and dicts with
+ the content of all contained files (non-recursively).
+
+ >>> import os, getpass
+ >>> tmpdir = '/tmp/mytempdir'
+ >>> os.makedirs(tmpdir)
+ >>> with open(os.path.join(tmpdir, 'foo'), 'w') as f:
+ ... _ = f.write("foobar\\nbar")
+ ... _ = f.seek(0)
+ ... result = debug_directory(getpass.getuser(), tmpdir)
+ >>> os.remove(os.path.join(tmpdir, 'foo'))
+ >>> os.rmdir(tmpdir)
+ >>> result
+ {...[{...['foobar', 'bar']...}]}
"""
if not os.path.isdir(dir_name):
- return
-
- print()
+ return {'key': dir_name, 'content': 'Not found'}
# This check is not sufficient, see the comment at the top of the file
# for the complete requirements required for security
owner = getpwuid(os.stat(dir_name).st_uid).pw_name
if owner != user:
- print('WARNING: not opening directory {}, '.format(dir_name), end='')
- print('because it is owned by {} instead of {}'.format(owner, user))
- return
+ return {'key': dir_name, 'content': '''WARNING: not opening directory {}, because '''
+ '''it is owned by {} instead of {}'''.format(dir_name, owner, user)}
files = os.listdir(dir_name)
- print('===== listing of {} ====='.format(dir_name))
- for f in files:
- print(f)
+ listing = []
for f in files:
- debug_file(user, f)
+ listing.append(debug_file(user, os.path.join(dir_name, f)))
+ return {'key': dir_name, 'content': listing}
if __name__ == '__main__':
diff --git a/config/chroot_local-includes/usr/local/sbin/unsafe-browser b/config/chroot_local-includes/usr/local/sbin/unsafe-browser
index 0ca9eb6..24eb30e 100755
--- a/config/chroot_local-includes/usr/local/sbin/unsafe-browser
+++ b/config/chroot_local-includes/usr/local/sbin/unsafe-browser
@@ -82,7 +82,6 @@ CHROOT="${CONF_DIR}/chroot"
BROWSER_NAME="unsafe-browser"
BROWSER_USER="clearnet"
HUMAN_READABLE_NAME="`gettext \"Unsafe Browser\"`"
-NM_ENV_FILE="/var/lib/NetworkManager/env"
WARNING_PAGE='/usr/share/doc/tails/website/misc/unsafe_browser_warning'
HOME_PAGE="$(localized_tails_doc_page "${WARNING_PAGE}")"
@@ -112,8 +111,13 @@ else
fi
echo "* Starting Unsafe Browser"
+# Do not localize the 5th argument: it becomes WM_CLASS and then GNOME
+# displays the localized app name found in the matching .desktop file;
+# if WM_CLASS were localized then not only string encoding problems
+# would happen, but GNOME would pick the wrong icon.
run_browser_in_chroot "${CHROOT}" "${BROWSER_NAME}" "${BROWSER_USER}" \
- "${SUDO_USER}" || \
+ "${SUDO_USER}" \
+ 'Unsafe Browser' || \
error "`gettext \"Failed to run browser.\"`"
echo "* Exiting the Unsafe Browser"