externalcommandhelper.cpp 14.6 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)) {
David Edmundson's avatar
David Edmundson committed
47
        ::exit(-1);
48
49
    }

David Edmundson's avatar
David Edmundson committed
50
51
52
    if (!QDBusConnection::systemBus().registerService(QStringLiteral("org.kde.kpmcore.helperinterface"))) {
        ::exit(-1);
    }
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
*/
Shubham  .'s avatar
Shubham . committed
74
bool ExternalCommandHelper::readData(const QString& sourceDevice, QByteArray& buffer, const qint64 offset, const qint64 size)
75
{
76
    QFile device(sourceDevice);
77

78
79
80
81
    if (!device.open(QIODevice::ReadOnly | QIODevice::Unbuffered)) {
        qCritical() << xi18n("Could not open device <filename>%1</filename> for reading.", sourceDevice);
        return false;
    }
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, sourceDevice);
86
        return false;
87
    }
88
89
90
91
92

    buffer = device.read(size);

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

    return true;
97
98
}

99
/** Writes the data from buffer to a given device.
100
101
102
103
104
    @param targetDevice device or file to write to
    @param buffer the data that we write
    @param offset offset where to begin writing
    @return true on success
*/
Shubham  .'s avatar
Shubham . committed
105
bool ExternalCommandHelper::writeData(const QString &targetDevice, const QByteArray& buffer, const qint64 offset)
106
{
107
    QFile device(targetDevice);
Shubham  .'s avatar
Shubham . committed
108

109
    auto flags = QIODevice::WriteOnly | QIODevice::Unbuffered;
110
    if (!device.open(flags)) {
111
112
113
        qCritical() << xi18n("Could not open device <filename>%1</filename> for writing.", targetDevice);
        return false;
    }
114

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

120
121
122
123
    if (device.write(buffer) != buffer.size()) {
        qCritical() << xi18n("Could not write to device <filename>%1</filename>.", targetDevice);
        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
139
140
141
    // Do not allow using this helper for writing to arbitrary location
    if ( !filePath.contains(QStringLiteral("/etc/fstab")) )
        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::CopyBlocks(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
    QVariantMap reply;
    reply[QStringLiteral("success")] = true;

178
179
180
181
182
183
184
185
186
187
    // 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,
    };
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
    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;
203
204
    }

205
    const qint64 blocksToCopy = sourceLength / blockSize;
206
    const qint64 lastBlock = sourceLength % blockSize;
207

Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
208
    qint64 bytesWritten = 0;
209
210
    qint64 blocksCopied = 0;

Andrius Štikonas's avatar
Andrius Štikonas committed
211
    QByteArray buffer;
212
    int percent = 0;
213
    QElapsedTimer timer;
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
214

215
    timer.start();
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
216

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

222
    bool rval = true;
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
223

224
    while (blocksCopied < blocksToCopy) {
225
        if (!(rval = readData(sourceDevice, buffer, readOffset + blockSize * blocksCopied * copyDirection, blockSize)))
226
227
            break;

228
        if (!(rval = writeData(targetDevice, buffer, writeOffset + blockSize * blocksCopied * copyDirection)))
229
            break;
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
230
231

        bytesWritten += buffer.size();
232
233
234
235

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

236
237
238
            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;
239
240
                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
241
            }
242
            Q_EMIT progress(percent);
243
244
245
246
247
248
249
        }
    }

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

250
251
        const qint64 lastBlockReadOffset = copyDirection == CopyDirection::Left ? readOffset + blockSize * blocksCopied : sourceOffset;
        const qint64 lastBlockWriteOffset = copyDirection == CopyDirection::Left ? writeOffset + blockSize * blocksCopied : targetOffset;
252
253
        reportText = xi18nc("@info:progress", "Copying remainder of block size %1 from %2 to %3.", lastBlock, lastBlockReadOffset, lastBlockWriteOffset);
        Q_EMIT report(reportText);
254
        rval = readData(sourceDevice, buffer, lastBlockReadOffset, lastBlock);
255

256
        if (rval) {
257
            rval = writeData(targetDevice, buffer, lastBlockWriteOffset);
258
        }
259

Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
260
        if (rval) {
261
            Q_EMIT progress(100);
Huzaifa Faruqui's avatar
Huzaifa Faruqui committed
262
263
            bytesWritten += buffer.size();
        }
264
265
    }

266
267
    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);
268

269
270
    reply[QStringLiteral("success")] = rval;
    return reply;
271
}
272

273
274
275
QByteArray ExternalCommandHelper::ReadData(const QString& device, const qint64 offset, const qint64 length)
{
    if (!isCallerAuthorized()) {
276
        return {};
277
278
279
    }

    if (length > MiB) {
280
281
282
283
284
        return {};
    }
    if (!std::filesystem::is_block_file(device.toStdString())) {
        qWarning() << "Not a block device";
        return {};
285
286
287
288
289
290
291
292
293
294
    }

    QByteArray buffer;
    bool rval = readData(device, buffer, offset, length);
    if (rval) {
        return buffer;
    }
    return QByteArray();
}

295
bool ExternalCommandHelper::WriteData(const QByteArray& buffer, const QString& targetDevice, const qint64 targetOffset)
296
{
David Edmundson's avatar
David Edmundson committed
297
298
299
    if (!isCallerAuthorized()) {
        return false;
    }
300
    // Do not allow using this helper for writing to arbitrary location
301
    if ( targetDevice.left(5) != QStringLiteral("/dev/") )
302
303
        return false;

304
    return writeData(targetDevice, buffer, targetOffset);
305
306
}

Andrius Štikonas's avatar
Andrius Štikonas committed
307
QVariantMap ExternalCommandHelper::RunCommand(const QString& command, const QStringList& arguments, const QByteArray& input, const int processChannelMode)
308
{
David Edmundson's avatar
David Edmundson committed
309
    if (!isCallerAuthorized()) {
310
        return {};
David Edmundson's avatar
David Edmundson committed
311
    }
312
    QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
313
    QVariantMap reply;
314
    reply[QStringLiteral("success")] = true;
315

316
317
318
319
320
    if (command.isEmpty()) {
        reply[QStringLiteral("success")] = false;
        return reply;
    }

321
322
    // Compare with command whitelist
    QString basename = command.mid(command.lastIndexOf(QLatin1Char('/')) + 1);
Andrius Štikonas's avatar
Andrius Štikonas committed
323
    if (allowedCommands.find(basename) == allowedCommands.end()) { // TODO: C++20: replace with contains
324
        qInfo() << command <<" command is not one of the whitelisted command";
325
326
        reply[QStringLiteral("success")] = false;
        return reply;
327
328
    }

329
//  connect(&cmd, &QProcess::readyReadStandardOutput, this, &ExternalCommandHelper::onReadOutput);
330

331
332
333
334
335
336
337
338
    QProcess cmd;
    cmd.setEnvironment( { QStringLiteral("LVM_SUPPRESS_FD_WARNINGS=1") } );
    cmd.setProcessChannelMode(static_cast<QProcess::ProcessChannelMode>(processChannelMode));
    cmd.start(command, arguments);
    cmd.write(input);
    cmd.closeWriteChannel();
    cmd.waitForFinished(-1);
    QByteArray output = cmd.readAllStandardOutput();
339
    reply[QStringLiteral("output")] = output;
340
    reply[QStringLiteral("exitCode")] = cmd.exitCode();
341
342
343
344
345
346

    return reply;
}

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

349
350
351
352
353
      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;
     }
354

355
     output += s;
356

357
358
     if (report())
         *report() << QString::fromLocal8Bit(s);*/
359
360
}

David Edmundson's avatar
David Edmundson committed
361
362
363
364
365
366
bool ExternalCommandHelper::isCallerAuthorized()
{
    if (!calledFromDBus()) {
        return false;
    }

367
368
    // Cache successful authentication requests, so that clients don't need
    // to authenticate multiple times during long partitioning operations.
369
370
371
    // 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.
372
373
374
    if (m_serviceWatcher->watchedServices().contains(message().service())) {
        return true;
    }
David Edmundson's avatar
David Edmundson committed
375
376
377
378
379
380

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

    PolkitQt1::Authority::Result result;
    QEventLoop e;
David Edmundson's avatar
David Edmundson committed
381
    connect(authority, &PolkitQt1::Authority::checkAuthorizationFinished, &e, [&e, &result](PolkitQt1::Authority::Result _result) {
David Edmundson's avatar
David Edmundson committed
382
383
384
385
386
387
388
389
390
391
392
393
394
395
        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:
396
397
        // 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
398
399
400
        return true;
    default:
        sendErrorReply(QDBusError::AccessDenied);
401
402
        if (m_serviceWatcher->watchedServices().isEmpty())
            qApp->quit();
David Edmundson's avatar
David Edmundson committed
403
404
405
406
407
408
409
410
411
412
        return false;
    }
}

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