File indexing completed on 2025-01-26 04:52:43

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     command.cpp
0003 
0004     This file is part of KleopatraClient, the Kleopatra interface library
0005     SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include <config-kleopatra.h>
0011 
0012 #include "command.h"
0013 #include "command_p.h"
0014 
0015 #include <QtSystemDetection> // Q_OS_WIN
0016 
0017 #ifdef Q_OS_WIN // HACK: AllowSetForegroundWindow needs _WIN32_WINDOWS >= 0x0490 set
0018 #ifndef _WIN32_WINDOWS
0019 #define _WIN32_WINDOWS 0x0500
0020 #endif
0021 #ifndef _WIN32_WINNT
0022 #define _WIN32_WINNT 0x0500 // good enough for Vista too
0023 #endif
0024 #include <utils/gnupg-registry.h>
0025 #include <windows.h>
0026 #endif
0027 
0028 #include "libkleopatraclientcore_debug.h"
0029 #include <KLocalizedString>
0030 #include <QDir>
0031 #include <QFile>
0032 #include <QMutexLocker>
0033 #include <QProcess>
0034 
0035 #include <assuan.h>
0036 #include <gpg-error.h>
0037 #include <gpgme++/error.h>
0038 
0039 #include <algorithm>
0040 #include <memory>
0041 #include <sstream>
0042 #include <string>
0043 #include <type_traits>
0044 
0045 using namespace KleopatraClientCopy;
0046 
0047 // copied from kleopatra/utils/hex.cpp
0048 static std::string hexencode(const std::string &in)
0049 {
0050     std::string result;
0051     result.reserve(3 * in.size());
0052 
0053     static const char hex[] = "0123456789ABCDEF";
0054 
0055     for (std::string::const_iterator it = in.begin(), end = in.end(); it != end; ++it)
0056         switch (const unsigned char ch = *it) {
0057         default:
0058             if ((ch >= '!' && ch <= '~') || ch > 0xA0) {
0059                 result += ch;
0060                 break;
0061             }
0062             [[fallthrough]];
0063         // else fall through
0064         case ' ':
0065             result += '+';
0066             break;
0067         case '"':
0068         case '#':
0069         case '$':
0070         case '%':
0071         case '\'':
0072         case '+':
0073         case '=':
0074             result += '%';
0075             result += hex[(ch & 0xF0) >> 4];
0076             result += hex[(ch & 0x0F)];
0077             break;
0078         }
0079 
0080     return result;
0081 }
0082 
0083 #ifdef UNUSED
0084 static std::string hexencode(const char *in)
0085 {
0086     if (!in) {
0087         return std::string();
0088     }
0089     return hexencode(std::string(in));
0090 }
0091 #endif
0092 
0093 // changed from returning QByteArray to returning std::string
0094 static std::string hexencode(const QByteArray &in)
0095 {
0096     if (in.isNull()) {
0097         return std::string();
0098     }
0099     return hexencode(std::string(in.data(), in.size()));
0100 }
0101 // end copied from kleopatra/utils/hex.cpp
0102 
0103 Command::Command(QObject *p)
0104     : QObject(p)
0105     , d(new Private(this))
0106 {
0107     d->init();
0108 }
0109 
0110 Command::Command(Private *pp, QObject *p)
0111     : QObject(p)
0112     , d(pp)
0113 {
0114     d->init();
0115 }
0116 
0117 Command::~Command()
0118 {
0119     delete d;
0120     d = nullptr;
0121 }
0122 
0123 void Command::Private::init()
0124 {
0125     connect(this, &QThread::started, q, &Command::started);
0126     connect(this, &QThread::finished, q, &Command::finished);
0127 }
0128 
0129 void Command::setParentWId(WId wid)
0130 {
0131     const QMutexLocker locker(&d->mutex);
0132     d->inputs.parentWId = wid;
0133 }
0134 
0135 WId Command::parentWId() const
0136 {
0137     const QMutexLocker locker(&d->mutex);
0138     return d->inputs.parentWId;
0139 }
0140 
0141 void Command::setServerLocation(const QString &location)
0142 {
0143     const QMutexLocker locker(&d->mutex);
0144     d->outputs.serverLocation = location;
0145 }
0146 
0147 QString Command::serverLocation() const
0148 {
0149     const QMutexLocker locker(&d->mutex);
0150     return d->outputs.serverLocation;
0151 }
0152 
0153 bool Command::waitForFinished()
0154 {
0155     return d->wait();
0156 }
0157 
0158 bool Command::waitForFinished(unsigned long ms)
0159 {
0160     return d->wait(ms);
0161 }
0162 
0163 bool Command::error() const
0164 {
0165     const QMutexLocker locker(&d->mutex);
0166     return !d->outputs.errorString.isEmpty();
0167 }
0168 
0169 bool Command::wasCanceled() const
0170 {
0171     const QMutexLocker locker(&d->mutex);
0172     return d->outputs.canceled;
0173 }
0174 
0175 QString Command::errorString() const
0176 {
0177     const QMutexLocker locker(&d->mutex);
0178     return d->outputs.errorString;
0179 }
0180 
0181 qint64 Command::serverPid() const
0182 {
0183     const QMutexLocker locker(&d->mutex);
0184     return d->outputs.serverPid;
0185 }
0186 
0187 void Command::start()
0188 {
0189     d->start();
0190 }
0191 
0192 void Command::cancel()
0193 {
0194     qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "Sorry, not implemented: KleopatraClient::Command::Cancel";
0195 }
0196 
0197 void Command::setOptionValue(const char *name, const QVariant &value, bool critical)
0198 {
0199     if (!name || !*name) {
0200         return;
0201     }
0202     const Private::Option opt = {value, true, critical};
0203     const QMutexLocker locker(&d->mutex);
0204     d->inputs.options[name] = opt;
0205 }
0206 
0207 QVariant Command::optionValue(const char *name) const
0208 {
0209     if (!name || !*name) {
0210         return QVariant();
0211     }
0212     const QMutexLocker locker(&d->mutex);
0213 
0214     const auto it = d->inputs.options.find(name);
0215     if (it == d->inputs.options.end()) {
0216         return QVariant();
0217     } else {
0218         return it->second.value;
0219     }
0220 }
0221 
0222 void Command::setOption(const char *name, bool critical)
0223 {
0224     if (!name || !*name) {
0225         return;
0226     }
0227     const QMutexLocker locker(&d->mutex);
0228 
0229     if (isOptionSet(name)) {
0230         unsetOption(name);
0231     }
0232 
0233     const Private::Option opt = {QVariant(), false, critical};
0234 
0235     d->inputs.options[name] = opt;
0236 }
0237 
0238 void Command::unsetOption(const char *name)
0239 {
0240     if (!name || !*name) {
0241         return;
0242     }
0243     const QMutexLocker locker(&d->mutex);
0244     d->inputs.options.erase(name);
0245 }
0246 
0247 bool Command::isOptionSet(const char *name) const
0248 {
0249     if (!name || !*name) {
0250         return false;
0251     }
0252     const QMutexLocker locker(&d->mutex);
0253     return d->inputs.options.count(name);
0254 }
0255 
0256 bool Command::isOptionCritical(const char *name) const
0257 {
0258     if (!name || !*name) {
0259         return false;
0260     }
0261     const QMutexLocker locker(&d->mutex);
0262     const auto it = d->inputs.options.find(name);
0263     return it != d->inputs.options.end() && it->second.isCritical;
0264 }
0265 
0266 void Command::setFilePaths(const QStringList &filePaths)
0267 {
0268     const QMutexLocker locker(&d->mutex);
0269     d->inputs.filePaths = filePaths;
0270 }
0271 
0272 QStringList Command::filePaths() const
0273 {
0274     const QMutexLocker locker(&d->mutex);
0275     return d->inputs.filePaths;
0276 }
0277 
0278 void Command::setRecipients(const QStringList &recipients, bool informative)
0279 {
0280     const QMutexLocker locker(&d->mutex);
0281     d->inputs.recipients = recipients;
0282     d->inputs.areRecipientsInformative = informative;
0283 }
0284 
0285 QStringList Command::recipients() const
0286 {
0287     const QMutexLocker locker(&d->mutex);
0288     return d->inputs.recipients;
0289 }
0290 
0291 bool Command::areRecipientsInformative() const
0292 {
0293     const QMutexLocker locker(&d->mutex);
0294     return d->inputs.areRecipientsInformative;
0295 }
0296 
0297 void Command::setSenders(const QStringList &senders, bool informative)
0298 {
0299     const QMutexLocker locker(&d->mutex);
0300     d->inputs.senders = senders;
0301     d->inputs.areSendersInformative = informative;
0302 }
0303 
0304 QStringList Command::senders() const
0305 {
0306     const QMutexLocker locker(&d->mutex);
0307     return d->inputs.senders;
0308 }
0309 
0310 bool Command::areSendersInformative() const
0311 {
0312     const QMutexLocker locker(&d->mutex);
0313     return d->inputs.areSendersInformative;
0314 }
0315 
0316 void Command::setInquireData(const char *what, const QByteArray &data)
0317 {
0318     const QMutexLocker locker(&d->mutex);
0319     d->inputs.inquireData[what] = data;
0320 }
0321 
0322 void Command::unsetInquireData(const char *what)
0323 {
0324     const QMutexLocker locker(&d->mutex);
0325     d->inputs.inquireData.erase(what);
0326 }
0327 
0328 QByteArray Command::inquireData(const char *what) const
0329 {
0330     const QMutexLocker locker(&d->mutex);
0331     const auto it = d->inputs.inquireData.find(what);
0332     if (it == d->inputs.inquireData.end()) {
0333         return QByteArray();
0334     } else {
0335         return it->second;
0336     }
0337 }
0338 
0339 bool Command::isInquireDataSet(const char *what) const
0340 {
0341     const QMutexLocker locker(&d->mutex);
0342     const auto it = d->inputs.inquireData.find(what);
0343     return it != d->inputs.inquireData.end();
0344 }
0345 
0346 QByteArray Command::receivedData() const
0347 {
0348     const QMutexLocker locker(&d->mutex);
0349     return d->outputs.data;
0350 }
0351 
0352 void Command::setCommand(const char *command)
0353 {
0354     const QMutexLocker locker(&d->mutex);
0355     d->inputs.command = command;
0356 }
0357 
0358 QByteArray Command::command() const
0359 {
0360     const QMutexLocker locker(&d->mutex);
0361     return d->inputs.command;
0362 }
0363 
0364 //
0365 // here comes the ugly part
0366 //
0367 
0368 static void my_assuan_release(assuan_context_t ctx)
0369 {
0370     if (ctx) {
0371         assuan_release(ctx);
0372     }
0373 }
0374 
0375 using AssuanContextBase = std::shared_ptr<std::remove_pointer<assuan_context_t>::type>;
0376 namespace
0377 {
0378 struct AssuanClientContext : AssuanContextBase {
0379     AssuanClientContext()
0380         : AssuanContextBase()
0381     {
0382     }
0383     explicit AssuanClientContext(assuan_context_t ctx)
0384         : AssuanContextBase(ctx, &my_assuan_release)
0385     {
0386     }
0387     void reset(assuan_context_t ctx = nullptr)
0388     {
0389         AssuanContextBase::reset(ctx, &my_assuan_release);
0390     }
0391 };
0392 }
0393 
0394 static gpg_error_t my_assuan_transact(const AssuanClientContext &ctx,
0395                                       const char *command,
0396                                       gpg_error_t (*data_cb)(void *, const void *, size_t) = nullptr,
0397                                       void *data_cb_arg = nullptr,
0398                                       gpg_error_t (*inquire_cb)(void *, const char *) = nullptr,
0399                                       void *inquire_cb_arg = nullptr,
0400                                       gpg_error_t (*status_cb)(void *, const char *) = nullptr,
0401                                       void *status_cb_arg = nullptr)
0402 {
0403     return assuan_transact(ctx.get(), command, data_cb, data_cb_arg, inquire_cb, inquire_cb_arg, status_cb, status_cb_arg);
0404 }
0405 
0406 static QString to_error_string(int err)
0407 {
0408     char buffer[1024];
0409     gpg_strerror_r(static_cast<gpg_error_t>(err), buffer, sizeof buffer);
0410     buffer[sizeof buffer - 1] = '\0';
0411     return QString::fromLocal8Bit(buffer);
0412 }
0413 
0414 static QString gnupg_home_directory()
0415 {
0416     static const char *hDir = GpgME::dirInfo("homedir");
0417     return QFile::decodeName(hDir);
0418 }
0419 
0420 static QString get_default_socket_name()
0421 {
0422     const QString socketPath{QString::fromUtf8(GpgME::dirInfo("uiserver-socket"))};
0423     if (!socketPath.isEmpty()) {
0424         // Note: The socket directory exists after GpgME::dirInfo() has been called.
0425         return socketPath;
0426     }
0427     const QString homeDir = gnupg_home_directory();
0428     if (homeDir.isEmpty()) {
0429         return QString();
0430     }
0431     return QDir(homeDir).absoluteFilePath(QStringLiteral("S.uiserver"));
0432 }
0433 
0434 static QString default_socket_name()
0435 {
0436     static QString name = get_default_socket_name();
0437     return name;
0438 }
0439 
0440 static QString uiserver_executable()
0441 {
0442     return QStringLiteral("kleopatra");
0443 }
0444 
0445 static QString start_uiserver()
0446 {
0447     // Warning: Don't assume that the program needs to be in PATH. On Windows, it will also be found next to the calling process.
0448     if (!QProcess::startDetached(uiserver_executable(), QStringList() << QStringLiteral("--daemon"))) {
0449         return i18n("Failed to start uiserver %1", uiserver_executable());
0450     } else {
0451         return QString();
0452     }
0453 }
0454 
0455 static gpg_error_t getinfo_pid_cb(void *opaque, const void *buffer, size_t length)
0456 {
0457     qint64 &pid = *static_cast<qint64 *>(opaque);
0458     pid = QByteArray(static_cast<const char *>(buffer), length).toLongLong();
0459     return 0;
0460 }
0461 
0462 static gpg_error_t command_data_cb(void *opaque, const void *buffer, size_t length)
0463 {
0464     QByteArray &ba = *static_cast<QByteArray *>(opaque);
0465     ba.append(QByteArray(static_cast<const char *>(buffer), length));
0466     return 0;
0467 }
0468 
0469 namespace
0470 {
0471 struct inquire_data {
0472     const std::map<std::string, QByteArray> *map;
0473     const AssuanClientContext *ctx;
0474 };
0475 }
0476 
0477 static gpg_error_t command_inquire_cb(void *opaque, const char *what)
0478 {
0479     if (!opaque) {
0480         return 0;
0481     }
0482     const inquire_data &id = *static_cast<const inquire_data *>(opaque);
0483     const auto it = id.map->find(what);
0484     if (it != id.map->end()) {
0485         const QByteArray &v = it->second;
0486         assuan_send_data(id.ctx->get(), v.data(), v.size());
0487     }
0488     return 0;
0489 }
0490 
0491 static inline std::ostream &operator<<(std::ostream &s, const QByteArray &ba)
0492 {
0493     return s << std::string(ba.data(), ba.size());
0494 }
0495 
0496 static gpg_error_t send_option(const AssuanClientContext &ctx, const char *name, const QVariant &value)
0497 {
0498     std::stringstream ss;
0499     ss << "OPTION " << name;
0500     if (value.isValid()) {
0501         ss << '=' << value.toString().toUtf8();
0502     }
0503     return my_assuan_transact(ctx, ss.str().c_str());
0504 }
0505 
0506 static gpg_error_t send_file(const AssuanClientContext &ctx, const QString &file)
0507 {
0508     std::stringstream ss;
0509     ss << "FILE " << hexencode(QFile::encodeName(file));
0510     return my_assuan_transact(ctx, ss.str().c_str());
0511 }
0512 
0513 static gpg_error_t send_recipient(const AssuanClientContext &ctx, const QString &recipient, bool info)
0514 {
0515     std::stringstream ss;
0516     ss << "RECIPIENT ";
0517     if (info) {
0518         ss << "--info ";
0519     }
0520     ss << "--" << hexencode(recipient.toUtf8());
0521     return my_assuan_transact(ctx, ss.str().c_str());
0522 }
0523 
0524 static gpg_error_t send_sender(const AssuanClientContext &ctx, const QString &sender, bool info)
0525 {
0526     std::stringstream ss;
0527     ss << "SENDER ";
0528     if (info) {
0529         ss << "--info ";
0530     }
0531     ss << "--" << hexencode(sender.toUtf8());
0532     return my_assuan_transact(ctx, ss.str().c_str());
0533 }
0534 
0535 void Command::Private::run()
0536 {
0537     // Take a snapshot of the input data, and clear the output data:
0538     Inputs in;
0539     Outputs out;
0540     {
0541         const QMutexLocker locker(&mutex);
0542         in = inputs;
0543         outputs = out;
0544     }
0545 
0546     out.canceled = false;
0547 
0548     if (out.serverLocation.isEmpty()) {
0549         out.serverLocation = default_socket_name();
0550     }
0551 
0552     AssuanClientContext ctx;
0553     gpg_error_t err = 0;
0554 
0555     inquire_data id = {&in.inquireData, &ctx};
0556 
0557     const QString socketName = out.serverLocation;
0558     if (socketName.isEmpty()) {
0559         out.errorString = i18n("Invalid socket name!");
0560         goto leave;
0561     }
0562 
0563     {
0564         assuan_context_t naked_ctx = nullptr;
0565         err = assuan_new(&naked_ctx);
0566         if (err) {
0567             out.errorString = i18n("Could not allocate resources to connect to Kleopatra UI server at %1: %2", socketName, to_error_string(err));
0568             goto leave;
0569         }
0570 
0571         ctx.reset(naked_ctx);
0572     }
0573 
0574     err = assuan_socket_connect(ctx.get(), socketName.toUtf8().constData(), -1, 0);
0575     if (err) {
0576         qDebug("UI server not running, starting it");
0577 
0578         const QString errorString = start_uiserver();
0579         if (!errorString.isEmpty()) {
0580             out.errorString = errorString;
0581             goto leave;
0582         }
0583 
0584         // give it a bit of time to start up and try a couple of times
0585         for (int i = 0; err && i < 20; ++i) {
0586             msleep(500);
0587             err = assuan_socket_connect(ctx.get(), socketName.toUtf8().constData(), -1, 0);
0588         }
0589     }
0590 
0591     if (err) {
0592         out.errorString = i18n("Could not connect to Kleopatra UI server at %1: %2", socketName, to_error_string(err));
0593         goto leave;
0594     }
0595 
0596     out.serverPid = -1;
0597     err = my_assuan_transact(ctx, "GETINFO pid", &getinfo_pid_cb, &out.serverPid);
0598     if (err || out.serverPid <= 0) {
0599         out.errorString = i18n("Could not get the process-id of the Kleopatra UI server at %1: %2", socketName, to_error_string(err));
0600         goto leave;
0601     }
0602 
0603     qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "Server PID =" << out.serverPid;
0604 
0605 #if defined(Q_OS_WIN)
0606     if (!AllowSetForegroundWindow((pid_t)out.serverPid)) {
0607         qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "AllowSetForegroundWindow(" << out.serverPid << ") failed: " << GetLastError();
0608     }
0609 #endif
0610 
0611     if (in.command.isEmpty()) {
0612         goto leave;
0613     }
0614 
0615     if (in.parentWId) {
0616         err = send_option(ctx, "window-id", QString::number(in.parentWId, 16));
0617         if (err) {
0618             qDebug("sending option window-id failed - ignoring");
0619         }
0620     }
0621 
0622     for (auto it = in.options.begin(), end = in.options.end(); it != end; ++it)
0623         if ((err = send_option(ctx, it->first.c_str(), it->second.hasValue ? it->second.value.toString() : QVariant()))) {
0624             if (it->second.isCritical) {
0625                 out.errorString = i18n("Failed to send critical option %1: %2", QString::fromLatin1(it->first.c_str()), to_error_string(err));
0626                 goto leave;
0627             } else {
0628                 qCDebug(LIBKLEOPATRACLIENTCORE_LOG) << "Failed to send non-critical option" << it->first.c_str() << ":" << to_error_string(err);
0629             }
0630         }
0631 
0632     for (const QString &filePath : std::as_const(in.filePaths)) {
0633         if ((err = send_file(ctx, filePath))) {
0634             out.errorString = i18n("Failed to send file path %1: %2", filePath, to_error_string(err));
0635             goto leave;
0636         }
0637     }
0638 
0639     for (const QString &sender : std::as_const(in.senders)) {
0640         if ((err = send_sender(ctx, sender, in.areSendersInformative))) {
0641             out.errorString = i18n("Failed to send sender %1: %2", sender, to_error_string(err));
0642             goto leave;
0643         }
0644     }
0645 
0646     for (const QString &recipient : std::as_const(in.recipients)) {
0647         if ((err = send_recipient(ctx, recipient, in.areRecipientsInformative))) {
0648             out.errorString = i18n("Failed to send recipient %1: %2", recipient, to_error_string(err));
0649             goto leave;
0650         }
0651     }
0652 
0653 #if 0
0654     setup I / O;
0655 #endif
0656 
0657     err = my_assuan_transact(ctx, in.command.constData(), &command_data_cb, &out.data, &command_inquire_cb, &id);
0658     if (err) {
0659         if (GpgME::Error{err}.isCanceled()) {
0660             out.canceled = true;
0661         } else {
0662             out.errorString = i18n("Command (%1) failed: %2", QString::fromLatin1(in.command.constData()), to_error_string(err));
0663         }
0664         goto leave;
0665     }
0666 
0667 leave:
0668     const QMutexLocker locker(&mutex);
0669     // copy outputs to where Command can see them:
0670     outputs = out;
0671 }
0672 
0673 #include "moc_command_p.cpp"
0674 
0675 #include "moc_command.cpp"