File indexing completed on 2024-06-23 05:14:14

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     uiserver/uiserver.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include <config-kleopatra.h>
0011 
0012 #include "uiserver.h"
0013 #include "uiserver_p.h"
0014 
0015 #include "sessiondata.h"
0016 
0017 #include <Libkleo/GnuPG>
0018 #include <utils/detail_p.h>
0019 
0020 #include <Libkleo/KleoException>
0021 #include <Libkleo/Stl_Util>
0022 
0023 #include "kleopatra_debug.h"
0024 #include <KLocalizedString>
0025 
0026 #include <gpgme++/global.h>
0027 
0028 #include <QDir>
0029 #include <QEventLoop>
0030 #include <QFile>
0031 #include <QTcpSocket>
0032 #include <QTimer>
0033 
0034 #include <algorithm>
0035 #include <cerrno>
0036 
0037 using namespace Kleo;
0038 
0039 // static
0040 void UiServer::setLogStream(FILE *stream)
0041 {
0042     assuan_set_assuan_log_stream(stream);
0043 }
0044 
0045 UiServer::Private::Private(UiServer *qq)
0046     : QTcpServer()
0047     , q(qq)
0048     , file()
0049     , factories()
0050     , connections()
0051     , suggestedSocketName()
0052     , actualSocketName()
0053     , cryptoCommandsEnabled(false)
0054 {
0055     assuan_set_gpg_err_source(GPG_ERR_SOURCE_DEFAULT);
0056     assuan_sock_init();
0057 }
0058 
0059 bool UiServer::Private::isStaleAssuanSocket(const QString &fileName)
0060 {
0061     assuan_context_t ctx = nullptr;
0062     const bool error = assuan_new(&ctx) || assuan_socket_connect(ctx, QFile::encodeName(fileName).constData(), ASSUAN_INVALID_PID, 0);
0063     if (!error)
0064         assuan_release(ctx);
0065     return error;
0066 }
0067 
0068 UiServer::UiServer(const QString &socket, QObject *p)
0069     : QObject(p)
0070     , d(new Private(this))
0071 {
0072     d->suggestedSocketName = d->makeFileName(socket);
0073 }
0074 
0075 UiServer::~UiServer()
0076 {
0077     if (QFile::exists(d->actualSocketName)) {
0078         QFile::remove(d->actualSocketName);
0079     }
0080 }
0081 
0082 namespace
0083 {
0084 using Iterator = std::vector<std::shared_ptr<AssuanCommandFactory>>::iterator;
0085 static bool empty(std::pair<Iterator, Iterator> iters)
0086 {
0087     return iters.first == iters.second;
0088 }
0089 }
0090 
0091 bool UiServer::registerCommandFactory(const std::shared_ptr<AssuanCommandFactory> &cf)
0092 {
0093     if (cf && empty(std::equal_range(d->factories.begin(), d->factories.end(), cf, _detail::ByName<std::less>()))) {
0094         d->factories.push_back(cf);
0095         std::inplace_merge(d->factories.begin(), d->factories.end() - 1, d->factories.end(), _detail::ByName<std::less>());
0096         return true;
0097     } else {
0098         if (!cf) {
0099             qCWarning(KLEOPATRA_LOG) << "NULL factory";
0100         } else {
0101             qCWarning(KLEOPATRA_LOG) << (void *)cf.get() << " factory already registered";
0102         }
0103 
0104         return false;
0105     }
0106 }
0107 
0108 void UiServer::start()
0109 {
0110     d->makeListeningSocket();
0111 }
0112 
0113 void UiServer::stop()
0114 {
0115     d->close();
0116 
0117     if (d->file.exists()) {
0118         d->file.remove();
0119     }
0120 
0121     if (isStopped()) {
0122         SessionDataHandler::instance()->clear();
0123         Q_EMIT stopped();
0124     }
0125 }
0126 
0127 void UiServer::enableCryptoCommands(bool on)
0128 {
0129     if (on == d->cryptoCommandsEnabled) {
0130         return;
0131     }
0132     d->cryptoCommandsEnabled = on;
0133     std::for_each(d->connections.cbegin(), d->connections.cend(), [on](std::shared_ptr<AssuanServerConnection> conn) {
0134         conn->enableCryptoCommands(on);
0135     });
0136 }
0137 
0138 QString UiServer::socketName() const
0139 {
0140     return d->actualSocketName;
0141 }
0142 
0143 bool UiServer::waitForStopped(unsigned int ms)
0144 {
0145     if (isStopped()) {
0146         return true;
0147     }
0148     QEventLoop loop;
0149     QTimer timer;
0150     timer.setInterval(ms);
0151     timer.setSingleShot(true);
0152     connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
0153     connect(this, &UiServer::stopped, &loop, &QEventLoop::quit);
0154     loop.exec();
0155     return !timer.isActive();
0156 }
0157 
0158 bool UiServer::isStopped() const
0159 {
0160     return d->connections.empty() && !d->isListening();
0161 }
0162 
0163 bool UiServer::isStopping() const
0164 {
0165     return !d->connections.empty() && !d->isListening();
0166 }
0167 
0168 void UiServer::Private::slotConnectionClosed(Kleo::AssuanServerConnection *conn)
0169 {
0170     qCDebug(KLEOPATRA_LOG) << "UiServer: connection " << (void *)conn << " closed";
0171     connections.erase(std::remove_if(connections.begin(),
0172                                      connections.end(),
0173                                      [conn](const std::shared_ptr<AssuanServerConnection> &other) {
0174                                          return conn == other.get();
0175                                      }),
0176                       connections.end());
0177     if (q->isStopped()) {
0178         SessionDataHandler::instance()->clear();
0179         Q_EMIT q->stopped();
0180     }
0181 }
0182 
0183 void UiServer::Private::incomingConnection(qintptr fd)
0184 {
0185     try {
0186         qCDebug(KLEOPATRA_LOG) << "UiServer: client connect on fd " << fd;
0187         if (assuan_sock_check_nonce((assuan_fd_t)fd, &nonce)) {
0188             qCDebug(KLEOPATRA_LOG) << "UiServer: nonce check failed";
0189             assuan_sock_close((assuan_fd_t)fd);
0190             return;
0191         }
0192         const std::shared_ptr<AssuanServerConnection> c(new AssuanServerConnection((assuan_fd_t)fd, factories));
0193         connect(c.get(), &AssuanServerConnection::closed, this, &Private::slotConnectionClosed);
0194         connect(c.get(), &AssuanServerConnection::startKeyManagerRequested, q, &UiServer::startKeyManagerRequested, Qt::QueuedConnection);
0195         connect(c.get(), &AssuanServerConnection::startConfigDialogRequested, q, &UiServer::startConfigDialogRequested, Qt::QueuedConnection);
0196         c->enableCryptoCommands(cryptoCommandsEnabled);
0197         connections.push_back(c);
0198         qCDebug(KLEOPATRA_LOG) << "UiServer: client connection " << (void *)c.get() << " established successfully";
0199     } catch (const Exception &e) {
0200         qCDebug(KLEOPATRA_LOG) << "UiServer: client connection failed: " << e.what();
0201         QTcpSocket s;
0202         s.setSocketDescriptor(fd);
0203         QTextStream(&s) << "ERR " << e.error_code() << " " << e.what() << "\r\n";
0204         s.waitForBytesWritten();
0205         s.close();
0206     } catch (...) {
0207         qCDebug(KLEOPATRA_LOG) << "UiServer: client connection failed: unknown exception caught";
0208         // this should never happen...
0209         QTcpSocket s;
0210         s.setSocketDescriptor(fd);
0211         QTextStream(&s) << "ERR 63 unknown exception caught\r\n";
0212         s.waitForBytesWritten();
0213         s.close();
0214     }
0215 }
0216 
0217 QString UiServer::Private::makeFileName(const QString &socket) const
0218 {
0219     if (!socket.isEmpty()) {
0220         return socket;
0221     }
0222     const QString socketPath{QString::fromUtf8(GpgME::dirInfo("uiserver-socket"))};
0223     if (!socketPath.isEmpty()) {
0224         // Note: The socket directory exists after GpgME::dirInfo() has been called.
0225         return socketPath;
0226     }
0227     // GPGME (or GnuPG) is too old to return the socket path.
0228     // In this case we fallback to assume that the socket directory is
0229     // the home directory as we did in the past.  This is not correct but
0230     // probably the safest fallback we can do despite that it is a
0231     // bug to assume the socket directory in the home directory.  See
0232     // https://dev.gnupg.org/T5613
0233     const QString gnupgHome = gnupgHomeDirectory();
0234     if (gnupgHome.isEmpty()) {
0235         throw_<std::runtime_error>(i18n("Could not determine the GnuPG home directory. Consider setting the GNUPGHOME environment variable."));
0236     }
0237     // We should not create the home directory, but this only happens for very
0238     // old and long unsupported versions of gnupg.
0239     ensureDirectoryExists(gnupgHome);
0240     const QDir dir(gnupgHome);
0241     Q_ASSERT(dir.exists());
0242     return dir.absoluteFilePath(QStringLiteral("S.uiserver"));
0243 }
0244 
0245 void UiServer::Private::ensureDirectoryExists(const QString &path) const
0246 {
0247     const QFileInfo info(path);
0248     if (info.exists() && !info.isDir()) {
0249         throw_<std::runtime_error>(i18n("Cannot determine the GnuPG home directory: %1 exists but is not a directory.", path));
0250     }
0251     if (info.exists()) {
0252         return;
0253     }
0254     const QDir dummy; // there is no static QDir::mkpath()...
0255     errno = 0;
0256     if (!dummy.mkpath(path)) {
0257         throw_<std::runtime_error>(i18n("Could not create GnuPG home directory %1: %2", path, systemErrorString()));
0258     }
0259 }
0260 
0261 void UiServer::Private::makeListeningSocket()
0262 {
0263     // First, create a file (we do this only for the name, gmpfh)
0264     const QString fileName = suggestedSocketName;
0265 
0266     if (QFile::exists(fileName)) {
0267         if (isStaleAssuanSocket(fileName)) {
0268             QFile::remove(fileName);
0269         } else {
0270             throw_<std::runtime_error>(i18n("Detected another running gnupg UI server listening at %1.", fileName));
0271         }
0272     }
0273 
0274     doMakeListeningSocket(fileName.toUtf8());
0275 
0276     actualSocketName = suggestedSocketName;
0277 }
0278 
0279 #include "moc_uiserver_p.cpp"
0280 
0281 #include "moc_uiserver.cpp"