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
/src/node_modules
/src/js
/src/*.js
/src/*.log
!/src/*.src.js

View File

@@ -15,7 +15,8 @@ CHROME_FILES := manifest.json \
popup/*.svg
CHROME_FILES := $(wildcard $(addprefix src/,$(CHROME_FILES))) \
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))
.PHONY: chrome

View File

@@ -5,7 +5,7 @@ CLEAN_FILES := js
PRETTIER_FILES := $(wildcard *.js popup/*.js *.css popup/*.css)
.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
deps:
@@ -23,6 +23,10 @@ js/popup.dist.js: $(BROWSERIFY) popup/*.js
[ -d js ] || mkdir -p 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
clean:
rm -rf $(CLEAN_FILES)

View File

@@ -60,6 +60,12 @@ async function handleMessage(settings, message, sendResponse) {
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) {
case "getSettings":
sendResponse({
@@ -76,7 +82,53 @@ async function handleMessage(settings, message, sendResponse) {
var response = await hostAction(settings, "list");
sendResponse(response.data.files);
} 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;
default:
@@ -110,6 +162,65 @@ function hostAction(settings, action, params = {}) {
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
*

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

View File

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

View File

@@ -33,11 +33,14 @@ browser.tabs.query({ active: true, currentWindow: true }, async function(tabs) {
* @since 3.0.0
*
* @param Error error Error object
* @param string type Error type
*/
function handleError(error) {
console.log(error);
function handleError(error, type = "error") {
if (type == "error") {
console.log(error);
}
var errorNode = document.createElement("div");
errorNode.setAttribute("class", "part error");
errorNode.setAttribute("class", "part " + type);
errorNode.textContent = error.toString();
document.body.innerHTML = "";
document.body.appendChild(errorNode);
@@ -115,9 +118,31 @@ async function run(settings) {
*/
async function withLogin(action) {
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 });
if (response.status != "ok") {
throw new Error(response.message);
} else {
window.close();
}
} catch (e) {
handleError(e);

View File

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