Create new UrlTools class

Includes "Fix ifdefs with UrlTools"
This commit is contained in:
Jonathan White 2024-01-11 11:13:55 -05:00
parent 416581b179
commit 1cbbcff259
15 changed files with 420 additions and 234 deletions

View file

@ -1,4 +1,4 @@
# Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
# Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
# Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
#
# 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

View file

@ -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"
@ -499,33 +500,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();
@ -1290,18 +1264,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(".");
@ -1465,7 +1427,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;
}
@ -1475,34 +1437,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()

View file

@ -83,8 +83,6 @@ public:
QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
bool isPasswordGeneratorRequested() const;
bool isUrlIdentical(const QString& first, const QString& second) const;
QSharedPointer<Database> selectedDatabase();
#ifdef WITH_XC_BROWSER_PASSKEYS
QJsonObject
@ -175,7 +173,6 @@ private:
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);
@ -194,8 +191,6 @@ private:
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();
QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid();

View file

@ -5,7 +5,7 @@
* Copyright (C) 2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com,
* author Giuseppe D'Angelo <giuseppe.dangelo@kdab.com>
* Copyright (C) 2021 The Qt Company Ltd.
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -274,35 +274,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 <dangelog@gmail.com>.

View file

@ -1,6 +1,6 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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);

173
src/core/UrlTools.cpp Normal file
View file

@ -0,0 +1,173 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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"
#if defined(WITH_XC_NETWORKING) || defined(WITH_XC_BROWSER)
#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;
}
#if defined(WITH_XC_NETWORKING) || defined(WITH_XC_BROWSER)
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;
}

56
src/core/UrlTools.h Normal file
View file

@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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/>.
*/
#ifndef KEEPASSXC_URLTOOLS_H
#define KEEPASSXC_URLTOOLS_H
#include "config-keepassx.h"
#include <QNetworkReply>
#include <QObject>
#include <QUrl>
#include <QVariant>
class UrlTools : public QObject
{
Q_OBJECT
public:
explicit UrlTools() = default;
static UrlTools* instance();
#if defined(WITH_XC_NETWORKING) || defined(WITH_XC_BROWSER)
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

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 <QBuffer>
#include <QHostInfo>
@ -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<QUrl>()) {
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;

View file

@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2014 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
*
* 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()));

View file

@ -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);