externalcommandhelper.cpp 15.7 KB
Newer Older
1
2
3
4
5
6
7
/*
    SPDX-FileCopyrightText: 2017-2020 Andrius Štikonas <andrius@stikonas.eu>
    SPDX-FileCopyrightText: 2018 Huzaifa Faruqui <huzaifafaruqui@gmail.com>
    SPDX-FileCopyrightText: 2018 Caio Jordão Carvalho <caiojcarvalho@gmail.com>
    SPDX-FileCopyrightText: 2018-2019 Harald Sitter <sitter@kde.org>
    SPDX-FileCopyrightText: 2018 Simon Depiets <sdepiets@gmail.com>
    SPDX-FileCopyrightText: 2019 Shubham Jangra <aryan100jangid@gmail.com>
8
    SPDX-FileCopyrightText: 2020 David Edmundson <kde@davidedmundson.co.uk>
9
10
11

    SPDX-License-Identifier: GPL-3.0-or-later
*/
12
13

#include "externalcommandhelper.h"
14
#include "externalcommand_whitelist.h"
15

16
17
#include <filesystem>

18
#include <QtDBus>
David Edmundson's avatar
David Edmundson committed
19

Harald Sitter's avatar
Harald Sitter committed
20
#include <QCoreApplication>
21
#include <QDebug>
22
#include <QElapsedTimer>
23
#include <QFile>
24
#include <QString>
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
25
26
27
#include <QVariant>

#include <KLocalizedString>
David Edmundson's avatar
David Edmundson committed
28
29
30
31
#include <PolkitQt1/Authority>
#include <PolkitQt1/Subject>

#include <polkitqt1-version.h>
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
32

33
/** Initialize ExternalCommandHelper Daemon and prepare DBus interface
34
 *
35
 * This helper runs in the background until all applications using it exit.
36
 * If helper is not busy then it exits when the client services gets
37
38
 * unregistered. In case the client crashes, the helper waits
 * for the current job to finish before exiting, to avoid leaving partially moved data.
David Edmundson's avatar
David Edmundson committed
39
 *
40
41
 * This helper starts DBus interface where it listens to command execution requests.
 * New clients connecting to the helper have to authenticate using Polkit.
42
*/
David Edmundson's avatar
David Edmundson committed
43
44

ExternalCommandHelper::ExternalCommandHelper()
45
{
46
    if (!QDBusConnection::systemBus().registerObject(QStringLiteral("/Helper"), this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
47
        exit(-1);
48
49
    }

David Edmundson's avatar
David Edmundson committed
50
    if (!QDBusConnection::systemBus().registerService(QStringLiteral("org.kde.kpmcore.helperinterface"))) {
51
        exit(-1);
David Edmundson's avatar
David Edmundson committed
52
    }
53

David Edmundson's avatar
David Edmundson committed
54
55
56
57
    // we know this service must be registered already as DBus policy blocks calls from anyone else
    m_serviceWatcher = new QDBusServiceWatcher(this);
    m_serviceWatcher->setConnection(QDBusConnection ::systemBus());
    m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
Harald Sitter's avatar
Harald Sitter committed
58

David Edmundson's avatar
David Edmundson committed
59
60
61
62
63
64
    connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, qApp, [this](const QString &service) {
        m_serviceWatcher->removeWatchedService(service);
        if (m_serviceWatcher->watchedServices().isEmpty()) {
            qApp->quit();
        }
    });
65
66
}

67
68
69
70
71
72
73
/** Reads the given number of bytes from the sourceDevice into the given buffer.
    @param sourceDevice device or file to read from
    @param buffer buffer to store the bytes read in
    @param offset offset where to begin reading
    @param size the number of bytes to read
    @return true on success
*/
74
bool ExternalCommandHelper::readData(QFile& device, QByteArray& buffer, const qint64 offset, const qint64 size)
75
{
76
77
78
79
80
    if (!device.isOpen()) {
        if (!device.open(QIODevice::ReadOnly | QIODevice::Unbuffered)) {
            qCritical() << xi18n("Could not open device <filename>%1</filename> for reading.", device.fileName());
            return false;
        }
81
    }
82

83
84
    // Sequential devices such as /dev/zero or /dev/urandom return false on seek().
    if (!device.isSequential() && !device.seek(offset)) {
85
        qCritical() << xi18n("Could not seek position %1 on device <filename>%2</filename>.", offset, device.fileName());
86
        return false;
87
    }
88
89
90
91

    buffer = device.read(size);

    if (size != buffer.size()) {
92
        qCritical() << xi18n("Could not read from device <filename>%1</filename>.", device.fileName());
93
        return false;
94
95
96
    }

    return true;
97
98
}

99
/** Writes the data from buffer to a given device.
100
    @param device device or file to write to
101
102
103
104
    @param buffer the data that we write
    @param offset offset where to begin writing
    @return true on success
*/
105
bool ExternalCommandHelper::writeData(QFile& device, const QByteArray& buffer, const qint64 offset)
106
{
107
    auto flags = QIODevice::WriteOnly | QIODevice::Unbuffered;
108
109
110
111
112
    if (!device.isOpen()) {
        if (!device.open(flags)) {
            qCritical() << xi18n("Could not open device <filename>%1</filename> for writing.", device.fileName());
            return false;
        }
113
    }
114

115
    if (!device.seek(offset)) {
116
        qCritical() << xi18n("Could not seek position %1 on device <filename>%2</filename>.", offset, device.fileName());
117
        return false;
118
119
    }

120
    if (device.write(buffer) != buffer.size()) {
121
        qCritical() << xi18n("Could not write to device <filename>%1</filename>.", device.fileName());
122
123
        return false;
    }
Shubham  .'s avatar
Shubham . committed
124

125
    return true;
126
127
}

128
129
130
131
132
/** Creates a new file with given contents.
    @param filePath file to write to
    @param fileContents the data that we write
    @return true on success
*/
Andrius Štikonas's avatar
Andrius Štikonas committed
133
bool ExternalCommandHelper::CreateFile(const QString &filePath, const QByteArray& fileContents)
134
{
David Edmundson's avatar
David Edmundson committed
135
136
137
    if (!isCallerAuthorized()) {
        return false;
    }
138
    // Do not allow using this helper for writing to arbitrary location
139
    if ( filePath != QStringLiteral("/etc/fstab") )
140
141
        return false;

142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
    QFile device(filePath);

    auto flags = QIODevice::WriteOnly | QIODevice::Unbuffered;
    if (!device.open(flags)) {
        qCritical() << xi18n("Could not open file <filename>%1</filename> for writing.", filePath);
        return false;
    }

    if (device.write(fileContents) != fileContents.size()) {
        qCritical() << xi18n("Could not write to file <filename>%1</filename>.", filePath);
        return false;
    }

    return true;
}

158
// If targetDevice is empty then return QByteArray with data that was read from disk.
159
QVariantMap ExternalCommandHelper::CopyFileData(const QString& sourceDevice, const qint64 sourceOffset, const qint64 sourceLength, const QString& targetDevice, const qint64 targetOffset, const qint64 blockSize)
160
{
David Edmundson's avatar
David Edmundson committed
161
    if (!isCallerAuthorized()) {
162
        return {};
David Edmundson's avatar
David Edmundson committed
163
    }
Andrius Štikonas's avatar
Andrius Štikonas committed
164
165
166

    // Avoid division by zero further down
    if (!blockSize) {
167
        return {};
Andrius Štikonas's avatar
Andrius Štikonas committed
168
    }
169
170

    // Prevent some out of memory situations
171
    if (blockSize > 100 * MiB) {
172
        return {};
173
    }
174

175
176
177
178
179
180
181
    // Check for relative paths
    std::filesystem::path sourcePath(sourceDevice.toStdU16String());
    std::filesystem::path targetPath(targetDevice.toStdU16String());
    if(sourcePath.is_relative() || targetPath.is_relative()) {
        return {};
    }

182
183
184
185
186
    // Only allow writing to existing files.
    if(!std::filesystem::exists(targetPath)) {
        return {};
    }

187
188
189
    QVariantMap reply;
    reply[QStringLiteral("success")] = true;

190
191
192
193
194
195
196
197
198
199
    // This enum specified whether individual blocks are moved left or right
    // When partition is moved to the left, we start with the leftmost block,
    // and move it further left, then second leftmost block and so on.
    // But when we move partition to the right, we start with rightmost block.
    // To account for this difference, we introduce CopyDirection variable which takes
    // care of some of the differences between these two cases.
    enum CopyDirection : qint8 {
        Left = 1,
        Right = -1,
    };
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
    qint8 copyDirection = targetOffset > sourceOffset ? CopyDirection::Right : CopyDirection::Left;

    // Let readOffset (r) and writeOffset (w) be the offsets of the first block that we move.
    // When we move data to the left:
    // ______target______         ______source______
    // r                     <-   w=================
    qint64 readOffset = sourceOffset;
    qint64 writeOffset = targetOffset;

    // When we move data to the right, we start moving data from the last block
    // ______source______         ______target______
    // =================r    ->                    w
    if (copyDirection == CopyDirection::Right) {
        readOffset = sourceOffset + sourceLength - blockSize;
        writeOffset = targetOffset + sourceLength - blockSize;
215
216
    }

217
    const qint64 blocksToCopy = sourceLength / blockSize;
218
    const qint64 lastBlock = sourceLength % blockSize;
219

Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
220
    qint64 bytesWritten = 0;
221
222
    qint64 blocksCopied = 0;

Andrius Štikonas's avatar
Andrius Štikonas committed
223
    QByteArray buffer;
224
    int percent = 0;
225
    QElapsedTimer timer;
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
226

227
    timer.start();
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
228

229
    QString reportText = xi18nc("@info:progress", "Copying %1 blocks (%2 bytes) from %3 to %4, direction: %5.", blocksToCopy,
230
                                              sourceLength, readOffset, writeOffset, copyDirection == CopyDirection::Left ? i18nc("direction: left", "left")
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
231
                                              : i18nc("direction: right", "right"));
232
    Q_EMIT report(reportText);
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
233

234
    bool rval = true;
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
235

236
237
    QFile target(targetDevice);
    QFile source(sourceDevice);
238
    while (blocksCopied < blocksToCopy) {
239
        if (!(rval = readData(source, buffer, readOffset + blockSize * blocksCopied * copyDirection, blockSize)))
240
241
            break;

242
        if (!(rval = writeData(target, buffer, writeOffset + blockSize * blocksCopied * copyDirection)))
243
            break;
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
244
245

        bytesWritten += buffer.size();
246
247
248
249

        if (++blocksCopied * 100 / blocksToCopy != percent) {
            percent = blocksCopied * 100 / blocksToCopy;

250
251
252
            if (percent % 5 == 0 && timer.elapsed() > 1000) {
                const qint64 mibsPerSec = (blocksCopied * blockSize / 1024 / 1024) / (timer.elapsed() / 1000);
                const qint64 estSecsLeft = (100 - percent) * timer.elapsed() / percent / 1000;
253
254
                reportText = xi18nc("@info:progress", "Copying %1 MiB/second, estimated time left: %2", mibsPerSec, QTime(0, 0).addSecs(estSecsLeft).toString());
                Q_EMIT report(reportText);
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
255
            }
256
            Q_EMIT progress(percent);
257
258
259
260
261
262
263
        }
    }

    // copy the remainder
    if (rval && lastBlock > 0) {
        Q_ASSERT(lastBlock < blockSize);

264
265
        const qint64 lastBlockReadOffset = copyDirection == CopyDirection::Left ? readOffset + blockSize * blocksCopied : sourceOffset;
        const qint64 lastBlockWriteOffset = copyDirection == CopyDirection::Left ? writeOffset + blockSize * blocksCopied : targetOffset;
266
267
        reportText = xi18nc("@info:progress", "Copying remainder of block size %1 from %2 to %3.", lastBlock, lastBlockReadOffset, lastBlockWriteOffset);
        Q_EMIT report(reportText);
268
        rval = readData(source, buffer, lastBlockReadOffset, lastBlock);
269

270
        if (rval) {
271
            rval = writeData(target, buffer, lastBlockWriteOffset);
272
        }
273

Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
274
        if (rval) {
275
            Q_EMIT progress(100);
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
276
277
            bytesWritten += buffer.size();
        }
278
279
    }

280
281
    reportText = xi18ncp("@info:progress argument 2 is a string such as 7 bytes (localized accordingly)", "Copying 1 block (%2) finished.", "Copying %1 blocks (%2) finished.", blocksCopied, i18np("1 byte", "%1 bytes", bytesWritten));
    Q_EMIT report(reportText);
282

283
284
    reply[QStringLiteral("success")] = rval;
    return reply;
285
}
286

287
288
289
QByteArray ExternalCommandHelper::ReadData(const QString& device, const qint64 offset, const qint64 length)
{
    if (!isCallerAuthorized()) {
290
        return {};
291
292
293
    }

    if (length > MiB) {
294
295
        return {};
    }
296
    if (!std::filesystem::is_block_file(device.toStdU16String())) {
297
298
        qWarning() << "Not a block device";
        return {};
299
300
301
    }

    QByteArray buffer;
302
303
    QFile sourceDevice(device);
    bool rval = readData(sourceDevice, buffer, offset, length);
304
305
306
307
308
309
    if (rval) {
        return buffer;
    }
    return QByteArray();
}

310
bool ExternalCommandHelper::WriteData(const QByteArray& buffer, const QString& targetDevice, const qint64 targetOffset)
311
{
David Edmundson's avatar
David Edmundson committed
312
313
314
    if (!isCallerAuthorized()) {
        return false;
    }
315
    // Do not allow using this helper for writing to arbitrary location
316
    if ( targetDevice.left(5) != QStringLiteral("/dev/") )
317
318
        return false;

319
320
321
322
323
324
325
    auto targetPath = std::filesystem::path(targetDevice.toStdU16String());
    if (!std::filesystem::is_block_file(targetDevice.toStdU16String())) {
        qWarning() << "Not a block device";
        return {};
    }

    auto canonicalTargetPath = std::filesystem::canonical(targetPath);
Andrius Štikonas's avatar
Andrius Štikonas committed
326
    // TODO: Qt6 supports std::filesystem::path
327
328
    QFile device(QLatin1String(canonicalTargetPath.c_str()));
    return writeData(device, buffer, targetOffset);
329
330
}

Andrius Štikonas's avatar
Andrius Štikonas committed
331
QVariantMap ExternalCommandHelper::RunCommand(const QString& command, const QStringList& arguments, const QByteArray& input, const int processChannelMode)
332
{
David Edmundson's avatar
David Edmundson committed
333
    if (!isCallerAuthorized()) {
334
        return {};
David Edmundson's avatar
David Edmundson committed
335
    }
Andrius Štikonas's avatar
Andrius Štikonas committed
336
337

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
338
    QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
Andrius Štikonas's avatar
Andrius Štikonas committed
339
340
#endif

341
    QVariantMap reply;
342
    reply[QStringLiteral("success")] = true;
343

344
345
346
347
348
    if (command.isEmpty()) {
        reply[QStringLiteral("success")] = false;
        return reply;
    }

349
350
    // Compare with command whitelist
    QString basename = command.mid(command.lastIndexOf(QLatin1Char('/')) + 1);
Andrius Štikonas's avatar
Andrius Štikonas committed
351
    if (allowedCommands.find(basename) == allowedCommands.end()) { // TODO: C++20: replace with contains
352
        qInfo() << command <<" command is not one of the whitelisted command";
353
354
        reply[QStringLiteral("success")] = false;
        return reply;
355
356
    }

357
//  connect(&cmd, &QProcess::readyReadStandardOutput, this, &ExternalCommandHelper::onReadOutput);
358

359
360
    QProcess cmd;
    cmd.setEnvironment( { QStringLiteral("LVM_SUPPRESS_FD_WARNINGS=1") } );
361
362
363
364
365

    if((processChannelMode != QProcess::SeparateChannels) && (processChannelMode != QProcess::MergedChannels)) {
        reply[QStringLiteral("success")] = false;
        return reply;
    }
366
367
368
369
370
371
    cmd.setProcessChannelMode(static_cast<QProcess::ProcessChannelMode>(processChannelMode));
    cmd.start(command, arguments);
    cmd.write(input);
    cmd.closeWriteChannel();
    cmd.waitForFinished(-1);
    QByteArray output = cmd.readAllStandardOutput();
372
    reply[QStringLiteral("output")] = output;
373
    reply[QStringLiteral("exitCode")] = cmd.exitCode();
374
375
376
377
378
379

    return reply;
}

void ExternalCommandHelper::onReadOutput()
{
380
/*    const QByteArray s = cmd.readAllStandardOutput();
381

382
383
384
385
386
      if(output.length() > 10*1024*1024) { // prevent memory overflow for badly corrupted file systems
        if (report())
            report()->line() << xi18nc("@info:status", "(Command is printing too much output)");
            return;
     }
387

388
     output += s;
389

390
391
     if (report())
         *report() << QString::fromLocal8Bit(s);*/
392
393
}

David Edmundson's avatar
David Edmundson committed
394
395
396
397
398
399
bool ExternalCommandHelper::isCallerAuthorized()
{
    if (!calledFromDBus()) {
        return false;
    }

400
401
    // Cache successful authentication requests, so that clients don't need
    // to authenticate multiple times during long partitioning operations.
402
403
404
    // auth_admin_keep is not used intentionally because with current architecture
    // it might lead to data loss if user cancels sfdisk partition boundary adjustment
    // after partition data was moved.
405
406
407
    if (m_serviceWatcher->watchedServices().contains(message().service())) {
        return true;
    }
David Edmundson's avatar
David Edmundson committed
408
409
410
411
412
413

    PolkitQt1::SystemBusNameSubject subject(message().service());
    PolkitQt1::Authority *authority = PolkitQt1::Authority::instance();

    PolkitQt1::Authority::Result result;
    QEventLoop e;
David Edmundson's avatar
David Edmundson committed
414
    connect(authority, &PolkitQt1::Authority::checkAuthorizationFinished, &e, [&e, &result](PolkitQt1::Authority::Result _result) {
David Edmundson's avatar
David Edmundson committed
415
416
417
418
419
420
421
422
423
424
425
426
427
428
        result = _result;
        e.quit();
    });

    authority->checkAuthorization(QStringLiteral("org.kde.kpmcore.externalcommand.init"), subject, PolkitQt1::Authority::AllowUserInteraction);
    e.exec();

    if (authority->hasError()) {
        qDebug() << "Encountered error while checking authorization, error code:" << authority->lastError() << authority->errorDetails();
        authority->clearError();
    }

    switch (result) {
    case PolkitQt1::Authority::Yes:
429
430
        // track who called into us so we can close when all callers have gone away
        m_serviceWatcher->addWatchedService(message().service());
David Edmundson's avatar
David Edmundson committed
431
432
433
        return true;
    default:
        sendErrorReply(QDBusError::AccessDenied);
434
435
        if (m_serviceWatcher->watchedServices().isEmpty())
            qApp->quit();
David Edmundson's avatar
David Edmundson committed
436
437
438
439
440
441
442
443
444
445
        return false;
    }
}

int main(int argc, char ** argv)
{
    QCoreApplication app(argc, argv);
    ExternalCommandHelper helper;
    app.exec();
}