TestBrowser.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. /*
  2. * Copyright (C) 2021 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 CLIENTID = "testClient";
  34. void TestBrowser::initTestCase()
  35. {
  36. QVERIFY(Crypto::init());
  37. m_browserService = browserService();
  38. browserSettings()->setBestMatchOnly(false);
  39. }
  40. void TestBrowser::init()
  41. {
  42. m_browserAction.reset(new BrowserAction());
  43. }
  44. /**
  45. * Tests for BrowserAction
  46. */
  47. void TestBrowser::testChangePublicKeys()
  48. {
  49. QJsonObject json;
  50. json["action"] = "change-public-keys";
  51. json["publicKey"] = PUBLICKEY;
  52. json["nonce"] = NONCE;
  53. auto response = m_browserAction->processClientMessage(nullptr, json);
  54. QCOMPARE(response["action"].toString(), QString("change-public-keys"));
  55. QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false);
  56. QCOMPARE(response["success"].toString(), TRUE_STR);
  57. }
  58. void TestBrowser::testEncryptMessage()
  59. {
  60. QJsonObject message;
  61. message["action"] = "test-action";
  62. m_browserAction->m_publicKey = SERVERPUBLICKEY;
  63. m_browserAction->m_secretKey = SERVERSECRETKEY;
  64. m_browserAction->m_clientPublicKey = PUBLICKEY;
  65. auto encrypted = browserMessageBuilder()->encryptMessage(message, NONCE, PUBLICKEY, SERVERSECRETKEY);
  66. QCOMPARE(encrypted, QString("+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP"));
  67. }
  68. void TestBrowser::testDecryptMessage()
  69. {
  70. QString message = "+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP";
  71. m_browserAction->m_publicKey = SERVERPUBLICKEY;
  72. m_browserAction->m_secretKey = SERVERSECRETKEY;
  73. m_browserAction->m_clientPublicKey = PUBLICKEY;
  74. auto decrypted = browserMessageBuilder()->decryptMessage(message, NONCE, PUBLICKEY, SERVERSECRETKEY);
  75. QCOMPARE(decrypted["action"].toString(), QString("test-action"));
  76. }
  77. void TestBrowser::testGetBase64FromKey()
  78. {
  79. unsigned char pk[crypto_box_PUBLICKEYBYTES];
  80. for (unsigned int i = 0; i < crypto_box_PUBLICKEYBYTES; ++i) {
  81. pk[i] = i;
  82. }
  83. auto response = browserMessageBuilder()->getBase64FromKey(pk, crypto_box_PUBLICKEYBYTES);
  84. QCOMPARE(response, QString("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="));
  85. }
  86. void TestBrowser::testIncrementNonce()
  87. {
  88. auto result = browserMessageBuilder()->incrementNonce(NONCE);
  89. QCOMPARE(result, QString("zRKdvTjL5bgWaKMCTut/8soM/uoMrFoZ"));
  90. }
  91. /**
  92. * Tests for BrowserService
  93. */
  94. void TestBrowser::testTopLevelDomain()
  95. {
  96. QString url1 = "https://another.example.co.uk";
  97. QString url2 = "https://www.example.com";
  98. QString url3 = "http://test.net";
  99. QString url4 = "http://so.many.subdomains.co.jp";
  100. QString url5 = "https://192.168.0.1";
  101. QString url6 = "https://192.168.0.1:8000";
  102. QString res1 = m_browserService->getTopLevelDomainFromUrl(url1);
  103. QString res2 = m_browserService->getTopLevelDomainFromUrl(url2);
  104. QString res3 = m_browserService->getTopLevelDomainFromUrl(url3);
  105. QString res4 = m_browserService->getTopLevelDomainFromUrl(url4);
  106. QString res5 = m_browserService->getTopLevelDomainFromUrl(url5);
  107. QString res6 = m_browserService->getTopLevelDomainFromUrl(url6);
  108. QCOMPARE(res1, QString("example.co.uk"));
  109. QCOMPARE(res2, QString("example.com"));
  110. QCOMPARE(res3, QString("test.net"));
  111. QCOMPARE(res4, QString("subdomains.co.jp"));
  112. QCOMPARE(res5, QString("192.168.0.1"));
  113. QCOMPARE(res6, QString("192.168.0.1"));
  114. }
  115. void TestBrowser::testIsIpAddress()
  116. {
  117. auto host1 = "example.com"; // Not valid
  118. auto host2 = "192.168.0.1";
  119. auto host3 = "278.21.2.0"; // Not valid
  120. auto host4 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334";
  121. auto host5 = "2001:db8:0:1:1:1:1:1";
  122. auto host6 = "fe80::1ff:fe23:4567:890a";
  123. auto host7 = "2001:20::1";
  124. auto host8 = "2001:0db8:85y3:0000:0000:8a2e:0370:7334"; // Not valid
  125. QVERIFY(!m_browserService->isIpAddress(host1));
  126. QVERIFY(m_browserService->isIpAddress(host2));
  127. QVERIFY(!m_browserService->isIpAddress(host3));
  128. QVERIFY(m_browserService->isIpAddress(host4));
  129. QVERIFY(m_browserService->isIpAddress(host5));
  130. QVERIFY(m_browserService->isIpAddress(host6));
  131. QVERIFY(m_browserService->isIpAddress(host7));
  132. QVERIFY(!m_browserService->isIpAddress(host8));
  133. }
  134. void TestBrowser::testSortPriority()
  135. {
  136. QFETCH(QString, entryUrl);
  137. QFETCH(QString, siteUrl);
  138. QFETCH(QString, formUrl);
  139. QFETCH(int, expectedScore);
  140. QScopedPointer<Entry> entry(new Entry());
  141. entry->setUrl(entryUrl);
  142. QCOMPARE(m_browserService->sortPriority(m_browserService->getEntryURLs(entry.data()), siteUrl, formUrl),
  143. expectedScore);
  144. }
  145. void TestBrowser::testSortPriority_data()
  146. {
  147. const QString siteUrl = "https://github.com/login";
  148. const QString formUrl = "https://github.com/session";
  149. QTest::addColumn<QString>("entryUrl");
  150. QTest::addColumn<QString>("siteUrl");
  151. QTest::addColumn<QString>("formUrl");
  152. QTest::addColumn<int>("expectedScore");
  153. QTest::newRow("Exact Match") << siteUrl << siteUrl << siteUrl << 100;
  154. QTest::newRow("Exact Match (site)") << siteUrl << siteUrl << formUrl << 100;
  155. QTest::newRow("Exact Match (form)") << siteUrl << "https://github.net" << siteUrl << 100;
  156. QTest::newRow("Exact Match No Trailing Slash") << "https://github.com"
  157. << "https://github.com/" << formUrl << 100;
  158. QTest::newRow("Exact Match No Scheme") << "github.com/login" << siteUrl << formUrl << 100;
  159. QTest::newRow("Exact Match with Query") << "https://github.com/login?test=test#fragment"
  160. << "https://github.com/login?test=test" << formUrl << 100;
  161. QTest::newRow("Site Query Mismatch") << siteUrl << siteUrl + "?test=test" << formUrl << 90;
  162. QTest::newRow("Path Mismatch (site)") << "https://github.com/" << siteUrl << formUrl << 85;
  163. QTest::newRow("Path Mismatch (site) No Scheme") << "github.com" << siteUrl << formUrl << 85;
  164. QTest::newRow("Path Mismatch (form)") << "https://github.com/"
  165. << "https://github.net" << formUrl << 85;
  166. QTest::newRow("Path Mismatch (diff parent)") << "https://github.com/keepassxreboot" << siteUrl << formUrl << 80;
  167. QTest::newRow("Path Mismatch (diff parent, form)") << "https://github.com/keepassxreboot"
  168. << "https://github.net" << formUrl << 70;
  169. QTest::newRow("Subdomain Mismatch (site)") << siteUrl << "https://sub.github.com/"
  170. << "https://github.net/" << 60;
  171. QTest::newRow("Subdomain Mismatch (form)") << siteUrl << "https://github.net/"
  172. << "https://sub.github.com/" << 50;
  173. QTest::newRow("Scheme Mismatch") << "http://github.com" << siteUrl << formUrl << 0;
  174. QTest::newRow("Scheme Mismatch w/path") << "http://github.com/login" << siteUrl << formUrl << 0;
  175. QTest::newRow("Invalid URL") << "http://github" << siteUrl << formUrl << 0;
  176. }
  177. void TestBrowser::testSearchEntries()
  178. {
  179. auto db = QSharedPointer<Database>::create();
  180. auto* root = db->rootGroup();
  181. QStringList urls = {"https://github.com/login_page",
  182. "https://github.com/login",
  183. "https://github.com/",
  184. "github.com/login",
  185. "http://github.com",
  186. "http://github.com/login",
  187. "github.com",
  188. "github.com/login",
  189. "https://github", // Invalid URL
  190. "github.com"};
  191. createEntries(urls, root);
  192. browserSettings()->setMatchUrlScheme(false);
  193. auto result =
  194. m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); // db, url, submitUrl
  195. QCOMPARE(result.length(), 9);
  196. QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
  197. QCOMPARE(result[1]->url(), QString("https://github.com/login"));
  198. QCOMPARE(result[2]->url(), QString("https://github.com/"));
  199. QCOMPARE(result[3]->url(), QString("github.com/login"));
  200. QCOMPARE(result[4]->url(), QString("http://github.com"));
  201. QCOMPARE(result[5]->url(), QString("http://github.com/login"));
  202. // With matching there should be only 3 results + 4 without a scheme
  203. browserSettings()->setMatchUrlScheme(true);
  204. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  205. QCOMPARE(result.length(), 7);
  206. QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
  207. QCOMPARE(result[1]->url(), QString("https://github.com/login"));
  208. QCOMPARE(result[2]->url(), QString("https://github.com/"));
  209. QCOMPARE(result[3]->url(), QString("github.com/login"));
  210. }
  211. void TestBrowser::testSearchEntriesByPath()
  212. {
  213. auto db = QSharedPointer<Database>::create();
  214. auto* root = db->rootGroup();
  215. QStringList urlsRoot = {"https://root.example.com/", "root.example.com/login"};
  216. auto entriesRoot = createEntries(urlsRoot, root);
  217. auto* groupLevel1 = new Group();
  218. groupLevel1->setParent(root);
  219. groupLevel1->setName("TestGroup1");
  220. QStringList urlsLevel1 = {"https://1.example.com/", "1.example.com/login"};
  221. auto entriesLevel1 = createEntries(urlsLevel1, groupLevel1);
  222. auto* groupLevel2 = new Group();
  223. groupLevel2->setParent(groupLevel1);
  224. groupLevel2->setName("TestGroup2");
  225. QStringList urlsLevel2 = {"https://2.example.com/", "2.example.com/login"};
  226. auto entriesLevel2 = createEntries(urlsLevel2, groupLevel2);
  227. compareEntriesByPath(db, entriesRoot, "");
  228. compareEntriesByPath(db, entriesLevel1, "TestGroup1/");
  229. compareEntriesByPath(db, entriesLevel2, "TestGroup1/TestGroup2/");
  230. }
  231. void TestBrowser::compareEntriesByPath(QSharedPointer<Database> db, QList<Entry*> entries, QString path)
  232. {
  233. for (Entry* entry : entries) {
  234. QString testUrl = "keepassxc://by-path/" + path + entry->title();
  235. /* Look for an entry with that path. First using handleEntry, then through the search */
  236. QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true);
  237. auto result = m_browserService->searchEntries(db, testUrl, "");
  238. QCOMPARE(result.length(), 1);
  239. QCOMPARE(result[0], entry);
  240. }
  241. }
  242. void TestBrowser::testSearchEntriesByUUID()
  243. {
  244. auto db = QSharedPointer<Database>::create();
  245. auto* root = db->rootGroup();
  246. /* The URLs don't really matter for this test, we just need some entries */
  247. QStringList urls = {"https://github.com/login_page",
  248. "https://github.com/login",
  249. "https://github.com/",
  250. "github.com/login",
  251. "http://github.com",
  252. "http://github.com/login",
  253. "github.com",
  254. "github.com/login",
  255. "https://github",
  256. "github.com",
  257. "",
  258. "not an URL"};
  259. auto entries = createEntries(urls, root);
  260. for (Entry* entry : entries) {
  261. QString testUrl = "keepassxc://by-uuid/" + entry->uuidToHex();
  262. /* Look for an entry with that UUID. First using handleEntry, then through the search */
  263. QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true);
  264. auto result = m_browserService->searchEntries(db, testUrl, "");
  265. QCOMPARE(result.length(), 1);
  266. QCOMPARE(result[0], entry);
  267. }
  268. /* Test for entries that don't exist */
  269. QStringList uuids = {"00000000000000000000000000000000",
  270. "00000000000000000000000000000001",
  271. "00000000000000000000000000000002/",
  272. "invalid uuid",
  273. "000000000000000000000000000000000000000"
  274. "00000000000000000000000"};
  275. for (QString uuid : uuids) {
  276. QString testUrl = "keepassxc://by-uuid/" + uuid;
  277. for (Entry* entry : entries) {
  278. QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), false);
  279. }
  280. auto result = m_browserService->searchEntries(db, testUrl, "");
  281. QCOMPARE(result.length(), 0);
  282. }
  283. }
  284. void TestBrowser::testSearchEntriesWithPort()
  285. {
  286. auto db = QSharedPointer<Database>::create();
  287. auto* root = db->rootGroup();
  288. QStringList urls = {"http://127.0.0.1:443", "http://127.0.0.1:80"};
  289. createEntries(urls, root);
  290. auto result = m_browserService->searchEntries(db, "http://127.0.0.1:443", "http://127.0.0.1");
  291. QCOMPARE(result.length(), 1);
  292. QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443"));
  293. }
  294. void TestBrowser::testSearchEntriesWithAdditionalURLs()
  295. {
  296. auto db = QSharedPointer<Database>::create();
  297. auto* root = db->rootGroup();
  298. QStringList urls = {"https://github.com/", "https://www.example.com", "http://domain.com"};
  299. auto entries = createEntries(urls, root);
  300. // Add an additional URL to the first entry
  301. entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://keepassxc.org");
  302. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  303. QCOMPARE(result.length(), 1);
  304. QCOMPARE(result[0]->url(), QString("https://github.com/"));
  305. // Search the additional URL. It should return the same entry
  306. auto additionalResult = m_browserService->searchEntries(db, "https://keepassxc.org", "https://keepassxc.org");
  307. QCOMPARE(additionalResult.length(), 1);
  308. QCOMPARE(additionalResult[0]->url(), QString("https://github.com/"));
  309. }
  310. void TestBrowser::testInvalidEntries()
  311. {
  312. auto db = QSharedPointer<Database>::create();
  313. auto* root = db->rootGroup();
  314. const QString url("https://github.com");
  315. const QString submitUrl("https://github.com/session");
  316. QStringList urls = {
  317. "https://github.com/login",
  318. "https:///github.com/", // Extra '/'
  319. "http://github.com/**//*",
  320. "http://*.github.com/login",
  321. "//github.com", // fromUserInput() corrects this one.
  322. "github.com/{}<>",
  323. "http:/example.com",
  324. };
  325. createEntries(urls, root);
  326. browserSettings()->setMatchUrlScheme(true);
  327. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  328. QCOMPARE(result.length(), 2);
  329. QCOMPARE(result[0]->url(), QString("https://github.com/login"));
  330. QCOMPARE(result[1]->url(), QString("//github.com"));
  331. // Test the URL's directly
  332. QCOMPARE(m_browserService->handleURL(urls[0], url, submitUrl), true);
  333. QCOMPARE(m_browserService->handleURL(urls[1], url, submitUrl), false);
  334. QCOMPARE(m_browserService->handleURL(urls[2], url, submitUrl), false);
  335. QCOMPARE(m_browserService->handleURL(urls[3], url, submitUrl), false);
  336. QCOMPARE(m_browserService->handleURL(urls[4], url, submitUrl), true);
  337. QCOMPARE(m_browserService->handleURL(urls[5], url, submitUrl), false);
  338. }
  339. void TestBrowser::testSubdomainsAndPaths()
  340. {
  341. auto db = QSharedPointer<Database>::create();
  342. auto* root = db->rootGroup();
  343. QStringList urls = {
  344. "https://www.github.com/login/page.xml",
  345. "https://login.github.com/",
  346. "https://github.com",
  347. "http://www.github.com",
  348. "http://login.github.com/pathtonowhere",
  349. ".github.com", // Invalid URL
  350. "www.github.com/",
  351. "https://github", // Invalid URL
  352. "https://hub.com" // Should not return
  353. };
  354. createEntries(urls, root);
  355. browserSettings()->setMatchUrlScheme(false);
  356. auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  357. QCOMPARE(result.length(), 1);
  358. QCOMPARE(result[0]->url(), QString("https://github.com"));
  359. // With www subdomain
  360. result = m_browserService->searchEntries(db, "https://www.github.com", "https://www.github.com/session");
  361. QCOMPARE(result.length(), 4);
  362. QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
  363. QCOMPARE(result[1]->url(), QString("https://github.com")); // Accepts any subdomain
  364. QCOMPARE(result[2]->url(), QString("http://www.github.com"));
  365. QCOMPARE(result[3]->url(), QString("www.github.com/"));
  366. // With www subdomain omitted
  367. root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Enable);
  368. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  369. root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Inherit);
  370. QCOMPARE(result.length(), 4);
  371. QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
  372. QCOMPARE(result[1]->url(), QString("https://github.com"));
  373. QCOMPARE(result[2]->url(), QString("http://www.github.com"));
  374. QCOMPARE(result[3]->url(), QString("www.github.com/"));
  375. // With scheme matching there should be only 1 result
  376. browserSettings()->setMatchUrlScheme(true);
  377. result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
  378. QCOMPARE(result.length(), 1);
  379. QCOMPARE(result[0]->url(), QString("https://github.com"));
  380. // Test site with subdomain in the site URL
  381. QStringList entryURLs = {
  382. "https://accounts.example.com",
  383. "https://accounts.example.com/path",
  384. "https://subdomain.example.com/",
  385. "https://another.accounts.example.com/",
  386. "https://another.subdomain.example.com/",
  387. "https://example.com/",
  388. "https://example" // Invalid URL
  389. };
  390. createEntries(entryURLs, root);
  391. result = m_browserService->searchEntries(db, "https://accounts.example.com/", "https://accounts.example.com/");
  392. QCOMPARE(result.length(), 3);
  393. QCOMPARE(result[0]->url(), QString("https://accounts.example.com"));
  394. QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
  395. QCOMPARE(result[2]->url(), QString("https://example.com/")); // Accepts any subdomain
  396. result = m_browserService->searchEntries(
  397. db, "https://another.accounts.example.com/", "https://another.accounts.example.com/");
  398. QCOMPARE(result.length(), 4);
  399. QCOMPARE(result[0]->url(),
  400. QString("https://accounts.example.com")); // Accepts any subdomain under accounts.example.com
  401. QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
  402. QCOMPARE(result[2]->url(), QString("https://another.accounts.example.com/"));
  403. QCOMPARE(result[3]->url(), QString("https://example.com/")); // Accepts one or more subdomains
  404. // Test local files. It should be a direct match.
  405. QStringList localFiles = {"file:///Users/testUser/tests/test.html"};
  406. createEntries(localFiles, root);
  407. // With local files, url is always set to the file scheme + ://. Submit URL holds the actual URL.
  408. result = m_browserService->searchEntries(db, "file://", "file:///Users/testUser/tests/test.html");
  409. QCOMPARE(result.length(), 1);
  410. }
  411. QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const
  412. {
  413. QList<Entry*> entries;
  414. for (int i = 0; i < urls.length(); ++i) {
  415. auto entry = new Entry();
  416. entry->setGroup(root);
  417. entry->beginUpdate();
  418. entry->setUrl(urls[i]);
  419. entry->setUsername(QString("User %1").arg(i));
  420. entry->setUuid(QUuid::createUuid());
  421. entry->setTitle(QString("Name_%1").arg(entry->uuidToHex()));
  422. entry->endUpdate();
  423. entries.push_back(entry);
  424. }
  425. return entries;
  426. }
  427. void TestBrowser::testValidURLs()
  428. {
  429. QHash<QString, bool> urls;
  430. urls["https://github.com/login"] = true;
  431. urls["https:///github.com/"] = false;
  432. urls["http://github.com/**//*"] = false;
  433. urls["http://*.github.com/login"] = false;
  434. urls["//github.com"] = true;
  435. urls["github.com/{}<>"] = false;
  436. urls["http:/example.com"] = false;
  437. urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true;
  438. urls["file:///Users/testUser/Code/test.html"] = true;
  439. urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true;
  440. QHashIterator<QString, bool> i(urls);
  441. while (i.hasNext()) {
  442. i.next();
  443. QCOMPARE(Tools::checkUrlValid(i.key()), i.value());
  444. }
  445. }
  446. void TestBrowser::testBestMatchingCredentials()
  447. {
  448. auto db = QSharedPointer<Database>::create();
  449. auto* root = db->rootGroup();
  450. // Test with simple URL entries
  451. QStringList urls = {"https://github.com/loginpage", "https://github.com/justsomepage", "https://github.com/"};
  452. auto entries = createEntries(urls, root);
  453. browserSettings()->setBestMatchOnly(true);
  454. QString siteUrl = "https://github.com/loginpage";
  455. auto result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  456. auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  457. QCOMPARE(sorted.size(), 1);
  458. QCOMPARE(sorted[0]->url(), siteUrl);
  459. siteUrl = "https://github.com/justsomepage";
  460. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  461. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  462. QCOMPARE(sorted.size(), 1);
  463. QCOMPARE(sorted[0]->url(), siteUrl);
  464. siteUrl = "https://github.com/";
  465. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  466. sorted = m_browserService->sortEntries(entries, siteUrl, siteUrl);
  467. QCOMPARE(sorted.size(), 1);
  468. QCOMPARE(sorted[0]->url(), siteUrl);
  469. // Without best-matching the URL with the path should be returned first
  470. browserSettings()->setBestMatchOnly(false);
  471. siteUrl = "https://github.com/loginpage";
  472. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  473. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  474. QCOMPARE(sorted.size(), 3);
  475. QCOMPARE(sorted[0]->url(), siteUrl);
  476. // Test with subdomains
  477. QStringList subdomainsUrls = {"https://sub.github.com/loginpage",
  478. "https://sub.github.com/justsomepage",
  479. "https://bus.github.com/justsomepage",
  480. "https://subdomain.example.com/",
  481. "https://subdomain.example.com",
  482. "https://example.com"};
  483. entries = createEntries(subdomainsUrls, root);
  484. browserSettings()->setBestMatchOnly(true);
  485. siteUrl = "https://sub.github.com/justsomepage";
  486. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  487. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  488. QCOMPARE(sorted.size(), 1);
  489. QCOMPARE(sorted[0]->url(), siteUrl);
  490. siteUrl = "https://github.com/justsomepage";
  491. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  492. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  493. QCOMPARE(sorted.size(), 1);
  494. QCOMPARE(sorted[0]->url(), siteUrl);
  495. siteUrl = "https://sub.github.com/justsomepage?wehavesomeextra=here";
  496. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  497. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  498. QCOMPARE(sorted.size(), 1);
  499. QCOMPARE(sorted[0]->url(), QString("https://sub.github.com/justsomepage"));
  500. // The matching should not care if there's a / path or not.
  501. siteUrl = "https://subdomain.example.com/";
  502. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  503. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  504. QCOMPARE(sorted.size(), 2);
  505. QCOMPARE(sorted[0]->url(), QString("https://subdomain.example.com"));
  506. QCOMPARE(sorted[1]->url(), QString("https://subdomain.example.com/"));
  507. // Entries with https://example.com should be still returned even if the site URL has a subdomain. Those have the
  508. // best match.
  509. db = QSharedPointer<Database>::create();
  510. root = db->rootGroup();
  511. QStringList domainUrls = {"https://example.com", "https://example.com", "https://other.example.com"};
  512. entries = createEntries(domainUrls, root);
  513. siteUrl = "https://subdomain.example.com";
  514. result = m_browserService->searchEntries(db, siteUrl, siteUrl);
  515. sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
  516. QCOMPARE(sorted.size(), 2);
  517. QCOMPARE(sorted[0]->url(), QString("https://example.com"));
  518. QCOMPARE(sorted[1]->url(), QString("https://example.com"));
  519. // https://github.com/keepassxreboot/keepassxc/issues/4754
  520. db = QSharedPointer<Database>::create();
  521. root = db->rootGroup();
  522. QStringList fooUrls = {"https://example.com/foo", "https://example.com/bar"};
  523. entries = createEntries(fooUrls, root);
  524. for (const auto& url : fooUrls) {
  525. result = m_browserService->searchEntries(db, url, url);
  526. sorted = m_browserService->sortEntries(result, url, url);
  527. QCOMPARE(sorted.size(), 1);
  528. QCOMPARE(sorted[0]->url(), QString(url));
  529. }
  530. // https://github.com/keepassxreboot/keepassxc/issues/4734
  531. db = QSharedPointer<Database>::create();
  532. root = db->rootGroup();
  533. QStringList testUrls = {"http://some.domain.tld/somePath", "http://some.domain.tld/otherPath"};
  534. entries = createEntries(testUrls, root);
  535. for (const auto& url : testUrls) {
  536. result = m_browserService->searchEntries(db, url, url);
  537. sorted = m_browserService->sortEntries(result, url, url);
  538. QCOMPARE(sorted.size(), 1);
  539. QCOMPARE(sorted[0]->url(), QString(url));
  540. }
  541. }
  542. void TestBrowser::testBestMatchingWithAdditionalURLs()
  543. {
  544. auto db = QSharedPointer<Database>::create();
  545. auto* root = db->rootGroup();
  546. QStringList urls = {"https://github.com/loginpage", "https://test.github.com/", "https://github.com/"};
  547. auto entries = createEntries(urls, root);
  548. browserSettings()->setBestMatchOnly(true);
  549. // Add an additional URL to the first entry
  550. entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://test.github.com/anotherpage");
  551. // The first entry should be triggered
  552. auto result = m_browserService->searchEntries(
  553. db, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
  554. auto sorted = m_browserService->sortEntries(
  555. result, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
  556. QCOMPARE(sorted.length(), 1);
  557. QCOMPARE(sorted[0]->url(), urls[0]);
  558. }