summaryrefslogtreecommitdiffstats
path: root/config/chroot_local-includes/usr/local/sbin/tails-additional-software
blob: ab31a968aa3ee21602d6e54a3fada633601b3c4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/usr/bin/env python3

import gettext
import os
import os.path
import shutil
import subprocess
import sys
import syslog

_ = gettext.gettext

PERSISTENCE_DIR = "/live/persistence/TailsData_unlocked"
PACKAGES_LIST_FILE = os.path.join(
    PERSISTENCE_DIR, "live-additional-software.conf")
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 _launch_apt_get(specific_args):
    """Launch apt-get with given arguments.

    Launch apt-get with given arguments list, log its standard and error output
    and return its returncode."""
    apt_get_env = os.environ.copy()
    # The environnment provided in GDM PostLogin hooks doesn't contain /sbin/
    # which is required by dpkg. Let's use the default path for root in Tails.
    apt_get_env['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:" \
                          "/usr/bin:/sbin:/bin"
    # We will log the output and want it in English when included in bug
    # reports
    apt_get_env['LANG'] = "C"
    apt_get_env['DEBIAN_PRIORITY'] = "critical"
    args = ["apt-get", "--quiet", "--yes"]
    args.extend(specific_args)
    apt_get = subprocess.Popen(args,
                               env=apt_get_env,
                               universal_newlines=True,
                               stderr=subprocess.STDOUT,
                               stdout=subprocess.PIPE)
    for line in iter(apt_get.stdout.readline, ''):
        if not line.startswith('('):
            syslog.syslog(line.rstrip())
    apt_get.wait()
    if apt_get.returncode:
        syslog.syslog(syslog.LOG_WARNING,
                      "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"
    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))
    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))


def has_additional_packages_list():
    """Return true iff PACKAGES_LIST_FILE exists."""
    return os.path.isfile(PACKAGES_LIST_FILE)


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


def delete_old_apt_lists(old_apt_lists_dir=OLD_APT_LISTS_DIR):
    """Delete the copy of the old APT lists, if any."""
    shutil.rmtree(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.")
        delete_old_apt_lists(destdir)
    shutil.copytree(srcdir, destdir, symlinks=True)


# Note: we can't do nicer delete + move operations because the directory
# we want to replace is bind-mounted. So we have to delete the content
# we want to replace, and then move the content we want to restore.
def restore_old_apt_lists(srcdir=OLD_APT_LISTS_DIR, dstdir=APT_LISTS_DIR):
    """Restore the copy of the old APT lists."""
    # Empty dstdir
    for basename in os.listdir(dstdir):
        path = os.path.join(dstdir, basename)
        if os.path.isfile(path):
            os.remove(path)
        elif os.path.isdir(path):
            shutil.rmtree(path)
    # Move the content of srcdir to dstdir
    for basename in os.listdir(srcdir):
        path = os.path.join(srcdir, basename)
        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")
        return True

    # If a copy of old APT lists is found, then the previous upgrade
    # attempt has not completed successfully (it may have failed e.g.
    # due to network problems, or it may have been interrupted).
    # In many of these cases, the APT package cache lacks some
    # packages the new APT lists reference, so the (offline)
    # 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.")
        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)
        # 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
        # restored, chances are restoration will fail next time
        # as well.
        delete_old_apt_lists()

    packages = get_additional_packages()
    if not packages:
        syslog.syslog(syslog.LOG_WARNING,
                      "Warning: no packages to install, exiting")
        return True
    syslog.syslog("Will install the following packages: %s"
                  % " ".join(packages))
    apt_get_returncode = _launch_apt_get(
        ["--no-remove",
         "--option", "DPkg::Options::=--force-confold",
         "install"] + 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."))
        return False
    else:
        syslog.syslog("Installation completed successfully.")
        _notify(_("Your additional software are installed"),
                _("Your additional software are ready to use."))
        return True


def upgrade_additional_packages():
    """Subcommand which upgrades all additional packages."""
    # 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...")
    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."))
        return False
    if install_additional_packages(ignore_old_apt_lists=True):
        _notify(_("Your additional software are up to date"),
                _("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."))
        return False

    # We now know that the APT packages cache is up-to-date wrt. the APT lists,
    # so we can delete the copy of the old lists
    delete_old_apt_lists()

    # Remove outdated packages from the local package cache. This is needed as
    # we disable apt-daily.timer, which would else take care of this cleanup.
    # We do this after the upgrade has succeeded so that the old packages
    # remain available in the cache in case we have to restore the old lists.
    # In the past we did this before upgrading in order to remove the
    # i386 packages from the cache before downloading amd64 ones, but
    # this does not matter anymore now that all persistent volumes
    # must have been upgraded already.
    apt_get_returncode = _launch_apt_get(["autoclean"])
    if apt_get_returncode:
        syslog.syslog(syslog.LOG_WARNING,
                      "Warning: autoclean failed.")
    return True


def print_help():
    """Subcommand which displays help."""
    sys.stderr.write("Usage: %s <subcommand>\n" % program_name)
    sys.stderr.write("""Subcommands:
    install: install additional software
    upgrade: upgrade additional software\n""")


if __name__ == "__main__":
    program_name = os.path.basename(sys.argv[0])

    syslog.openlog("%s[%i]" % (program_name, os.getpid()))
    gettext.install("tails")

    if len(sys.argv) < 2:
        print_help()
        sys.exit(2)

    if sys.argv[1] == "install":
        if not install_additional_packages():
            sys.exit(150)
    elif sys.argv[1] == "upgrade":
        if not upgrade_additional_packages():
            sys.exit(151)
    else:
        print_help()
        sys.exit(2)