diff options
Diffstat (limited to 'lang/js/src/Connection.js')
-rw-r--r-- | lang/js/src/Connection.js | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/lang/js/src/Connection.js b/lang/js/src/Connection.js new file mode 100644 index 00000000..9c2a6428 --- /dev/null +++ b/lang/js/src/Connection.js @@ -0,0 +1,241 @@ +/* gpgme.js - Javascript integration for gpgme + * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik + * + * This file is part of GPGME. + * + * GPGME is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * GPGME 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see <http://www.gnu.org/licenses/>. + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/** + * A connection port will be opened for each communication between gpgmejs and + * gnupg. It should be alive as long as there are additional messages to be + * expected. + */ +import { permittedOperations } from './permittedOperations' +import { gpgme_error } from "./Errors" +import { GPGME_Message } from "./Message"; + +/** + * A Connection handles the nativeMessaging interaction. + */ +export class Connection{ + + constructor(){ + this.connect(); + let me = this; + } + + /** + * (Simple) Connection check. + * @returns {Boolean} true if the onDisconnect event has not been fired. + * Please note that the event listener of the port takes some time + * (5 ms seems enough) to react after the port is created. Then this will + * return undefined + */ + get isConnected(){ + return this._isConnected; + } + + /** + * Immediately closes the open port. + */ + disconnect() { + if (this._connection){ + this._connection.disconnect(); + } + } + + /** + * Opens a nativeMessaging port. + */ + connect(){ + if (this._isConnected === true){ + gpgme_error('CONN_ALREADY_CONNECTED'); + } else { + this._isConnected = true; + this._connection = chrome.runtime.connectNative('gpgmejson'); + let me = this; + this._connection.onDisconnect.addListener( + function(){ + me._isConnected = false; + } + ); + } + } + + /** + * Sends a message and resolves with the answer. + * @param {GPGME_Message} message + * @returns {Promise<Object>} the gnupg answer, or rejection with error + * information. + */ + post(message){ + if (!this.isConnected){ + return Promise.reject(gpgme_error('CONN_DISCONNECTED')); + } + if (!message || !message instanceof GPGME_Message){ + return Promise.reject(gpgme_error('PARAM_WRONG'), message); + } + if (message.isComplete !== true){ + return Promise.reject(gpgme_error('MSG_INCOMPLETE')); + } + let me = this; + return new Promise(function(resolve, reject){ + let answer = new Answer(message); + let listener = function(msg) { + if (!msg){ + me._connection.onMessage.removeListener(listener) + reject(gpgme_error('CONN_EMPTY_GPG_ANSWER')); + } else if (msg.type === "error"){ + me._connection.onMessage.removeListener(listener); + reject(gpgme_error('GNUPG_ERROR', msg.msg)); + } else { + let answer_result = answer.add(msg); + if (answer_result !== true){ + me._connection.onMessage.removeListener(listener); + reject(answer_result); + } + if (msg.more === true){ + me._connection.postMessage({'op': 'getmore'}); + } else { + me._connection.onMessage.removeListener(listener) + resolve(answer.message); + } + } + }; + + me._connection.onMessage.addListener(listener); + if (permittedOperations[message.operation].pinentry){ + return me._connection.postMessage(message.message); + } else { + return Promise.race([ + me._connection.postMessage(message.message), + function(resolve, reject){ + setTimeout(function(){ + reject(gpgme_error('CONN_TIMEOUT')); + }, 5000); + }]).then(function(result){ + return result; + }, function(reject){ + if(!reject instanceof Error) { + return gpgme_error('GNUPG_ERROR', reject); + } else { + return reject; + } + }); + } + }); + } +}; + +/** + * A class for answer objects, checking and processing the return messages of + * the nativeMessaging communication. + * @param {String} operation The operation, to look up validity of returning messages + */ +class Answer{ + + constructor(message){ + this.operation = message.operation; + this.expected = message.expected; + } + + /** + * Add the information to the answer + * @param {Object} msg The message as received with nativeMessaging + * returns true if successfull, gpgme_error otherwise + */ + add(msg){ + if (this._response === undefined){ + this._response = {}; + } + let messageKeys = Object.keys(msg); + let poa = permittedOperations[this.operation].answer; + if (messageKeys.length === 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + for (let i= 0; i < messageKeys.length; i++){ + let key = messageKeys[i]; + switch (key) { + case 'type': + if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){ + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + break; + case 'more': + break; + default: + //data should be concatenated + if (poa.data.indexOf(key) >= 0){ + if (!this._response.hasOwnProperty(key)){ + this._response[key] = ''; + } + this._response[key] += msg[key]; + } + //params should not change through the message + else if (poa.params.indexOf(key) >= 0){ + if (!this._response.hasOwnProperty(key)){ + this._response[key] = msg[key]; + } + else if (this._response[key] !== msg[key]){ + return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]); + } + } + //infos may be json objects etc. Not yet defined. + // Pushing them into arrays for now + else if (poa.infos.indexOf(key) >= 0){ + if (!this._response.hasOwnProperty(key)){ + this._response[key] = []; + } + this._response.push(msg[key]); + } + else { + return gpgme_error('CONN_UNEXPECTED_ANSWER'); + } + break; + } + } + return true; + } + + /** + * @returns {Object} the assembled message, original data assumed to be + * (javascript-) strings + */ + get message(){ + let keys = Object.keys(this._response); + let msg = {}; + let poa = permittedOperations[this.operation].answer; + for (let i=0; i < keys.length; i++) { + if (poa.data.indexOf(keys[i]) >= 0 + && this._response.base64 === true + ) { + msg[keys[i]] = atob(this._response[keys[i]]); + if (this.expected === 'base64'){ + msg[keys[i]] = this._response[keys[i]]; + } else { + msg[keys[i]] = decodeURIComponent( + atob(this._response[keys[i]]).split('').map(function(c) { + return '%' + + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + } + } else { + msg[keys[i]] = this._response[keys[i]]; + } + } + return msg; + } +} |