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

Create new UrlTools class #9935

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2018 KeePassXC Team <[email protected]>
# Copyright (C) 2023 KeePassXC Team <[email protected]>
# Copyright (C) 2010 Felix Geyer <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -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
Expand Down
70 changes: 2 additions & 68 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(".");
Expand Down Expand Up @@ -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;
}

Expand All @@ -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<Database> BrowserService::getDatabase()
Expand Down
4 changes: 0 additions & 4 deletions src/browser/BrowserService.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -146,16 +145,13 @@ private slots:
Group* getDefaultEntryGroup(const QSharedPointer<Database>& 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);
bool handleURL(const QString& entryUrl,
const QString& siteUrl,
const QString& formUrl,
const bool omitWwwSubdomain = false);
QString getTopLevelDomainFromUrl(const QString& url) const;
QString baseDomain(const QString& hostname) const;
QSharedPointer<Database> getDatabase();
QSharedPointer<Database> selectedDatabase();
QString getDatabaseRootUuid();
Expand Down
31 changes: 1 addition & 30 deletions src/core/Tools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Copyright (C) 2020 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected],
* author Giuseppe D'Angelo <[email protected]>
* Copyright (C) 2021 The Qt Company Ltd.
* Copyright (C) 2021 KeePassXC Team <[email protected]>
* Copyright (C) 2023 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 Down Expand Up @@ -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 <[email protected]>.
Expand Down
3 changes: 1 addition & 2 deletions src/core/Tools.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2012 Felix Geyer <[email protected]>
* Copyright (C) 2021 KeePassXC Team <[email protected]>
* Copyright (C) 2023 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 Down Expand Up @@ -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);
Expand Down
173 changes: 173 additions & 0 deletions src/core/UrlTools.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright (C) 2023 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
* 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 <http://www.gnu.org/licenses/>.
*/

#include "UrlTools.h"
#ifdef WITH_XC_NETWORKING
#include <QHostAddress>
#include <QNetworkCookie>
#include <QNetworkCookieJar>
#endif
#include <QRegularExpression>
#include <QUrl>

Q_GLOBAL_STATIC(UrlTools, s_urlTools)

UrlTools* UrlTools::instance()
{
return s_urlTools;
}

QUrl UrlTools::convertVariantToUrl(const QVariant& var) const
{
QUrl url;
if (var.canConvert<QUrl>()) {
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;
}
Loading