Skip to content

Commit

Permalink
Browser: Add a new group setting for omitting WWW subdomain when matc…
Browse files Browse the repository at this point in the history
…hing URLs
  • Loading branch information
varjolintu authored and droidmonkey committed Jun 12, 2022
1 parent a2aac70 commit 6cb6f1f
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 68 deletions.
8 changes: 8 additions & 0 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3088,6 +3088,14 @@ Would you like to correct it?</source>
<source>Do not use HTTP Auth toggle for this and sub groups</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Omit WWW subdomain from matching:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Omit WWW subdomain from matching toggle for this and sub groups</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditGroupWidgetKeeShare</name>
Expand Down
124 changes: 69 additions & 55 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserS
const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry");
const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth");
const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww");
// Multiple URL's
const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");

Expand Down Expand Up @@ -426,22 +427,22 @@ QString BrowserService::getKey(const QString& id)
}

QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
const QString& siteUrlStr,
const QString& formUrlStr,
const QString& siteUrl,
const QString& formUrl,
const QString& realm,
const StringPairList& keyList,
const bool httpAuth)
{
Q_UNUSED(dbid);
const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess();
const bool ignoreHttpAuth = browserSettings()->httpAuthPermission();
const QString siteHost = QUrl(siteUrlStr).host();
const QString formHost = QUrl(formUrlStr).host();
const QString siteHost = QUrl(siteUrl).host();
const QString formHost = QUrl(formUrl).host();

// Check entries for authorization
QList<Entry*> pwEntriesToConfirm;
QList<Entry*> pwEntries;
for (auto* entry : searchEntries(siteUrlStr, formUrlStr, keyList)) {
for (auto* entry : searchEntries(siteUrl, formUrl, keyList)) {
auto entryCustomData = entry->customData();

if (!httpAuth
Expand Down Expand Up @@ -484,7 +485,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,

// Confirm entries
QList<Entry*> selectedEntriesToConfirm =
confirmEntries(pwEntriesToConfirm, siteUrlStr, siteHost, formHost, realm, httpAuth);
confirmEntries(pwEntriesToConfirm, siteUrl, siteHost, formHost, realm, httpAuth);
if (!selectedEntriesToConfirm.isEmpty()) {
pwEntries.append(selectedEntriesToConfirm);
}
Expand All @@ -499,7 +500,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
}

// Sort results
pwEntries = sortEntries(pwEntries, siteUrlStr, formUrlStr);
pwEntries = sortEntries(pwEntries, siteUrl, formUrl);

// Fill the list
QJsonArray result;
Expand All @@ -513,8 +514,8 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
void BrowserService::addEntry(const QString& dbid,
const QString& login,
const QString& password,
const QString& siteUrlStr,
const QString& formUrlStr,
const QString& siteUrl,
const QString& formUrl,
const QString& realm,
const QString& group,
const QString& groupUuid,
Expand All @@ -530,8 +531,8 @@ void BrowserService::addEntry(const QString& dbid,

auto* entry = new Entry();
entry->setUuid(QUuid::createUuid());
entry->setTitle(QUrl(siteUrlStr).host());
entry->setUrl(siteUrlStr);
entry->setTitle(QUrl(siteUrl).host());
entry->setUrl(siteUrl);
entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
entry->setUsername(login);
entry->setPassword(password);
Expand All @@ -550,8 +551,8 @@ void BrowserService::addEntry(const QString& dbid,
entry->setGroup(getDefaultEntryGroup(db));
}

const QString host = QUrl(siteUrlStr).host();
const QString submitHost = QUrl(formUrlStr).host();
const QString host = QUrl(siteUrl).host();
const QString submitHost = QUrl(formUrl).host();
BrowserEntryConfig config;
config.allow(host);

Expand All @@ -572,8 +573,8 @@ bool BrowserService::updateEntry(const QString& dbid,
const QString& uuid,
const QString& login,
const QString& password,
const QString& siteUrlStr,
const QString& formUrlStr)
const QString& siteUrl,
const QString& formUrl)
{
// TODO: select database based on this key id
Q_UNUSED(dbid);
Expand All @@ -585,7 +586,7 @@ bool BrowserService::updateEntry(const QString& dbid,
Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
if (!entry) {
// If entry is not found for update, add a new one to the selected database
addEntry(dbid, login, password, siteUrlStr, formUrlStr, "", "", "", db);
addEntry(dbid, login, password, siteUrl, formUrl, "", "", "", db);
return true;
}

Expand Down Expand Up @@ -614,7 +615,7 @@ bool BrowserService::updateEntry(const QString& dbid,
dialogResult = MessageBox::question(
nullptr,
tr("KeePassXC: Update Entry"),
tr("Do you want to update the information in %1 - %2?").arg(QUrl(siteUrlStr).host(), username),
tr("Do you want to update the information in %1 - %2?").arg(QUrl(siteUrl).host(), username),
MessageBox::Save | MessageBox::Cancel,
MessageBox::Cancel,
MessageBox::Raise);
Expand Down Expand Up @@ -663,7 +664,7 @@ bool BrowserService::deleteEntry(const QString& uuid)
}

QList<Entry*>
BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& siteUrlStr, const QString& formUrlStr)
BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& siteUrl, const QString& formUrl)
{
QList<Entry*> entries;
auto* rootGroup = db->rootGroup();
Expand All @@ -677,23 +678,17 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
continue;
}

const auto omitWwwSubdomain =
group->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW) == Group::Enable;

for (auto* entry : group->entries()) {
if (entry->isRecycled()
|| (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
&& entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR)) {
continue;
}

// Search for additional URL's starting with KP2A_URL
for (const auto& key : entry->attributes()->keys()) {
if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), siteUrlStr, formUrlStr)
&& !entries.contains(entry)) {
entries.append(entry);
continue;
}
}

if (!handleEntry(entry, siteUrlStr, formUrlStr)) {
if (!shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) {
continue;
}

Expand All @@ -708,7 +703,7 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
}

QList<Entry*>
BrowserService::searchEntries(const QString& siteUrlStr, const QString& formUrlStr, const StringPairList& keyList)
BrowserService::searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList)
{
// Check if database is connected with KeePassXC-Browser
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
Expand Down Expand Up @@ -738,11 +733,11 @@ BrowserService::searchEntries(const QString& siteUrlStr, const QString& formUrlS
}

// Search entries matching the hostname
QString hostname = QUrl(siteUrlStr).host();
QString hostname = QUrl(siteUrl).host();
QList<Entry*> entries;
do {
for (const auto& db : databases) {
entries << searchEntries(db, siteUrlStr, formUrlStr);
entries << searchEntries(db, siteUrl, formUrl);
}
} while (entries.isEmpty() && removeFirstDomain(hostname));

Expand Down Expand Up @@ -853,9 +848,9 @@ BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr,
}

QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
const QString& siteUrlStr,
const QString& siteUrl,
const QString& siteHost,
const QString& formUrlStr,
const QString& formUrl,
const QString& realm,
const bool httpAuth)
{
Expand All @@ -874,16 +869,16 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
BrowserEntryConfig config;
config.load(entry);
config.deny(siteHost);
if (!formUrlStr.isEmpty() && siteHost != formUrlStr) {
config.deny(formUrlStr);
if (!formUrl.isEmpty() && siteHost != formUrl) {
config.deny(formUrl);
}
if (!realm.isEmpty()) {
config.setRealm(realm);
}
config.save(entry);
});

accessControlDialog.setItems(pwEntriesToConfirm, siteUrlStr, httpAuth);
accessControlDialog.setItems(pwEntriesToConfirm, siteUrl, httpAuth);

QList<Entry*> allowedEntries;
if (accessControlDialog.exec() == QDialog::Accepted) {
Expand All @@ -894,8 +889,8 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
BrowserEntryConfig config;
config.load(entry);
config.allow(siteHost);
if (!formUrlStr.isEmpty() && siteHost != formUrlStr) {
config.allow(formUrlStr);
if (!formUrl.isEmpty() && siteHost != formUrl) {
config.allow(formUrl);
}
if (!realm.isEmpty()) {
config.setRealm(realm);
Expand Down Expand Up @@ -1006,14 +1001,14 @@ Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& sele

// Returns the maximum sort priority given a set of match urls and the
// extension provided site and form url.
int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr)
int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl)
{
QList<int> priorityList;
// NOTE: QUrl::matches is utterly broken in Qt < 5.11, so we work around that
// by removing parts of the url that we don't match and direct matching others
const auto stdOpts = QUrl::RemoveFragment | QUrl::RemoveUserInfo;
const auto siteUrl = QUrl(siteUrlStr).adjusted(stdOpts);
const auto formUrl = QUrl(formUrlStr).adjusted(stdOpts);
const auto adjustedSiteUrl = QUrl(siteUrl).adjusted(stdOpts);
const auto adjustedFormUrl = QUrl(formUrl).adjusted(stdOpts);

auto getPriority = [&](const QString& givenUrl) {
auto url = QUrl::fromUserInput(givenUrl).adjusted(stdOpts);
Expand All @@ -1031,38 +1026,38 @@ int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl

// Reject invalid urls and hosts, except 'localhost', and scheme mismatch
if (!url.isValid() || (!url.host().contains(".") && url.host() != "localhost")
|| url.scheme() != siteUrl.scheme()) {
|| url.scheme() != adjustedSiteUrl.scheme()) {
return 0;
}

// Exact match with site url or form url
if (url.matches(siteUrl, QUrl::None) || url.matches(formUrl, QUrl::None)) {
if (url.matches(adjustedSiteUrl, QUrl::None) || url.matches(adjustedFormUrl, QUrl::None)) {
return 100;
}

// Exact match without the query string
if (url.matches(siteUrl, QUrl::RemoveQuery) || url.matches(formUrl, QUrl::RemoveQuery)) {
if (url.matches(adjustedSiteUrl, QUrl::RemoveQuery) || url.matches(adjustedFormUrl, QUrl::RemoveQuery)) {
return 90;
}

// Parent directory match
if (url.isParentOf(siteUrl) || url.isParentOf(formUrl)) {
if (url.isParentOf(adjustedSiteUrl) || url.isParentOf(adjustedFormUrl)) {
return 85;
}

// Match without path (ie, FQDN match), form url prioritizes lower than site url
if (url.host() == siteUrl.host()) {
if (url.host() == adjustedSiteUrl.host()) {
return 80;
}
if (url.host() == formUrl.host()) {
if (url.host() == adjustedFormUrl.host()) {
return 70;
}

// Site/form url ends with given url (subdomain mismatch)
if (siteUrl.host().endsWith(url.host())) {
if (adjustedSiteUrl.host().endsWith(url.host())) {
return 60;
}
if (formUrl.host().endsWith(url.host())) {
if (adjustedFormUrl.host().endsWith(url.host())) {
return 50;
}

Expand Down Expand Up @@ -1108,18 +1103,32 @@ bool BrowserService::removeFirstDomain(QString& hostname)

/* Test if a search URL matches a custom entry. If the URL has the schema "keepassxc", some special checks will be made.
* Otherwise, this simply delegates to handleURL(). */
bool BrowserService::handleEntry(Entry* entry, const QString& url, const QString& submitUrl)
bool BrowserService::shouldIncludeEntry(Entry* entry,
const QString& url,
const QString& submitUrl,
const bool omitWwwSubdomain)
{
// Use this special scheme to find entries by UUID
if (url.startsWith("keepassxc://by-uuid/")) {
return url.endsWith("by-uuid/" + entry->uuidToHex());
} else if (url.startsWith("keepassxc://by-path/")) {
return url.endsWith("by-path/" + entry->path());
}
return handleURL(entry->url(), url, submitUrl);

const auto allEntryUrls = entry->getAllUrls();
for (const auto& entryUrl : allEntryUrls) {
if (handleURL(entryUrl, url, submitUrl, omitWwwSubdomain)) {
return true;
}
}

return false;
}

bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr)
bool BrowserService::handleURL(const QString& entryUrl,
const QString& siteUrl,
const QString& formUrl,
const bool omitWwwSubdomain)
{
if (entryUrl.isEmpty()) {
return false;
Expand All @@ -1136,9 +1145,14 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt
}
}

// Remove WWW subdomain from matching if group setting is enabled
if (omitWwwSubdomain && entryQUrl.host().startsWith("www.")) {
entryQUrl.setHost(entryQUrl.host().remove("www."));
}

// Make a direct compare if a local file is used
if (siteUrlStr.startsWith("file://")) {
return entryUrl == formUrlStr;
if (siteUrl.startsWith("file://")) {
return entryUrl == formUrl;
}

// URL host validation fails
Expand All @@ -1147,7 +1161,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt
}

// Match port, if used
QUrl siteQUrl(siteUrlStr);
QUrl siteQUrl(siteUrl);
if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) {
return false;
}
Expand Down
Loading

0 comments on commit 6cb6f1f

Please sign in to comment.