From 402abe431f13d1989a09ba5dabf19ebe81b69364 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Sat, 14 Oct 2023 16:18:27 +0300 Subject: [PATCH] Create new UrlTools class --- src/CMakeLists.txt | 3 +- src/browser/BrowserService.cpp | 70 +------------ src/browser/BrowserService.h | 4 - src/core/Tools.cpp | 31 +----- src/core/Tools.h | 3 +- src/core/UrlTools.cpp | 173 ++++++++++++++++++++++++++++++++ src/core/UrlTools.h | 56 +++++++++++ src/gui/IconDownloader.cpp | 38 +------ src/gui/URLEdit.cpp | 5 +- src/gui/entry/EntryURLModel.cpp | 8 +- tests/CMakeLists.txt | 2 + tests/TestBrowser.cpp | 84 ---------------- tests/TestBrowser.h | 4 - tests/TestUrlTools.cpp | 129 ++++++++++++++++++++++++ tests/TestUrlTools.h | 41 ++++++++ 15 files changed, 418 insertions(+), 233 deletions(-) create mode 100644 src/core/UrlTools.cpp create mode 100644 src/core/UrlTools.h create mode 100644 tests/TestUrlTools.cpp create mode 100644 tests/TestUrlTools.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b8099eed59..056df57863 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (C) 2018 KeePassXC Team +# Copyright (C) 2023 KeePassXC Team # Copyright (C) 2010 Felix Geyer # # This program is free software: you can redistribute it and/or modify @@ -60,6 +60,7 @@ set(keepassx_SOURCES core/TimeInfo.cpp core/Tools.cpp core/Translator.cpp + core/UrlTools.cpp cli/Utils.cpp cli/TextStream.cpp crypto/Crypto.cpp diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index b412409a59..1ebf79f3ff 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -25,6 +25,7 @@ #include "BrowserMessageBuilder.h" #include "BrowserSettings.h" #include "core/Tools.h" +#include "core/UrlTools.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" #include "gui/osutils/OSUtils.h" @@ -544,33 +545,6 @@ bool BrowserService::isPasswordGeneratorRequested() const return m_passwordGeneratorRequested; } -// Returns true if URLs are identical. Paths with "/" are removed during comparison. -// URLs without scheme reverts to https. -// Special handling is needed because QUrl::matches() with QUrl::StripTrailingSlash does not strip "/" paths. -bool BrowserService::isUrlIdentical(const QString& first, const QString& second) const -{ - auto trimUrl = [](QString url) { - url = url.trimmed(); - if (url.endsWith("/")) { - url.remove(url.length() - 1, 1); - } - - return url; - }; - - if (first.isEmpty() || second.isEmpty()) { - return false; - } - - const auto firstUrl = trimUrl(first); - const auto secondUrl = trimUrl(second); - if (firstUrl == secondUrl) { - return true; - } - - return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash); -} - QString BrowserService::storeKey(const QString& key) { auto db = getDatabase(); @@ -1080,18 +1054,6 @@ int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl return *std::max_element(priorityList.begin(), priorityList.end()); } -bool BrowserService::schemeFound(const QString& url) -{ - QUrl address(url); - return !address.scheme().isEmpty(); -} - -bool BrowserService::isIpAddress(const QString& host) const -{ - QHostAddress address(host); - return address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol; -} - bool BrowserService::removeFirstDomain(QString& hostname) { int pos = hostname.indexOf("."); @@ -1187,7 +1149,7 @@ bool BrowserService::handleURL(const QString& entryUrl, } // Match the base domain - if (getTopLevelDomainFromUrl(siteQUrl.host()) != getTopLevelDomainFromUrl(entryQUrl.host())) { + if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) { return false; } @@ -1197,34 +1159,6 @@ bool BrowserService::handleURL(const QString& entryUrl, } return false; -}; - -/** - * Gets the base domain of URL. - * - * Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk - */ -QString BrowserService::getTopLevelDomainFromUrl(const QString& url) const -{ - QUrl qurl = QUrl::fromUserInput(url); - QString host = qurl.host(); - - // If the hostname is an IP address, return it directly - if (isIpAddress(host)) { - return host; - } - - if (host.isEmpty() || !host.contains(qurl.topLevelDomain())) { - return {}; - } - - // Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example - host.chop(qurl.topLevelDomain().length()); - // Split the URL and select the last part, e.g. https://another.example -> example - QString baseDomain = host.split('.').last(); - // Append the top level domain back to the URL, e.g. example -> example.co.uk - baseDomain.append(qurl.topLevelDomain()); - return baseDomain; } QSharedPointer BrowserService::getDatabase() diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index 46bffef014..ca3579e024 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -82,7 +82,6 @@ class BrowserService : public QObject QString getCurrentTotp(const QString& uuid); void showPasswordGenerator(const KeyPairMessage& keyPairMessage); bool isPasswordGeneratorRequested() const; - bool isUrlIdentical(const QString& first, const QString& second) const; void addEntry(const EntryParameters& entryParameters, const QString& group, @@ -146,7 +145,6 @@ private slots: Group* getDefaultEntryGroup(const QSharedPointer& selectedDb = {}); int sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl); bool schemeFound(const QString& url); - bool isIpAddress(const QString& host) const; bool removeFirstDomain(QString& hostname); bool shouldIncludeEntry(Entry* entry, const QString& url, const QString& submitUrl, const bool omitWwwSubdomain = false); @@ -154,8 +152,6 @@ private slots: const QString& siteUrl, const QString& formUrl, const bool omitWwwSubdomain = false); - QString getTopLevelDomainFromUrl(const QString& url) const; - QString baseDomain(const QString& hostname) const; QSharedPointer getDatabase(); QSharedPointer selectedDatabase(); QString getDatabaseRootUuid(); diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 6577971169..824f9ff924 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -5,7 +5,7 @@ * Copyright (C) 2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, * author Giuseppe D'Angelo * Copyright (C) 2021 The Qt Company Ltd. - * Copyright (C) 2021 KeePassXC Team + * Copyright (C) 2023 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 @@ -271,35 +271,6 @@ 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)) { - return true; - } - - QUrl url; - if (urlField.contains("://")) { - url = urlField; - } else { - url = QUrl::fromUserInput(urlField); - } - - if (url.scheme() != "file" && url.host().isEmpty()) { - return false; - } - - // Check for illegal characters. Adds also the wildcard * to the list - QRegularExpression re("[<>\\^`{|}\\*]"); - auto match = re.match(urlField); - if (match.hasMatch()) { - return false; - } - - return true; - } - /**************************************************************************** * * Copyright (C) 2020 Giuseppe D'Angelo . diff --git a/src/core/Tools.h b/src/core/Tools.h index a8094d0a30..3df2ca0085 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -1,6 +1,6 @@ /* * Copyright (C) 2012 Felix Geyer - * Copyright (C) 2021 KeePassXC Team + * Copyright (C) 2023 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 @@ -38,7 +38,6 @@ namespace Tools bool isBase64(const QByteArray& ba); void sleep(int ms); void wait(int ms); - bool checkUrlValid(const QString& urlField); QString uuidToHex(const QUuid& uuid); QUuid hexToUuid(const QString& uuid); bool isValidUuid(const QString& uuidStr); diff --git a/src/core/UrlTools.cpp b/src/core/UrlTools.cpp new file mode 100644 index 0000000000..bd6db52718 --- /dev/null +++ b/src/core/UrlTools.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 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 + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "UrlTools.h" +#ifdef WITH_XC_NETWORKING +#include +#include +#include +#endif +#include +#include + +Q_GLOBAL_STATIC(UrlTools, s_urlTools) + +UrlTools* UrlTools::instance() +{ + return s_urlTools; +} + +QUrl UrlTools::convertVariantToUrl(const QVariant& var) const +{ + QUrl url; + if (var.canConvert()) { + url = var.toUrl(); + } + return url; +} + +#ifdef WITH_XC_NETWORKING +QUrl UrlTools::getRedirectTarget(QNetworkReply* reply) const +{ + QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + QUrl url = convertVariantToUrl(var); + return url; +} + +/** + * Gets the base domain of URL or hostname. + * + * Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk + * Up-to-date list can be found: https://publicsuffix.org/list/public_suffix_list.dat + */ +QString UrlTools::getBaseDomainFromUrl(const QString& url) const +{ + auto qUrl = QUrl::fromUserInput(url); + + auto host = qUrl.host(); + if (isIpAddress(host)) { + return host; + } + + const auto tld = getTopLevelDomainFromUrl(qUrl.toString()); + if (tld.isEmpty() || tld.length() + 1 >= host.length()) { + return host; + } + + // Remove the top level domain part from the hostname, e.g. https://another.example.co.uk -> https://another.example + host.chop(tld.length() + 1); + // Split the URL and select the last part, e.g. https://another.example -> example + QString baseDomain = host.split('.').last(); + // Append the top level domain back to the URL, e.g. example -> example.co.uk + baseDomain.append(QString(".%1").arg(tld)); + + return baseDomain; +} + +/** + * Gets the top level domain from URL. + * + * Returns the TLD e.g. https://another.example.co.uk -> co.uk + */ +QString UrlTools::getTopLevelDomainFromUrl(const QString& url) const +{ + auto host = QUrl::fromUserInput(url).host(); + if (isIpAddress(host)) { + return host; + } + + const auto numberOfDomainParts = host.split('.').length(); + static const auto dummy = QByteArrayLiteral(""); + + // Only loop the amount of different parts found + for (auto i = 0; i < numberOfDomainParts; ++i) { + // Cut the first part from host + host = host.mid(host.indexOf('.') + 1); + + QNetworkCookie cookie(dummy, dummy); + cookie.setDomain(host); + + // Check if dummy cookie's domain/TLD matches with public suffix list + if (!QNetworkCookieJar{}.setCookiesFromUrl(QList{cookie}, url)) { + return host; + } + } + + return host; +} + +bool UrlTools::isIpAddress(const QString& host) const +{ + QHostAddress address(host); + return address.protocol() == QAbstractSocket::IPv4Protocol || address.protocol() == QAbstractSocket::IPv6Protocol; +} +#endif + +// Returns true if URLs are identical. Paths with "/" are removed during comparison. +// URLs without scheme reverts to https. +// Special handling is needed because QUrl::matches() with QUrl::StripTrailingSlash does not strip "/" paths. +bool UrlTools::isUrlIdentical(const QString& first, const QString& second) const +{ + auto trimUrl = [](QString url) { + url = url.trimmed(); + if (url.endsWith("/")) { + url.remove(url.length() - 1, 1); + } + + return url; + }; + + if (first.isEmpty() || second.isEmpty()) { + return false; + } + + const auto firstUrl = trimUrl(first); + const auto secondUrl = trimUrl(second); + if (firstUrl == secondUrl) { + return true; + } + + return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash); +} + +bool UrlTools::isUrlValid(const QString& urlField) const +{ + if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive) + || urlField.startsWith("kdbx://", Qt::CaseInsensitive) || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) { + return true; + } + + QUrl url; + if (urlField.contains("://")) { + url = urlField; + } else { + url = QUrl::fromUserInput(urlField); + } + + if (url.scheme() != "file" && url.host().isEmpty()) { + return false; + } + + // Check for illegal characters. Adds also the wildcard * to the list + QRegularExpression re("[<>\\^`{|}\\*]"); + auto match = re.match(urlField); + if (match.hasMatch()) { + return false; + } + + return true; +} diff --git a/src/core/UrlTools.h b/src/core/UrlTools.h new file mode 100644 index 0000000000..c86152d038 --- /dev/null +++ b/src/core/UrlTools.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 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 + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_URLTOOLS_H +#define KEEPASSXC_URLTOOLS_H + +#include "config-keepassx.h" +#include +#include +#include +#include + +class UrlTools : public QObject +{ + Q_OBJECT + +public: + explicit UrlTools() = default; + static UrlTools* instance(); + +#ifdef WITH_XC_NETWORKING + QUrl getRedirectTarget(QNetworkReply* reply) const; + QString getBaseDomainFromUrl(const QString& url) const; + QString getTopLevelDomainFromUrl(const QString& url) const; + bool isIpAddress(const QString& host) const; +#endif + bool isUrlIdentical(const QString& first, const QString& second) const; + bool isUrlValid(const QString& urlField) const; + +private: + QUrl convertVariantToUrl(const QVariant& var) const; + +private: + Q_DISABLE_COPY(UrlTools); +}; + +static inline UrlTools* urlTools() +{ + return UrlTools::instance(); +} + +#endif // KEEPASSXC_URLTOOLS_H diff --git a/src/gui/IconDownloader.cpp b/src/gui/IconDownloader.cpp index 7e3fff0aec..1adb269229 100644 --- a/src/gui/IconDownloader.cpp +++ b/src/gui/IconDownloader.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 KeePassXC Team + * Copyright (C) 2023 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 @@ -18,6 +18,7 @@ #include "IconDownloader.h" #include "core/Config.h" #include "core/NetworkManager.h" +#include "core/UrlTools.h" #include #include @@ -40,37 +41,6 @@ IconDownloader::~IconDownloader() abortDownload(); } -namespace -{ - // Try to get the 2nd level domain of the host part of a QUrl. For example, - // "foo.bar.example.com" would become "example.com", and "foo.bar.example.co.uk" - // would become "example.co.uk". - QString getSecondLevelDomain(const QUrl& url) - { - QString fqdn = url.host(); - fqdn.truncate(fqdn.length() - url.topLevelDomain().length()); - QStringList parts = fqdn.split('.'); - QString newdom = parts.takeLast() + url.topLevelDomain(); - return newdom; - } - - QUrl convertVariantToUrl(const QVariant& var) - { - QUrl url; - if (var.canConvert()) { - url = var.toUrl(); - } - return url; - } - - QUrl getRedirectTarget(QNetworkReply* reply) - { - QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - QUrl url = convertVariantToUrl(var); - return url; - } -} // namespace - void IconDownloader::setUrl(const QString& entryUrl) { m_url = entryUrl; @@ -114,7 +84,7 @@ void IconDownloader::setUrl(const QString& entryUrl) // Determine the second-level domain, if available QString secondLevelDomain; if (!hostIsIp) { - secondLevelDomain = getSecondLevelDomain(url); + secondLevelDomain = urlTools()->getBaseDomainFromUrl(url.toString()); } // Start with the "fallback" url (if enabled) to try to get the best favicon @@ -202,7 +172,7 @@ void IconDownloader::fetchFinished() QString url = m_url; bool error = (m_reply->error() != QNetworkReply::NoError); - QUrl redirectTarget = getRedirectTarget(m_reply); + QUrl redirectTarget = urlTools()->getRedirectTarget(m_reply); m_reply->deleteLater(); m_reply = nullptr; diff --git a/src/gui/URLEdit.cpp b/src/gui/URLEdit.cpp index d249ddd850..f5fbbb24be 100644 --- a/src/gui/URLEdit.cpp +++ b/src/gui/URLEdit.cpp @@ -1,6 +1,6 @@ /* + * Copyright (C) 2023 KeePassXC Team * Copyright (C) 2014 Felix Geyer - * Copyright (C) 2020 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 @@ -19,6 +19,7 @@ #include "URLEdit.h" #include "core/Tools.h" +#include "core/UrlTools.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" @@ -44,7 +45,7 @@ void URLEdit::updateStylesheet() { const QString stylesheetTemplate("QLineEdit { background: %1; }"); - if (!Tools::checkUrlValid(text())) { + if (!urlTools()->isUrlValid(text())) { StateColorPalette statePalette; QColor color = statePalette.color(StateColorPalette::ColorRole::Error); setStyleSheet(stylesheetTemplate.arg(color.name())); diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp index 55d8dd51cd..9a4340f5cd 100644 --- a/src/gui/entry/EntryURLModel.cpp +++ b/src/gui/entry/EntryURLModel.cpp @@ -20,7 +20,7 @@ #include "browser/BrowserService.h" #include "core/EntryAttributes.h" -#include "core/Tools.h" +#include "core/UrlTools.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" @@ -67,14 +67,14 @@ QVariant EntryURLModel::data(const QModelIndex& index, int role) const } const auto value = m_entryAttributes->value(key); - const auto urlValid = Tools::checkUrlValid(value); + const auto urlValid = urlTools()->isUrlValid(value); // Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison. auto customAttributeKeys = m_entryAttributes->customKeys().filter(BrowserService::ADDITIONAL_URL); customAttributeKeys.removeOne(key); - const auto duplicateUrl = m_entryAttributes->values(customAttributeKeys).contains(value) - || browserService()->isUrlIdentical(value, m_entryUrl); + const auto duplicateUrl = + m_entryAttributes->values(customAttributeKeys).contains(value) || urlTools()->isUrlIdentical(value, m_entryUrl); if (role == Qt::BackgroundRole && (!urlValid || duplicateUrl)) { StateColorPalette statePalette; return statePalette.color(StateColorPalette::ColorRole::Error); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index db82da1639..1abe869a41 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -150,6 +150,8 @@ if(WITH_XC_NETWORKING) LIBS ${TEST_LIBRARIES}) add_unit_test(NAME testicondownloader SOURCES TestIconDownloader.cpp LIBS ${TEST_LIBRARIES}) + + add_unit_test(NAME testurltools SOURCES TestUrlTools.cpp LIBS ${TEST_LIBRARIES}) endif() if(WITH_XC_AUTOTYPE) diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index d7345537d6..aa084921ed 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -144,54 +144,6 @@ void TestBrowser::testBuildResponse() QCOMPARE(firstArr["test"].toBool(), true); } -/** - * Tests for BrowserService - */ -void TestBrowser::testTopLevelDomain() -{ - QString url1 = "https://another.example.co.uk"; - QString url2 = "https://www.example.com"; - QString url3 = "http://test.net"; - QString url4 = "http://so.many.subdomains.co.jp"; - QString url5 = "https://192.168.0.1"; - QString url6 = "https://192.168.0.1:8000"; - - QString res1 = m_browserService->getTopLevelDomainFromUrl(url1); - QString res2 = m_browserService->getTopLevelDomainFromUrl(url2); - QString res3 = m_browserService->getTopLevelDomainFromUrl(url3); - QString res4 = m_browserService->getTopLevelDomainFromUrl(url4); - QString res5 = m_browserService->getTopLevelDomainFromUrl(url5); - QString res6 = m_browserService->getTopLevelDomainFromUrl(url6); - - QCOMPARE(res1, QString("example.co.uk")); - QCOMPARE(res2, QString("example.com")); - QCOMPARE(res3, QString("test.net")); - QCOMPARE(res4, QString("subdomains.co.jp")); - QCOMPARE(res5, QString("192.168.0.1")); - QCOMPARE(res6, QString("192.168.0.1")); -} - -void TestBrowser::testIsIpAddress() -{ - auto host1 = "example.com"; // Not valid - auto host2 = "192.168.0.1"; - auto host3 = "278.21.2.0"; // Not valid - auto host4 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; - auto host5 = "2001:db8:0:1:1:1:1:1"; - auto host6 = "fe80::1ff:fe23:4567:890a"; - auto host7 = "2001:20::1"; - auto host8 = "2001:0db8:85y3:0000:0000:8a2e:0370:7334"; // Not valid - - QVERIFY(!m_browserService->isIpAddress(host1)); - QVERIFY(m_browserService->isIpAddress(host2)); - QVERIFY(!m_browserService->isIpAddress(host3)); - QVERIFY(m_browserService->isIpAddress(host4)); - QVERIFY(m_browserService->isIpAddress(host5)); - QVERIFY(m_browserService->isIpAddress(host6)); - QVERIFY(m_browserService->isIpAddress(host7)); - QVERIFY(!m_browserService->isIpAddress(host8)); -} - void TestBrowser::testSortPriority() { QFETCH(QString, entryUrl); @@ -583,26 +535,6 @@ QList TestBrowser::createEntries(QStringList& urls, Group* root) const return entries; } -void TestBrowser::testValidURLs() -{ - QHash urls; - urls["https://github.com/login"] = true; - urls["https:///github.com/"] = false; - urls["http://github.com/**//*"] = false; - urls["http://*.github.com/login"] = false; - urls["//github.com"] = true; - urls["github.com/{}<>"] = false; - urls["http:/example.com"] = false; - urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true; - urls["file:///Users/testUser/Code/test.html"] = true; - urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true; - - QHashIterator i(urls); - while (i.hasNext()) { - i.next(); - QCOMPARE(Tools::checkUrlValid(i.key()), i.value()); - } -} void TestBrowser::testBestMatchingCredentials() { @@ -741,19 +673,3 @@ void TestBrowser::testBestMatchingWithAdditionalURLs() QCOMPARE(sorted.length(), 1); QCOMPARE(sorted[0]->url(), urls[0]); } - -void TestBrowser::testIsUrlIdentical() -{ - QVERIFY(browserService()->isUrlIdentical("https://example.com", "https://example.com")); - QVERIFY(browserService()->isUrlIdentical("https://example.com", " https://example.com ")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com", "https://example2.com")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com/", "https://example.com/#login")); - QVERIFY(browserService()->isUrlIdentical("https://example.com", "https://example.com/")); - QVERIFY(browserService()->isUrlIdentical("https://example.com/", "https://example.com")); - QVERIFY(browserService()->isUrlIdentical("https://example.com/ ", " https://example.com")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com/", " example.com")); - QVERIFY(browserService()->isUrlIdentical("https://example.com/path/to/nowhere", - "https://example.com/path/to/nowhere/")); - QVERIFY(!browserService()->isUrlIdentical("https://example.com/", "://example.com/")); - QVERIFY(browserService()->isUrlIdentical("ftp://127.0.0.1/", "ftp://127.0.0.1")); -} diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h index ed8146b574..48ac3b1cd5 100644 --- a/tests/TestBrowser.h +++ b/tests/TestBrowser.h @@ -37,8 +37,6 @@ private slots: void testGetBase64FromKey(); void testIncrementNonce(); void testBuildResponse(); - void testTopLevelDomain(); - void testIsIpAddress(); void testSortPriority(); void testSortPriority_data(); void testSearchEntries(); @@ -49,10 +47,8 @@ private slots: void testSearchEntriesWithAdditionalURLs(); void testInvalidEntries(); void testSubdomainsAndPaths(); - void testValidURLs(); void testBestMatchingCredentials(); void testBestMatchingWithAdditionalURLs(); - void testIsUrlIdentical(); private: QList createEntries(QStringList& urls, Group* root) const; diff --git a/tests/TestUrlTools.cpp b/tests/TestUrlTools.cpp new file mode 100644 index 0000000000..0e3ef844ee --- /dev/null +++ b/tests/TestUrlTools.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 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 + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestUrlTools.h" +#include + +QTEST_GUILESS_MAIN(TestUrlTools) + +void TestUrlTools::initTestCase() +{ + m_urlTools = urlTools(); +} + +void TestUrlTools::init() +{ +} + +void TestUrlTools::testTopLevelDomain() +{ + // Create list of URLs and expected TLD responses + QList> tldUrls{ + {QString("https://another.example.co.uk"), QString("co.uk")}, + {QString("https://www.example.com"), QString("com")}, + {QString("https://github.com"), QString("com")}, + {QString("http://test.net"), QString("net")}, + {QString("http://so.many.subdomains.co.jp"), QString("co.jp")}, + {QString("https://192.168.0.1"), QString("192.168.0.1")}, + {QString("https://192.168.0.1:8000"), QString("192.168.0.1")}, + {QString("https://www.nic.ar"), QString("ar")}, + {QString("https://no.no.no"), QString("no")}, + {QString("https://www.blogspot.com.ar"), QString("blogspot.com.ar")}, // blogspot.com.ar is a TLD + {QString("https://jap.an.ide.kyoto.jp"), QString("ide.kyoto.jp")}, // ide.kyoto.jp is a TLD + {QString("ar"), QString("ar")}, + }; + + for (const auto& u : tldUrls) { + QCOMPARE(urlTools()->getTopLevelDomainFromUrl(u.first), u.second); + } + + // Create list of URLs and expected base URL responses + QList> baseUrls{ + {QString("https://another.example.co.uk"), QString("example.co.uk")}, + {QString("https://www.example.com"), QString("example.com")}, + {QString("http://test.net"), QString("test.net")}, + {QString("http://so.many.subdomains.co.jp"), QString("subdomains.co.jp")}, + {QString("https://192.168.0.1"), QString("192.168.0.1")}, + {QString("https://192.168.0.1:8000"), QString("192.168.0.1")}, + {QString("https://www.nic.ar"), QString("nic.ar")}, + {QString("https://www.blogspot.com.ar"), QString("www.blogspot.com.ar")}, // blogspot.com.ar is a TLD + {QString("https://www.arpa"), QString("www.arpa")}, + {QString("https://jap.an.ide.kyoto.jp"), QString("an.ide.kyoto.jp")}, // ide.kyoto.jp is a TLD + {QString("https://kobe.jp"), QString("kobe.jp")}, + }; + + for (const auto& u : baseUrls) { + QCOMPARE(urlTools()->getBaseDomainFromUrl(u.first), u.second); + } +} + +void TestUrlTools::testIsIpAddress() +{ + auto host1 = "example.com"; // Not valid + auto host2 = "192.168.0.1"; + auto host3 = "278.21.2.0"; // Not valid + auto host4 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + auto host5 = "2001:db8:0:1:1:1:1:1"; + auto host6 = "fe80::1ff:fe23:4567:890a"; + auto host7 = "2001:20::1"; + auto host8 = "2001:0db8:85y3:0000:0000:8a2e:0370:7334"; // Not valid + + QVERIFY(!urlTools()->isIpAddress(host1)); + QVERIFY(urlTools()->isIpAddress(host2)); + QVERIFY(!urlTools()->isIpAddress(host3)); + QVERIFY(urlTools()->isIpAddress(host4)); + QVERIFY(urlTools()->isIpAddress(host5)); + QVERIFY(urlTools()->isIpAddress(host6)); + QVERIFY(urlTools()->isIpAddress(host7)); + QVERIFY(!urlTools()->isIpAddress(host8)); +} + +void TestUrlTools::testIsUrlIdentical() +{ + QVERIFY(urlTools()->isUrlIdentical("https://example.com", "https://example.com")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com", " https://example.com ")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com", "https://example2.com")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com/", "https://example.com/#login")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com", "https://example.com/")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com/", "https://example.com")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com/ ", " https://example.com")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com/", " example.com")); + QVERIFY(urlTools()->isUrlIdentical("https://example.com/path/to/nowhere", "https://example.com/path/to/nowhere/")); + QVERIFY(!urlTools()->isUrlIdentical("https://example.com/", "://example.com/")); + QVERIFY(urlTools()->isUrlIdentical("ftp://127.0.0.1/", "ftp://127.0.0.1")); +} + +void TestUrlTools::testIsUrlValid() +{ + QHash urls; + urls["https://github.com/login"] = true; + urls["https:///github.com/"] = false; + urls["http://github.com/**//*"] = false; + urls["http://*.github.com/login"] = false; + urls["//github.com"] = true; + urls["github.com/{}<>"] = false; + urls["http:/example.com"] = false; + urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true; + urls["file:///Users/testUser/Code/test.html"] = true; + urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true; + + QHashIterator i(urls); + while (i.hasNext()) { + i.next(); + QCOMPARE(urlTools()->isUrlValid(i.key()), i.value()); + } +} diff --git a/tests/TestUrlTools.h b/tests/TestUrlTools.h new file mode 100644 index 0000000000..d26e470406 --- /dev/null +++ b/tests/TestUrlTools.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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 + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_TESTURLTOOLS_H +#define KEEPASSXC_TESTURLTOOLS_H + +#include "core/UrlTools.h" +#include +#include + +class TestUrlTools : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void init(); + + void testTopLevelDomain(); + void testIsIpAddress(); + void testIsUrlIdentical(); + void testIsUrlValid(); + +private: + QPointer m_urlTools; +}; +#endif // KEEPASSXC_TESTURLTOOLS_H