Fix URL matching

This commit is contained in:
varjolintu 2019-11-01 20:13:12 +02:00 committed by Jonathan White
parent 4437e6a609
commit 3d0964bce9
4 changed files with 174 additions and 49 deletions

View File

@ -18,6 +18,7 @@
*/ */
#include <QCheckBox> #include <QCheckBox>
#include <QHostAddress>
#include <QInputDialog> #include <QInputDialog>
#include <QJsonArray> #include <QJsonArray>
#include <QMessageBox> #include <QMessageBox>
@ -600,17 +601,20 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
continue; continue;
} }
auto domain = baseDomain(hostname);
// Search for additional URL's starting with KP2A_URL // Search for additional URL's starting with KP2A_URL
if (entry->attributes()->keys().contains(ADDITIONAL_URL)) { if (entry->attributes()->keys().contains(ADDITIONAL_URL)) {
for (const auto& key : entry->attributes()->keys()) { for (const auto& key : entry->attributes()->keys()) {
if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), hostname, url)) { if (key.startsWith(ADDITIONAL_URL)
&& handleURL(entry->attributes()->value(key), domain, url)) {
entries.append(entry); entries.append(entry);
continue; continue;
} }
} }
} }
if (!handleURL(entry->url(), hostname, url)) { if (!handleURL(entry->url(), domain, url)) {
continue; continue;
} }
@ -928,12 +932,15 @@ int BrowserService::sortPriority(const Entry* entry,
{ {
QUrl url(entry->url()); QUrl url(entry->url());
if (url.scheme().isEmpty()) { if (url.scheme().isEmpty()) {
url.setScheme("http"); url.setScheme("https");
} }
const QString entryURL = url.toString(QUrl::StripTrailingSlash); const QString entryURL = url.toString(QUrl::StripTrailingSlash);
const QString baseEntryURL = const QString baseEntryURL =
url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment); url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment);
if (!url.host().contains(".") && url.host() != "localhost") {
return 0;
}
if (submitUrl == entryURL) { if (submitUrl == entryURL) {
return 100; return 100;
} }
@ -970,7 +977,7 @@ int BrowserService::sortPriority(const Entry* entry,
return 0; return 0;
} }
bool BrowserService::matchUrlScheme(const QString& url) bool BrowserService::schemeFound(const QString& url)
{ {
QUrl address(url); QUrl address(url);
return !address.scheme().isEmpty(); return !address.scheme().isEmpty();
@ -995,21 +1002,49 @@ bool BrowserService::removeFirstDomain(QString& hostname)
bool BrowserService::handleURL(const QString& entryUrl, const QString& hostname, const QString& url) bool BrowserService::handleURL(const QString& entryUrl, const QString& hostname, const QString& url)
{ {
QUrl entryQUrl(entryUrl); if (entryUrl.isEmpty()) {
QString entryScheme = entryQUrl.scheme(); return false;
QUrl qUrl(url); }
// Ignore entry if port or scheme defined in the URL doesn't match QUrl entryQUrl;
if ((entryQUrl.port() > 0 && entryQUrl.port() != qUrl.port()) if (entryUrl.contains("://")) {
|| (browserSettings()->matchUrlScheme() && !entryScheme.isEmpty() && entryScheme.compare(qUrl.scheme()) != 0)) { entryQUrl = entryUrl;
} else {
entryQUrl = QUrl::fromUserInput(entryUrl);
if (browserSettings()->matchUrlScheme()) {
entryQUrl.setScheme("https");
}
}
// URL host validation fails
if (browserSettings()->matchUrlScheme() && entryQUrl.host().isEmpty()) {
return false;
}
// Match port, if used
QUrl qUrl(url);
if (entryQUrl.port() > 0 && entryQUrl.port() != qUrl.port()) {
return false;
}
// Match scheme
if (browserSettings()->matchUrlScheme() && !entryQUrl.scheme().isEmpty() && entryQUrl.scheme().compare(qUrl.scheme()) != 0) {
return false;
}
// Check for illegal characters
QRegularExpression re("[<>\\^`{|}]");
auto match = re.match(entryUrl);
if (match.hasMatch()) {
return false; return false;
} }
// Filter to match hostname in URL field // Filter to match hostname in URL field
if ((!entryUrl.isEmpty() && hostname.contains(entryUrl)) if (entryQUrl.host().endsWith(hostname)) {
|| (matchUrlScheme(entryUrl) && hostname.endsWith(entryQUrl.host()))) {
return true; return true;
} }
return false; return false;
}; };
@ -1018,19 +1053,25 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& hostname,
* *
* Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk * Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk
*/ */
QString BrowserService::baseDomain(const QString& url) const QString BrowserService::baseDomain(const QString& hostname) const
{ {
QUrl qurl = QUrl::fromUserInput(url); QUrl qurl = QUrl::fromUserInput(hostname);
QString hostname = qurl.host(); QString host = qurl.host();
if (hostname.isEmpty() || !hostname.contains(qurl.topLevelDomain())) { // If the hostname is an IP address, return it directly
QHostAddress hostAddress(hostname);
if (!hostAddress.isNull()) {
return hostname;
}
if (host.isEmpty() || !host.contains(qurl.topLevelDomain())) {
return {}; return {};
} }
// Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example // Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example
hostname.chop(qurl.topLevelDomain().length()); host.chop(qurl.topLevelDomain().length());
// Split the URL and select the last part, e.g. https://another.example -> example // Split the URL and select the last part, e.g. https://another.example -> example
QString baseDomain = hostname.split('.').last(); QString baseDomain = host.split('.').last();
// Append the top level domain back to the URL, e.g. example -> example.co.uk // Append the top level domain back to the URL, e.g. example -> example.co.uk
baseDomain.append(qurl.topLevelDomain()); baseDomain.append(qurl.topLevelDomain());
return baseDomain; return baseDomain;
@ -1070,7 +1111,7 @@ QSharedPointer<Database> BrowserService::selectedDatabase()
if (res == QDialog::Accepted) { if (res == QDialog::Accepted) {
const auto selectedDatabase = browserEntrySaveDialog.getSelected(); const auto selectedDatabase = browserEntrySaveDialog.getSelected();
if (selectedDatabase.length() > 0) { if (selectedDatabase.length() > 0) {
int index = selectedDatabase[0]->data(Qt::UserRole).toUInt(); int index = selectedDatabase[0]->data(Qt::UserRole).toInt();
return databaseWidgets[index]->database(); return databaseWidgets[index]->database();
} }
} else { } else {
@ -1188,7 +1229,7 @@ void BrowserService::raiseWindow(const bool force)
m_prevWindowState = WindowState::Minimized; m_prevWindowState = WindowState::Minimized;
} }
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
Q_UNUSED(force); Q_UNUSED(force)
if (macUtils()->isHidden()) { if (macUtils()->isHidden()) {
m_prevWindowState = WindowState::Hidden; m_prevWindowState = WindowState::Hidden;

View File

@ -128,10 +128,10 @@ private:
Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {}); Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {});
int int
sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const; sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const;
bool matchUrlScheme(const QString& url); bool schemeFound(const QString& url);
bool removeFirstDomain(QString& hostname); bool removeFirstDomain(QString& hostname);
bool handleURL(const QString& entryUrl, const QString& hostname, const QString& url); bool handleURL(const QString& entryUrl, const QString& hostname, const QString& url);
QString baseDomain(const QString& url) const; QString baseDomain(const QString& hostname) const;
QSharedPointer<Database> getDatabase(); QSharedPointer<Database> getDatabase();
QSharedPointer<Database> selectedDatabase(); QSharedPointer<Database> selectedDatabase();
QJsonArray getChildrenFromGroup(Group* group); QJsonArray getChildrenFromGroup(Group* group);

View File

@ -147,7 +147,7 @@ void TestBrowser::testSortPriority()
entry6->setUrl("http://github.com/login"); entry6->setUrl("http://github.com/login");
entry7->setUrl("github.com"); entry7->setUrl("github.com");
entry8->setUrl("github.com/login"); entry8->setUrl("github.com/login");
entry9->setUrl("https://github"); entry9->setUrl("https://github"); // Invalid URL
entry10->setUrl("github.com"); entry10->setUrl("github.com");
// The extension uses the submitUrl as default for comparison // The extension uses the submitUrl as default for comparison
@ -170,7 +170,7 @@ void TestBrowser::testSortPriority()
QCOMPARE(res6, 0); QCOMPARE(res6, 0);
QCOMPARE(res7, 0); QCOMPARE(res7, 0);
QCOMPARE(res8, 0); QCOMPARE(res8, 0);
QCOMPARE(res9, 90); QCOMPARE(res9, 0);
QCOMPARE(res10, 0); QCOMPARE(res10, 0);
} }
@ -188,7 +188,7 @@ void TestBrowser::testSearchEntries()
urls.push_back("http://github.com/login"); urls.push_back("http://github.com/login");
urls.push_back("github.com"); urls.push_back("github.com");
urls.push_back("github.com/login"); urls.push_back("github.com/login");
urls.push_back("https://github"); urls.push_back("https://github"); // Invalid URL
urls.push_back("github.com"); urls.push_back("github.com");
for (int i = 0; i < urls.length(); ++i) { for (int i = 0; i < urls.length(); ++i) {
@ -202,24 +202,23 @@ void TestBrowser::testSearchEntries()
browserSettings()->setMatchUrlScheme(false); browserSettings()->setMatchUrlScheme(false);
auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
QCOMPARE(result.length(), 9);
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
browserSettings()->setMatchUrlScheme(true);
result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
QCOMPARE(result.length(), 7); QCOMPARE(result.length(), 7);
QCOMPARE(result[0]->url(), QString("https://github.com/login_page")); QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
QCOMPARE(result[1]->url(), QString("https://github.com/login")); QCOMPARE(result[1]->url(), QString("https://github.com/login"));
QCOMPARE(result[2]->url(), QString("https://github.com/")); QCOMPARE(result[2]->url(), QString("https://github.com/"));
QCOMPARE(result[3]->url(), QString("http://github.com")); QCOMPARE(result[3]->url(), QString("github.com/login"));
QCOMPARE(result[4]->url(), QString("http://github.com/login"));
QCOMPARE(result[5]->url(), QString("github.com"));
QCOMPARE(result[6]->url(), QString("github.com"));
// With matching there should be only 5 results
browserSettings()->setMatchUrlScheme(true);
result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
QCOMPARE(result.length(), 5);
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"));
QCOMPARE(result[4]->url(), QString("github.com"));
} }
void TestBrowser::testSearchEntriesWithPort() void TestBrowser::testSearchEntriesWithPort()
@ -242,7 +241,7 @@ void TestBrowser::testSearchEntriesWithPort()
auto result = m_browserService->searchEntries(db, "127.0.0.1", "http://127.0.0.1:443"); // db, hostname, url auto result = m_browserService->searchEntries(db, "127.0.0.1", "http://127.0.0.1:443"); // db, hostname, url
QCOMPARE(result.length(), 1); QCOMPARE(result.length(), 1);
QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443")); QCOMPARE(result[0]->url(), urls[0]);
} }
void TestBrowser::testSearchEntriesWithAdditionalURLs() void TestBrowser::testSearchEntriesWithAdditionalURLs()
@ -271,12 +270,94 @@ void TestBrowser::testSearchEntriesWithAdditionalURLs()
auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
QCOMPARE(result.length(), 1); QCOMPARE(result.length(), 1);
QCOMPARE(result[0]->url(), QString("https://github.com/")); QCOMPARE(result[0]->url(), urls[0]);
// Search the additional URL. It should return the same entry // Search the additional URL. It should return the same entry
auto additionalResult = m_browserService->searchEntries(db, "keepassxc.org", "https://keepassxc.org"); auto additionalResult = m_browserService->searchEntries(db, "keepassxc.org", "https://keepassxc.org");
QCOMPARE(additionalResult.length(), 1); QCOMPARE(additionalResult.length(), 1);
QCOMPARE(additionalResult[0]->url(), QString("https://github.com/")); QCOMPARE(additionalResult[0]->url(), urls[0]);
}
void TestBrowser::testInvalidEntries()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
QList<QString> urls;
urls.push_back("https://github.com/login");
urls.push_back("https:///github.com/"); // Extra '/'
urls.push_back("http://github.com/**//*");
urls.push_back("http://*.github.com/login");
urls.push_back("//github.com"); // fromUserInput() corrects this one.
urls.push_back("github.com/{}<>");
for (int i = 0; i < urls.length(); ++i) {
auto entry = new Entry();
entry->setGroup(root);
entry->beginUpdate();
entry->setUrl(urls[i]);
entry->setUsername(QString("User %1").arg(i));
entry->endUpdate();
}
browserSettings()->setMatchUrlScheme(true);
auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
QCOMPARE(result.length(), 2);
QCOMPARE(result[0]->url(), QString("https://github.com/login"));
QCOMPARE(result[1]->url(), QString("//github.com"));
// Test the URL's directly
QCOMPARE(m_browserService->handleURL(urls[0], "github.com", "https://github.com"), true);
QCOMPARE(m_browserService->handleURL(urls[1], "github.com", "https://github.com"), false);
QCOMPARE(m_browserService->handleURL(urls[2], "github.com", "https://github.com"), false);
QCOMPARE(m_browserService->handleURL(urls[3], "github.com", "https://github.com"), false);
QCOMPARE(m_browserService->handleURL(urls[4], "github.com", "https://github.com"), true);
QCOMPARE(m_browserService->handleURL(urls[5], "github.com", "https://github.com"), false);
}
void TestBrowser::testSubdomainsAndPaths()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
QList<QString> urls;
urls.push_back("https://www.github.com/login/page.xml");
urls.push_back("https://login.github.com/");
urls.push_back("https://github.com");
urls.push_back("http://www.github.com");
urls.push_back("http://login.github.com/pathtonowhere");
urls.push_back(".github.com"); // Invalid URL
urls.push_back("www.github.com/");
urls.push_back("https://github"); // Invalid URL
for (int i = 0; i < urls.length(); ++i) {
auto entry = new Entry();
entry->setGroup(root);
entry->beginUpdate();
entry->setUrl(urls[i]);
entry->setUsername(QString("User %1").arg(i));
entry->endUpdate();
}
browserSettings()->setMatchUrlScheme(false);
auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
QCOMPARE(result.length(), 6);
QCOMPARE(result[0]->url(), urls[0]);
QCOMPARE(result[1]->url(), urls[1]);
QCOMPARE(result[2]->url(), urls[2]);
QCOMPARE(result[3]->url(), urls[3]);
QCOMPARE(result[4]->url(), urls[4]);
QCOMPARE(result[5]->url(), urls[6]);
// With matching there should be only 3 results
browserSettings()->setMatchUrlScheme(true);
result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
QCOMPARE(result.length(), 4);
QCOMPARE(result[0]->url(), urls[0]);
QCOMPARE(result[1]->url(), urls[1]);
QCOMPARE(result[2]->url(), urls[2]);
QCOMPARE(result[3]->url(), urls[6]);
} }
void TestBrowser::testSortEntries() void TestBrowser::testSortEntries()
@ -293,7 +374,7 @@ void TestBrowser::testSortEntries()
urls.push_back("http://github.com/login"); urls.push_back("http://github.com/login");
urls.push_back("github.com"); urls.push_back("github.com");
urls.push_back("github.com/login"); urls.push_back("github.com/login");
urls.push_back("https://github"); urls.push_back("https://github"); // Invalid URL
urls.push_back("github.com"); urls.push_back("github.com");
QList<Entry*> entries; QList<Entry*> entries;
@ -313,12 +394,13 @@ void TestBrowser::testSortEntries()
QCOMPARE(result.size(), 10); QCOMPARE(result.size(), 10);
QCOMPARE(result[0]->username(), QString("User 2")); QCOMPARE(result[0]->username(), QString("User 2"));
QCOMPARE(result[0]->url(), QString("https://github.com/")); QCOMPARE(result[0]->url(), QString("https://github.com/"));
QCOMPARE(result[1]->username(), QString("User 8")); QCOMPARE(result[1]->username(), QString("User 0"));
QCOMPARE(result[1]->url(), QString("https://github")); QCOMPARE(result[1]->url(), QString("https://github.com/login_page"));
QCOMPARE(result[2]->username(), QString("User 0")); QCOMPARE(result[2]->username(), QString("User 1"));
QCOMPARE(result[2]->url(), QString("https://github.com/login_page")); QCOMPARE(result[2]->url(), QString("https://github.com/login"));
QCOMPARE(result[3]->username(), QString("User 1")); QCOMPARE(result[3]->username(), QString("User 3"));
QCOMPARE(result[3]->url(), QString("https://github.com/login")); QCOMPARE(result[3]->url(), QString("github.com/login"));
} }
void TestBrowser::testGetDatabaseGroups() void TestBrowser::testGetDatabaseGroups()

View File

@ -43,6 +43,8 @@ private slots:
void testSearchEntries(); void testSearchEntries();
void testSearchEntriesWithPort(); void testSearchEntriesWithPort();
void testSearchEntriesWithAdditionalURLs(); void testSearchEntriesWithAdditionalURLs();
void testInvalidEntries();
void testSubdomainsAndPaths();
void testSortEntries(); void testSortEntries();
void testGetDatabaseGroups(); void testGetDatabaseGroups();