1/*
2 This file is part of the KDE Password Server
3 SPDX-FileCopyrightText: 2002 Waldo Bastian <bastian@kde.org>
4 SPDX-FileCopyrightText: 2005 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2012 Dawit Alemayehu <adawit@kde.org>
6 SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
7
8 SPDX-License-Identifier: GPL-2.0-only
9*/
10
11// KDE Password Server
12
13#include "kpasswdserver.h"
14
15#include "kpasswdserveradaptor.h"
16
17#include <KLocalizedString>
18#include <KMessageDialog>
19#include <KPasswordDialog>
20#include <kwindowsystem.h>
21
22#ifdef HAVE_KF6WALLET
23#include <KWallet>
24#endif
25
26#include <QPushButton>
27#include <QTimer>
28#include <ctime>
29
30#include "../gui/config-kiogui.h"
31
32#if HAVE_X11
33#include <KUserTimestamp>
34#include <KX11Extras>
35#endif
36
37Q_LOGGING_CATEGORY(category, "kf.kio.kpasswdserver", QtInfoMsg)
38
39static const char s_domain[] = "domain";
40static const char s_anonymous[] = "anonymous";
41static const char s_bypassCacheAndKwallet[] = "bypass-cache-and-kwallet";
42static const char s_skipCachingOnQuery[] = "skip-caching-on-query";
43static const char s_hideUsernameInput[] = "hide-username-line";
44static const char s_usernameContextHelp[] = "username-context-help";
45
46static qlonglong getRequestId()
47{
48 static qlonglong nextRequestId = 0;
49 return nextRequestId++;
50}
51
52bool KPasswdServer::AuthInfoContainer::Sorter::operator()(const AuthInfoContainer &n1, const AuthInfoContainer &n2) const
53{
54 const int l1 = n1.directory.length();
55 const int l2 = n2.directory.length();
56 return l1 < l2;
57}
58
59KPasswdServer::KPasswdServer(QObject *parent, const QList<QVariant> &)
60 : KDEDModule(parent)
61{
62 KIO::AuthInfo::registerMetaTypes();
63
64 m_seqNr = 0;
65 m_wallet = nullptr;
66 m_walletDisabled = false;
67
68 KPasswdServerAdaptor *adaptor = new KPasswdServerAdaptor(this);
69 // connect signals to the adaptor
70 connect(sender: this, signal: &KPasswdServer::checkAuthInfoAsyncResult, context: adaptor, slot: &KPasswdServerAdaptor::checkAuthInfoAsyncResult);
71 connect(sender: this, signal: &KPasswdServer::queryAuthInfoAsyncResult, context: adaptor, slot: &KPasswdServerAdaptor::queryAuthInfoAsyncResult);
72
73 connect(sender: this, signal: &KDEDModule::windowUnregistered, context: this, slot: &KPasswdServer::removeAuthForWindowId);
74
75#if HAVE_X11
76 connect(sender: KX11Extras::self(), signal: &KX11Extras::windowRemoved, context: this, slot: &KPasswdServer::windowRemoved);
77#endif
78}
79
80KPasswdServer::~KPasswdServer()
81{
82 // TODO: what about clients waiting for requests? will they just
83 // notice kpasswdserver is gone from the dbus?
84 qDeleteAll(c: m_authPending);
85 qDeleteAll(c: m_authWait);
86 qDeleteAll(c: m_authDict);
87 qDeleteAll(c: m_authInProgress);
88 qDeleteAll(c: m_authRetryInProgress);
89
90#ifdef HAVE_KF6WALLET
91 delete m_wallet;
92#endif
93}
94
95#ifdef HAVE_KF6WALLET
96
97// Helper - returns the wallet key to use for read/store/checking for existence.
98static QString makeWalletKey(const QString &key, const QString &realm)
99{
100 return realm.isEmpty() ? key : key + QLatin1Char('-') + realm;
101}
102
103// Helper for storeInWallet/readFromWallet
104static QString makeMapKey(const char *key, int entryNumber)
105{
106 QString str = QLatin1String(key);
107 if (entryNumber > 1) {
108 str += QLatin1Char('-') + QString::number(entryNumber);
109 }
110 return str;
111}
112
113static bool storeInWallet(KWallet::Wallet *wallet, const QString &key, const KIO::AuthInfo &info)
114{
115 if (!wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
116 if (!wallet->createFolder(KWallet::Wallet::PasswordFolder())) {
117 return false;
118 }
119 }
120 wallet->setFolder(KWallet::Wallet::PasswordFolder());
121 // Before saving, check if there's already an entry with this login.
122 // If so, replace it (with the new password). Otherwise, add a new entry.
123 typedef QMap<QString, QString> Map;
124 int entryNumber = 1;
125 Map map;
126 QString walletKey = makeWalletKey(key, info.realmValue);
127 qCDebug(category) << "walletKey =" << walletKey << " reading existing map";
128 if (wallet->readMap(walletKey, map) == 0) {
129 Map::ConstIterator end = map.constEnd();
130 Map::ConstIterator it = map.constFind(QStringLiteral("login"));
131 while (it != end) {
132 if (it.value() == info.username) {
133 break; // OK, overwrite this entry
134 }
135 it = map.constFind(QStringLiteral("login-") + QString::number(++entryNumber));
136 }
137 // If no entry was found, create a new entry - entryNumber is set already.
138 }
139 const QString loginKey = makeMapKey("login", entryNumber);
140 const QString passwordKey = makeMapKey("password", entryNumber);
141 qCDebug(category) << "writing to " << loginKey << "," << passwordKey;
142 // note the overwrite=true by default
143 map.insert(loginKey, info.username);
144 map.insert(passwordKey, info.password);
145 wallet->writeMap(walletKey, map);
146 return true;
147}
148
149static bool readFromWallet(KWallet::Wallet *wallet,
150 const QString &key,
151 const QString &realm,
152 QString &username,
153 QString &password,
154 bool userReadOnly,
155 QMap<QString, QString> &knownLogins)
156{
157 // qCDebug(category) << "key =" << key << " username =" << username << " password =" /*<< password*/
158 // << " userReadOnly =" << userReadOnly << " realm =" << realm;
159 if (wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
160 wallet->setFolder(KWallet::Wallet::PasswordFolder());
161
162 QMap<QString, QString> map;
163 if (wallet->readMap(makeWalletKey(key, realm), map) == 0) {
164 typedef QMap<QString, QString> Map;
165 int entryNumber = 1;
166 Map::ConstIterator end = map.constEnd();
167 Map::ConstIterator it = map.constFind(QStringLiteral("login"));
168 while (it != end) {
169 // qCDebug(category) << "found " << it.key() << "=" << it.value();
170 Map::ConstIterator pwdIter = map.constFind(makeMapKey("password", entryNumber));
171 if (pwdIter != end) {
172 if (it.value() == username) {
173 password = pwdIter.value();
174 }
175 knownLogins.insert(it.value(), pwdIter.value());
176 }
177
178 it = map.constFind(QStringLiteral("login-") + QString::number(++entryNumber));
179 }
180 // qCDebug(category) << knownLogins.count() << " known logins";
181
182 if (!userReadOnly && !knownLogins.isEmpty() && username.isEmpty()) {
183 // Pick one, any one...
184 username = knownLogins.begin().key();
185 password = knownLogins.begin().value();
186 // qCDebug(category) << "picked the first one:" << username;
187 }
188
189 return true;
190 }
191 }
192 return false;
193}
194
195#endif
196
197bool KPasswdServer::hasPendingQuery(const QString &key, const KIO::AuthInfo &info)
198{
199 const QString path2(info.url.path().left(n: info.url.path().indexOf(c: QLatin1Char('/')) + 1));
200 for (const Request *request : std::as_const(t&: m_authPending)) {
201 if (request->key != key) {
202 continue;
203 }
204
205 if (info.verifyPath) {
206 const QString path1(request->info.url.path().left(n: info.url.path().indexOf(c: QLatin1Char('/')) + 1));
207 if (!path2.startsWith(s: path1)) {
208 continue;
209 }
210 }
211
212 return true;
213 }
214
215 return false;
216}
217
218// deprecated method, not used anymore. TODO KF6: REMOVE
219QByteArray KPasswdServer::checkAuthInfo(const QByteArray &data, qlonglong windowId, qlonglong usertime)
220{
221 KIO::AuthInfo info;
222 QDataStream stream(data);
223 stream >> info;
224#if HAVE_X11
225 if (usertime != 0) {
226 KUserTimestamp::updateUserTimestamp(time: usertime);
227 }
228#endif
229
230 // if the check depends on a pending query, delay it
231 // until that query is finished.
232 const QString key(createCacheKey(info));
233 if (hasPendingQuery(key, info)) {
234 setDelayedReply(true);
235 Request *pendingCheck = new Request;
236 pendingCheck->isAsync = false;
237 if (calledFromDBus()) {
238 pendingCheck->transaction = message();
239 }
240 pendingCheck->key = key;
241 pendingCheck->info = info;
242 m_authWait.append(t: pendingCheck);
243 return data; // return value will be ignored
244 }
245
246 // qCDebug(category) << "key =" << key << "user =" << info.username << "windowId =" << windowId;
247 const AuthInfoContainer *result = findAuthInfoItem(key, info);
248 if (!result || result->isCanceled) {
249#ifdef HAVE_KF6WALLET
250 if (!result && !m_walletDisabled && (info.username.isEmpty() || info.password.isEmpty())
251 && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), makeWalletKey(key, info.realmValue))) {
252 QMap<QString, QString> knownLogins;
253 if (openWallet(windowId)) {
254 if (readFromWallet(m_wallet, key, info.realmValue, info.username, info.password, info.readOnly, knownLogins)) {
255 info.setModified(true);
256 // fall through
257 }
258 }
259 } else {
260 info.setModified(false);
261 }
262#else
263 info.setModified(false);
264#endif
265 } else {
266 qCDebug(category) << "Found cached authentication for" << key;
267 updateAuthExpire(key, result, windowId, keep: false);
268 copyAuthInfo(result, info);
269 }
270
271 QByteArray data2;
272 QDataStream stream2(&data2, QIODevice::WriteOnly);
273 stream2 << info;
274 return data2;
275}
276
277qlonglong KPasswdServer::checkAuthInfoAsync(KIO::AuthInfo info, qlonglong windowId, qlonglong usertime)
278{
279#if HAVE_X11
280 if (usertime != 0) {
281 KUserTimestamp::updateUserTimestamp(time: usertime);
282 }
283#endif
284
285 // send the request id back to the client
286 qlonglong requestId = getRequestId();
287 qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId;
288 if (calledFromDBus()) {
289 QDBusMessage reply(message().createReply(argument: requestId));
290 QDBusConnection::sessionBus().send(message: reply);
291 }
292
293 // if the check depends on a pending query, delay it
294 // until that query is finished.
295 const QString key(createCacheKey(info));
296 if (hasPendingQuery(key, info)) {
297 Request *pendingCheck = new Request;
298 pendingCheck->isAsync = true;
299 pendingCheck->requestId = requestId;
300 pendingCheck->key = key;
301 pendingCheck->info = info;
302 m_authWait.append(t: pendingCheck);
303 return 0; // ignored as we already sent a reply
304 }
305
306 const AuthInfoContainer *result = findAuthInfoItem(key, info);
307 if (!result || result->isCanceled) {
308#ifdef HAVE_KF6WALLET
309 if (!result && !m_walletDisabled && (info.username.isEmpty() || info.password.isEmpty())
310 && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::PasswordFolder(), makeWalletKey(key, info.realmValue))) {
311 QMap<QString, QString> knownLogins;
312 if (openWallet(windowId)) {
313 if (readFromWallet(m_wallet, key, info.realmValue, info.username, info.password, info.readOnly, knownLogins)) {
314 info.setModified(true);
315 // fall through
316 }
317 }
318 } else {
319 info.setModified(false);
320 }
321#else
322 info.setModified(false);
323#endif
324 } else {
325 // qCDebug(category) << "Found cached authentication for" << key;
326 updateAuthExpire(key, result, windowId, keep: false);
327 copyAuthInfo(result, info);
328 }
329
330 Q_EMIT checkAuthInfoAsyncResult(requestId, seqNr: m_seqNr, info);
331 return 0; // ignored
332}
333
334// deprecated method, not used anymore. TODO KF6: REMOVE
335QByteArray KPasswdServer::queryAuthInfo(const QByteArray &data, const QString &errorMsg, qlonglong windowId, qlonglong seqNr, qlonglong usertime)
336{
337 KIO::AuthInfo info;
338 QDataStream stream(data);
339 stream >> info;
340
341 qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId << "seqNr =" << seqNr << ", errorMsg =" << errorMsg;
342
343 if (!info.password.isEmpty()) { // should we really allow the caller to pre-fill the password?
344 qCDebug(category) << "password was set by caller";
345 }
346#if HAVE_X11
347 if (usertime != 0) {
348 KUserTimestamp::updateUserTimestamp(time: usertime);
349 }
350#endif
351
352 const QString key(createCacheKey(info));
353 Request *request = new Request;
354 setDelayedReply(true);
355 request->isAsync = false;
356 request->transaction = message();
357 request->key = key;
358 request->info = info;
359 request->windowId = windowId;
360 request->seqNr = seqNr;
361 if (errorMsg == QLatin1String("<NoAuthPrompt>")) {
362 request->errorMsg.clear();
363 request->prompt = false;
364 } else {
365 request->errorMsg = errorMsg;
366 request->prompt = true;
367 }
368 m_authPending.append(t: request);
369
370 if (m_authPending.count() == 1) {
371 QTimer::singleShot(interval: 0, receiver: this, slot: &KPasswdServer::processRequest);
372 }
373
374 return QByteArray(); // return value is going to be ignored
375}
376
377qlonglong KPasswdServer::queryAuthInfoAsync(const KIO::AuthInfo &info, const QString &errorMsg, qlonglong windowId, qlonglong seqNr, qlonglong usertime)
378{
379 qCDebug(category) << "User =" << info.username << ", WindowId =" << windowId << "seqNr =" << seqNr << ", errorMsg =" << errorMsg;
380
381 if (!info.password.isEmpty()) {
382 qCDebug(category) << "password was set by caller";
383 }
384#if HAVE_X11
385 if (usertime != 0) {
386 KUserTimestamp::updateUserTimestamp(time: usertime);
387 }
388#endif
389
390 const QString key(createCacheKey(info));
391 Request *request = new Request;
392 request->isAsync = true;
393 request->requestId = getRequestId();
394 request->key = key;
395 request->info = info;
396 request->windowId = windowId;
397 request->seqNr = seqNr;
398 if (errorMsg == QLatin1String("<NoAuthPrompt>")) {
399 request->errorMsg.clear();
400 request->prompt = false;
401 } else {
402 request->errorMsg = errorMsg;
403 request->prompt = true;
404 }
405 m_authPending.append(t: request);
406
407 if (m_authPending.count() == 1) {
408 QTimer::singleShot(interval: 0, receiver: this, slot: &KPasswdServer::processRequest);
409 }
410
411 return request->requestId;
412}
413
414void KPasswdServer::addAuthInfo(const KIO::AuthInfo &info, qlonglong windowId)
415{
416 qCDebug(category) << "User =" << info.username << ", Realm =" << info.realmValue << ", WindowId =" << windowId;
417 if (!info.keepPassword) {
418 qWarning() << "This KIO worker is caching a password in KWallet even though the user didn't ask for it!";
419 }
420 const QString key(createCacheKey(info));
421
422 m_seqNr++;
423
424#ifdef HAVE_KF6WALLET
425 if (!m_walletDisabled && openWallet(windowId) && storeInWallet(m_wallet, key, info)) {
426 // Since storing the password in the wallet succeeded, make sure the
427 // password information is stored in memory only for the duration the
428 // windows associated with it are still around.
429 KIO::AuthInfo authToken(info);
430 authToken.keepPassword = false;
431 addAuthInfoItem(key, authToken, windowId, m_seqNr, false);
432 return;
433 }
434#endif
435
436 addAuthInfoItem(key, info, windowId, seqNr: m_seqNr, canceled: false);
437}
438
439// deprecated method, not used anymore. TODO KF6: REMOVE
440void KPasswdServer::addAuthInfo(const QByteArray &data, qlonglong windowId)
441{
442 KIO::AuthInfo info;
443 QDataStream stream(data);
444 stream >> info;
445 addAuthInfo(info, windowId);
446}
447
448void KPasswdServer::removeAuthInfo(const QString &host, const QString &protocol, const QString &user)
449{
450 qCDebug(category) << protocol << host << user;
451
452 QHashIterator<QString, AuthInfoContainerList *> dictIterator(m_authDict);
453 while (dictIterator.hasNext()) {
454 dictIterator.next();
455
456 const AuthInfoContainerList *authList = dictIterator.value();
457 if (!authList) {
458 continue;
459 }
460
461 for (const AuthInfoContainer &current : *authList) {
462 qCDebug(category) << "Evaluating: " << current.info.url.scheme() << current.info.url.host() << current.info.username;
463 if (current.info.url.scheme() == protocol && current.info.url.host() == host && (current.info.username == user || user.isEmpty())) {
464 qCDebug(category) << "Removing this entry";
465 removeAuthInfoItem(key: dictIterator.key(), info: current.info); // warning, this can modify m_authDict!
466 }
467 }
468 }
469}
470
471#ifdef HAVE_KF6WALLET
472bool KPasswdServer::openWallet(qlonglong windowId)
473{
474 if (m_wallet && !m_wallet->isOpen()) { // forced closed
475 delete m_wallet;
476 m_wallet = nullptr;
477 }
478 if (!m_wallet) {
479 m_wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), static_cast<WId>(windowId));
480 }
481 return m_wallet != nullptr;
482}
483#endif
484
485void KPasswdServer::processRequest()
486{
487 if (m_authPending.isEmpty()) {
488 return;
489 }
490
491 std::unique_ptr<Request> request(m_authPending.takeFirst());
492
493 // Prevent multiple prompts originating from the same window or the same
494 // key (server address).
495 const QString windowIdStr = QString::number(request->windowId);
496 if (m_authPrompted.contains(str: windowIdStr) || m_authPrompted.contains(str: request->key)) {
497 m_authPending.prepend(t: request.release()); // put it back.
498 return;
499 }
500
501 m_authPrompted.append(t: windowIdStr);
502 m_authPrompted.append(t: request->key);
503
504 KIO::AuthInfo &info = request->info;
505
506 // NOTE: If info.username is empty and info.url.userName() is not, set
507 // info.username to info.url.userName() to ensure proper caching. See
508 // note passwordDialogDone.
509 if (info.username.isEmpty() && !info.url.userName().isEmpty()) {
510 info.username = info.url.userName();
511 }
512 const bool bypassCacheAndKWallet = info.getExtraField(fieldName: QString::fromLatin1(ba: s_bypassCacheAndKwallet)).toBool();
513
514 const AuthInfoContainer *result = findAuthInfoItem(key: request->key, info: request->info);
515 qCDebug(category) << "key=" << request->key << ", user=" << info.username << "seqNr: request=" << request->seqNr
516 << ", result=" << (result ? result->seqNr : -1);
517
518 if (!bypassCacheAndKWallet && result && (request->seqNr < result->seqNr)) {
519 qCDebug(category) << "auto retry!";
520 if (result->isCanceled) {
521 info.setModified(false);
522 } else {
523 updateAuthExpire(key: request->key, result, windowId: request->windowId, keep: false);
524 copyAuthInfo(result, info);
525 }
526 } else {
527 m_seqNr++;
528 if (result && !request->errorMsg.isEmpty()) {
529 const QString prompt = request->errorMsg.trimmed() + QLatin1Char('\n') + i18n("Do you want to retry?");
530
531 KMessageDialog *dlg = new KMessageDialog(KMessageDialog::WarningContinueCancel, prompt, nullptr);
532 dlg->setAttribute(Qt::WA_DeleteOnClose);
533 dlg->setWindowTitle(i18n("Retry Authentication"));
534 dlg->setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-password")));
535 dlg->setObjectName(QStringLiteral("warningOKCancel"));
536 KGuiItem retryButton(i18nc("@action:button filter-continue", "Retry"));
537
538 dlg->setButtons(primaryAction: retryButton);
539
540 connect(sender: dlg, signal: &QDialog::finished, context: this, slot: [this, dlg](int result) {
541 retryDialogDone(result, sender: dlg);
542 });
543
544 dlg->setAttribute(Qt::WA_NativeWindow, on: true);
545 KWindowSystem::setMainWindow(subwindow: dlg->windowHandle(), mainwindow: request->windowId);
546
547 qCDebug(category) << "Calling open on retry dialog" << dlg;
548 m_authRetryInProgress.insert(key: dlg, value: request.release());
549 dlg->open();
550 return;
551 }
552
553 if (request->prompt) {
554 showPasswordDialog(request: request.release());
555 return;
556 } else {
557 if (!bypassCacheAndKWallet && request->prompt) {
558 addAuthInfoItem(key: request->key, info, windowId: 0, seqNr: m_seqNr, canceled: true);
559 }
560 info.setModified(false);
561 }
562 }
563
564 sendResponse(request: request.get());
565}
566
567QString KPasswdServer::createCacheKey(const KIO::AuthInfo &info)
568{
569 if (!info.url.isValid()) {
570 // Note that a null key will break findAuthInfoItem later on...
571 qCWarning(category) << "createCacheKey: invalid URL " << info.url;
572 return QString();
573 }
574
575 // Generate the basic key sequence.
576 QString key = info.url.scheme();
577 key += QLatin1Char('-');
578 if (!info.url.userName().isEmpty()) {
579 key += info.url.userName() + QLatin1Char('@');
580 }
581 key += info.url.host();
582 int port = info.url.port();
583 if (port) {
584 key += QLatin1Char(':') + QString::number(port);
585 }
586
587 return key;
588}
589
590void KPasswdServer::copyAuthInfo(const AuthInfoContainer *i, KIO::AuthInfo &info)
591{
592 info = i->info;
593 info.setModified(true);
594}
595
596const KPasswdServer::AuthInfoContainer *KPasswdServer::findAuthInfoItem(const QString &key, const KIO::AuthInfo &info)
597{
598 // qCDebug(category) << "key=" << key << ", user=" << info.username;
599
600 AuthInfoContainerList *authList = m_authDict.value(key);
601 if (authList) {
602 QString path2 = info.url.path().left(n: info.url.path().indexOf(c: QLatin1Char('/')) + 1);
603 auto it = authList->begin();
604 while (it != authList->end()) {
605 const AuthInfoContainer &current = (*it);
606 if (current.expire == AuthInfoContainer::expTime && static_cast<qulonglong>(time(timer: nullptr)) > current.expireTime) {
607 it = authList->erase(pos: it);
608 continue;
609 }
610
611 if (info.verifyPath) {
612 const QString path1 = current.directory;
613 if (path2.startsWith(s: path1) && (info.username.isEmpty() || info.username == current.info.username)) {
614 return &current;
615 }
616 } else {
617 if (current.info.realmValue == info.realmValue && (info.username.isEmpty() || info.username == current.info.username)) {
618 return &current; // TODO: Update directory info
619 }
620 }
621
622 ++it;
623 }
624 }
625 return nullptr;
626}
627
628void KPasswdServer::removeAuthInfoItem(const QString &key, const KIO::AuthInfo &info)
629{
630 AuthInfoContainerList *authList = m_authDict.value(key);
631 if (!authList) {
632 return;
633 }
634
635 auto it = authList->begin();
636 while (it != authList->end()) {
637 if ((*it).info.realmValue == info.realmValue) {
638 it = authList->erase(pos: it);
639 } else {
640 ++it;
641 }
642 }
643 if (authList->isEmpty()) {
644 delete m_authDict.take(key);
645 }
646}
647
648void KPasswdServer::addAuthInfoItem(const QString &key, const KIO::AuthInfo &info, qlonglong windowId, qlonglong seqNr, bool canceled)
649{
650 qCDebug(category) << "key=" << key << "window-id=" << windowId << "username=" << info.username << "realm=" << info.realmValue << "seqNr=" << seqNr
651 << "keepPassword?" << info.keepPassword << "canceled?" << canceled;
652 AuthInfoContainerList *authList = m_authDict.value(key);
653 if (!authList) {
654 authList = new AuthInfoContainerList;
655 m_authDict.insert(key, value: authList);
656 }
657 bool found = false;
658 AuthInfoContainer authItem;
659 auto it = authList->begin();
660 while (it != authList->end()) {
661 if ((*it).info.realmValue == info.realmValue) {
662 authItem = (*it);
663 it = authList->erase(pos: it);
664 found = true;
665 break;
666 } else {
667 ++it;
668 }
669 }
670
671 if (!found) {
672 qCDebug(category) << "Creating AuthInfoContainer";
673 authItem.expire = AuthInfoContainer::expTime;
674 }
675
676 authItem.info = info;
677 authItem.directory = info.url.path().left(n: info.url.path().indexOf(c: QLatin1Char('/')) + 1);
678 authItem.seqNr = seqNr;
679 authItem.isCanceled = canceled;
680
681 updateAuthExpire(key, &authItem, windowId, keep: (info.keepPassword && !canceled));
682
683 // Insert into list, keep the list sorted "longest path" first.
684 authList->append(t: authItem);
685 std::sort(first: authList->begin(), last: authList->end(), comp: AuthInfoContainer::Sorter());
686}
687
688void KPasswdServer::updateAuthExpire(const QString &key, const AuthInfoContainer *auth, qlonglong windowId, bool keep)
689{
690 AuthInfoContainer *current = const_cast<AuthInfoContainer *>(auth);
691 Q_ASSERT(current);
692
693 qCDebug(category) << "key=" << key << "expire=" << current->expire << "window-id=" << windowId << "keep=" << keep;
694
695 if (keep && !windowId) {
696 current->expire = AuthInfoContainer::expNever;
697 } else if (windowId && (current->expire != AuthInfoContainer::expNever)) {
698 current->expire = AuthInfoContainer::expWindowClose;
699 if (!current->windowList.contains(t: windowId)) {
700 current->windowList.append(t: windowId);
701 }
702 } else if (current->expire == AuthInfoContainer::expTime) {
703 current->expireTime = time(timer: nullptr) + 10;
704 }
705
706 // Update mWindowIdList
707 if (windowId) {
708 QStringList &keysChanged = mWindowIdList[windowId]; // find or insert
709 if (!keysChanged.contains(str: key)) {
710 keysChanged.append(t: key);
711 }
712 }
713}
714
715void KPasswdServer::removeAuthForWindowId(qlonglong windowId)
716{
717 const QStringList keysChanged = mWindowIdList.value(key: windowId);
718 for (const QString &key : keysChanged) {
719 AuthInfoContainerList *authList = m_authDict.value(key);
720 if (!authList) {
721 continue;
722 }
723
724 QMutableListIterator<AuthInfoContainer> it(*authList);
725 while (it.hasNext()) {
726 AuthInfoContainer &current = it.next();
727 if (current.expire == AuthInfoContainer::expWindowClose) {
728 if (current.windowList.removeAll(t: windowId) && current.windowList.isEmpty()) {
729 it.remove();
730 }
731 }
732 }
733 }
734}
735
736void KPasswdServer::showPasswordDialog(KPasswdServer::Request *request)
737{
738 KIO::AuthInfo &info = request->info;
739 QString username = info.username;
740 QString password = info.password;
741 bool hasWalletData = false;
742 QMap<QString, QString> knownLogins;
743
744#ifdef HAVE_KF6WALLET
745 const bool bypassCacheAndKWallet = info.getExtraField(QString::fromLatin1(s_bypassCacheAndKwallet)).toBool();
746 if (!bypassCacheAndKWallet && (username.isEmpty() || password.isEmpty()) && !m_walletDisabled
747 && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(),
748 KWallet::Wallet::PasswordFolder(),
749 makeWalletKey(request->key, info.realmValue))) {
750 // no login+pass provided, check if kwallet has one
751 if (openWallet(request->windowId)) {
752 hasWalletData = readFromWallet(m_wallet, request->key, info.realmValue, username, password, info.readOnly, knownLogins);
753 }
754 }
755#endif
756
757 // assemble dialog-flags
758 KPasswordDialog::KPasswordDialogFlags dialogFlags;
759
760 if (info.getExtraField(fieldName: QString::fromLatin1(ba: s_domain)).isValid()) {
761 dialogFlags |= KPasswordDialog::ShowDomainLine;
762 if (info.getExtraFieldFlags(fieldName: QString::fromLatin1(ba: s_domain)) & KIO::AuthInfo::ExtraFieldReadOnly) {
763 dialogFlags |= KPasswordDialog::DomainReadOnly;
764 }
765 }
766
767 if (info.getExtraField(fieldName: QString::fromLatin1(ba: s_anonymous)).isValid()) {
768 dialogFlags |= KPasswordDialog::ShowAnonymousLoginCheckBox;
769 }
770
771 if (!info.getExtraField(fieldName: QString::fromLatin1(ba: s_hideUsernameInput)).toBool()) {
772 dialogFlags |= KPasswordDialog::ShowUsernameLine;
773 }
774
775#ifdef HAVE_KF6WALLET
776 // If wallet is not enabled and the caller explicitly requested for it,
777 // do not show the keep password checkbox.
778 if (info.keepPassword && KWallet::Wallet::isEnabled()) {
779 dialogFlags |= KPasswordDialog::ShowKeepPassword;
780 }
781#endif
782
783 // instantiate dialog
784 qCDebug(category) << "Widget for" << request->windowId << QWidget::find(request->windowId);
785
786 KPasswordDialog *dlg = new KPasswordDialog(nullptr, dialogFlags);
787 dlg->setAttribute(Qt::WA_DeleteOnClose);
788
789 connect(sender: dlg, signal: &QDialog::finished, context: this, slot: [this, dlg](int result) {
790 passwordDialogDone(result, sender: dlg);
791 });
792
793 dlg->setPrompt(info.prompt);
794 dlg->setUsername(username);
795 if (info.caption.isEmpty()) {
796 dlg->setWindowTitle(i18n("Authentication Dialog"));
797 } else {
798 dlg->setWindowTitle(info.caption);
799 }
800
801 if (!info.comment.isEmpty()) {
802 dlg->addCommentLine(label: info.commentLabel, comment: info.comment);
803 }
804
805 if (!password.isEmpty()) {
806 dlg->setPassword(password);
807 }
808
809 if (info.readOnly) {
810 dlg->setUsernameReadOnly(true);
811 } else {
812 dlg->setKnownLogins(knownLogins);
813 }
814
815 if (hasWalletData) {
816 dlg->setKeepPassword(true);
817 }
818
819 if (info.getExtraField(fieldName: QString::fromLatin1(ba: s_domain)).isValid()) {
820 dlg->setDomain(info.getExtraField(fieldName: QString::fromLatin1(ba: s_domain)).toString());
821 }
822
823 if (info.getExtraField(fieldName: QString::fromLatin1(ba: s_anonymous)).isValid() && password.isEmpty() && username.isEmpty()) {
824 dlg->setAnonymousMode(info.getExtraField(fieldName: QString::fromLatin1(ba: s_anonymous)).toBool());
825 }
826
827 const QVariant userContextHelp = info.getExtraField(fieldName: QString::fromLatin1(ba: s_usernameContextHelp));
828 if (userContextHelp.isValid()) {
829 dlg->setUsernameContextHelp(userContextHelp.toString());
830 }
831
832#ifndef Q_OS_MACOS
833 dlg->setAttribute(Qt::WA_NativeWindow, on: true);
834 KWindowSystem::setMainWindow(subwindow: dlg->windowHandle(), mainwindow: request->windowId);
835#endif
836
837 qCDebug(category) << "Showing password dialog" << dlg << ", window-id=" << request->windowId;
838 m_authInProgress.insert(key: dlg, value: request);
839 dlg->open();
840}
841
842void KPasswdServer::sendResponse(KPasswdServer::Request *request)
843{
844 Q_ASSERT(request);
845 if (!request) {
846 return;
847 }
848
849 qCDebug(category) << "key=" << request->key;
850 if (request->isAsync) {
851 Q_EMIT queryAuthInfoAsyncResult(requestId: request->requestId, seqNr: m_seqNr, request->info);
852 } else {
853 QByteArray replyData;
854 QDataStream stream2(&replyData, QIODevice::WriteOnly);
855 stream2 << request->info;
856 QDBusConnection::sessionBus().send(message: request->transaction.createReply(arguments: QVariantList{QVariant(replyData), QVariant(m_seqNr)}));
857 }
858
859 // Check all requests in the wait queue.
860 Request *waitRequest;
861 QMutableListIterator<Request *> it(m_authWait);
862 while (it.hasNext()) {
863 waitRequest = it.next();
864
865 if (!hasPendingQuery(key: waitRequest->key, info: waitRequest->info)) {
866 const AuthInfoContainer *result = findAuthInfoItem(key: waitRequest->key, info: waitRequest->info);
867 QByteArray replyData;
868
869 QDataStream stream2(&replyData, QIODevice::WriteOnly);
870
871 KIO::AuthInfo rcinfo;
872 if (!result || result->isCanceled) {
873 waitRequest->info.setModified(false);
874 stream2 << waitRequest->info;
875 } else {
876 updateAuthExpire(key: waitRequest->key, auth: result, windowId: waitRequest->windowId, keep: false);
877 copyAuthInfo(i: result, info&: rcinfo);
878 stream2 << rcinfo;
879 }
880
881 if (waitRequest->isAsync) {
882 Q_EMIT checkAuthInfoAsyncResult(requestId: waitRequest->requestId, seqNr: m_seqNr, rcinfo);
883 } else {
884 QDBusConnection::sessionBus().send(message: waitRequest->transaction.createReply(arguments: QVariantList{QVariant(replyData), QVariant(m_seqNr)}));
885 }
886
887 delete waitRequest;
888 it.remove();
889 }
890 }
891
892 // Re-enable password request processing for the current window id again.
893 m_authPrompted.removeAll(t: QString::number(request->windowId));
894 m_authPrompted.removeAll(t: request->key);
895
896 if (!m_authPending.isEmpty()) {
897 QTimer::singleShot(interval: 0, receiver: this, slot: &KPasswdServer::processRequest);
898 }
899}
900
901void KPasswdServer::passwordDialogDone(int result, KPasswordDialog *sender)
902{
903 std::unique_ptr<Request> request(m_authInProgress.take(key: sender));
904 Q_ASSERT(request); // request should never be nullptr.
905
906 if (request) {
907 KIO::AuthInfo &info = request->info;
908 const bool bypassCacheAndKWallet = info.getExtraField(fieldName: QString::fromLatin1(ba: s_bypassCacheAndKwallet)).toBool();
909
910 qCDebug(category) << "dialog result=" << result << ", bypassCacheAndKWallet?" << bypassCacheAndKWallet;
911 if (sender && result == QDialog::Accepted) {
912 info.username = sender->username();
913 info.password = sender->password();
914 info.keepPassword = sender->keepPassword();
915
916 if (info.getExtraField(fieldName: QString::fromLatin1(ba: s_domain)).isValid()) {
917 info.setExtraField(fieldName: QString::fromLatin1(ba: s_domain), value: sender->domain());
918 }
919 if (info.getExtraField(fieldName: QString::fromLatin1(ba: s_anonymous)).isValid()) {
920 info.setExtraField(fieldName: QString::fromLatin1(ba: s_anonymous), value: sender->anonymousMode());
921 }
922
923 // When the user checks "keep password", that means:
924 // * if the wallet is enabled, store it there for long-term, and in kpasswdserver
925 // only for the duration of the window (#92928)
926 // * otherwise store in kpasswdserver for the duration of the KDE session.
927 if (!bypassCacheAndKWallet) {
928 /*
929 NOTE: The following code changes the key under which the auth
930 info is stored in memory if the request url contains a username.
931 e.g. "ftp://user@localhost", but the user changes that username
932 in the password dialog.
933
934 Since the key generated to store the credential contains the
935 username from the request URL, the key must be updated on such
936 changes. Otherwise, the key will not be found on subsequent
937 requests and the user will be end up being prompted over and
938 over to re-enter the password unnecessarily.
939 */
940 if (!info.url.userName().isEmpty() && info.username != info.url.userName()) {
941 const QString oldKey(request->key);
942 removeAuthInfoItem(key: oldKey, info);
943 info.url.setUserName(userName: info.username);
944 request->key = createCacheKey(info);
945 updateCachedRequestKey(m_authPending, oldKey, newKey: request->key);
946 updateCachedRequestKey(m_authWait, oldKey, newKey: request->key);
947 }
948
949#ifdef HAVE_KF6WALLET
950 const bool skipAutoCaching = info.getExtraField(QString::fromLatin1(s_skipCachingOnQuery)).toBool();
951 if (!skipAutoCaching && info.keepPassword && openWallet(request->windowId)) {
952 if (storeInWallet(m_wallet, request->key, info)) {
953 // password is in wallet, don't keep it in memory after window is closed
954 info.keepPassword = false;
955 }
956 }
957#endif
958 addAuthInfoItem(key: request->key, info, windowId: request->windowId, seqNr: m_seqNr, canceled: false);
959 }
960 info.setModified(true);
961 } else {
962 if (!bypassCacheAndKWallet && request->prompt) {
963 addAuthInfoItem(key: request->key, info, windowId: 0, seqNr: m_seqNr, canceled: true);
964 }
965 info.setModified(false);
966 }
967
968 sendResponse(request: request.get());
969 }
970}
971
972void KPasswdServer::retryDialogDone(int result, KMessageDialog *sender)
973{
974 std::unique_ptr<Request> request(m_authRetryInProgress.take(key: sender));
975 Q_ASSERT(request);
976
977 if (request) {
978 if (result == KMessageDialog::PrimaryAction) {
979 showPasswordDialog(request: request.release());
980 } else {
981 // NOTE: If the user simply cancels the retry dialog, we remove the
982 // credential stored under this key because the original attempt to
983 // use it has failed. Otherwise, the failed credential would be cached
984 // and used subsequently.
985 //
986 // TODO: decide whether it should be removed from the wallet too.
987 KIO::AuthInfo &info = request->info;
988 removeAuthInfoItem(key: request->key, info: request->info);
989 info.setModified(false);
990 sendResponse(request: request.get());
991 }
992 }
993}
994
995void KPasswdServer::windowRemoved(WId id)
996{
997 bool foundMatch = false;
998 if (!m_authInProgress.isEmpty()) {
999 const qlonglong windowId = static_cast<qlonglong>(id);
1000 QMutableHashIterator<QObject *, Request *> it(m_authInProgress);
1001 while (it.hasNext()) {
1002 it.next();
1003 if (it.value()->windowId == windowId) {
1004 Request *request = it.value();
1005 QObject *obj = it.key();
1006 it.remove();
1007 m_authPrompted.removeAll(t: QString::number(request->windowId));
1008 m_authPrompted.removeAll(t: request->key);
1009 delete obj;
1010 delete request;
1011 foundMatch = true;
1012 }
1013 }
1014 }
1015
1016 if (!foundMatch && !m_authRetryInProgress.isEmpty()) {
1017 const qlonglong windowId = static_cast<qlonglong>(id);
1018 QMutableHashIterator<QObject *, Request *> it(m_authRetryInProgress);
1019 while (it.hasNext()) {
1020 it.next();
1021 if (it.value()->windowId == windowId) {
1022 Request *request = it.value();
1023 QObject *obj = it.key();
1024 it.remove();
1025 delete obj;
1026 delete request;
1027 }
1028 }
1029 }
1030}
1031
1032void KPasswdServer::updateCachedRequestKey(QList<KPasswdServer::Request *> &list, const QString &oldKey, const QString &newKey)
1033{
1034 QListIterator<Request *> it(list);
1035 while (it.hasNext()) {
1036 Request *r = it.next();
1037 if (r->key == oldKey) {
1038 r->key = newKey;
1039 }
1040 }
1041}
1042
1043#include "moc_kpasswdserver.cpp"
1044

source code of kio/src/kpasswdserver/kpasswdserver.cpp