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"