js: change in initialization ancd connection handling
[gpgme.git] / lang / js / src / Connection.js
index e8fea54..e6ff67b 100644 (file)
+import { GPGME_Message } from "./Message";
+
+/* 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 { GPGMEJS_Error} from "./Errors"
+
+/**
+ * A Connection handles the nativeMessaging interaction.
+ */
+export class Connection{
 
-export function Connection(){
-    if (!this.connection){
-        this.connection = connect();
-        this._msg = {
-            'always-trust': true,
-            // 'no-encrypt-to': false,
-            // 'no-compress': true,
-            // 'throw-keyids': false,
-            // 'wrap': false,
-            'armor': true,
-            'base64': false
-        };
-    };
+    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;
+    }
 
-    this.disconnect = function () {
-        if (this.connection){
-            this.connection.disconnect();
+    /**
+     * Immediately closes the open port.
+     */
+    disconnect() {
+        if (this._connection){
+            this._connection.disconnect();
         }
-    };
+    }
 
     /**
-     * Sends a message and resolves with the answer.
-     * @param {*} operation The interaction requested from gpgme
-     * @param {*} message A json-capable object to pass the operation details.
-     * TODO: _msg should contain configurable parameters
+     * Opens a nativeMessaging port.
+     * TODO: Error handling ALREADY_CONNECTED
      */
-    this.post = function(operation, message){
-        let timeout = 5000;
-        let me = this;
-        if (!message || !operation){
-            return Promise.reject('no message'); // TBD
+    connect(){
+        if (this._isConnected === true){
+            return new GPGMEJS_Error('ALREADY_CONNECTED');
         }
+        this._isConnected = true;
+        this._connection = chrome.runtime.connectNative('gpgmejson');
+        let me = this;
+        this._connection.onDisconnect.addListener(
+            function(){
+                me._isConnected = false;
+            }
+        );
+    }
 
-        let keys = Object.keys(message);
-        for (let i=0; i < keys.length; i++){
-            let property = keys[i];
-            me._msg[property] = message[property];
+    /**
+     * 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(new GPGMEJS_Error('NO_CONNECT'));
+        }
+        if (!message || !message instanceof GPGME_Message){
+            return Promise.reject(new GPGMEJS_Error('WRONGPARAM'));
         }
-        me._msg['op'] = operation;
-        // TODO fancier checks if what we want is consistent with submitted content
+        if (message.isComplete !== true){
+            return Promise.reject(new GPGMEJS_Error('MSG_INCOMPLETE'));
+        }
+        // let timeout = 5000; //TODO config
+        let me = this;
         return new Promise(function(resolve, reject){
-            me.connection.onMessage.addListener(function(msg) {
+            let answer = new Answer(message.operation);
+            let listener = function(msg) {
                 if (!msg){
-                    reject('empty answer.');
-                }
-                if (msg.type === "error"){
+                    me._connection.onMessage.removeListener(listener)
+                    reject(new GPGMEJS_Error('EMPTY_GPG_ANSWER'));
+                } else if (msg.type === "error"){
+                    me._connection.onMessage.removeListener(listener)
+                    //TODO: GPGMEJS_Error?
                     reject(msg.msg);
+                } else {
+                    answer.add(msg);
+                    if (msg.more === true){
+                        me._connection.postMessage({'op': 'getmore'});
+                    } else {
+                        me._connection.onMessage.removeListener(listener)
+                        resolve(answer.message);
+                    }
                 }
-                    resolve(msg);
-            });
+            };
 
-            me.connection.postMessage(me._msg);
-            setTimeout(
-                function(){
-                    me.disconnect();
-                    reject('Timeout');
-                }, timeout);
+            me._connection.onMessage.addListener(listener);
+            me._connection.postMessage(message.message);
+            //TBD: needs to be aware if there is a pinentry pending
+            // setTimeout(
+            //     function(){
+            //         me.disconnect();
+            //         reject(new GPGMEJS_Error('TIMEOUT', 5000));
+            //     }, timeout);
         });
-     };
+     }
 };
 
+/**
+ * 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{
 
-function connect(){
-    let connection = chrome.runtime.connectNative('gpgmejson');
-    if (!connection){
-        let msg = chrome.runtime.lastError || 'no message'; //TBD
-        throw(msg);
+    constructor(operation){
+        this.operation = operation;
     }
-    return connection;
-};
+
+    /**
+     * Add the information to the answer
+     * @param {Object} msg The message as received with nativeMessaging
+     */
+    add(msg){
+        if (this._response === undefined){
+            this._response = {};
+        }
+        let messageKeys = Object.keys(msg);
+        let poa = permittedOperations[this.operation].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 new GPGMEJS_Error('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] = this._response[key].concat(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 new GPGMEJS_Error('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 new GPGMEJS_Error('UNEXPECTED_ANSWER', key);
+                    }
+                    break;
+            }
+        }
+    }
+
+    /**
+     * @returns {Object} the assembled message.
+     * TODO: does not care yet if completed.
+     */
+    get message(){
+        return this._response;
+    }
+}