/* * 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 ' + JSON.stringify(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 ' + JSON.stringify('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); } } } })();