From 3750346a8e8736c726e6ad8fcb8ad82d81975fbc Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 16 Mar 2015 02:05:26 +0100 Subject: XUL extension --- xul-ext/COPYING | 1 + xul-ext/Makefile | 11 ++ xul-ext/chrome.manifest | 3 + xul-ext/chrome/content/icevault.js | 223 +++++++++++++++++++++++++++++++ xul-ext/chrome/content/icevault.xul | 31 +++++ xul-ext/chrome/locale/en-US/messages | 0 xul-ext/chrome/locale/en-US/messages.dtd | 0 xul-ext/install.rdf | 42 ++++++ 8 files changed, 311 insertions(+) create mode 120000 xul-ext/COPYING create mode 100644 xul-ext/Makefile create mode 100644 xul-ext/chrome.manifest create mode 100644 xul-ext/chrome/content/icevault.js create mode 100644 xul-ext/chrome/content/icevault.xul create mode 100644 xul-ext/chrome/locale/en-US/messages create mode 100644 xul-ext/chrome/locale/en-US/messages.dtd create mode 100644 xul-ext/install.rdf diff --git a/xul-ext/COPYING b/xul-ext/COPYING new file mode 120000 index 0000000..012065c --- /dev/null +++ b/xul-ext/COPYING @@ -0,0 +1 @@ +../COPYING \ No newline at end of file diff --git a/xul-ext/Makefile b/xul-ext/Makefile new file mode 100644 index 0000000..d44f648 --- /dev/null +++ b/xul-ext/Makefile @@ -0,0 +1,11 @@ +XPI_NAME:=$(shell sed -nr '/.*([^<]+)<\/em:id>.*/ {s//..\/\1.xpi/p;q}' install.rdf) + +all: $(XPI_NAME) + +$(XPI_NAME): chrome.manifest install.rdf COPYING $(shell find chrome/ -type f) + zip $@ $^ + +clean: + rm -f $(XPI_NAME) + +.PHONY: all clean diff --git a/xul-ext/chrome.manifest b/xul-ext/chrome.manifest new file mode 100644 index 0000000..14cc449 --- /dev/null +++ b/xul-ext/chrome.manifest @@ -0,0 +1,3 @@ +content icevault chrome/content/ +locale icevault en-US chrome/locale/en-US/ +overlay chrome://browser/content/browser.xul chrome://icevault/content/icevault.xul diff --git a/xul-ext/chrome/content/icevault.js b/xul-ext/chrome/content/icevault.js new file mode 100644 index 0000000..2006e3a --- /dev/null +++ b/xul-ext/chrome/content/icevault.js @@ -0,0 +1,223 @@ +/* + * IceVault - An external password manager for firefox (XUL extension) + * Copyright © 2015 Guilhem Moulin + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// IceVault global namespace +var icevault = (function() { + + const Cc = Components.classes, + CC = Components.Constructor, + Ci = Components.interfaces; + + const UnixServerSocket = CC( "@mozilla.org/network/server-socket;1" + , "nsIServerSocket" + , "initWithFilename" ), + InputPump = CC( "@mozilla.org/network/input-stream-pump;1" + , "nsIInputStreamPump" + , "init" ), + BinaryInputStream = CC( "@mozilla.org/binaryinputstream;1" + , "nsIBinaryInputStream" + , "setInputStream" ), + ConverterOutputStream = CC( "@mozilla.org/intl/converter-output-stream;1" + , "nsIConverterOutputStream" + , "init" ); + + // XXX should be changeable from the preferences + let sockName = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + sockName.append("S.IceVault"); + const sockPerms = parseInt("0770", 8); + + const convertStringToUTF8 = Cc["@mozilla.org/intl/utf8converterservice;1"] + .getService(Ci.nsIUTF8ConverterService) + .convertStringToUTF8; + + // Write the given UTF8 string to the ConverterOutputStream, then flush + var send = function(stream, msg) { + //console.log("Sending: " + msg) + try { + stream.writeString(msg + "\n"); + } + finally { + stream.flush(); + } + }; + + // Refresh the internal state: URI is set to the current tab's and the + // form state is cleared + var refreshDocument = function(state) { + let tab = Application.activeWindow.activeTab; + state.document = tab.document; + state.forms = null; + send(state.outStream, 'OK ' + JSON.stringify(tab.uri.scheme+'://'+tab.uri.hostPort)); + }; + + // Process the command + var process = function(state, data) { + data = convertStringToUTF8(data, "UTF-8", false, true); // convert the bytes to UTF8 + //console.log("Received: " + data.replace(/\n$/,'')); + let command = /^\S*/.exec(data)[0]; + + switch (command) { + case "REFRESH": + // refresh current (active) tab + refreshDocument(state); + break; + + case "GETFORMS": + // refresh the form list + let forms = [], getForms = []; + for (let i = 0; i < state.document.forms.length; i++) { + let form = state.document.forms[i], rect = form.getBoundingClientRect(); + if (rect.width == 0 || rect.height == 0) + continue; // invisible form + let fields = [], getFields = []; + for (let i = 0; i < form.elements.length; i++) { + let field = form.elements[i]; + if (["text","email","password"].indexOf(field.type) > -1) { + // store only these kinds of fields + let getField = { name: field.name, type: field.type, value: field.value }; + if (field.maxLength > -1) getField.maxLength = field.maxLength; + fields.push(field) + getFields.push(getField) + } + } + + if (fields.length) { + // keep the element objects in the state, and build an assoc + // list with the wanted field properties + forms.push(fields); + getForms.push({ method: form.method.toUpperCase(), action: form.action, fields: getFields }); + } + } + + state.forms = forms; + send(state.outStream, 'OK ' + JSON.stringify(getForms)); + break; + + case "FILL": + // fill a form with the given values (unless it's null) + try { + if (state.forms == null) + throw "Must issue a GETFORMS command first"; + let formIdx = /^FILL (\d+) /.exec(data); + if (formIdx == null) + throw "Syntax error" + formIdx = formIdx[1]; + let values = JSON.parse(data.replace(/^FILL \d+ /, '')); + if (formIdx < 0 || formIdx >= state.forms.length) + throw (formIdx + ": Form index out of bounds"); + let form = state.forms[formIdx]; + if (form.length < values.length) + throw (form.length + '<' + values.length + ": Too many values"); + for (let i = 0; i < values.length; i++) + if (values[i] != null) + form[i].value = values[i] + send(state.outStream, 'OK'); + } + catch (e if typeof e == "string" || e instanceof SyntaxError) { + send(state.outStream, 'ERROR ' + (typeof e == "string" ? e : e.message)); + } + break; + + case "QUIT": + state.inStream.close(); + send(state.outStream, 'BYE'); + state.outStream.close(); + break; + + default: + send(state.outStream, 'ERROR Invalid command: ' + command); + } + }; + + var serverSocketListener = { + onSocketAccepted: function(server, transport) { + // New connection: asynchronously read data on the (binary) input + // stream, and open the output stream as an UTF-8. We don't use + // ConverterInputStream as onDataAvailable's counts bytes not + // chars. + let inputStream = transport.openInputStream(0, 0, 0), + serverInput = new InputPump(inputStream, -1, -1, 0, 0, false); + outputStream = transport.openOutputStream(0, 0, 0); + + let state = { inStream: BinaryInputStream(inputStream) + , outStream: ConverterOutputStream(outputStream, 'UTF-8', 0, 0x0000) + }; + + console.log("Connection accepted"); + serverInput.asyncRead(streamListener(state), null); // spawn the listener + refreshDocument(state); + }, + + onStopListening: function(server, StatusCode) { + // server is dead + console.log('Server stopped listening'); + }, + }; + + var streamListener = function(state) { + return { + onStartRequest: function(request, context) { + this.data = ''; + }, + onDataAvailable: function(request, context, stream, offset, count) { + this.data += state.inStream.readBytes(count); + if (this.data.slice(-1) == "\n") { + // full input, process the command then clear the cache + process(state, this.data); + this.data = ''; + } + }, + onStopRequest: function(request, context, statusCode) { + state.inStream.close(); + state.outStream.close(); + } + } + }; + + + // External interface + return { + init: function() { + let server; // share the server accross multiple browsers (windows) + if (Application.storage.has("IceVault.serverSocket")) + server = Application.storage.get("IceVault.serverSocket", null); + else { + console.log("Initializing IceVault"); + try { + if (sockName.exists()) + sockName.remove(false); // remove stalled socket + server = new UnixServerSocket(sockName, sockPerms, -1); + } + finally { + Application.storage.set("IceVault.serverSocket", server); + } + } + server.asyncListen(serverSocketListener); + }, + + clean: function() { + if (!Application.windows.length && sockName.exists()) { + // remove the socket if the last window was just closed + console.log("Removing " + sockName.path); + sockName.remove(false); + } + } + } +})(); diff --git a/xul-ext/chrome/content/icevault.xul b/xul-ext/chrome/content/icevault.xul new file mode 100644 index 0000000..6aca893 --- /dev/null +++ b/xul-ext/chrome/content/icevault.xul @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/xul-ext/chrome/locale/en-US/messages b/xul-ext/chrome/locale/en-US/messages new file mode 100644 index 0000000..e69de29 diff --git a/xul-ext/chrome/locale/en-US/messages.dtd b/xul-ext/chrome/locale/en-US/messages.dtd new file mode 100644 index 0000000..e69de29 diff --git a/xul-ext/install.rdf b/xul-ext/install.rdf new file mode 100644 index 0000000..d804731 --- /dev/null +++ b/xul-ext/install.rdf @@ -0,0 +1,42 @@ + + + + + + + + icevault@guilhem.org + IceVault + A secure external password manager + 0.1 + Guilhem Moulin + https://guilhem.org/icevault/ + 2 + + + + + {ec8030f7-c20a-464f-9b0e-13a3a9e97384} + 3.5 + 26.* + + + + -- cgit v1.2.3