Skip to content

Commit f19a44d

Browse files
eraydmax-baz
authored andcommitted
Replace tldjs with @browserpass/url, check port (#179)
1 parent 02efc62 commit f19a44d

File tree

7 files changed

+82
-45
lines changed

7 files changed

+82
-45
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ If not, repeat the installation instructions for the extension.
103103

104104
Browserpass was designed with an assumption that certain conventions are being followed when organizing your password store.
105105

106-
1. In order to benefit of phishing attack protection, a password entry file, or any of its parent folders, must contain a full domain name (including TLD like `.com`) in their name in order to automatically match a website. However, entries which do not contain such a domain in their name may still be manually selected.
106+
1. In order to benefit of phishing attack protection, a password entry file, or any of its parent folders, must contain a full domain name (including TLD like `.com`) and optionally port in their name in order to automatically match a website. However, entries which do not contain such a domain in their name may still be manually selected.
107107

108108
File names are not allowed to contain `\` or `/` characters, because both of them are considered to be path separators.
109109

@@ -171,6 +171,8 @@ In order for Browserpass to correctly determine matching entries, it is expected
171171
172172
Browserpass will display entries for the current domain, as well as all parent entries, but not entries from different subdomains. Suppose you are currently on `https://v3.app.example.com`, Browserpass will present all the following entries in popup (if they exist): `v3.app.example.com`, `app.example.com`, `example.com`; but it will not present entries like `v2.app.example.com` or `wiki.example.com`.
173173
174+
Browserpass can also distinguish credentials meant for different ports, so for example an entry `example.com.gpg` will show up in Browserpass popup when you browse `example.com` on any port, however an entry `example.com:8080.gpg` will only show up on `8080` port.
175+
174176
Finally Browserpass will also present entries that you have recently used on this domain, even if they don't actually meet the usual matching requirements. Suppose you have a password for `amazon.com`, but you open `https://amazon.co.uk`, at first Browserpass will present no entries (because nothing matches `amazon.co.uk`), but if you hit <kbd>Backspace</kbd>, find `amazon.com` and use it to login, next time you visit `https://amazon.co.uk` and open Browserpass, `amazon.com` entry will already be present.
175177
176178
The sorting algorithm implemented in Browserpass will use several intuitions to try to order results in the most expected way for a user:

src/background.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require("chrome-extension-async");
55
const sha1 = require("sha1");
66
const idb = require("idb");
7+
const BrowserpassURL = require("@browserpass/url");
78
const helpers = require("./helpers");
89

910
// native application id
@@ -137,7 +138,7 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) {
137138

138139
try {
139140
const tab = await chrome.tabs.get(tabId);
140-
badgeCache.settings.host = new URL(tab.url).hostname;
141+
badgeCache.settings.origin = new URL(tab.url).origin;
141142
} catch (e) {
142143
throw new Error(`Unable to determine domain of the tab with id ${tabId}`);
143144
}
@@ -146,7 +147,7 @@ async function updateMatchingPasswordsCount(tabId, forceRefresh = false) {
146147
const files = helpers.ignoreFiles(badgeCache.files, badgeCache.settings);
147148
const logins = helpers.prepareLogins(files, badgeCache.settings);
148149
const matchedPasswordsCount = logins.reduce(
149-
(acc, login) => acc + (login.recent.count || login.inCurrentDomain ? 1 : 0),
150+
(acc, login) => acc + (login.recent.count || login.inCurrentHost ? 1 : 0),
150151
0
151152
);
152153

@@ -228,13 +229,14 @@ async function saveRecent(settings, login, remove = false) {
228229
login.recent.count++;
229230
}
230231
login.recent.when = Date.now();
231-
settings.recent[sha1(settings.host + sha1(login.store.id + sha1(login.login)))] = login.recent;
232+
settings.recent[sha1(settings.origin + sha1(login.store.id + sha1(login.login)))] =
233+
login.recent;
232234

233235
// save to local storage
234236
localStorage.setItem("recent", JSON.stringify(settings.recent));
235237

236238
// a new entry was added to the popup matching list, need to refresh the count
237-
if (!login.inCurrentDomain && login.recent.count === 1) {
239+
if (!login.inCurrentHost && login.recent.count === 1) {
238240
updateMatchingPasswordsCount(settings.tab.id, true);
239241
}
240242

@@ -246,7 +248,7 @@ async function saveRecent(settings, login, remove = false) {
246248
db.createObjectStore("log", { keyPath: "time" });
247249
}
248250
});
249-
await db.add("log", { time: Date.now(), host: settings.host, login: login.login });
251+
await db.add("log", { time: Date.now(), host: settings.origin, login: login.login });
250252
} catch {
251253
// ignore any errors and proceed without saving a log entry to Indexed DB
252254
}
@@ -266,7 +268,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS
266268
request = Object.assign(deepCopy(request), {
267269
allowForeign: allowForeign,
268270
allowNoSecret: allowNoSecret,
269-
foreignFills: settings.foreignFills[settings.host] || {}
271+
foreignFills: settings.foreignFills[settings.origin] || {}
270272
});
271273

272274
let perFrameResults = await chrome.tabs.executeScript(settings.tab.id, {
@@ -284,10 +286,10 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS
284286
let foreignFillsChanged = false;
285287
for (let frame of perFrameResults) {
286288
if (typeof frame.foreignFill !== "undefined") {
287-
if (typeof settings.foreignFills[settings.host] === "undefined") {
288-
settings.foreignFills[settings.host] = {};
289+
if (typeof settings.foreignFills[settings.origin] === "undefined") {
290+
settings.foreignFills[settings.origin] = {};
289291
}
290-
settings.foreignFills[settings.host][frame.foreignOrigin] = frame.foreignFill;
292+
settings.foreignFills[settings.origin][frame.foreignOrigin] = frame.foreignFill;
291293
foreignFillsChanged = true;
292294
}
293295
}
@@ -310,7 +312,7 @@ async function dispatchFill(settings, request, allFrames, allowForeign, allowNoS
310312
async function dispatchFocusOrSubmit(settings, request, allFrames, allowForeign) {
311313
request = Object.assign(deepCopy(request), {
312314
allowForeign: allowForeign,
313-
foreignFills: settings.foreignFills[settings.host] || {}
315+
foreignFills: settings.foreignFills[settings.origin] || {}
314316
});
315317

316318
let perFrameResults = await chrome.tabs.executeScript(settings.tab.id, {
@@ -417,7 +419,7 @@ async function fillFields(settings, login, fields) {
417419
// try again using all available frames if we couldn't fill an "important" field
418420
if (
419421
!filledFields.includes(importantFieldToFill) &&
420-
settings.foreignFills[settings.host] !== false
422+
settings.foreignFills[settings.origin] !== false
421423
) {
422424
allowForeign = true;
423425
filledFields = filledFields.concat(
@@ -455,7 +457,7 @@ async function fillFields(settings, login, fields) {
455457
}
456458

457459
// try again using all available frames
458-
if (!filledFields.length && settings.foreignFills[settings.host] !== false) {
460+
if (!filledFields.length && settings.foreignFills[settings.origin] !== false) {
459461
allowForeign = true;
460462
filledFields = filledFields.concat(
461463
await dispatchFill(
@@ -581,7 +583,9 @@ async function getFullSettings() {
581583
// Fill current tab info
582584
try {
583585
settings.tab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0];
584-
settings.host = new URL(settings.tab.url).hostname;
586+
let originInfo = new BrowserpassURL(settings.tab.url);
587+
settings.host = originInfo.host; // TODO remove this after OTP extension is migrated
588+
settings.origin = originInfo.origin;
585589
} catch (e) {}
586590

587591
return settings;
@@ -772,7 +776,7 @@ async function handleMessage(settings, message, sendResponse) {
772776
case "launch":
773777
case "launchInNewTab":
774778
try {
775-
var url = message.login.fields.url || message.login.domain;
779+
var url = message.login.fields.url || message.login.host;
776780
if (!url) {
777781
throw new Error("No URL is defined for this entry");
778782
}
@@ -1084,6 +1088,7 @@ function triggerOTPExtension(settings, action, otp) {
10841088
otp: otp,
10851089
settings: {
10861090
host: settings.host,
1091+
origin: settings.origin,
10871092
tab: settings.tab
10881093
}
10891094
})

src/helpers.js

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
"use strict";
33

44
const FuzzySort = require("fuzzysort");
5-
const TldJS = require("tldjs");
65
const sha1 = require("sha1");
76
const ignore = require("ignore");
7+
const BrowserpassURL = require("@browserpass/url");
88

99
module.exports = {
10-
pathToDomain,
1110
prepareLogins,
1211
filterSortLogins,
1312
ignoreFiles
@@ -18,30 +17,30 @@ module.exports = {
1817
/**
1918
* Get the deepest available domain component of a path
2019
*
21-
* @since 3.0.0
20+
* @since 3.2.3
2221
*
2322
* @param string path Path to parse
24-
* @param string currentHost Current hostname for the active tab
25-
* @return string|null Extracted domain
23+
* @param object currentHost Current host info for the active tab
24+
* @return object|null Extracted domain info
2625
*/
27-
function pathToDomain(path, currentHost) {
26+
function pathToInfo(path, currentHost) {
2827
var parts = path.split(/\//).reverse();
2928
for (var key in parts) {
3029
if (parts[key].indexOf("@") >= 0) {
3130
continue;
3231
}
33-
var t = TldJS.parse(parts[key]);
32+
var info = BrowserpassURL.parseHost(parts[key]);
3433

3534
// Part is considered to be a domain component in one of the following cases:
3635
// - it is a valid domain with well-known TLD (github.com, login.github.com)
37-
// - it is a valid domain with unknown TLD but the current host is it's subdomain (login.pi.hole)
38-
// - it is or isnt a valid domain but the current host matches it EXACTLY (localhost, pi.hole)
36+
// - it is or isn't a valid domain with any TLD but the current host matches it EXACTLY (localhost, pi.hole)
37+
// - it is or isn't a valid domain with any TLD but the current host is its subdomain (login.pi.hole)
3938
if (
40-
t.isValid &&
41-
((t.domain !== null && (t.tldExists || currentHost.endsWith(`.${t.hostname}`))) ||
42-
currentHost === t.hostname)
39+
info.validDomain ||
40+
currentHost.hostname === info.hostname ||
41+
currentHost.hostname.endsWith(`.${info.hostname}`)
4342
) {
44-
return t.hostname;
43+
return info;
4544
}
4645
}
4746

@@ -60,6 +59,7 @@ function pathToDomain(path, currentHost) {
6059
function prepareLogins(files, settings) {
6160
const logins = [];
6261
let index = 0;
62+
let origin = new BrowserpassURL(settings.origin);
6363

6464
for (let storeId in files) {
6565
for (let key in files[storeId]) {
@@ -70,11 +70,40 @@ function prepareLogins(files, settings) {
7070
login: files[storeId][key].replace(/\.gpg$/i, ""),
7171
allowFill: true
7272
};
73-
login.domain = pathToDomain(storeId + "/" + login.login, settings.host);
74-
login.inCurrentDomain =
75-
settings.host == login.domain || settings.host.endsWith("." + login.domain);
73+
74+
// extract url info from path
75+
let pathInfo = pathToInfo(storeId + "/" + login.login, origin);
76+
if (pathInfo) {
77+
// set assumed host
78+
login.host = pathInfo.port
79+
? `${pathInfo.hostname}:${pathInfo.port}`
80+
: pathInfo.hostname;
81+
82+
// check whether extracted path info matches the current origin
83+
login.inCurrentHost = origin.hostname === pathInfo.hostname;
84+
85+
// check whether the current origin is subordinate to extracted path info, meaning:
86+
// - that the path info is not a single level domain (e.g. com, net, local)
87+
// - and that the current origin is a subdomain of that path info
88+
if (
89+
pathInfo.hostname.includes(".") &&
90+
origin.hostname.endsWith(`.${pathInfo.hostname}`)
91+
) {
92+
login.inCurrentHost = true;
93+
}
94+
95+
// filter out entries with a non-matching port
96+
if (pathInfo.port && pathInfo.port !== origin.port) {
97+
login.inCurrentHost = false;
98+
}
99+
} else {
100+
login.host = null;
101+
login.inCurrentHost = false;
102+
}
103+
104+
// update recent counter
76105
login.recent =
77-
settings.recent[sha1(settings.host + sha1(login.store.id + sha1(login.login)))];
106+
settings.recent[sha1(settings.origin + sha1(login.store.id + sha1(login.login)))];
78107
if (!login.recent) {
79108
login.recent = {
80109
when: 0,
@@ -124,7 +153,7 @@ function filterSortLogins(logins, searchQuery, currentDomainOnly) {
124153
return false;
125154
});
126155
var remainingInCurrentDomain = candidates.filter(
127-
login => login.inCurrentDomain && !login.recent.count
156+
login => login.inCurrentHost && !login.recent.count
128157
);
129158
candidates = recent.concat(remainingInCurrentDomain);
130159
}

src/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
}
1616
],
1717
"dependencies": {
18+
"@browserpass/url": "^1.1.3",
1819
"chrome-extension-async": "^3.3.2",
1920
"fuzzysort": "^1.1.4",
2021
"idb": "^4.0.3",
2122
"ignore": "^5.1.4",
2223
"mithril": "^1.1.0",
2324
"moment": "^2.24.0",
24-
"sha1": "^1.1.1",
25-
"tldjs": "^2.3.0"
25+
"sha1": "^1.1.1"
2626
},
2727
"devDependencies": {
2828
"browserify": "^16.2.3",

src/popup/popup.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async function run() {
4747
throw new Error(settings.hostError.params.message);
4848
}
4949

50-
if (typeof settings.host === "undefined") {
50+
if (typeof settings.origin === "undefined") {
5151
throw new Error("Unable to retrieve current tab information");
5252
}
5353

src/popup/searchinterface.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ function SearchInterface(popup) {
2929
*/
3030
function view(ctl, params) {
3131
var self = this;
32+
var host = new URL(this.popup.settings.origin).host;
3233
return m(
3334
"form.part.search",
3435
{
@@ -59,7 +60,7 @@ function view(ctl, params) {
5960
[
6061
this.popup.currentDomainOnly
6162
? m("div.hint.badge", [
62-
this.popup.settings.host,
63+
host,
6364
m("div.remove-hint", {
6465
onclick: function(e) {
6566
var target = document.querySelector(

src/yarn.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
# yarn lockfile v1
33

44

5+
"@browserpass/url@^1.1.3":
6+
version "1.1.3"
7+
resolved "https://registry.yarnpkg.com/@browserpass/url/-/url-1.1.3.tgz#8b94896198e7032b80c7d82e836d59e42b769669"
8+
integrity sha512-XWhYNZo0BGcyFxkum8g1DeUkFw9QyqcaRTOEinyPSpIubh+aL2VQxbdItitVtS5qkkowqaA5Xt1H2pAqu8ic8w==
9+
dependencies:
10+
punycode "^2.1.1"
11+
512
JSONStream@^1.0.3:
613
version "1.3.5"
714
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@@ -1089,7 +1096,7 @@ punycode@^1.3.2, punycode@^1.4.1:
10891096
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
10901097
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
10911098

1092-
punycode@^2.1.0:
1099+
punycode@^2.1.0, punycode@^2.1.1:
10931100
version "2.1.1"
10941101
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
10951102
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -1347,13 +1354,6 @@ timers-browserify@^1.0.1:
13471354
dependencies:
13481355
process "~0.11.0"
13491356

1350-
tldjs@^2.3.0:
1351-
version "2.3.1"
1352-
resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039"
1353-
integrity sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==
1354-
dependencies:
1355-
punycode "^1.4.1"
1356-
13571357
to-arraybuffer@^1.0.0:
13581358
version "1.0.1"
13591359
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"

0 commit comments

Comments
 (0)