Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for URL wildcards and exact URL #9835

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 67 additions & 6 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww")
// Multiple URL's
const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");

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

Q_GLOBAL_STATIC(BrowserService, s_browserService);

BrowserService::BrowserService()
Expand Down Expand Up @@ -562,8 +564,9 @@ bool BrowserService::isUrlIdentical(const QString& first, const QString& second)
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("*", BrowserService::URL_WILDCARD));
const auto secondUrl = trimUrl(QString(second).replace("*", BrowserService::URL_WILDCARD));
if (firstUrl == secondUrl) {
return true;
}
Expand Down Expand Up @@ -1142,11 +1145,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("*", BrowserService::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 @@ -1170,7 +1186,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 @@ -1186,6 +1202,11 @@ bool BrowserService::handleURL(const QString& entryUrl,
return false;
}

// Use wildcard matching instead
if (isWildcardUrl) {
return handleURLWithWildcards(entryQUrl, siteUrl);
}

// Match the base domain
if (getTopLevelDomainFromUrl(siteQUrl.host()) != getTopLevelDomainFromUrl(entryQUrl.host())) {
return false;
Expand All @@ -1197,7 +1218,47 @@ 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(BrowserService::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(BrowserService::URL_WILDCARD), "(.*?)");
}

// Append a + to the end of regex to match all paths after the last asterisk
if (re.endsWith(BrowserService::URL_WILDCARD)) {
re.append("+");
}

// Replace any remaining wildcards for paths
re = re.replace(BrowserService::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;
}

/**
* Gets the base domain of URL.
Expand Down
5 changes: 4 additions & 1 deletion src/browser/BrowserService.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class BrowserService : public QObject
static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW;
static const QString ADDITIONAL_URL;
static const QString URL_WILDCARD;

signals:
void requestUnlock();
Expand Down Expand Up @@ -130,7 +131,8 @@ private slots:
Hidden
};

QList<Entry*> searchEntries(const QSharedPointer<Database>& db, const QString& siteUrl, const QString& formUrl);
QList<Entry*>
searchEntries(const QSharedPointer<Database>& db, const QString& siteUrl, const QString& formUrl = {});
QList<Entry*> searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList);
QList<Entry*> sortEntries(QList<Entry*>& entries, const QString& siteUrl, const QString& formUrl);
QList<Entry*> confirmEntries(QList<Entry*>& entriesToConfirm,
Expand All @@ -154,6 +156,7 @@ private slots:
const QString& siteUrl,
const QString& formUrl,
const bool omitWwwSubdomain = false);
bool handleURLWithWildcards(const QUrl& entryQUrl, const QString& siteUrl);
QString getTopLevelDomainFromUrl(const QString& url) const;
QString baseDomain(const QString& hostname) const;
QSharedPointer<Database> getDatabase();
Expand Down
16 changes: 10 additions & 6 deletions src/core/Tools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,24 +274,28 @@ namespace Tools
bool checkUrlValid(const QString& urlField)
{
if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive)
|| urlField.startsWith("kdbx://", Qt::CaseInsensitive)
|| urlField.startsWith("{REF:A", Qt::CaseInsensitive)) {
|| urlField.startsWith("kdbx://", Qt::CaseInsensitive) || urlField.startsWith("{REF:A", Qt::CaseInsensitive)
|| (urlField.startsWith("\"") && urlField.endsWith("\""))) {
return true;
}

// Replace wildcards
auto tempUrl = urlField;
tempUrl.replace("*", "1kpxcwc1");

QUrl url;
if (urlField.contains("://")) {
url = urlField;
url = tempUrl;
} else {
url = QUrl::fromUserInput(urlField);
url = QUrl::fromUserInput(tempUrl);
}

if (url.scheme() != "file" && url.host().isEmpty()) {
return false;
}

// Check for illegal characters. Adds also the wildcard * to the list
QRegularExpression re("[<>\\^`{|}\\*]");
// Check for illegal characters
QRegularExpression re("[<>\\^`{|}]");
auto match = re.match(urlField);
if (match.hasMatch()) {
return false;
Expand Down
128 changes: 113 additions & 15 deletions tests/TestBrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,35 +249,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 @@ -446,6 +449,101 @@ 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");
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");
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");
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");
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");
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");
QCOMPARE(result.length(), 1);
QCOMPARE(result[0]->url(), QString("https://example.com/*/*"));

result = m_browserService->searchEntries(db, "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/");
QCOMPARE(result.length(), 2);

result = m_browserService->searchEntries(db, "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/");
QCOMPARE(result.length(), 0);

result = m_browserService->searchEntries(db, "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");

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 Expand Up @@ -588,8 +686,8 @@ void TestBrowser::testValidURLs()
QHash<QString, bool> urls;
urls["https://github.com/login"] = true;
urls["https:///github.com/"] = false;
urls["http://github.com/**//*"] = false;
urls["http://*.github.com/login"] = false;
urls["http://github.com/**//*"] = true;
urls["http://*.github.com/login"] = true;
urls["//github.com"] = true;
urls["github.com/{}<>"] = false;
urls["http:/example.com"] = false;
Expand Down
1 change: 1 addition & 0 deletions tests/TestBrowser.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private slots:
void testSearchEntriesByReference();
void testSearchEntriesWithPort();
void testSearchEntriesWithAdditionalURLs();
void testSearchEntriesWithWildcardURLs();
void testInvalidEntries();
void testSubdomainsAndPaths();
void testValidURLs();
Expand Down