TestBrowser.cpp 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. /*
  2. * Copyright (C) 2025 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 4 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::testSearchEntriesWithWildcardURLs()
  328. {
  329. auto db = QSharedPointer<Database>::create();
  330. auto* root = db->rootGroup();
  331. QStringList urls = {
  332. "https://github.com/login_page/*",
  333. "https://github.com/*/second",
  334. "https://github.com/*",
  335. "http://github.com/*",
  336. "github.com/*", // Defaults to https
  337. "https://*.github.com/*",
  338. "https://subdomain.*.github.com/*/second",
  339. "https://*.sub.github.com/*",
  340. "https://********", // Invalid wildcard URL
  341. "https://subdomain.yes.github.com/*",
  342. "https://example.com:8448/*",
  343. "https://example.com/*/*",
  344. "https://example.com/$/*",
  345. "https://127.128.129.*:8448/",
  346. "https://127.128.*/",
  347. "https://127.160.*.2/login",
  348. "http://[2001:db8:85a3:8d3:1319:8a2e:370:*]/",
  349. "https://[2001:db8:85a3:8d3:*]:443/",
  350. "fe80::1ff:fe23:4567:890a",
  351. "2001-db8-85a3-8d3-1319-8a2e-370-7348.ipv6-literal.net",
  352. "\"https://thisisatest.com/login.php\"" // Exact URL
  353. };
  354. createEntries(urls, root, true);
  355. browserSettings()->setMatchUrlScheme(false);
  356. // Return first Additional URL
  357. auto firstUrl = [&](Entry* entry) { return entry->attributes()->value(EntryAttributes::AdditionalUrlAttribute); };
  358. auto result = m_browserService->searchEntries(
  359. db, "https://github.com/login_page/second", "https://github.com/login_page/second");
  360. QCOMPARE(result.length(), 6);
  361. QCOMPARE(firstUrl(result[0]), QString("https://github.com/login_page/*"));
  362. QCOMPARE(firstUrl(result[1]), QString("https://github.com/*/second"));
  363. QCOMPARE(firstUrl(result[2]), QString("https://github.com/*"));
  364. QCOMPARE(firstUrl(result[3]), QString("http://github.com/*"));
  365. QCOMPARE(firstUrl(result[4]), QString("github.com/*"));
  366. QCOMPARE(firstUrl(result[5]), QString("https://*.github.com/*"));
  367. result = m_browserService->searchEntries(
  368. db, "https://subdomain.sub.github.com/login_page/second", "https://subdomain.sub.github.com/login_page/second");
  369. QCOMPARE(result.length(), 3);
  370. QCOMPARE(firstUrl(result[0]), QString("https://*.github.com/*"));
  371. QCOMPARE(firstUrl(result[1]), QString("https://subdomain.*.github.com/*/second"));
  372. QCOMPARE(firstUrl(result[2]), QString("https://*.sub.github.com/*"));
  373. result = m_browserService->searchEntries(
  374. db, "https://subdomain.sub.github.com/other_page", "https://subdomain.sub.github.com/other_page");
  375. QCOMPARE(result.length(), 2);
  376. QCOMPARE(firstUrl(result[0]), QString("https://*.github.com/*"));
  377. QCOMPARE(firstUrl(result[1]), QString("https://*.sub.github.com/*"));
  378. result = m_browserService->searchEntries(
  379. db, "https://subdomain.yes.github.com/other_page/second", "https://subdomain.yes.github.com/other_page/second");
  380. QCOMPARE(result.length(), 3);
  381. QCOMPARE(firstUrl(result[0]), QString("https://*.github.com/*"));
  382. QCOMPARE(firstUrl(result[1]), QString("https://subdomain.*.github.com/*/second"));
  383. QCOMPARE(firstUrl(result[2]), QString("https://subdomain.yes.github.com/*"));
  384. result = m_browserService->searchEntries(
  385. db, "https://example.com:8448/login/page", "https://example.com:8448/login/page");
  386. QCOMPARE(result.length(), 2);
  387. QCOMPARE(firstUrl(result[0]), QString("https://example.com:8448/*"));
  388. QCOMPARE(firstUrl(result[1]), QString("https://example.com/*/*"));
  389. result = m_browserService->searchEntries(
  390. db, "https://example.com:8449/login/page", "https://example.com:8449/login/page");
  391. QCOMPARE(result.length(), 1);
  392. QCOMPARE(firstUrl(result[0]), QString("https://example.com/*/*"));
  393. result =
  394. m_browserService->searchEntries(db, "https://example.com/$/login_page", "https://example.com/$/login_page");
  395. QCOMPARE(result.length(), 2);
  396. QCOMPARE(firstUrl(result[0]), QString("https://example.com/*/*"));
  397. QCOMPARE(firstUrl(result[1]), QString("https://example.com/$/*"));
  398. result = m_browserService->searchEntries(db, "https://127.128.129.130:8448/", "https://127.128.129.130:8448/");
  399. QCOMPARE(result.length(), 2);
  400. result = m_browserService->searchEntries(db, "https://127.128.129.130/", "https://127.128.129.130/");
  401. QCOMPARE(result.length(), 1);
  402. QCOMPARE(firstUrl(result[0]), QString("https://127.128.*/"));
  403. result = m_browserService->searchEntries(db, "https://127.1.129.130/", "https://127.1.129.130/");
  404. QCOMPARE(result.length(), 0);
  405. result = m_browserService->searchEntries(db, "https://127.160.8.2/login", "https://127.160.8.2/login");
  406. QCOMPARE(result.length(), 1);
  407. QCOMPARE(firstUrl(result[0]), QString("https://127.160.*.2/login"));
  408. // Exact URL
  409. result =
  410. m_browserService->searchEntries(db, "https://thisisatest.com/login.php", "https://thisisatest.com/login.php");
  411. QCOMPARE(result.length(), 1);
  412. QCOMPARE(firstUrl(result[0]), QString("\"https://thisisatest.com/login.php\""));
  413. // With scheme matching enabled
  414. browserSettings()->setMatchUrlScheme(true);
  415. result = m_browserService->searchEntries(
  416. db, "https://github.com/login_page/second", "https://github.com/login_page/second");
  417. QCOMPARE(result.length(), 5);
  418. QCOMPARE(firstUrl(result[0]), QString("https://github.com/login_page/*"));
  419. QCOMPARE(firstUrl(result[1]), QString("https://github.com/*/second"));
  420. QCOMPARE(firstUrl(result[2]), QString("https://github.com/*"));
  421. QCOMPARE(firstUrl(result[3]), QString("github.com/*")); // Defaults to https
  422. QCOMPARE(firstUrl(result[4]), QString("https://*.github.com/*"));
  423. }
  424. void TestBrowser::testInvalidEntries()
  425. {
  426. auto db = QSharedPointer<Database>::create();
  427. auto* root = db->rootGroup();
  428. const QString url("https://github.com");
  429. const QString submitUrl("https://github.com/session");
  430. QStringList urls = {
  431. "https://github.com/login",
  432. "https:///github.com/", // Extra '/'
  433. "http://github.com/**//*",
  434. "http://*.github.com/login",
  435. "//github.com", // fromUserInput() corrects this one.
  436. "github.com/{}<>",
  437. "http:/example.com",
  438. };
  439. createEntries(urls, root);
  440. browserSettings()->setMatchUrlScheme(true);
  441. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  442. QCOMPARE(result.length(), 2);
  443. QCOMPARE(result[0]->url(), QString("https://github.com/login"));
  444. QCOMPARE(result[1]->url(), QString("//github.com"));
  445. // Test the URL's directly
  446. QCOMPARE(m_browserService->handleURL(urls[0], url, submitUrl), true);
  447. QCOMPARE(m_browserService->handleURL(urls[1], url, submitUrl), false);
  448. QCOMPARE(m_browserService->handleURL(urls[2], url, submitUrl), false);
  449. QCOMPARE(m_browserService->handleURL(urls[3], url, submitUrl), false);
  450. QCOMPARE(m_browserService->handleURL(urls[4], url, submitUrl), true);
  451. QCOMPARE(m_browserService->handleURL(urls[5], url, submitUrl), false);
  452. }
  453. void TestBrowser::testSubdomainsAndPaths()
  454. {
  455. auto db = QSharedPointer<Database>::create();
  456. auto* root = db->rootGroup();
  457. QStringList urls = {
  458. "https://www.github.com/login/page.xml",
  459. "https://login.github.com/",
  460. "https://github.com",
  461. "http://www.github.com",
  462. "http://login.github.com/pathtonowhere",
  463. ".github.com", // Invalid URL
  464. "www.github.com/",
  465. "https://github", // Invalid URL
  466. "https://hub.com" // Should not return
  467. };
  468. createEntries(urls, root);
  469. browserSettings()->setMatchUrlScheme(false);
  470. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  471. QCOMPARE(result.length(), 1);
  472. QCOMPARE(result[0]->url(), QString("https://github.com"));
  473. // With www subdomain
  474. result = m_browserService->searchEntries(db, "https://www.github.com", "https://www.github.com/session");
  475. QCOMPARE(result.length(), 4);
  476. QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
  477. QCOMPARE(result[1]->url(), QString("https://github.com")); // Accepts any subdomain
  478. QCOMPARE(result[2]->url(), QString("http://www.github.com"));
  479. QCOMPARE(result[3]->url(), QString("www.github.com/"));
  480. // With www subdomain omitted
  481. root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Enable);
  482. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  483. root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Inherit);
  484. QCOMPARE(result.length(), 4);
  485. QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
  486. QCOMPARE(result[1]->url(), QString("https://github.com"));
  487. QCOMPARE(result[2]->url(), QString("http://www.github.com"));
  488. QCOMPARE(result[3]->url(), QString("www.github.com/"));
  489. // With scheme matching there should be only 1 result
  490. browserSettings()->setMatchUrlScheme(true);
  491. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  492. QCOMPARE(result.length(), 1);
  493. QCOMPARE(result[0]->url(), QString("https://github.com"));
  494. // Test site with subdomain in the site URL
  495. QStringList entryURLs = {
  496. "https://accounts.example.com",
  497. "https://accounts.example.com/path",
  498. "https://subdomain.example.com/",
  499. "https://another.accounts.example.com/",
  500. "https://another.subdomain.example.com/",
  501. "https://example.com/",
  502. "https://example" // Invalid URL
  503. };
  504. createEntries(entryURLs, root);
  505. result = m_browserService->searchEntries(db, "https://accounts.example.com/", "https://accounts.example.com/");
  506. QCOMPARE(result.length(), 3);
  507. QCOMPARE(result[0]->url(), QString("https://accounts.example.com"));
  508. QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
  509. QCOMPARE(result[2]->url(), QString("https://example.com/")); // Accepts any subdomain
  510. result = m_browserService->searchEntries(
  511. db, "https://another.accounts.example.com/", "https://another.accounts.example.com/");
  512. QCOMPARE(result.length(), 4);
  513. QCOMPARE(result[0]->url(),
  514. QString("https://accounts.example.com")); // Accepts any subdomain under accounts.example.com
  515. QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
  516. QCOMPARE(result[2]->url(), QString("https://another.accounts.example.com/"));
  517. QCOMPARE(result[3]->url(), QString("https://example.com/")); // Accepts one or more subdomains
  518. // Test local files. It should be a direct match.
  519. QStringList localFiles = {"file:///Users/testUser/tests/test.html"};
  520. createEntries(localFiles, root);
  521. // With local files, url is always set to the file scheme + ://. Submit URL holds the actual URL.
  522. result = m_browserService->searchEntries(db, "file://", "file:///Users/testUser/tests/test.html");
  523. QCOMPARE(result.length(), 1);
  524. }
  525. QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root, bool additionalUrl) const
  526. {
  527. QList<Entry*> entries;
  528. for (int i = 0; i < urls.length(); ++i) {
  529. auto entry = new Entry();
  530. entry->setGroup(root);
  531. entry->beginUpdate();
  532. if (additionalUrl) {
  533. entry->attributes()->set(EntryAttributes::AdditionalUrlAttribute, urls[i]);
  534. } else {
  535. entry->setUrl(urls[i]);
  536. }
  537. entry->setUsername(QString("User %1").arg(i));
  538. entry->setUuid(QUuid::createUuid());
  539. entry->setTitle(QString("Name_%1").arg(entry->uuidToHex()));
  540. entry->endUpdate();
  541. entries.push_back(entry);
  542. }
  543. return entries;
  544. }
  545. void TestBrowser::testBestMatchingCredentials()
  546. {
  547. auto db = QSharedPointer<Database>::create();
  548. auto* root = db->rootGroup();
  549. // Test with simple URL entries
  550. QStringList urls = {"https://github.com/loginpage", "https://github.com/justsomepage", "https://github.com/"};
  551. auto entries = createEntries(urls, root);
  552. browserSettings()->setBestMatchOnly(true);
  553. QString siteUrl = "https://github.com/loginpage";
  554. auto result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  555. auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  556. QCOMPARE(sorted.size(), 1);
  557. QCOMPARE(sorted[0]->url(), siteUrl);
  558. siteUrl = "https://github.com/justsomepage";
  559. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  560. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  561. QCOMPARE(sorted.size(), 1);
  562. QCOMPARE(sorted[0]->url(), siteUrl);
  563. siteUrl = "https://github.com/";
  564. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  565. sorted = m_browserService->sortEntries(entries, siteUrl, siteUrl);
  566. QCOMPARE(sorted.size(), 1);
  567. QCOMPARE(sorted[0]->url(), siteUrl);
  568. // Without best-matching the URL with the path should be returned first
  569. browserSettings()->setBestMatchOnly(false);
  570. siteUrl = "https://github.com/loginpage";
  571. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  572. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  573. QCOMPARE(sorted.size(), 3);
  574. QCOMPARE(sorted[0]->url(), siteUrl);
  575. // Test with subdomains
  576. QStringList subdomainsUrls = {"https://sub.github.com/loginpage",
  577. "https://sub.github.com/justsomepage",
  578. "https://bus.github.com/justsomepage",
  579. "https://subdomain.example.com/",
  580. "https://subdomain.example.com",
  581. "https://example.com"};
  582. entries = createEntries(subdomainsUrls, root);
  583. browserSettings()->setBestMatchOnly(true);
  584. siteUrl = "https://sub.github.com/justsomepage";
  585. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  586. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  587. QCOMPARE(sorted.size(), 1);
  588. QCOMPARE(sorted[0]->url(), siteUrl);
  589. siteUrl = "https://github.com/justsomepage";
  590. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  591. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  592. QCOMPARE(sorted.size(), 1);
  593. QCOMPARE(sorted[0]->url(), siteUrl);
  594. siteUrl = "https://sub.github.com/justsomepage?wehavesomeextra=here";
  595. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  596. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  597. QCOMPARE(sorted.size(), 1);
  598. QCOMPARE(sorted[0]->url(), QString("https://sub.github.com/justsomepage"));
  599. // The matching should not care if there's a / path or not.
  600. siteUrl = "https://subdomain.example.com/";
  601. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  602. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  603. QCOMPARE(sorted.size(), 2);
  604. QCOMPARE(sorted[0]->url(), QString("https://subdomain.example.com"));
  605. QCOMPARE(sorted[1]->url(), QString("https://subdomain.example.com/"));
  606. // Entries with https://example.com should be still returned even if the site URL has a subdomain. Those have the
  607. // best match.
  608. db = QSharedPointer<Database>::create();
  609. root = db->rootGroup();
  610. QStringList domainUrls = {"https://example.com", "https://example.com", "https://other.example.com"};
  611. entries = createEntries(domainUrls, root);
  612. siteUrl = "https://subdomain.example.com";
  613. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  614. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  615. QCOMPARE(sorted.size(), 2);
  616. QCOMPARE(sorted[0]->url(), QString("https://example.com"));
  617. QCOMPARE(sorted[1]->url(), QString("https://example.com"));
  618. // https://github.com/keepassxreboot/keepassxc/issues/4754
  619. db = QSharedPointer<Database>::create();
  620. root = db->rootGroup();
  621. QStringList fooUrls = {"https://example.com/foo", "https://example.com/bar"};
  622. entries = createEntries(fooUrls, root);
  623. for (const auto& url : fooUrls) {
  624. result = m_browserService->searchEntries(db, url, url);
  625. sorted = m_browserService->sortEntries(result, url, url);
  626. QCOMPARE(sorted.size(), 1);
  627. QCOMPARE(sorted[0]->url(), QString(url));
  628. }
  629. // https://github.com/keepassxreboot/keepassxc/issues/4734
  630. db = QSharedPointer<Database>::create();
  631. root = db->rootGroup();
  632. QStringList testUrls = {"http://some.domain.tld/somePath", "http://some.domain.tld/otherPath"};
  633. entries = createEntries(testUrls, root);
  634. for (const auto& url : testUrls) {
  635. result = m_browserService->searchEntries(db, url, url);
  636. sorted = m_browserService->sortEntries(result, url, url);
  637. QCOMPARE(sorted.size(), 1);
  638. QCOMPARE(sorted[0]->url(), QString(url));
  639. }
  640. }
  641. void TestBrowser::testBestMatchingWithAdditionalURLs()
  642. {
  643. auto db = QSharedPointer<Database>::create();
  644. auto* root = db->rootGroup();
  645. QStringList urls = {"https://github.com/loginpage", "https://test.github.com/", "https://github.com/"};
  646. auto entries = createEntries(urls, root);
  647. browserSettings()->setBestMatchOnly(true);
  648. // Add an additional URL to the first entry
  649. entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://test.github.com/anotherpage");
  650. // The first entry should be triggered
  651. auto result = m_browserService->searchEntries(
  652. db, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
  653. auto sorted = m_browserService->sortEntries(
  654. result, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
  655. QCOMPARE(sorted.length(), 1);
  656. QCOMPARE(sorted[0]->url(), urls[0]);
  657. }
  658. void TestBrowser::testRestrictBrowserKey()
  659. {
  660. auto db = QSharedPointer<Database>::create();
  661. auto* root = db->rootGroup();
  662. // Group 0 (root): No browser key restriction given
  663. QStringList urlsRoot = {"https://example.com/0"};
  664. auto entriesRoot = createEntries(urlsRoot, root);
  665. // Group 1: restricted to browser with 'key1'
  666. auto* group1 = new Group();
  667. group1->setParent(root);
  668. group1->setName("TestGroup1");
  669. group1->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key1");
  670. QStringList urls1 = {"https://example.com/1"};
  671. auto entries1 = createEntries(urls1, group1);
  672. // Group 2: restricted to browser with 'key2'
  673. auto* group2 = new Group();
  674. group2->setParent(root);
  675. group2->setName("TestGroup2");
  676. group2->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key2");
  677. QStringList urls2 = {"https://example.com/2"};
  678. auto entries2 = createEntries(urls2, group2);
  679. // Group 2b: inherits parent group (2) restriction
  680. auto* group2b = new Group();
  681. group2b->setParent(group2);
  682. group2b->setName("TestGroup2b");
  683. QStringList urls2b = {"https://example.com/2b"};
  684. auto entries2b = createEntries(urls2b, group2b);
  685. // Group 3: inherits parent group (root) - any browser can see
  686. auto* group3 = new Group();
  687. group3->setParent(root);
  688. group3->setName("TestGroup3");
  689. QStringList urls3 = {"https://example.com/3"};
  690. auto entries3 = createEntries(urls3, group3);
  691. // Browser 'key0': Groups 1 and 2 are excluded, so entries 0 and 3 will be found
  692. auto siteUrl = QString("https://example.com");
  693. auto result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key0"});
  694. auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  695. QCOMPARE(sorted.size(), 2);
  696. QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
  697. QCOMPARE(sorted[1]->url(), QString("https://example.com/0"));
  698. // Browser 'key1': Group 2 will be excluded, so entries 0, 1, and 3 will be found
  699. result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key1"});
  700. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  701. QCOMPARE(sorted.size(), 3);
  702. QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
  703. QCOMPARE(sorted[1]->url(), QString("https://example.com/1"));
  704. QCOMPARE(sorted[2]->url(), QString("https://example.com/0"));
  705. // Browser 'key2': Group 1 will be excluded, so entries 0, 2, 2b, 3 will be found
  706. result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key2"});
  707. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  708. QCOMPARE(sorted.size(), 4);
  709. QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
  710. QCOMPARE(sorted[1]->url(), QString("https://example.com/2b"));
  711. QCOMPARE(sorted[2]->url(), QString("https://example.com/2"));
  712. QCOMPARE(sorted[3]->url(), QString("https://example.com/0"));
  713. }