Implement fetch (#4)

* Implement fetch & launch action
* Fix scroll behavior
* Remove horizontal scrollbar
* Add stubs for filling username / password
* Don't pre-filter for chrome:// URLs
* Add inject.js
This commit is contained in:
Erayd
2018-04-20 00:40:22 +12:00
committed by GitHub
parent 92f2ecea1b
commit 9ca48cea41
9 changed files with 175 additions and 13 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,4 @@
/chrome /chrome
/src/node_modules /src/node_modules
/src/js /src/js
/src/*.js
/src/*.log /src/*.log
!/src/*.src.js

View File

@@ -15,7 +15,8 @@ CHROME_FILES := manifest.json \
popup/*.svg popup/*.svg
CHROME_FILES := $(wildcard $(addprefix src/,$(CHROME_FILES))) \ CHROME_FILES := $(wildcard $(addprefix src/,$(CHROME_FILES))) \
src/js/background.dist.js \ src/js/background.dist.js \
src/js/popup.dist.js src/js/popup.dist.js \
src/js/inject.dist.js
CHROME_FILES := $(patsubst src/%,chrome/%,$(CHROME_FILES)) CHROME_FILES := $(patsubst src/%,chrome/%,$(CHROME_FILES))
.PHONY: chrome .PHONY: chrome

View File

@@ -5,7 +5,7 @@ CLEAN_FILES := js
PRETTIER_FILES := $(wildcard *.js popup/*.js *.css popup/*.css) PRETTIER_FILES := $(wildcard *.js popup/*.js *.css popup/*.css)
.PHONY: all .PHONY: all
all: deps prettier js/background.dist.js js/popup.dist.js all: deps prettier js/background.dist.js js/popup.dist.js js/inject.dist.js
.PHONY: deps .PHONY: deps
deps: deps:
@@ -23,6 +23,10 @@ js/popup.dist.js: $(BROWSERIFY) popup/*.js
[ -d js ] || mkdir -p js [ -d js ] || mkdir -p js
$(BROWSERIFY) -o js/popup.dist.js popup/popup.js $(BROWSERIFY) -o js/popup.dist.js popup/popup.js
js/inject.dist.js: $(BROWSERIFY) inject.js
[ -d js ] || mkdir -p js
$(BROWSERIFY) -o js/inject.dist.js inject.js
.PHONY: clean .PHONY: clean
clean: clean:
rm -rf $(CLEAN_FILES) rm -rf $(CLEAN_FILES)

View File

@@ -60,6 +60,12 @@ async function handleMessage(settings, message, sendResponse) {
sendResponse({ status: "error", message: "Action is missing" }); sendResponse({ status: "error", message: "Action is missing" });
} }
// fetch file & parse fields if a login entry is present
if (typeof message.login !== "undefined") {
await parseFields(settings, message.login);
}
// route action
switch (message.action) { switch (message.action) {
case "getSettings": case "getSettings":
sendResponse({ sendResponse({
@@ -76,7 +82,53 @@ async function handleMessage(settings, message, sendResponse) {
var response = await hostAction(settings, "list"); var response = await hostAction(settings, "list");
sendResponse(response.data.files); sendResponse(response.data.files);
} catch (e) { } catch (e) {
console.log(e); sendResponse({
status: "error",
message: "Unable to enumerate password files" + e.toString()
});
}
break;
case "launch":
try {
var tab = (await browser.tabs.query({ active: true, currentWindow: true }))[0];
var url = message.login.fields.url ? message.login.fields.url : response.login.url;
if (!url.match(/:\/\//)) {
url = "http://" + url;
}
chrome.tabs.update(tab.id, { url: url });
sendResponse({ status: "ok" });
} catch (e) {
sendResponse({
status: "error",
message: "Unable to launch URL: " + e.toString()
});
}
break;
case "fill":
try {
var tab = (await browser.tabs.query({ active: true, currentWindow: true }))[0];
await browser.tabs.executeScript(tab.id, { file: "js/inject.dist.js" });
// check login fields
if (message.login.fields.login === null) {
throw new Error("No login is available");
}
if (message.login.fields.secret === null) {
throw new Error("No password is available");
}
var fillFields = JSON.stringify({
login: message.login.fields.login,
secret: message.login.fields.secret
});
// fill form via injected script
await browser.tabs.executeScript(tab.id, {
code: `window.browserpass.fillLogin(${fillFields});`
});
sendResponse({ status: "ok" });
} catch (e) {
sendResponse({
status: "error",
message: "Unable to fill credentials: " + e.toString()
});
} }
break; break;
default: default:
@@ -110,6 +162,65 @@ function hostAction(settings, action, params = {}) {
return browser.runtime.sendNativeMessage(appID, request); return browser.runtime.sendNativeMessage(appID, request);
} }
/**
* Fetch file & parse fields
*
* @since 3.0.0
*
* @param object settings Settings object
* @param object login Login object
* @return void
*/
async function parseFields(settings, login) {
var response = await hostAction(settings, "fetch", {
store: login.store,
file: login.login + ".gpg"
});
if (response.status != "ok") {
throw new Error(JSON.stringify(response)); // TODO handle host error
}
// save raw data inside login
login.raw = response.data.contents;
// parse lines
login.fields = {
secret: ["secret", "password", "pass"],
login: ["login", "username", "user", "email"],
url: ["url", "uri", "website", "site", "link", "launch"]
};
var lines = login.raw.split(/[\r\n]+/).filter(line => line.trim().length > 0);
lines.forEach(function(line) {
// split key / value
var parts = line
.split(":", 2)
.map(value => value.trim())
.filter(value => value.length);
if (parts.length != 2) {
return;
}
// assign to fields
for (var key in login.fields) {
if (Array.isArray(login.fields[key]) && login.fields[key].indexOf(parts[0]) >= 0) {
login.fields[key] = parts[1];
break;
}
}
});
// clean up unassigned fields
for (var key in login.fields) {
if (Array.isArray(login.fields[key])) {
if (key == "secret" && lines.length) {
login.fields.secret = lines[0];
} else {
login.fields[key] = null;
}
}
}
}
/** /**
* Wrap inbound messages to fetch native configuration * Wrap inbound messages to fetch native configuration
* *

18
src/inject.js Normal file
View File

@@ -0,0 +1,18 @@
(function() {
/**
* Fill password
*
* @since 3.0.0
*
* @param object login Login fields
* @return void
*/
function fillLogin(login) {
alert("Fill login: " + JSON.stringify(login));
}
// set window object
window.browserpass = {
fillLogin: fillLogin
};
})();

View File

@@ -23,7 +23,7 @@ function Interface(settings, logins) {
this.settings = settings; this.settings = settings;
this.logins = logins; this.logins = logins;
this.results = []; this.results = [];
this.active = true; this.active = !settings.tab.url.match(/^chrome:\/\//);
this.searchPart = new SearchInterface(this); this.searchPart = new SearchInterface(this);
// initialise with empty search // initialise with empty search
@@ -69,6 +69,7 @@ function view(ctl, params) {
result.doAction("fill"); result.doAction("fill");
}, },
onkeydown: function(e) { onkeydown: function(e) {
e.preventDefault();
switch (e.code) { switch (e.code) {
case "ArrowDown": case "ArrowDown":
if (e.target.nextSibling) { if (e.target.nextSibling) {
@@ -87,6 +88,9 @@ function view(ctl, params) {
case "Enter": case "Enter":
result.doAction("fill"); result.doAction("fill");
break; break;
case "KeyG":
result.doAction("launch");
break;
} }
} }
}, },
@@ -104,7 +108,7 @@ function view(ctl, params) {
title: "Copy username", title: "Copy username",
onclick: function(e) { onclick: function(e) {
e.stopPropagation(); e.stopPropagation();
result.doAction("copyUser"); result.doAction("copyUsername");
} }
}), }),
m("div.action.launch", { m("div.action.launch", {

View File

@@ -1,6 +1,7 @@
html, html,
body { body {
min-width: 260px; min-width: 260px;
overflow-x: hidden;
white-space: nowrap; white-space: nowrap;
} }
@@ -25,6 +26,7 @@ body {
flex-shrink: 0; flex-shrink: 0;
min-height: 29px; min-height: 29px;
padding: 6px; padding: 6px;
width: 100%;
} }
.part:last-child { .part:last-child {

View File

@@ -33,11 +33,14 @@ browser.tabs.query({ active: true, currentWindow: true }, async function(tabs) {
* @since 3.0.0 * @since 3.0.0
* *
* @param Error error Error object * @param Error error Error object
* @param string type Error type
*/ */
function handleError(error) { function handleError(error, type = "error") {
console.log(error); if (type == "error") {
console.log(error);
}
var errorNode = document.createElement("div"); var errorNode = document.createElement("div");
errorNode.setAttribute("class", "part error"); errorNode.setAttribute("class", "part " + type);
errorNode.textContent = error.toString(); errorNode.textContent = error.toString();
document.body.innerHTML = ""; document.body.innerHTML = "";
document.body.appendChild(errorNode); document.body.appendChild(errorNode);
@@ -115,9 +118,31 @@ async function run(settings) {
*/ */
async function withLogin(action) { async function withLogin(action) {
try { try {
// replace popup with a "please wait" notice
switch (action) {
case "fill":
handleError("Filling login details...", "notice");
break;
case "launch":
handleError("Launching URL...", "notice");
break;
case "copyPassword":
handleError("Copying password to clipboard...", "notice");
break;
case "copyUsername":
handleError("Copying username to clipboard...", "notice");
break;
default:
handleError("Please wait...", "notice");
break;
}
// hand off action to background script
var response = await browser.runtime.sendMessage({ action: action, login: this }); var response = await browser.runtime.sendMessage({ action: action, login: this });
if (response.status != "ok") { if (response.status != "ok") {
throw new Error(response.message); throw new Error(response.message);
} else {
window.close();
} }
} catch (e) { } catch (e) {
handleError(e); handleError(e);

View File

@@ -32,17 +32,16 @@ function view(ctl, params) {
return m( return m(
"form.part.search", "form.part.search",
{ {
onsubmit: function(e) {
e.preventDefault();
},
onkeydown: function(e) { onkeydown: function(e) {
switch (e.code) { switch (e.code) {
case "ArrowDown": case "ArrowDown":
e.preventDefault();
if (self.popup.results.length) { if (self.popup.results.length) {
document.querySelector("*[tabindex]").focus(); document.querySelector("*[tabindex]").focus();
} }
break; break;
case "Enter": case "Enter":
e.preventDefault();
if (self.popup.results.length) { if (self.popup.results.length) {
self.popup.results[0].doAction("fill"); self.popup.results[0].doAction("fill");
} }