Skip to content

Commit

Permalink
Add support for URL wildcards
Browse files Browse the repository at this point in the history
  • Loading branch information
varjolintu committed Jun 22, 2024
1 parent 198889c commit 229704d
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 31 deletions.
64 changes: 61 additions & 3 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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;
}

Expand All @@ -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;
Expand All @@ -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<Database> BrowserService::getDatabase(const QUuid& rootGroupUuid)
{
if (!rootGroupUuid.isNull()) {
Expand Down
2 changes: 2 additions & 0 deletions src/browser/BrowserService.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions src/core/UrlTools.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 KeePassXC Team <[email protected]>
* Copyright (C) 2024 KeePassXC Team <[email protected]>
*
* 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
Expand All @@ -24,6 +24,8 @@
#include <QRegularExpression>
#include <QUrl>

const QString UrlTools::URL_WILDCARD = "1kpxcwc1";

Q_GLOBAL_STATIC(UrlTools, s_urlTools)

UrlTools* UrlTools::instance()
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/UrlTools.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
152 changes: 127 additions & 25 deletions tests/TestBrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,26 +170,22 @@ void TestBrowser::testSortPriority_data()
QTest::newRow("Exact Match") << siteUrl << siteUrl << siteUrl << 100;
QTest::newRow("Exact Match (site)") << siteUrl << siteUrl << formUrl << 100;
QTest::newRow("Exact Match (form)") << siteUrl << "https://github.net" << siteUrl << 100;
QTest::newRow("Exact Match No Trailing Slash") << "https://github.com"
<< "https://github.com/" << formUrl << 100;
QTest::newRow("Exact Match No Trailing Slash") << "https://github.com" << "https://github.com/" << formUrl << 100;
QTest::newRow("Exact Match No Scheme") << "github.com/login" << siteUrl << formUrl << 100;
QTest::newRow("Exact Match with Query") << "https://github.com/login?test=test#fragment"
<< "https://github.com/login?test=test" << formUrl << 100;
QTest::newRow("Exact Match with Query")
<< "https://github.com/login?test=test#fragment" << "https://github.com/login?test=test" << formUrl << 100;

QTest::newRow("Site Query Mismatch") << siteUrl << siteUrl + "?test=test" << formUrl << 90;

QTest::newRow("Path Mismatch (site)") << "https://github.com/" << siteUrl << formUrl << 85;
QTest::newRow("Path Mismatch (site) No Scheme") << "github.com" << siteUrl << formUrl << 85;
QTest::newRow("Path Mismatch (form)") << "https://github.com/"
<< "https://github.net" << formUrl << 85;
QTest::newRow("Path Mismatch (form)") << "https://github.com/" << "https://github.net" << formUrl << 85;
QTest::newRow("Path Mismatch (diff parent)") << "https://github.com/keepassxreboot" << siteUrl << formUrl << 80;
QTest::newRow("Path Mismatch (diff parent, form)") << "https://github.com/keepassxreboot"
<< "https://github.net" << formUrl << 70;
QTest::newRow("Path Mismatch (diff parent, form)")
<< "https://github.com/keepassxreboot" << "https://github.net" << formUrl << 70;

QTest::newRow("Subdomain Mismatch (site)") << siteUrl << "https://sub.github.com/"
<< "https://github.net/" << 60;
QTest::newRow("Subdomain Mismatch (form)") << siteUrl << "https://github.net/"
<< "https://sub.github.com/" << 50;
QTest::newRow("Subdomain Mismatch (site)") << siteUrl << "https://sub.github.com/" << "https://github.net/" << 60;
QTest::newRow("Subdomain Mismatch (form)") << siteUrl << "https://github.net/" << "https://sub.github.com/" << 50;

QTest::newRow("Scheme Mismatch") << "http://github.com" << siteUrl << formUrl << 0;
QTest::newRow("Scheme Mismatch w/path") << "http://github.com/login" << siteUrl << formUrl << 0;
Expand All @@ -201,35 +197,38 @@ void TestBrowser::testSearchEntries()
auto db = QSharedPointer<Database>::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);

browserSettings()->setMatchUrlScheme(false);
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/"));
QCOMPARE(result[3]->url(), QString("github.com/login"));
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/"));
Expand Down Expand Up @@ -400,6 +399,109 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs()
QCOMPARE(additionalResult[0]->url(), QString("https://github.com/"));
}

void TestBrowser::testSearchEntriesWithWildcardURLs()
{
auto db = QSharedPointer<Database>::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<Database>::create();
Expand Down
1 change: 1 addition & 0 deletions tests/TestBrowser.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ private slots:
void testSearchEntriesByReference();
void testSearchEntriesWithPort();
void testSearchEntriesWithAdditionalURLs();
void testSearchEntriesWithWildcardURLs();
void testInvalidEntries();
void testSubdomainsAndPaths();
void testBestMatchingCredentials();
Expand Down

0 comments on commit 229704d

Please sign in to comment.