Skip to content
This repository was archived by the owner on May 27, 2019. It is now read-only.

Commit 604f560

Browse files
eraydmax-baz
authored andcommitted
Add button to launch URLs & support basic auth (#224, fixes #214, fixes #103)
- Host app will now return a "url" field - Browser plugin has a "launch URL" button for each entry - Browser plugin has a new hotkey (g) to launch the URL - Supply basic auth credentials if necessary when launching via the extension Note that basic auth will only work in Firefox >= 54. Older versions don't have the required API to do this.
1 parent 31a3cfc commit 604f560

File tree

9 files changed

+229
-13
lines changed

9 files changed

+229
-13
lines changed

browserpass.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Login struct {
2323
Password string `json:"p"`
2424
OTP string `json:"digits"`
2525
OTPLabel string `json:"label"`
26+
URL string `json:"url"`
2627
}
2728

2829
var endianness = binary.LittleEndian
@@ -212,14 +213,21 @@ func parseLogin(r io.Reader) (*Login, error) {
212213
login.Password = scanner.Text()
213214

214215
// Keep reading file for string in "login:", "username:" or "user:" format (case insensitive).
215-
re := regexp.MustCompile("(?i)^(login|username|user):")
216+
userPattern := regexp.MustCompile("(?i)^(login|username|user):")
217+
urlPattern := regexp.MustCompile("(?i)^(url|link|website|web|site):")
216218
for scanner.Scan() {
217219
line := scanner.Text()
218220
parseTotp(line, login)
219-
replaced := re.ReplaceAllString(line, "")
221+
replaced := userPattern.ReplaceAllString(line, "")
220222
if len(replaced) != len(line) {
221223
login.Username = strings.TrimSpace(replaced)
222224
}
225+
if (login.URL == "") {
226+
replaced = urlPattern.ReplaceAllString(line, "")
227+
if len(replaced) != len(line) {
228+
login.URL = strings.TrimSpace(replaced)
229+
}
230+
}
223231
}
224232

225233
// if an unlabelled OTP is present, label it with the username

chrome/background.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,44 @@ function onMessage(request, sender, sendResponse) {
9595
localStorage.getItem("use_fuzzy_search") != "false";
9696
sendResponse({ use_fuzzy_search: use_fuzzy_search });
9797
}
98+
99+
// spawn a new tab with pre-provided credentials
100+
if (request.action == "launch") {
101+
chrome.tabs.create({url: request.url}, function (tab) {
102+
var authAttempted = false;
103+
chrome.webRequest.onAuthRequired.addListener(
104+
function authListener(requestDetails) {
105+
// only supply credentials if this is the first time for this tab
106+
if (authAttempted) {
107+
return {};
108+
}
109+
authAttempted = true;
110+
// remove event listeners once tab loading is complete
111+
chrome.tabs.onUpdated.addListener(function statusListener(tabId, info) {
112+
if (info.status === "complete") {
113+
chrome.tabs.onUpdated.removeListener(statusListener);
114+
chrome.webRequest.onAuthRequired.removeListener(authListener);
115+
}
116+
});
117+
// ask the user before sending credentials over an insecure connection
118+
if (!requestDetails.url.match(/^https:/i)) {
119+
var message =
120+
"You are about to send login credentials via an insecure connection!\n\n" +
121+
"Are you sure you want to do this? If there is an attacker watching your " +
122+
"network traffic, they may be able to see your username and password.\n\n" +
123+
"URL: " + requestDetails.url
124+
;
125+
if (!confirm(message)) {
126+
return {};
127+
}
128+
}
129+
return {authCredentials: {username: request.username, password: request.password}};
130+
},
131+
{urls: ["*://*/*"], tabId: tab.id},
132+
["blocking"]
133+
);
134+
});
135+
}
98136
}
99137

100138
function onTabUpdated(tabId, changeInfo, tab) {

chrome/icon-globe.svg

Lines changed: 89 additions & 0 deletions
Loading

chrome/manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"author": "Danny van Kooten",
88
"homepage_url": "https://github.com/dannyvankooten/browserpass",
99
"background": {
10-
"persistent": false,
10+
"persistent": true,
1111
"scripts": [
1212
"background.js"
1313
]
@@ -28,6 +28,8 @@
2828
"nativeMessaging",
2929
"notifications",
3030
"storage",
31+
"webRequest",
32+
"webRequestBlocking",
3133
"http://*/*",
3234
"https://*/*"
3335
],

chrome/script.browserify.js

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
var m = require("mithril");
44
var FuzzySort = require("fuzzysort");
5+
var Tldjs = require("tldjs");
56
var app = "com.dannyvankooten.browserpass";
67
var activeTab;
78
var searching = false;
@@ -45,6 +46,10 @@ function view() {
4546

4647
return m("div.entry", [
4748
m(selector, options, login),
49+
m("button.launch.url", {
50+
onclick: launchURL.bind({ entry: login, what: "url" }),
51+
tabindex: -1
52+
}),
4853
m("button.copy.username", {
4954
onclick: loginToClipboard.bind({ entry: login, what: "username" }),
5055
tabindex: -1
@@ -76,7 +81,7 @@ function view() {
7681
type: "text",
7782
id: "search-field",
7883
name: "s",
79-
placeholder: "Search passwords..",
84+
placeholder: "Search passwords...",
8085
autocomplete: "off",
8186
autofocus: "on",
8287
oninput: filterLogins
@@ -150,7 +155,6 @@ function showFilterHint(show=true) {
150155

151156
function submitSearchForm(e) {
152157
e.preventDefault();
153-
154158
if (fillOnSubmit && logins.length > 0) {
155159
// fill using the first result
156160
getLoginData.bind(logins[0])();
@@ -232,6 +236,50 @@ function getFaviconUrl(domain) {
232236
return null;
233237
}
234238

239+
function launchURL() {
240+
var what = this.what;
241+
var entry = this.entry;
242+
chrome.runtime.sendNativeMessage(
243+
app,
244+
{ action: "get", entry: this.entry },
245+
function(response) {
246+
if (chrome.runtime.lastError) {
247+
error = chrome.runtime.lastError.message;
248+
m.redraw();
249+
return;
250+
}
251+
// get url from login path if not available in the host app response
252+
if (!response.hasOwnProperty("url") || response.url.length == 0) {
253+
var parts = entry.split(/\//).reverse();
254+
for (var i in parts) {
255+
var part = parts[i];
256+
var info = Tldjs.parse(part);
257+
if (info.isValid && info.tldExists && info.domain !== null && info.hostname === part) {
258+
response.url = part;
259+
break;
260+
}
261+
}
262+
}
263+
// if a url is present, then launch a new tab via the background script
264+
if (response.hasOwnProperty("url") && response.url.length > 0) {
265+
var url = response.url.match(/^([a-z]+:)?\/\//i) ? response.url : "http://" + response.url;
266+
chrome.runtime.sendMessage({action: "launch", url: url, username: response.u, password: response.p});
267+
window.close();
268+
return;
269+
}
270+
// no url available
271+
if (!response.hasOwnProperty("url")) {
272+
resetWithError(
273+
"Unable to determine the URL for this entry. If you have defined one in the password file, " +
274+
"your host application must be at least v2.0.14 for this to be usable."
275+
);
276+
} else {
277+
resetWithError("Unable to determine the URL for this entry.");
278+
}
279+
}
280+
);
281+
}
282+
235283
function getLoginData() {
236284
searching = true;
237285
logins = resultLogins = [];
@@ -298,16 +346,18 @@ function keyHandler(e) {
298346
break;
299347
case "c":
300348
if (e.target.id != "search-field" && e.ctrlKey) {
301-
document.activeElement["nextElementSibling"][
302-
"nextElementSibling"
303-
].click();
349+
document.activeElement.parentNode.querySelector("button.copy.password").click();
304350
}
305351
break;
306352
case "C":
307353
if (e.target.id != "search-field") {
308-
document.activeElement["nextElementSibling"].click();
354+
document.activeElement.parentNode.querySelector("button.copy.username").click();
309355
}
310356
break;
357+
case "g":
358+
if (e.target.id != "search-field") {
359+
document.activeElement.parentNode.querySelector("button.launch.url").click();
360+
}
311361
}
312362
}
313363

@@ -336,3 +386,18 @@ function oncreate() {
336386
document.getElementById("search-field").focus();
337387
}, 100);
338388
}
389+
390+
function resetWithError(errMsg) {
391+
domain = '';
392+
logins = resultLogins = [];
393+
fillOnSubmit = false;
394+
searching = false;
395+
var filterSearch = document.getElementById("filter-search");
396+
filterSearch.style.display = "none";
397+
filterSearch.textContent = '';
398+
var searchField = document.getElementById("search-field");
399+
searchField.setAttribute("placeholder", "Search passwords...");
400+
error = errMsg;
401+
m.redraw();
402+
searchField.focus();
403+
}

chrome/styles.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,17 @@ body {
5858
border-bottom: 1px dotted #ccc;
5959
}
6060

61-
.copy {
61+
.copy, .launch {
6262
width: 32px;
6363
border: 0;
6464
cursor: pointer;
6565
}
6666

67+
.url {
68+
background: no-repeat url('icon-globe.svg') center;
69+
background-size: 16px 16px;
70+
}
71+
6772
.username {
6873
background: no-repeat url('icon-user.svg') center;
6974
background-size: 16px 16px;

firefox/manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"page": "options.html"
1010
},
1111
"background": {
12-
"persistent": false,
12+
"persistent": true,
1313
"scripts": [
1414
"background.js"
1515
]
@@ -26,6 +26,8 @@
2626
"nativeMessaging",
2727
"notifications",
2828
"storage",
29+
"webRequest",
30+
"webRequestBlocking",
2931
"http://*/*",
3032
"https://*/*"
3133
],

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"dependencies": {
33
"browserify": "^14.4.0",
44
"mithril": "^1.1.4",
5-
"fuzzysort": "^1.1.0"
5+
"fuzzysort": "^1.1.0",
6+
"tldjs": "^2.3.1"
67
}
78
}

yarn.lock

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ [email protected]:
638638
version "1.3.2"
639639
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
640640

641-
punycode@^1.3.2:
641+
punycode@^1.3.2, punycode@^1.4.1:
642642
version "1.4.1"
643643
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
644644

@@ -800,6 +800,12 @@ timers-browserify@^1.0.1:
800800
dependencies:
801801
process "~0.11.0"
802802

803+
tldjs@^2.3.1:
804+
version "2.3.1"
805+
resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039"
806+
dependencies:
807+
punycode "^1.4.1"
808+
803809
to-arraybuffer@^1.0.0:
804810
version "1.0.1"
805811
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"

0 commit comments

Comments
 (0)