TestBrowser.cpp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  1. /*
  2. * Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
  3. *
  4. * This program is free software: you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation, either version 2 or (at your option)
  7. * version 3 of the License.
  8. *
  9. * This program is distributed in the hope that it will be useful,
  10. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. * GNU General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. #include "TestBrowser.h"
  18. #include "browser/BrowserMessageBuilder.h"
  19. #include "browser/BrowserSettings.h"
  20. #include "core/Group.h"
  21. #include "core/Tools.h"
  22. #include "crypto/Crypto.h"
  23. #include <QJsonObject>
  24. #include <QTest>
  25. #include <botan/sodium.h>
  26. using namespace Botan::Sodium;
  27. QTEST_GUILESS_MAIN(TestBrowser)
  28. const QString PUBLICKEY = "UIIPObeoya1G8g1M5omgyoPR/j1mR1HlYHu0wHCgMhA=";
  29. const QString SECRETKEY = "B8ei4ZjQJkWzZU2SK/tBsrYRwp+6ztEMf5GFQV+i0yI=";
  30. const QString SERVERPUBLICKEY = "lKnbLhrVCOqzEjuNoUz1xj9EZlz8xeO4miZBvLrUPVQ=";
  31. const QString SERVERSECRETKEY = "tbPQcghxfOgbmsnEqG2qMIj1W2+nh+lOJcNsHncaz1Q=";
  32. const QString NONCE = "zBKdvTjL5bgWaKMCTut/8soM/uoMrFoZ";
  33. const QString INCREMENTEDNONCE = "zRKdvTjL5bgWaKMCTut/8soM/uoMrFoZ";
  34. const QString CLIENTID = "testClient";
  35. void TestBrowser::initTestCase()
  36. {
  37. QVERIFY(Crypto::init());
  38. m_browserService = browserService();
  39. browserSettings()->setBestMatchOnly(false);
  40. }
  41. void TestBrowser::init()
  42. {
  43. m_browserAction.reset(new BrowserAction());
  44. }
  45. /**
  46. * Tests for BrowserAction
  47. */
  48. void TestBrowser::testChangePublicKeys()
  49. {
  50. QJsonObject json;
  51. json["action"] = "change-public-keys";
  52. json["publicKey"] = PUBLICKEY;
  53. json["nonce"] = NONCE;
  54. auto response = m_browserAction->processClientMessage(nullptr, json);
  55. QCOMPARE(response["action"].toString(), QString("change-public-keys"));
  56. QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false);
  57. QCOMPARE(response["success"].toString(), TRUE_STR);
  58. }
  59. void TestBrowser::testEncryptMessage()
  60. {
  61. QJsonObject message;
  62. message["action"] = "test-action";
  63. m_browserAction->m_publicKey = SERVERPUBLICKEY;
  64. m_browserAction->m_secretKey = SERVERSECRETKEY;
  65. m_browserAction->m_clientPublicKey = PUBLICKEY;
  66. auto encrypted = browserMessageBuilder()->encryptMessage(message, NONCE, PUBLICKEY, SERVERSECRETKEY);
  67. QCOMPARE(encrypted, QString("+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP"));
  68. }
  69. void TestBrowser::testDecryptMessage()
  70. {
  71. QString message = "+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP";
  72. m_browserAction->m_publicKey = SERVERPUBLICKEY;
  73. m_browserAction->m_secretKey = SERVERSECRETKEY;
  74. m_browserAction->m_clientPublicKey = PUBLICKEY;
  75. auto decrypted = browserMessageBuilder()->decryptMessage(message, NONCE, PUBLICKEY, SERVERSECRETKEY);
  76. QCOMPARE(decrypted["action"].toString(), QString("test-action"));
  77. }
  78. void TestBrowser::testGetBase64FromKey()
  79. {
  80. unsigned char pk[crypto_box_PUBLICKEYBYTES];
  81. for (unsigned int i = 0; i < crypto_box_PUBLICKEYBYTES; ++i) {
  82. pk[i] = i;
  83. }
  84. auto response = browserMessageBuilder()->getBase64FromKey(pk, crypto_box_PUBLICKEYBYTES);
  85. QCOMPARE(response, QString("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="));
  86. }
  87. void TestBrowser::testIncrementNonce()
  88. {
  89. auto result = browserMessageBuilder()->incrementNonce(NONCE);
  90. QCOMPARE(result, INCREMENTEDNONCE);
  91. }
  92. void TestBrowser::testBuildResponse()
  93. {
  94. const auto object = QJsonObject{{"test", true}};
  95. const QJsonArray arr = {QJsonObject{{"test", true}}};
  96. const auto val = QString("value1");
  97. // Note: Passing a const QJsonObject will fail
  98. const Parameters params{
  99. {"test-param-1", val}, {"test-param-2", 2}, {"test-param-3", false}, {"object", object}, {"arr", arr}};
  100. const auto action = QString("test-action");
  101. const auto message = browserMessageBuilder()->buildResponse(action, NONCE, params, PUBLICKEY, SERVERSECRETKEY);
  102. QVERIFY(!message.isEmpty());
  103. QCOMPARE(message["action"].toString(), action);
  104. QCOMPARE(message["nonce"].toString(), NONCE);
  105. const auto decrypted =
  106. browserMessageBuilder()->decryptMessage(message["message"].toString(), NONCE, PUBLICKEY, SERVERSECRETKEY);
  107. QVERIFY(!decrypted.isEmpty());
  108. QCOMPARE(decrypted["test-param-1"].toString(), QString("value1"));
  109. QCOMPARE(decrypted["test-param-2"].toInt(), 2);
  110. QCOMPARE(decrypted["test-param-3"].toBool(), false);
  111. const auto objectResult = decrypted["object"].toObject();
  112. QCOMPARE(objectResult["test"].toBool(), true);
  113. const auto arrResult = decrypted["arr"].toArray();
  114. QCOMPARE(arrResult.size(), 1);
  115. const auto firstArr = arrResult[0].toObject();
  116. QCOMPARE(firstArr["test"].toBool(), true);
  117. }
  118. void TestBrowser::testSortPriority()
  119. {
  120. QFETCH(QString, entryUrl);
  121. QFETCH(QString, siteUrl);
  122. QFETCH(QString, formUrl);
  123. QFETCH(int, expectedScore);
  124. QScopedPointer<Entry> entry(new Entry());
  125. entry->setUrl(entryUrl);
  126. QCOMPARE(m_browserService->sortPriority(entry->getAllUrls(), siteUrl, formUrl), expectedScore);
  127. }
  128. void TestBrowser::testSortPriority_data()
  129. {
  130. const QString siteUrl = "https://github.com/login";
  131. const QString formUrl = "https://github.com/session";
  132. QTest::addColumn<QString>("entryUrl");
  133. QTest::addColumn<QString>("siteUrl");
  134. QTest::addColumn<QString>("formUrl");
  135. QTest::addColumn<int>("expectedScore");
  136. QTest::newRow("Exact Match") << siteUrl << siteUrl << siteUrl << 100;
  137. QTest::newRow("Exact Match (site)") << siteUrl << siteUrl << formUrl << 100;
  138. QTest::newRow("Exact Match (form)") << siteUrl << "https://github.net" << siteUrl << 100;
  139. QTest::newRow("Exact Match No Trailing Slash") << "https://github.com" << "https://github.com/" << formUrl << 100;
  140. QTest::newRow("Exact Match No Scheme") << "github.com/login" << siteUrl << formUrl << 100;
  141. QTest::newRow("Exact Match with Query")
  142. << "https://github.com/login?test=test#fragment" << "https://github.com/login?test=test" << formUrl << 100;
  143. QTest::newRow("Site Query Mismatch") << siteUrl << siteUrl + "?test=test" << formUrl << 90;
  144. QTest::newRow("Path Mismatch (site)") << "https://github.com/" << siteUrl << formUrl << 85;
  145. QTest::newRow("Path Mismatch (site) No Scheme") << "github.com" << siteUrl << formUrl << 85;
  146. QTest::newRow("Path Mismatch (form)") << "https://github.com/" << "https://github.net" << formUrl << 85;
  147. QTest::newRow("Path Mismatch (diff parent)") << "https://github.com/keepassxreboot" << siteUrl << formUrl << 80;
  148. QTest::newRow("Path Mismatch (diff parent, form)")
  149. << "https://github.com/keepassxreboot" << "https://github.net" << formUrl << 70;
  150. QTest::newRow("Subdomain Mismatch (site)") << siteUrl << "https://sub.github.com/" << "https://github.net/" << 60;
  151. QTest::newRow("Subdomain Mismatch (form)") << siteUrl << "https://github.net/" << "https://sub.github.com/" << 50;
  152. QTest::newRow("Scheme Mismatch") << "http://github.com" << siteUrl << formUrl << 0;
  153. QTest::newRow("Scheme Mismatch w/path") << "http://github.com/login" << siteUrl << formUrl << 0;
  154. QTest::newRow("Invalid URL") << "http://github" << siteUrl << formUrl << 0;
  155. }
  156. void TestBrowser::testSearchEntries()
  157. {
  158. auto db = QSharedPointer<Database>::create();
  159. auto* root = db->rootGroup();
  160. QStringList urls = {"https://github.com/login_page",
  161. "https://github.com/login",
  162. "https://github.com/",
  163. "github.com/login",
  164. "http://github.com",
  165. "http://github.com/login",
  166. "github.com",
  167. "github.com/login",
  168. "https://github", // Invalid URL
  169. "github.com"};
  170. createEntries(urls, root);
  171. browserSettings()->setMatchUrlScheme(false);
  172. auto result =
  173. m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); // db, url, submitUrl
  174. QCOMPARE(result.length(), 9);
  175. QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
  176. QCOMPARE(result[1]->url(), QString("https://github.com/login"));
  177. QCOMPARE(result[2]->url(), QString("https://github.com/"));
  178. QCOMPARE(result[3]->url(), QString("github.com/login"));
  179. QCOMPARE(result[4]->url(), QString("http://github.com"));
  180. QCOMPARE(result[5]->url(), QString("http://github.com/login"));
  181. // With matching there should be only 3 results + 4 without a scheme
  182. browserSettings()->setMatchUrlScheme(true);
  183. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  184. QCOMPARE(result.length(), 7);
  185. QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
  186. QCOMPARE(result[1]->url(), QString("https://github.com/login"));
  187. QCOMPARE(result[2]->url(), QString("https://github.com/"));
  188. QCOMPARE(result[3]->url(), QString("github.com/login"));
  189. }
  190. void TestBrowser::testSearchEntriesByPath()
  191. {
  192. auto db = QSharedPointer<Database>::create();
  193. auto* root = db->rootGroup();
  194. QStringList urlsRoot = {"https://root.example.com/", "root.example.com/login"};
  195. auto entriesRoot = createEntries(urlsRoot, root);
  196. auto* groupLevel1 = new Group();
  197. groupLevel1->setParent(root);
  198. groupLevel1->setName("TestGroup1");
  199. QStringList urlsLevel1 = {"https://1.example.com/", "1.example.com/login"};
  200. auto entriesLevel1 = createEntries(urlsLevel1, groupLevel1);
  201. auto* groupLevel2 = new Group();
  202. groupLevel2->setParent(groupLevel1);
  203. groupLevel2->setName("TestGroup2");
  204. QStringList urlsLevel2 = {"https://2.example.com/", "2.example.com/login"};
  205. auto entriesLevel2 = createEntries(urlsLevel2, groupLevel2);
  206. compareEntriesByPath(db, entriesRoot, "");
  207. compareEntriesByPath(db, entriesLevel1, "TestGroup1/");
  208. compareEntriesByPath(db, entriesLevel2, "TestGroup1/TestGroup2/");
  209. }
  210. void TestBrowser::compareEntriesByPath(QSharedPointer<Database> db, QList<Entry*> entries, QString path)
  211. {
  212. for (Entry* entry : entries) {
  213. QString testUrl = "keepassxc://by-path/" + path + entry->title();
  214. /* Look for an entry with that path. First using handleEntry, then through the search */
  215. QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true);
  216. auto result = m_browserService->searchEntries(db, testUrl, "");
  217. QCOMPARE(result.length(), 1);
  218. QCOMPARE(result[0], entry);
  219. }
  220. }
  221. void TestBrowser::testSearchEntriesByUUID()
  222. {
  223. auto db = QSharedPointer<Database>::create();
  224. auto* root = db->rootGroup();
  225. /* The URLs don't really matter for this test, we just need some entries */
  226. QStringList urls = {"https://github.com/login_page",
  227. "https://github.com/login",
  228. "https://github.com/",
  229. "github.com/login",
  230. "http://github.com",
  231. "http://github.com/login",
  232. "github.com",
  233. "github.com/login",
  234. "https://github",
  235. "github.com",
  236. "",
  237. "not an URL"};
  238. auto entries = createEntries(urls, root);
  239. for (Entry* entry : entries) {
  240. QString testUrl = "keepassxc://by-uuid/" + entry->uuidToHex();
  241. /* Look for an entry with that UUID. First using shouldIncludeEntry, then through the search */
  242. QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true);
  243. auto result = m_browserService->searchEntries(db, testUrl, "");
  244. QCOMPARE(result.length(), 1);
  245. QCOMPARE(result[0], entry);
  246. }
  247. /* Test for entries that don't exist */
  248. QStringList uuids = {"00000000000000000000000000000000",
  249. "00000000000000000000000000000001",
  250. "00000000000000000000000000000002/",
  251. "invalid uuid",
  252. "000000000000000000000000000000000000000"
  253. "00000000000000000000000"};
  254. for (QString uuid : uuids) {
  255. QString testUrl = "keepassxc://by-uuid/" + uuid;
  256. for (Entry* entry : entries) {
  257. QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), false);
  258. }
  259. auto result = m_browserService->searchEntries(db, testUrl, "");
  260. QCOMPARE(result.length(), 0);
  261. }
  262. }
  263. void TestBrowser::testSearchEntriesByReference()
  264. {
  265. auto db = QSharedPointer<Database>::create();
  266. auto* root = db->rootGroup();
  267. /* The URLs don't really matter for this test, we just need some entries */
  268. QStringList urls = {"https://subdomain.example.com",
  269. "example.com", // Only includes a partial URL for references
  270. "https://another.domain.com", // Additional URL as full reference
  271. "https://subdomain.somesite.com", // Additional URL as partial reference
  272. "", // Full reference will be added to https://subdomain.example.com
  273. "" // Partial reference will be added to https://subdomain.example.com
  274. "https://www.notincluded.com"}; // Should not show in search
  275. auto entries = createEntries(urls, root);
  276. auto firstEntryUuid = entries.first()->uuidToHex();
  277. auto secondEntryUuid = entries[1]->uuidToHex();
  278. auto fullReference = QString("{REF:A@I:%1}").arg(firstEntryUuid);
  279. auto partialReference = QString("https://subdomain.{REF:A@I:%1}").arg(secondEntryUuid);
  280. entries[2]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, fullReference);
  281. entries[3]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, partialReference);
  282. entries[4]->setUrl(fullReference);
  283. entries[5]->setUrl(partialReference);
  284. auto result = m_browserService->searchEntries(db, "https://subdomain.example.com", "");
  285. QCOMPARE(result.length(), 6);
  286. QCOMPARE(result[0]->url(), urls[0]);
  287. QCOMPARE(result[1]->url(), urls[1]);
  288. QCOMPARE(result[2]->url(), urls[2]);
  289. QCOMPARE(
  290. result[2]->resolveMultiplePlaceholders(result[2]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
  291. urls[0]);
  292. QCOMPARE(result[3]->url(), urls[3]);
  293. QCOMPARE(
  294. result[3]->resolveMultiplePlaceholders(result[3]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
  295. urls[0]);
  296. QCOMPARE(result[4]->url(), fullReference);
  297. QCOMPARE(result[4]->resolveMultiplePlaceholders(result[4]->url()), urls[0]); // Should be resolved to the main entry
  298. QCOMPARE(result[5]->url(), partialReference);
  299. QCOMPARE(result[5]->resolveMultiplePlaceholders(result[5]->url()), urls[0]); // Should be resolved to the main entry
  300. }
  301. void TestBrowser::testSearchEntriesWithPort()
  302. {
  303. auto db = QSharedPointer<Database>::create();
  304. auto* root = db->rootGroup();
  305. QStringList urls = {"http://127.0.0.1:443", "http://127.0.0.1:80"};
  306. createEntries(urls, root);
  307. auto result = m_browserService->searchEntries(db, "http://127.0.0.1:443", "http://127.0.0.1");
  308. QCOMPARE(result.length(), 1);
  309. QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443"));
  310. }
  311. void TestBrowser::testSearchEntriesWithAdditionalURLs()
  312. {
  313. auto db = QSharedPointer<Database>::create();
  314. auto* root = db->rootGroup();
  315. QStringList urls = {"https://github.com/", "https://www.example.com", "http://domain.com"};
  316. auto entries = createEntries(urls, root);
  317. // Add an additional URL to the first entry
  318. entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://keepassxc.org");
  319. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  320. QCOMPARE(result.length(), 1);
  321. QCOMPARE(result[0]->url(), QString("https://github.com/"));
  322. // Search the additional URL. It should return the same entry
  323. auto additionalResult = m_browserService->searchEntries(db, "https://keepassxc.org", "https://keepassxc.org");
  324. QCOMPARE(additionalResult.length(), 1);
  325. QCOMPARE(additionalResult[0]->url(), QString("https://github.com/"));
  326. }
  327. void TestBrowser::testInvalidEntries()
  328. {
  329. auto db = QSharedPointer<Database>::create();
  330. auto* root = db->rootGroup();
  331. const QString url("https://github.com");
  332. const QString submitUrl("https://github.com/session");
  333. QStringList urls = {
  334. "https://github.com/login",
  335. "https:///github.com/", // Extra '/'
  336. "http://github.com/**//*",
  337. "http://*.github.com/login",
  338. "//github.com", // fromUserInput() corrects this one.
  339. "github.com/{}<>",
  340. "http:/example.com",
  341. };
  342. createEntries(urls, root);
  343. browserSettings()->setMatchUrlScheme(true);
  344. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  345. QCOMPARE(result.length(), 2);
  346. QCOMPARE(result[0]->url(), QString("https://github.com/login"));
  347. QCOMPARE(result[1]->url(), QString("//github.com"));
  348. // Test the URL's directly
  349. QCOMPARE(m_browserService->handleURL(urls[0], url, submitUrl), true);
  350. QCOMPARE(m_browserService->handleURL(urls[1], url, submitUrl), false);
  351. QCOMPARE(m_browserService->handleURL(urls[2], url, submitUrl), false);
  352. QCOMPARE(m_browserService->handleURL(urls[3], url, submitUrl), false);
  353. QCOMPARE(m_browserService->handleURL(urls[4], url, submitUrl), true);
  354. QCOMPARE(m_browserService->handleURL(urls[5], url, submitUrl), false);
  355. }
  356. void TestBrowser::testSubdomainsAndPaths()
  357. {
  358. auto db = QSharedPointer<Database>::create();
  359. auto* root = db->rootGroup();
  360. QStringList urls = {
  361. "https://www.github.com/login/page.xml",
  362. "https://login.github.com/",
  363. "https://github.com",
  364. "http://www.github.com",
  365. "http://login.github.com/pathtonowhere",
  366. ".github.com", // Invalid URL
  367. "www.github.com/",
  368. "https://github", // Invalid URL
  369. "https://hub.com" // Should not return
  370. };
  371. createEntries(urls, root);
  372. browserSettings()->setMatchUrlScheme(false);
  373. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  374. QCOMPARE(result.length(), 1);
  375. QCOMPARE(result[0]->url(), QString("https://github.com"));
  376. // With www subdomain
  377. result = m_browserService->searchEntries(db, "https://www.github.com", "https://www.github.com/session");
  378. QCOMPARE(result.length(), 4);
  379. QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
  380. QCOMPARE(result[1]->url(), QString("https://github.com")); // Accepts any subdomain
  381. QCOMPARE(result[2]->url(), QString("http://www.github.com"));
  382. QCOMPARE(result[3]->url(), QString("www.github.com/"));
  383. // With www subdomain omitted
  384. root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Enable);
  385. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  386. root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Inherit);
  387. QCOMPARE(result.length(), 4);
  388. QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
  389. QCOMPARE(result[1]->url(), QString("https://github.com"));
  390. QCOMPARE(result[2]->url(), QString("http://www.github.com"));
  391. QCOMPARE(result[3]->url(), QString("www.github.com/"));
  392. // With scheme matching there should be only 1 result
  393. browserSettings()->setMatchUrlScheme(true);
  394. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  395. QCOMPARE(result.length(), 1);
  396. QCOMPARE(result[0]->url(), QString("https://github.com"));
  397. // Test site with subdomain in the site URL
  398. QStringList entryURLs = {
  399. "https://accounts.example.com",
  400. "https://accounts.example.com/path",
  401. "https://subdomain.example.com/",
  402. "https://another.accounts.example.com/",
  403. "https://another.subdomain.example.com/",
  404. "https://example.com/",
  405. "https://example" // Invalid URL
  406. };
  407. createEntries(entryURLs, root);
  408. result = m_browserService->searchEntries(db, "https://accounts.example.com/", "https://accounts.example.com/");
  409. QCOMPARE(result.length(), 3);
  410. QCOMPARE(result[0]->url(), QString("https://accounts.example.com"));
  411. QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
  412. QCOMPARE(result[2]->url(), QString("https://example.com/")); // Accepts any subdomain
  413. result = m_browserService->searchEntries(
  414. db, "https://another.accounts.example.com/", "https://another.accounts.example.com/");
  415. QCOMPARE(result.length(), 4);
  416. QCOMPARE(result[0]->url(),
  417. QString("https://accounts.example.com")); // Accepts any subdomain under accounts.example.com
  418. QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
  419. QCOMPARE(result[2]->url(), QString("https://another.accounts.example.com/"));
  420. QCOMPARE(result[3]->url(), QString("https://example.com/")); // Accepts one or more subdomains
  421. // Test local files. It should be a direct match.
  422. QStringList localFiles = {"file:///Users/testUser/tests/test.html"};
  423. createEntries(localFiles, root);
  424. // With local files, url is always set to the file scheme + ://. Submit URL holds the actual URL.
  425. result = m_browserService->searchEntries(db, "file://", "file:///Users/testUser/tests/test.html");
  426. QCOMPARE(result.length(), 1);
  427. }
  428. QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const
  429. {
  430. QList<Entry*> entries;
  431. for (int i = 0; i < urls.length(); ++i) {
  432. auto entry = new Entry();
  433. entry->setGroup(root);
  434. entry->beginUpdate();
  435. entry->setUrl(urls[i]);
  436. entry->setUsername(QString("User %1").arg(i));
  437. entry->setUuid(QUuid::createUuid());
  438. entry->setTitle(QString("Name_%1").arg(entry->uuidToHex()));
  439. entry->endUpdate();
  440. entries.push_back(entry);
  441. }
  442. return entries;
  443. }
  444. void TestBrowser::testBestMatchingCredentials()
  445. {
  446. auto db = QSharedPointer<Database>::create();
  447. auto* root = db->rootGroup();
  448. // Test with simple URL entries
  449. QStringList urls = {"https://github.com/loginpage", "https://github.com/justsomepage", "https://github.com/"};
  450. auto entries = createEntries(urls, root);
  451. browserSettings()->setBestMatchOnly(true);
  452. QString siteUrl = "https://github.com/loginpage";
  453. auto result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  454. auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  455. QCOMPARE(sorted.size(), 1);
  456. QCOMPARE(sorted[0]->url(), siteUrl);
  457. siteUrl = "https://github.com/justsomepage";
  458. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  459. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  460. QCOMPARE(sorted.size(), 1);
  461. QCOMPARE(sorted[0]->url(), siteUrl);
  462. siteUrl = "https://github.com/";
  463. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  464. sorted = m_browserService->sortEntries(entries, siteUrl, siteUrl);
  465. QCOMPARE(sorted.size(), 1);
  466. QCOMPARE(sorted[0]->url(), siteUrl);
  467. // Without best-matching the URL with the path should be returned first
  468. browserSettings()->setBestMatchOnly(false);
  469. siteUrl = "https://github.com/loginpage";
  470. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  471. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  472. QCOMPARE(sorted.size(), 3);
  473. QCOMPARE(sorted[0]->url(), siteUrl);
  474. // Test with subdomains
  475. QStringList subdomainsUrls = {"https://sub.github.com/loginpage",
  476. "https://sub.github.com/justsomepage",
  477. "https://bus.github.com/justsomepage",
  478. "https://subdomain.example.com/",
  479. "https://subdomain.example.com",
  480. "https://example.com"};
  481. entries = createEntries(subdomainsUrls, root);
  482. browserSettings()->setBestMatchOnly(true);
  483. siteUrl = "https://sub.github.com/justsomepage";
  484. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  485. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  486. QCOMPARE(sorted.size(), 1);
  487. QCOMPARE(sorted[0]->url(), siteUrl);
  488. siteUrl = "https://github.com/justsomepage";
  489. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  490. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  491. QCOMPARE(sorted.size(), 1);
  492. QCOMPARE(sorted[0]->url(), siteUrl);
  493. siteUrl = "https://sub.github.com/justsomepage?wehavesomeextra=here";
  494. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  495. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  496. QCOMPARE(sorted.size(), 1);
  497. QCOMPARE(sorted[0]->url(), QString("https://sub.github.com/justsomepage"));
  498. // The matching should not care if there's a / path or not.
  499. siteUrl = "https://subdomain.example.com/";
  500. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  501. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  502. QCOMPARE(sorted.size(), 2);
  503. QCOMPARE(sorted[0]->url(), QString("https://subdomain.example.com"));
  504. QCOMPARE(sorted[1]->url(), QString("https://subdomain.example.com/"));
  505. // Entries with https://example.com should be still returned even if the site URL has a subdomain. Those have the
  506. // best match.
  507. db = QSharedPointer<Database>::create();
  508. root = db->rootGroup();
  509. QStringList domainUrls = {"https://example.com", "https://example.com", "https://other.example.com"};
  510. entries = createEntries(domainUrls, root);
  511. siteUrl = "https://subdomain.example.com";
  512. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  513. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  514. QCOMPARE(sorted.size(), 2);
  515. QCOMPARE(sorted[0]->url(), QString("https://example.com"));
  516. QCOMPARE(sorted[1]->url(), QString("https://example.com"));
  517. // https://github.com/keepassxreboot/keepassxc/issues/4754
  518. db = QSharedPointer<Database>::create();
  519. root = db->rootGroup();
  520. QStringList fooUrls = {"https://example.com/foo", "https://example.com/bar"};
  521. entries = createEntries(fooUrls, root);
  522. for (const auto& url : fooUrls) {
  523. result = m_browserService->searchEntries(db, url, url);
  524. sorted = m_browserService->sortEntries(result, url, url);
  525. QCOMPARE(sorted.size(), 1);
  526. QCOMPARE(sorted[0]->url(), QString(url));
  527. }
  528. // https://github.com/keepassxreboot/keepassxc/issues/4734
  529. db = QSharedPointer<Database>::create();
  530. root = db->rootGroup();
  531. QStringList testUrls = {"http://some.domain.tld/somePath", "http://some.domain.tld/otherPath"};
  532. entries = createEntries(testUrls, root);
  533. for (const auto& url : testUrls) {
  534. result = m_browserService->searchEntries(db, url, url);
  535. sorted = m_browserService->sortEntries(result, url, url);
  536. QCOMPARE(sorted.size(), 1);
  537. QCOMPARE(sorted[0]->url(), QString(url));
  538. }
  539. }
  540. void TestBrowser::testBestMatchingWithAdditionalURLs()
  541. {
  542. auto db = QSharedPointer<Database>::create();
  543. auto* root = db->rootGroup();
  544. QStringList urls = {"https://github.com/loginpage", "https://test.github.com/", "https://github.com/"};
  545. auto entries = createEntries(urls, root);
  546. browserSettings()->setBestMatchOnly(true);
  547. // Add an additional URL to the first entry
  548. entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://test.github.com/anotherpage");
  549. // The first entry should be triggered
  550. auto result = m_browserService->searchEntries(
  551. db, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
  552. auto sorted = m_browserService->sortEntries(
  553. result, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
  554. QCOMPARE(sorted.length(), 1);
  555. QCOMPARE(sorted[0]->url(), urls[0]);
  556. }
  557. void TestBrowser::testRestrictBrowserKey()
  558. {
  559. auto db = QSharedPointer<Database>::create();
  560. auto* root = db->rootGroup();
  561. // Group 0 (root): No browser key restriction given
  562. QStringList urlsRoot = {"https://example.com/0"};
  563. auto entriesRoot = createEntries(urlsRoot, root);
  564. // Group 1: restricted to browser with 'key1'
  565. auto* group1 = new Group();
  566. group1->setParent(root);
  567. group1->setName("TestGroup1");
  568. group1->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key1");
  569. QStringList urls1 = {"https://example.com/1"};
  570. auto entries1 = createEntries(urls1, group1);
  571. // Group 2: restricted to browser with 'key2'
  572. auto* group2 = new Group();
  573. group2->setParent(root);
  574. group2->setName("TestGroup2");
  575. group2->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key2");
  576. QStringList urls2 = {"https://example.com/2"};
  577. auto entries2 = createEntries(urls2, group2);
  578. // Group 2b: inherits parent group (2) restriction
  579. auto* group2b = new Group();
  580. group2b->setParent(group2);
  581. group2b->setName("TestGroup2b");
  582. QStringList urls2b = {"https://example.com/2b"};
  583. auto entries2b = createEntries(urls2b, group2b);
  584. // Group 3: inherits parent group (root) - any browser can see
  585. auto* group3 = new Group();
  586. group3->setParent(root);
  587. group3->setName("TestGroup3");
  588. QStringList urls3 = {"https://example.com/3"};
  589. auto entries3 = createEntries(urls3, group3);
  590. // Browser 'key0': Groups 1 and 2 are excluded, so entries 0 and 3 will be found
  591. auto siteUrl = QString("https://example.com");
  592. auto result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key0"});
  593. auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  594. QCOMPARE(sorted.size(), 2);
  595. QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
  596. QCOMPARE(sorted[1]->url(), QString("https://example.com/0"));
  597. // Browser 'key1': Group 2 will be excluded, so entries 0, 1, and 3 will be found
  598. result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key1"});
  599. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  600. QCOMPARE(sorted.size(), 3);
  601. QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
  602. QCOMPARE(sorted[1]->url(), QString("https://example.com/1"));
  603. QCOMPARE(sorted[2]->url(), QString("https://example.com/0"));
  604. // Browser 'key2': Group 1 will be excluded, so entries 0, 2, 2b, 3 will be found
  605. result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key2"});
  606. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  607. QCOMPARE(sorted.size(), 4);
  608. QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
  609. QCOMPARE(sorted[1]->url(), QString("https://example.com/2b"));
  610. QCOMPARE(sorted[2]->url(), QString("https://example.com/2"));
  611. QCOMPARE(sorted[3]->url(), QString("https://example.com/0"));
  612. }