1 /*
2  *  Copyright (C) 2012 Tobias Tangemann
3  *  Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
4  *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation, either version 2 or (at your option)
9  *  version 3 of the License.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include "Application.h"
21 
22 #include "autotype/AutoType.h"
23 #include "core/Config.h"
24 #include "core/Global.h"
25 #include "core/Resources.h"
26 #include "gui/MainWindow.h"
27 #include "gui/osutils/OSUtils.h"
28 #include "gui/styles/dark/DarkStyle.h"
29 #include "gui/styles/light/LightStyle.h"
30 
31 #include <QFileInfo>
32 #include <QFileOpenEvent>
33 #include <QLockFile>
34 #include <QSocketNotifier>
35 #include <QStandardPaths>
36 #include <QtNetwork/QLocalSocket>
37 
38 #if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
39 #include "core/OSEventFilter.h"
40 #endif
41 
42 #if defined(Q_OS_UNIX)
43 #include <signal.h>
44 #include <sys/socket.h>
45 #include <unistd.h>
46 #endif
47 
48 namespace
49 {
50     constexpr int WaitTimeoutMSec = 150;
51     const char BlockSizeProperty[] = "blockSize";
52 } // namespace
53 
Application(int & argc,char ** argv)54 Application::Application(int& argc, char** argv)
55     : QApplication(argc, argv)
56 #ifdef Q_OS_UNIX
57     , m_unixSignalNotifier(nullptr)
58 #endif
59     , m_alreadyRunning(false)
60     , m_lockFile(nullptr)
61 #if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
62     , m_osEventFilter(new OSEventFilter())
63 {
64     installNativeEventFilter(m_osEventFilter.data());
65 #else
66 {
67 #endif
68 #if defined(Q_OS_UNIX)
69     registerUnixSignals();
70 #endif
71 
72     QString userName = qgetenv("USER");
73     if (userName.isEmpty()) {
74         userName = qgetenv("USERNAME");
75     }
76     QString identifier = "keepassxc";
77     if (!userName.isEmpty()) {
78         identifier += "-" + userName;
79     }
80 #ifdef QT_DEBUG
81     // In DEBUG mode don't interfere with Release instances
82     identifier += "-DEBUG";
83 #endif
84     QString lockName = identifier + ".lock";
85     m_socketName = identifier + ".socket";
86 
87     // According to documentation we should use RuntimeLocation on *nixes, but even Qt doesn't respect
88     // this and creates sockets in TempLocation, so let's be consistent.
89     m_lockFile = new QLockFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + lockName);
90     m_lockFile->setStaleLockTime(0);
91     m_lockFile->tryLock();
92 
93     m_lockServer.setSocketOptions(QLocalServer::UserAccessOption);
94     connect(&m_lockServer, SIGNAL(newConnection()), this, SIGNAL(anotherInstanceStarted()));
95     connect(&m_lockServer, SIGNAL(newConnection()), this, SLOT(processIncomingConnection()));
96 
97     switch (m_lockFile->error()) {
98     case QLockFile::NoError:
99         // No existing lock was found, start listener
100         m_lockServer.listen(m_socketName);
101         break;
102     case QLockFile::LockFailedError: {
103         if (config()->get(Config::SingleInstance).toBool()) {
104             // Attempt to connect to the existing instance
105             QLocalSocket client;
106             for (int i = 0; i < 3; ++i) {
107                 client.connectToServer(m_socketName);
108                 if (client.waitForConnected(WaitTimeoutMSec)) {
109                     // Connection succeeded, this will raise the existing window if minimized
110                     client.abort();
111                     m_alreadyRunning = true;
112                     break;
113                 }
114             }
115 
116             if (!m_alreadyRunning) {
117                 // If we get here then the original instance is likely dead
118                 qWarning() << QObject::tr("Existing single-instance lock file is invalid. Launching new instance.")
119                                   .toUtf8()
120                                   .constData();
121 
122                 // forceably reset the lock file
123                 m_lockFile->removeStaleLockFile();
124                 m_lockFile->tryLock();
125                 // start the listen server
126                 m_lockServer.listen(m_socketName);
127             }
128         }
129         break;
130     }
131     default:
132         qWarning()
133             << QObject::tr("The lock file could not be created. Single-instance mode disabled.").toUtf8().constData();
134     }
135 
136     connect(osUtils, &OSUtilsBase::interfaceThemeChanged, this, [this]() {
137         if (config()->get(Config::GUI_ApplicationTheme).toString() != "classic") {
138             applyTheme();
139         }
140     });
141 }
142 
143 Application::~Application()
144 {
145     m_lockServer.close();
146     if (m_lockFile) {
147         m_lockFile->unlock();
148         delete m_lockFile;
149     }
150 }
151 
152 void Application::applyTheme()
153 {
154     auto appTheme = config()->get(Config::GUI_ApplicationTheme).toString();
155     if (appTheme == "auto") {
156         appTheme = osUtils->isDarkMode() ? "dark" : "light";
157 #ifdef Q_OS_WIN
158         if (winUtils()->isHighContrastMode()) {
159             appTheme = "classic";
160         }
161 #endif
162     }
163     QPixmapCache::clear();
164     if (appTheme == "light") {
165         auto* s = new LightStyle;
166         setPalette(s->standardPalette());
167         setStyle(s);
168     } else if (appTheme == "dark") {
169         auto* s = new DarkStyle;
170         setPalette(s->standardPalette());
171         setStyle(s);
172         m_darkTheme = true;
173     } else {
174         // Classic mode, don't check for dark theme on Windows
175         // because Qt 5.x does not support it
176 #ifndef Q_OS_WIN
177         m_darkTheme = osUtils->isDarkMode();
178 #endif
179         QFile stylesheetFile(":/styles/base/classicstyle.qss");
180         if (stylesheetFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
181             setStyleSheet(stylesheetFile.readAll());
182             stylesheetFile.close();
183         }
184     }
185 }
186 
187 bool Application::event(QEvent* event)
188 {
189     // Handle Apple QFileOpenEvent from finder (double click on .kdbx file)
190     if (event->type() == QEvent::FileOpen) {
191         emit openFile(static_cast<QFileOpenEvent*>(event)->file());
192         return true;
193     }
194 #ifdef Q_OS_MACOS
195     // restore main window when clicking on the docker icon
196     else if (event->type() == QEvent::ApplicationActivate) {
197         emit applicationActivated();
198     }
199 #endif
200 
201     return QApplication::event(event);
202 }
203 
204 #if defined(Q_OS_UNIX)
205 int Application::unixSignalSocket[2];
206 
207 void Application::registerUnixSignals()
208 {
209     int result = ::socketpair(AF_UNIX, SOCK_STREAM, 0, unixSignalSocket);
210     Q_ASSERT(0 == result);
211     if (0 != result) {
212         // do not register handles when socket creation failed, otherwise
213         // application will be unresponsive to signals such as SIGINT or SIGTERM
214         return;
215     }
216 
217     QVector<int> const handledSignals = {SIGQUIT, SIGINT, SIGTERM, SIGHUP};
218     for (auto s : handledSignals) {
219         struct sigaction sigAction;
220 
221         sigAction.sa_handler = handleUnixSignal;
222         sigemptyset(&sigAction.sa_mask);
223         sigAction.sa_flags = 0 | SA_RESTART;
224         sigaction(s, &sigAction, nullptr);
225     }
226 
227     m_unixSignalNotifier = new QSocketNotifier(unixSignalSocket[1], QSocketNotifier::Read, this);
228     connect(m_unixSignalNotifier, SIGNAL(activated(int)), this, SLOT(quitBySignal()));
229 }
230 
231 void Application::handleUnixSignal(int sig)
232 {
233     switch (sig) {
234     case SIGQUIT:
235     case SIGINT:
236     case SIGTERM: {
237         char buf = 0;
238         Q_UNUSED(!::write(unixSignalSocket[0], &buf, sizeof(buf)));
239         return;
240     }
241     case SIGHUP:
242         return;
243     }
244 }
245 
246 void Application::quitBySignal()
247 {
248     m_unixSignalNotifier->setEnabled(false);
249     char buf;
250     Q_UNUSED(!::read(unixSignalSocket[1], &buf, sizeof(buf)));
251     emit quitSignalReceived();
252 }
253 #endif
254 
255 void Application::processIncomingConnection()
256 {
257     if (m_lockServer.hasPendingConnections()) {
258         QLocalSocket* socket = m_lockServer.nextPendingConnection();
259         socket->setProperty(BlockSizeProperty, 0);
260         connect(socket, SIGNAL(readyRead()), this, SLOT(socketReadyRead()));
261     }
262 }
263 
264 void Application::socketReadyRead()
265 {
266     QLocalSocket* socket = qobject_cast<QLocalSocket*>(sender());
267     if (!socket) {
268         return;
269     }
270 
271     QDataStream in(socket);
272     in.setVersion(QDataStream::Qt_5_0);
273 
274     int blockSize = socket->property(BlockSizeProperty).toInt();
275     if (blockSize == 0) {
276         // Relies on the fact that QDataStream format streams a quint32 into sizeof(quint32) bytes
277         if (socket->bytesAvailable() < qint64(sizeof(quint32))) {
278             return;
279         }
280         in >> blockSize;
281     }
282 
283     if (socket->bytesAvailable() < blockSize || in.atEnd()) {
284         socket->setProperty(BlockSizeProperty, blockSize);
285         return;
286     }
287 
288     QStringList fileNames;
289     quint32 id;
290     in >> id;
291 
292     // TODO: move constants to enum
293     switch (id) {
294     case 1:
295         in >> fileNames;
296         for (const QString& fileName : asConst(fileNames)) {
297             const QFileInfo fInfo(fileName);
298             if (fInfo.isFile() && fInfo.suffix().toLower() == "kdbx") {
299                 emit openFile(fileName);
300             }
301         }
302 
303         break;
304     case 2:
305         getMainWindow()->lockAllDatabases();
306         break;
307     }
308 
309     socket->deleteLater();
310 }
311 
312 bool Application::isAlreadyRunning() const
313 {
314 #ifdef QT_DEBUG
315     // In DEBUG mode we can run unlimited instances
316     return false;
317 #endif
318     return config()->get(Config::SingleInstance).toBool() && m_alreadyRunning;
319 }
320 
321 /**
322  * Send to-open file names to the running UI instance
323  *
324  * @param fileNames - list of file names to open
325  * @return true if all operations succeeded (connection made, data sent, connection closed)
326  */
327 bool Application::sendFileNamesToRunningInstance(const QStringList& fileNames)
328 {
329     QLocalSocket client;
330     client.connectToServer(m_socketName);
331     const bool connected = client.waitForConnected(WaitTimeoutMSec);
332     if (!connected) {
333         return false;
334     }
335 
336     QByteArray data;
337     QDataStream out(&data, QIODevice::WriteOnly);
338     out.setVersion(QDataStream::Qt_5_0);
339     out << quint32(0); // reserve space for block size
340     out << quint32(1); // ID for file name send. TODO: move to enum
341     out << fileNames; // send file names to be opened
342     out.device()->seek(0);
343     out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size
344 
345     const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec);
346     client.disconnectFromServer();
347     const bool disconnected =
348         client.state() == QLocalSocket::UnconnectedState || client.waitForDisconnected(WaitTimeoutMSec);
349     return writeOk && disconnected;
350 }
351 
352 /**
353  * Locks all open databases in the running instance
354  *
355  * @return true if the "please lock" signal was sent successfully
356  */
357 bool Application::sendLockToInstance()
358 {
359     // Make a connection to avoid SIGSEGV
360     QLocalSocket client;
361     client.connectToServer(m_socketName);
362     const bool connected = client.waitForConnected(WaitTimeoutMSec);
363     if (!connected) {
364         return false;
365     }
366 
367     // Send lock signal
368     QByteArray data;
369     QDataStream out(&data, QIODevice::WriteOnly);
370     out.setVersion(QDataStream::Qt_5_0);
371     out << quint32(0); // reserve space for block size
372     out << quint32(2); // ID for database lock. TODO: move to enum
373     out.device()->seek(0);
374     out << quint32(data.size() - sizeof(quint32)); // replace the previous constant 0 with block size
375 
376     // Finish gracefully
377     const bool writeOk = client.write(data) != -1 && client.waitForBytesWritten(WaitTimeoutMSec);
378     client.disconnectFromServer();
379     const bool disconnected =
380         client.state() == QLocalSocket::UnconnectedState || client.waitForConnected(WaitTimeoutMSec);
381     return writeOk && disconnected;
382 }
383 
384 bool Application::isDarkTheme() const
385 {
386     return m_darkTheme;
387 }
388 
389 void Application::restart()
390 {
391     // Disable single instance
392     m_lockServer.close();
393     if (m_lockFile) {
394         m_lockFile->unlock();
395         delete m_lockFile;
396         m_lockFile = nullptr;
397     }
398 
399     exit(RESTART_EXITCODE);
400 }
401