From 9ab03a5718a2e94e1854c4ecaada87063661959e Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sat, 9 Sep 2023 10:52:38 +0300 Subject: [PATCH] Add support for URL wildcards --- src/browser/BrowserService.cpp | 64 +++++++++++++++- src/browser/BrowserService.h | 2 + src/core/UrlTools.cpp | 9 ++- src/core/UrlTools.h | 2 + tests/TestBrowser.cpp | 132 +++++++++++++++++++++++++++++---- tests/TestBrowser.h | 1 + 6 files changed, 191 insertions(+), 19 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index cbdf2e2010..37ffc83a9a 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -1439,11 +1439,24 @@ bool BrowserService::handleURL(const QString& entryUrl, return false; } + // Exact match where URL is wrapped inside " characters + if (entryUrl.startsWith("\"") && entryUrl.endsWith("\"") && entryUrl.midRef(1, entryUrl.length() - 2) == siteUrl) { + return true; + } + + const auto isWildcardUrl = entryUrl.contains("*"); + + // Replace wildcards + auto tempUrl = entryUrl; + if (isWildcardUrl) { + tempUrl = tempUrl.replace("*", UrlTools::URL_WILDCARD); + } + QUrl entryQUrl; if (entryUrl.contains("://")) { - entryQUrl = entryUrl; + entryQUrl = tempUrl; } else { - entryQUrl = QUrl::fromUserInput(entryUrl); + entryQUrl = QUrl::fromUserInput(tempUrl); if (browserSettings()->matchUrlScheme()) { entryQUrl.setScheme("https"); @@ -1467,7 +1480,7 @@ bool BrowserService::handleURL(const QString& entryUrl, // Match port, if used QUrl siteQUrl(siteUrl); - if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) { + if ((entryQUrl.port() > 0) && entryQUrl.port() != siteQUrl.port()) { return false; } @@ -1483,6 +1496,11 @@ bool BrowserService::handleURL(const QString& entryUrl, return false; } + // Use wildcard matching instead + if (isWildcardUrl) { + return handleURLWithWildcards(entryQUrl, siteUrl); + } + // Match the base domain if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) { return false; @@ -1496,6 +1514,46 @@ bool BrowserService::handleURL(const QString& entryUrl, return false; } +bool BrowserService::handleURLWithWildcards(const QUrl& entryQUrl, const QString& siteUrl) +{ + auto matchWithRegex = [&](QString firstPart, const QString& secondPart, bool hostnameUsed = false) { + if (firstPart == secondPart) { + return true; + } + + // If there's no wildcard with hostname, just compare directly + if (hostnameUsed && !firstPart.contains(UrlTools::URL_WILDCARD) && firstPart != secondPart) { + return false; + } + + // Escape illegal characters + auto re = firstPart.replace(QRegularExpression(R"(([!\^\$\+\-\(\)@<>]))"), "\\\\1"); + + if (hostnameUsed) { + // Replace all host parts with wildcards + re = re.replace(QString("%1.").arg(UrlTools::URL_WILDCARD), "(.*?)"); + } + + // Append a + to the end of regex to match all paths after the last asterisk + if (re.endsWith(UrlTools::URL_WILDCARD)) { + re.append("+"); + } + + // Replace any remaining wildcards for paths + re = re.replace(UrlTools::URL_WILDCARD, "(.*?)"); + return QRegularExpression(re).match(secondPart).hasMatch(); + }; + + // Match hostname and path + QUrl siteQUrl = siteUrl; + if (!matchWithRegex(entryQUrl.host(), siteQUrl.host(), true) + || !matchWithRegex(entryQUrl.path(), siteQUrl.path())) { + return false; + } + + return true; +} + QSharedPointer BrowserService::getDatabase(const QUuid& rootGroupUuid) { if (!rootGroupUuid.isNull()) { diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 943e8bac1e..c786ba98c3 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -130,6 +130,7 @@ class BrowserService : public QObject static const QString OPTION_ONLY_HTTP_AUTH; static const QString OPTION_NOT_HTTP_AUTH; static const QString OPTION_OMIT_WWW; + static const QString ADDITIONAL_URL; static const QString OPTION_RESTRICT_KEY; signals: @@ -197,6 +198,7 @@ private slots: const QString& siteUrl, const QString& formUrl, const bool omitWwwSubdomain = false); + bool handleURLWithWildcards(const QUrl& entryQUrl, const QString& siteUrl); QString getDatabaseRootUuid(); QString getDatabaseRecycleBinUuid(); void hideWindow() const; diff --git a/src/core/UrlTools.cpp b/src/core/UrlTools.cpp index 508bbefdaa..f9c7218265 100644 --- a/src/core/UrlTools.cpp +++ b/src/core/UrlTools.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 KeePassXC Team + * Copyright (C) 2024 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +24,8 @@ #include #include +const QString UrlTools::URL_WILDCARD = "1kpxcwc1"; + Q_GLOBAL_STATIC(UrlTools, s_urlTools) UrlTools* UrlTools::instance() @@ -137,8 +139,9 @@ bool UrlTools::isUrlIdentical(const QString& first, const QString& second) const return false; } - const auto firstUrl = trimUrl(first); - const auto secondUrl = trimUrl(second); + // Replace URL wildcards for comparison if found + const auto firstUrl = trimUrl(QString(first).replace("*", UrlTools::URL_WILDCARD)); + const auto secondUrl = trimUrl(QString(second).replace("*", UrlTools::URL_WILDCARD)); if (firstUrl == secondUrl) { return true; } diff --git a/src/core/UrlTools.h b/src/core/UrlTools.h index 9a229e39f6..4ae74fc163 100644 --- a/src/core/UrlTools.h +++ b/src/core/UrlTools.h @@ -42,6 +42,8 @@ class UrlTools : public QObject bool isUrlValid(const QString& urlField) const; bool domainHasIllegalCharacters(const QString& domain) const; + static const QString URL_WILDCARD; + private: QUrl convertVariantToUrl(const QVariant& var) const; diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index 09bd94cfed..2e9ec03012 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -201,16 +201,19 @@ void TestBrowser::testSearchEntries() auto db = QSharedPointer::create(); auto* root = db->rootGroup(); - QStringList urls = {"https://github.com/login_page", - "https://github.com/login", - "https://github.com/", - "github.com/login", - "http://github.com", - "http://github.com/login", - "github.com", - "github.com/login", - "https://github", // Invalid URL - "github.com"}; + QStringList urls = { + "https://github.com/login_page", + "https://github.com/login", + "https://github.com/", + "github.com/login", + "http://github.com", + "http://github.com/login", + "github.com", + "github.com/login", + "https://github", // Invalid URL + "github.com", + "\"https://github.com\"" // Exact URL + }; createEntries(urls, root); @@ -218,7 +221,7 @@ void TestBrowser::testSearchEntries() auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); // db, url, submitUrl - QCOMPARE(result.length(), 9); + QCOMPARE(result.length(), 10); QCOMPARE(result[0]->url(), QString("https://github.com/login_page")); QCOMPARE(result[1]->url(), QString("https://github.com/login")); QCOMPARE(result[2]->url(), QString("https://github.com/")); @@ -226,10 +229,10 @@ void TestBrowser::testSearchEntries() QCOMPARE(result[4]->url(), QString("http://github.com")); QCOMPARE(result[5]->url(), QString("http://github.com/login")); - // With matching there should be only 3 results + 4 without a scheme + // With matching there should be only 4 results + 4 without a scheme browserSettings()->setMatchUrlScheme(true); result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); - QCOMPARE(result.length(), 7); + QCOMPARE(result.length(), 8); QCOMPARE(result[0]->url(), QString("https://github.com/login_page")); QCOMPARE(result[1]->url(), QString("https://github.com/login")); QCOMPARE(result[2]->url(), QString("https://github.com/")); @@ -400,6 +403,109 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs() QCOMPARE(additionalResult[0]->url(), QString("https://github.com/")); } +void TestBrowser::testSearchEntriesWithWildcardURLs() +{ + auto db = QSharedPointer::create(); + auto* root = db->rootGroup(); + + QStringList urls = {"https://github.com/login_page/*", + "https://github.com/*/second", + "https://github.com/*", + "http://github.com/*", + "github.com/*", // Defaults to https + "https://*.github.com/*", + "https://subdomain.*.github.com/*/second", + "https://*.sub.github.com/*", + "https://********", // Invalid wildcard URL + "https://subdomain.yes.github.com/*", + "https://example.com:8448/*", + "https://example.com/*/*", + "https://example.com/$/*", + "https://127.128.129.*:8448/", + "https://127.128.*/", + "https://127.160.*.2/login", + "http://[2001:db8:85a3:8d3:1319:8a2e:370:*]/", + "https://[2001:db8:85a3:8d3:*]:443/", + "fe80::1ff:fe23:4567:890a", + "2001-db8-85a3-8d3-1319-8a2e-370-7348.ipv6-literal.net"}; + + createEntries(urls, root); + browserSettings()->setMatchUrlScheme(false); + + auto result = m_browserService->searchEntries( + db, "https://github.com/login_page/second", "https://github.com/login_page/second"); + QCOMPARE(result.length(), 6); + QCOMPARE(result[0]->url(), QString("https://github.com/login_page/*")); + QCOMPARE(result[1]->url(), QString("https://github.com/*/second")); + QCOMPARE(result[2]->url(), QString("https://github.com/*")); + QCOMPARE(result[3]->url(), QString("http://github.com/*")); + QCOMPARE(result[4]->url(), QString("github.com/*")); + QCOMPARE(result[5]->url(), QString("https://*.github.com/*")); + + result = m_browserService->searchEntries( + db, "https://subdomain.sub.github.com/login_page/second", "https://subdomain.sub.github.com/login_page/second"); + QCOMPARE(result.length(), 3); + QCOMPARE(result[0]->url(), QString("https://*.github.com/*")); + QCOMPARE(result[1]->url(), QString("https://subdomain.*.github.com/*/second")); + QCOMPARE(result[2]->url(), QString("https://*.sub.github.com/*")); + + result = m_browserService->searchEntries( + db, "https://subdomain.sub.github.com/other_page", "https://subdomain.sub.github.com/other_page"); + QCOMPARE(result.length(), 2); + QCOMPARE(result[0]->url(), QString("https://*.github.com/*")); + QCOMPARE(result[1]->url(), QString("https://*.sub.github.com/*")); + + result = m_browserService->searchEntries( + db, "https://subdomain.yes.github.com/other_page/second", "https://subdomain.yes.github.com/other_page/second"); + QCOMPARE(result.length(), 3); + QCOMPARE(result[0]->url(), QString("https://*.github.com/*")); + QCOMPARE(result[1]->url(), QString("https://subdomain.*.github.com/*/second")); + QCOMPARE(result[2]->url(), QString("https://subdomain.yes.github.com/*")); + + result = m_browserService->searchEntries( + db, "https://example.com:8448/login/page", "https://example.com:8448/login/page"); + QCOMPARE(result.length(), 2); + QCOMPARE(result[0]->url(), QString("https://example.com:8448/*")); + QCOMPARE(result[1]->url(), QString("https://example.com/*/*")); + + result = m_browserService->searchEntries( + db, "https://example.com:8449/login/page", "https://example.com:8449/login/page"); + QCOMPARE(result.length(), 1); + QCOMPARE(result[0]->url(), QString("https://example.com/*/*")); + + result = + m_browserService->searchEntries(db, "https://example.com/$/login_page", "https://example.com/$/login_page"); + QCOMPARE(result.length(), 2); + QCOMPARE(result[0]->url(), QString("https://example.com/*/*")); + QCOMPARE(result[1]->url(), QString("https://example.com/$/*")); + + result = m_browserService->searchEntries(db, "https://127.128.129.130:8448/", "https://127.128.129.130:8448/"); + QCOMPARE(result.length(), 2); + + result = m_browserService->searchEntries(db, "https://127.128.129.130/", "https://127.128.129.130/"); + QCOMPARE(result.length(), 1); + QCOMPARE(result[0]->url(), QString("https://127.128.*/")); + + result = m_browserService->searchEntries(db, "https://127.1.129.130/", "https://127.1.129.130/"); + QCOMPARE(result.length(), 0); + + result = m_browserService->searchEntries(db, "https://127.160.8.2/login", "https://127.160.8.2/login"); + QCOMPARE(result.length(), 1); + QCOMPARE(result[0]->url(), QString("https://127.160.*.2/login")); + + // With scheme matching enabled + browserSettings()->setMatchUrlScheme(true); + result = m_browserService->searchEntries( + db, "https://github.com/login_page/second", "https://github.com/login_page/second"); + + QCOMPARE(result.length(), 5); + QCOMPARE(result[0]->url(), QString("https://github.com/login_page/*")); + QCOMPARE(result[1]->url(), QString("https://github.com/*/second")); + QCOMPARE(result[2]->url(), QString("https://github.com/*")); + QCOMPARE(result[3]->url(), QString("github.com/*")); // Defaults to https + QCOMPARE(result[4]->url(), QString("https://*.github.com/*")); +} + void TestBrowser::testInvalidEntries() { auto db = QSharedPointer::create(); diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h index 6b53a577d9..7ecfc8d1bc 100644 --- a/tests/TestBrowser.h +++ b/tests/TestBrowser.h @@ -45,6 +45,7 @@ private slots: void testSearchEntriesByReference(); void testSearchEntriesWithPort(); void testSearchEntriesWithAdditionalURLs(); + void testSearchEntriesWithWildcardURLs(); void testInvalidEntries(); void testSubdomainsAndPaths(); void testBestMatchingCredentials();