summaryrefslogtreecommitdiffstats
path: root/src/manager-lib/installationtask.cpp
blob: 30cbc74a7822dc4f02d7a133fa097de341245092 (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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
/****************************************************************************
**
** Copyright (C) 2019 Luxoft Sweden AB
** Copyright (C) 2018 Pelagicore AG
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Application Manager.
**
** $QT_BEGIN_LICENSE:LGPL-QTAS$
** Commercial License Usage
** Licensees holding valid commercial Qt Automotive Suite licenses may use
** this file in accordance with the commercial license agreement provided
** with the Software or, alternatively, in accordance with the terms
** contained in a written agreement between you and The Qt Company.  For
** licensing terms and conditions see https://www.qt.io/terms-conditions.
** For further information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
** SPDX-License-Identifier: LGPL-3.0
**
****************************************************************************/

#include <QTemporaryDir>
#include <QMessageAuthenticationCode>

#include "logging.h"
#include "packagemanager_p.h"
#include "packageinfo.h"
#include "packageextractor.h"
#include "yamlpackagescanner.h"
#include "exception.h"
#include "packagemanager.h"
#include "sudo.h"
#include "utilities.h"
#include "signature.h"
#include "sudo.h"
#include "installationtask.h"

/*
  Overview of what happens on an installation of an app with <id> to <location>:

  Step 1 -- startInstallation()
  =============================

  delete <location>/<id>+

  create dir <location>/<id>+
  set <extractiondir> to <location>/<id>+


  Step 2 -- unpack files
  ======================

  PackageExtractor does its job


  Step 3 -- finishInstallation()
  ================================

  if (exists <location>/<id>)
      set <isupdate> to <true>

  create installation report at <extractiondir>/.installation-report.yaml

  if (not <isupdate>)
      create document directory

  if (optional uid separation)
      chown/chmod recursively in <extractiondir> and document directory


  Step 3.1 -- final rename in finishInstallation()
  ==================================================

  if (<isupdate>)
      rename <location>/<id> to <location>/<id>-
  rename <location>/<id>+ to <location>/<id>
*/

QT_BEGIN_NAMESPACE_AM



// The standard QTemporaryDir destructor cannot cope with read-only sub-directories.
class TemporaryDir : public QTemporaryDir
{
public:
    TemporaryDir()
        : QTemporaryDir()
    { }
    explicit TemporaryDir(const QString &templateName)
        : QTemporaryDir(templateName)
    { }
    ~TemporaryDir()
    {
        recursiveOperation(path(), safeRemove);
    }
private:
    Q_DISABLE_COPY(TemporaryDir)
};


QMutex InstallationTask::s_serializeFinishInstallation { };

InstallationTask::InstallationTask(const QString &installationPath, const QString &documentPath,
                                   const QUrl &sourceUrl, QObject *parent)
    : AsynchronousTask(parent)
    , m_pm(PackageManager::instance())
    , m_installationPath(installationPath)
    , m_documentPath(documentPath)
    , m_sourceUrl(sourceUrl)
{ }

InstallationTask::~InstallationTask()
{ }

bool InstallationTask::cancel()
{
    QMutexLocker locker(&m_mutex);

    // we cannot cancel anymore after finishInstallation() has been called
    if (m_installationAcknowledged)
        return false;

    m_canceled = true;
    if (m_extractor)
        m_extractor->cancel();
    m_installationAcknowledgeWaitCondition.wakeAll();
    return true;
}

void InstallationTask::acknowledge()
{
    QMutexLocker locker(&m_mutex);

    if (m_canceled)
        return;

    m_installationAcknowledged = true;
    m_installationAcknowledgeWaitCondition.wakeAll();
}

void InstallationTask::execute()
{
    try {
        if (m_installationPath.isEmpty())
            throw Exception("no installation location was configured");

        TemporaryDir extractionDir;
        if (!extractionDir.isValid())
            throw Exception("could not create a temporary extraction directory");

        // protect m_canceled and changes to m_extractor
        QMutexLocker locker(&m_mutex);
        if (m_canceled)
            throw Exception(Error::Canceled, "canceled");

        m_extractor = new PackageExtractor(m_sourceUrl, QDir(extractionDir.path()));
        locker.unlock();

        connect(m_extractor, &PackageExtractor::progress, this, &AsynchronousTask::progress);

        m_extractor->setFileExtractedCallback(std::bind(&InstallationTask::checkExtractedFile,
                                                        this, std::placeholders::_1));

        if (!m_extractor->extract())
            throw Exception(m_extractor->errorCode(), m_extractor->errorString());

        if (!m_foundInfo || !m_foundIcon)
            throw Exception(Error::Package, "package did not contain a valid info.json and icon file");

        QList<QByteArray> chainOfTrust = m_pm->caCertificates();

        if (!m_pm->allowInstallationOfUnsignedPackages()) {
            if (!m_extractor->installationReport().storeSignature().isEmpty()) {
                // normal package from the store
                QByteArray sigDigest = m_extractor->installationReport().digest();
                bool sigOk = false;

                if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust)) {
                    sigOk = true;
                } else if (!m_pm->hardwareId().isEmpty()) {
                    // did not verify - if we have a hardware-id, try to verify with it
                    sigDigest = QMessageAuthenticationCode::hash(sigDigest, m_pm->hardwareId().toUtf8(), QCryptographicHash::Sha256);
                    if (Signature(sigDigest).verify(m_extractor->installationReport().storeSignature(), chainOfTrust))
                        sigOk = true;
                }
                if (!sigOk)
                    throw Exception(Error::Package, "could not verify the package's store signature");
            } else if (!m_extractor->installationReport().developerSignature().isEmpty()) {
                // developer package - needs a device in dev mode
                if (!m_pm->developmentMode())
                    throw Exception(Error::Package, "cannot install development packages on consumer devices");

                if (!Signature(m_extractor->installationReport().digest()).verify(m_extractor->installationReport().developerSignature(), chainOfTrust))
                    throw Exception(Error::Package, "could not verify the package's developer signature");

            } else {
                throw Exception(Error::Package, "cannot install unsigned packages");
            }
        }

        emit finishedPackageExtraction();
        setState(AwaitingAcknowledge);

        // now wait in a wait-condition until we get an acknowledge or we get canceled
        locker.relock();
        while (!m_canceled && !m_installationAcknowledged)
            m_installationAcknowledgeWaitCondition.wait(&m_mutex);

        // this is the last cancellation point
        if (m_canceled)
            throw Exception(Error::Canceled, "canceled");
        locker.unlock();

        setState(Installing);

        // However many downloads are allowed to happen in parallel: we need to serialize those
        // tasks here for the finishInstallation() step
        QMutexLocker finishLocker(&s_serializeFinishInstallation);

        finishInstallation();

        // At this point, the installation is done, so we cannot throw anymore.

        // we need to call those PackageManager methods in the correct thread
        bool finishOk = false;
        QMetaObject::invokeMethod(PackageManager::instance(), [this, &finishOk]()
            { finishOk = PackageManager::instance()->finishedPackageInstall(m_packageId); },
            Qt::BlockingQueuedConnection);

        if (!finishOk)
            qCWarning(LogInstaller) << "PackageManager rejected the installation of " << m_packageId;

    } catch (const Exception &e) {
        setError(e.errorCode(), e.errorString());

        if (m_managerApproval) {
            // we need to call those ApplicationManager methods in the correct thread
            bool cancelOk = false;
            QMetaObject::invokeMethod(PackageManager::instance(), [this, &cancelOk]()
                { cancelOk = PackageManager::instance()->canceledPackageInstall(m_packageId); },
                Qt::BlockingQueuedConnection);

            if (!cancelOk)
                qCWarning(LogInstaller) << "PackageManager could not remove package" << m_packageId << "after a failed installation";
        }
    }


    {
        QMutexLocker locker(&m_mutex);
        delete m_extractor;
        m_extractor = nullptr;
    }
}


void InstallationTask::checkExtractedFile(const QString &file) Q_DECL_NOEXCEPT_EXPR(false)
{
    ++m_extractedFileCount;

    if (m_extractedFileCount == 1) {
        if (file != qL1S("info.yaml"))
            throw Exception(Error::Package, "info.yaml must be the first file in the package. Got %1")
                .arg(file);

        YamlPackageScanner yps;
        m_package.reset(yps.scan(m_extractor->destinationDirectory().absoluteFilePath(file)));
        if (m_package->id() != m_extractor->installationReport().packageId())
            throw Exception(Error::Package, "the package identifiers in --PACKAGE-HEADER--' and info.yaml do not match");

        m_iconFileName = m_package->icon(); // store it separately as we will give away ApplicationInfo later on

        if (m_iconFileName.isEmpty())
            throw Exception(Error::Package, "the 'icon' field in info.yaml cannot be empty or absent.");

        m_packageId = m_package->id();

        m_foundInfo = true;
    } else if (m_extractedFileCount == 2) {
        // the second file must be the icon

        Q_ASSERT(m_foundInfo);
        Q_ASSERT(!m_foundIcon);

        if (file != m_iconFileName)
            throw Exception(Error::Package,
                    "The package icon (as stated in info.yaml) must be the second file in the package."
                    " Expected '%1', got '%2'").arg(m_iconFileName, file);

        QFile icon(m_extractor->destinationDirectory().absoluteFilePath(file));

        if (icon.size() > 256*1024)
            throw Exception(Error::Package, "the size of %1 is too large (max. 256KB)").arg(file);

        m_foundIcon = true;
    } else {
        throw Exception(Error::Package, "Could not find info.yaml and the icon file at the beginning of the package.");
    }

    if (m_foundIcon && m_foundInfo) {
        qCDebug(LogInstaller) << "emit taskRequestingInstallationAcknowledge" << id() << "for package" << m_package->id();

        QVariantMap nameMap;
        auto names = m_package->names();
        for (auto it = names.constBegin(); it != names.constEnd(); ++it)
            nameMap.insert(it.key(), it.value());

        QVariantMap applicationData {
            { qSL("id"), m_package->id() },
            { qSL("version"), m_package->version() },
            { qSL("icon"), m_package->icon() },
            { qSL("displayIcon"), m_package->icon() }, // legacy
            { qSL("name"), nameMap },
            { qSL("displayName"), nameMap }, // legacy
            { qSL("baseDir"), m_package->baseDir().absolutePath() },
            { qSL("codeDir"), m_package->baseDir().absolutePath() },     // 5.12 backward compatibility
            { qSL("manifestDir"), m_package->baseDir().absolutePath() }, // 5.12 backward compatibility
            { qSL("installationLocationId"), qSL("internal-0") } // 5.13 backward compatibility
        };
        emit m_pm->taskRequestingInstallationAcknowledge(id(), applicationData,
                                                         m_extractor->installationReport().extraMetaData(),
                                                         m_extractor->installationReport().extraSignedMetaData());

        QDir oldDestinationDirectory = m_extractor->destinationDirectory();

        startInstallation();

        QFile::copy(oldDestinationDirectory.filePath(qSL("info.yaml")), m_extractionDir.filePath(qSL("info.yaml")));
        QFile::copy(oldDestinationDirectory.filePath(m_iconFileName), m_extractionDir.filePath(m_iconFileName));

        {
            QMutexLocker locker(&m_mutex);
            m_extractor->setDestinationDirectory(m_extractionDir);

            QString path = m_extractionDir.absolutePath();
            path.chop(1); // remove the '+'
            m_package->setBaseDir(QDir(path));
        }
        // we need to find a free uid before we call startingApplicationInstallation
        m_package->m_uid = m_pm->findUnusedUserId();
        m_applicationUid = m_package->m_uid;

        // we need to call those ApplicationManager methods in the correct thread
        // this will also exclusively lock the application for us
        // m_package ownership is transferred to the ApplicationManager
        QString packageId = m_package->id(); // m_package is gone after the invoke
        QMetaObject::invokeMethod(PackageManager::instance(), [this]()
            { m_managerApproval = PackageManager::instance()->startingPackageInstallation(m_package.take()); },
            Qt::BlockingQueuedConnection);

        if (!m_managerApproval)
            throw Exception("PackageManager declined the installation of %1").arg(packageId);

        // we're not interested in any other files from here on...
        m_extractor->setFileExtractedCallback(nullptr);
    }
}

void InstallationTask::startInstallation() Q_DECL_NOEXCEPT_EXPR(false)
{
    // 2. delete old, partial installation

    QDir installationDir = QString(m_installationPath + qL1C('/'));
    QString installationTarget = m_packageId + qL1C('+');
    if (installationDir.exists(installationTarget)) {
        if (!removeRecursiveHelper(installationDir.absoluteFilePath(installationTarget)))
            throw Exception("could not remove old, partial installation %1/%2").arg(installationDir).arg(installationTarget);
    }

    // 4. create new installation
    if (!m_installationDirCreator.create(installationDir.absoluteFilePath(installationTarget)))
        throw Exception("could not create installation directory %1/%2").arg(installationDir).arg(installationTarget);
    m_extractionDir = installationDir;
    if (!m_extractionDir.cd(installationTarget))
        throw Exception("could not cd into installation directory %1/%2").arg(installationDir).arg(installationTarget);
    m_applicationDir.setPath(installationDir.absoluteFilePath(m_packageId));
}

void InstallationTask::finishInstallation() Q_DECL_NOEXCEPT_EXPR(false)
{
    QDir documentDirectory(m_documentPath);
    ScopedDirectoryCreator documentDirCreator;

    enum { Installation, Update } mode = Installation;

    if (m_applicationDir.exists())
        mode = Update;

    // create the installation report
    InstallationReport report = m_extractor->installationReport();

    QFile reportFile(m_extractionDir.absoluteFilePath(qSL(".installation-report.yaml")));
    if (!reportFile.open(QFile::WriteOnly) || !report.serialize(&reportFile))
        throw Exception(reportFile, "could not write the installation report");
    reportFile.close();

    // create the document directories when installing (not needed on updates)
    if (mode == Installation) {
        // this package may have been installed earlier and the document directory may not have been removed
        if (!documentDirectory.cd(m_packageId)) {
            if (!documentDirCreator.create(documentDirectory.absoluteFilePath(m_packageId)))
                throw Exception(Error::IO, "could not create the document directory %1").arg(documentDirectory.filePath(m_packageId));
        }
    }
#ifdef Q_OS_UNIX
    // update the owner, group and permission bits on both the installation and document directories
    SudoClient *root = SudoClient::instance();

    if (m_pm->isApplicationUserIdSeparationEnabled() && root) {
        uid_t uid = m_applicationUid;
        gid_t gid = m_pm->commonApplicationGroupId();

        if (!root->setOwnerAndPermissionsRecursive(documentDirectory.filePath(m_packageId), uid, gid, 02700)) {
            throw Exception(Error::IO, "could not recursively change the owner to %1:%2 and the permission bits to %3 in %4")
                    .arg(uid).arg(gid).arg(02700, 0, 8).arg(documentDirectory.filePath(m_packageId));
        }

        if (!root->setOwnerAndPermissionsRecursive(m_extractionDir.path(), uid, gid, 0440)) {
            throw Exception(Error::IO, "could not recursively change the owner to %1:%2 and the permission bits to %3 in %4")
                    .arg(uid).arg(gid).arg(0440, 0, 8).arg(m_extractionDir.absolutePath());
        }
    }
#endif

    // final rename

    // POSIX cannot atomically rename directories, if the destination directory exists
    // and is non-empty. We need to do a double-rename in this case, which might fail!
    // The image is a file, so this limitation does not apply!

    ScopedRenamer renameApplication;

    if (mode == Update) {
        if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName | ScopedRenamer::NameToNameMinus))
            throw Exception(Error::IO, "could not rename application directory %1+ to %1 (including a backup to %1-)").arg(m_applicationDir);
    } else {
        if (!renameApplication.rename(m_applicationDir, ScopedRenamer::NamePlusToName))
            throw Exception(Error::IO, "could not rename application directory %1+ to %1").arg(m_applicationDir);
    }

    // from this point onwards, we are not allowed to throw anymore, since the installation is "done"

    setState(CleaningUp);

    renameApplication.take();
    documentDirCreator.take();

    m_installationDirCreator.take();

    // this should not be necessary, but it also won't hurt
    if (mode == Update)
        removeRecursiveHelper(m_applicationDir.absolutePath() + qL1C('-'));

#ifdef Q_OS_UNIX
    // write files to the filesystem
    sync();
#endif

    m_errorString.clear();
}

QT_END_NAMESPACE_AM