js: Error handling for browser errors
authorMaximilian Krambach <maximilian.krambach@intevation.de>
Wed, 19 Jun 2019 10:57:20 +0000 (12:57 +0200)
committerMaximilian Krambach <maximilian.krambach@intevation.de>
Wed, 19 Jun 2019 10:58:44 +0000 (12:58 +0200)
--

* Connection.js
  - Add some meaningful nativeMessaging feedback for failing
    communication due to misconfiguration or other browser-originated
    fails
  - add an "isDisconnected" property
  - "isNativeHostUnknown" tries to match browser's feedback string if
    the browser does not find gpgme-json
* init.js
  - initialization will now reject with a more meaningful error if the
    configuration is not set up or other browser-based errors
    (chrome.runtime.lastError) are present. This should speed up
    the normal initialization (not having to waiting for a timeout
    any more in case of improper setup)
* errors.js
  - CONN_NATIVEMESSAGE: New error that passes the browser's
    nativeMessaging error
  - CONN_NO_CONFIG: native messaging error indicating that the
    nativeMessaging host was not set up properly
* unittests.js:
  - added the "isDisconnected" property to the startup tests
  - added tests for proper behavior of connection checks

lang/js/src/Connection.js
lang/js/src/Errors.js
lang/js/src/index.js
lang/js/unittests.js

index 8a96547..923698a 100644 (file)
@@ -40,7 +40,15 @@ import { decode, atobArray, Utf8ArrayToStr } from './Helpers';
 export class Connection{
 
     constructor (){
+        this._connectionError = null;
         this._connection = chrome.runtime.connectNative('gpgmejson');
+        this._connection.onDisconnect.addListener(() => {
+            if (chrome.runtime.lastError) {
+                this._connectionError = chrome.runtime.lastError.message;
+            } else {
+                this._connectionError = 'Disconnected without error message';
+            }
+        });
     }
 
     /**
@@ -50,9 +58,16 @@ export class Connection{
         if (this._connection){
             this._connection.disconnect();
             this._connection = null;
+            this._connectionError = 'Disconnect requested by gpgmejs';
         }
     }
 
+    /**
+     * Checks if the connection terminated with an error state
+     */
+    get isDisconnected (){
+        return this._connectionError !== null;
+    }
 
     /**
     * @typedef {Object} backEndDetails
@@ -126,9 +141,17 @@ export class Connection{
             this.disconnect();
             return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
         }
+        if (this.isDisconnected) {
+            if ( this.isNativeHostUnknown === true) {
+                return Promise.reject(gpgme_error('CONN_NO_CONFIG'));
+            } else {
+                return Promise.reject(gpgme_error(
+                    'CONN_NO_CONNECT', this._connectionError));
+            }
+        }
         let chunksize = message.chunksize;
         const me = this;
-        return new Promise(function (resolve, reject){
+        const nativeCommunication = new Promise(function (resolve, reject){
             let answer = new Answer(message);
             let listener = function (msg) {
                 if (!msg){
@@ -161,29 +184,21 @@ export class Connection{
                 }
             };
             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 (){
-                            me._connection.disconnect();
-                            reject(gpgme_error('CONN_TIMEOUT'));
-                        }, 5000);
-                    }
-                ]).then(function (result){
-                    return result;
-                }, function (reject){
-                    if (!(reject instanceof Error)) {
-                        me._connection.disconnect();
-                        return gpgme_error('GNUPG_ERROR', reject);
-                    } else {
-                        return reject;
-                    }
-                });
-            }
+            me._connection.postMessage(message.message);
         });
+        if (permittedOperations[message.operation].pinentry === true) {
+            return nativeCommunication;
+        } else {
+            return Promise.race([
+                nativeCommunication,
+                new Promise(function (resolve, reject){
+                    setTimeout(function (){
+                        me._connection.disconnect();
+                        reject(gpgme_error('CONN_TIMEOUT'));
+                    }, 5000);
+                })
+            ]);
+        }
     }
 }
 
@@ -213,6 +228,13 @@ class Answer{
     }
 
     /**
+     * Checks if an error matching browsers 'host not known' messages occurred
+     */
+    get isNativeHostUnknown () {
+        return this._connectionError === 'Specified native messaging host not found.';
+    }
+
+    /**
      * Adds incoming base64 encoded data to the existing response
      * @param {*} msg base64 encoded data.
      * @returns {Boolean}
index bf52cce..6189414 100644 (file)
@@ -35,6 +35,14 @@ export const err_list = {
         msg: 'The nativeMessaging answer was empty.',
         type: 'error'
     },
+    'CONN_NO_CONFIG':{
+        msg: 'The browser does not recognize the nativeMessaging host.',
+        type: 'error'
+    },
+    'CONN_NATIVEMESSAGE':{
+        msg: 'The native messaging was not successful.',
+        type: 'error'
+    },
     'CONN_TIMEOUT': {
         msg: 'A connection timeout was exceeded.',
         type: 'error'
@@ -156,8 +164,8 @@ export function gpgme_error (code = 'GENERIC_ERROR', info){
  */
 class GPGME_Error extends Error{
     constructor (code = 'GENERIC_ERROR', msg=''){
-
-        if (code === 'GNUPG_ERROR' && typeof (msg) === 'string'){
+        const verboseErrors = ['GNUPG_ERROR', 'CONN_NATIVEMESSAGE'];
+        if (verboseErrors.includes(code) && typeof (msg) === 'string'){
             super(msg);
         } else if (err_list.hasOwnProperty(code)){
             if (msg){
index 106086f..3c31a04 100644 (file)
@@ -33,7 +33,7 @@ import { Connection } from './Connection';
  * An unsuccessful attempt will reject as a GPGME_Error.
  * @param {Object} config (optional) configuration options
  * @param {Number} config.timeout set the timeout for the initial connection
- * check. On some machines and operating systems a default timeout of 500 ms is
+ * check. On some machines and operating systems a default timeout of 1000 ms is
  * too low, so a higher number might be attempted.
  * @returns {Promise<GpgME>}
  * @async
@@ -46,7 +46,17 @@ function init ({ timeout = 1000 } = {}){
                 if (result === true) {
                     resolve(new GpgME());
                 } else {
-                    reject(gpgme_error('CONN_NO_CONNECT'));
+                    if (connection._connectionError) {
+                        if (connection.isNativeHostUnknown){
+                            reject(gpgme_error('CONN_NO_CONFIG'));
+                        } else {
+                            reject(gpgme_error('CONN_NATIVEMESSAGE',
+                                connection._connectionError)
+                            );
+                        }
+                    } else {
+                        reject(gpgme_error('CONN_TIMEOUT'));
+                    }
                 }
             }, function (){ // unspecific connection error. Should not happen
                 reject(gpgme_error('CONN_NO_CONNECT'));
index 414d18d..45e2b93 100644 (file)
@@ -49,11 +49,42 @@ function unittests (){
                     expect(answer.info).to.be.an('Array');
                     expect(conn0.disconnect).to.be.a('function');
                     expect(conn0.post).to.be.a('function');
+                    expect(conn0.isDisconnected).to.be.false;
                     done();
                 });
 
         });
 
+        it('Simple connection check', function (done) {
+            let conn0 = new Connection;
+            conn0.checkConnection(false, connectionTimeout).then(
+                function (answer) {
+                    expect(answer).to.be.true;
+                    expect(conn0.isDisconnected).to.be.false;
+                    done();
+                });
+        });
+
+        it('Connection check with backend information', function (done) {
+            let conn0 = new Connection;
+            conn0.checkConnection(true, connectionTimeout).then(
+                function (answer) {
+                    expect(answer).to.be.an('Object');
+                    expect(answer.gpgme).to.be.a('String');
+                    expect(answer.info).to.be.an('Array');
+                    expect(answer.info.length).to.be.above(0);
+                    for (const item of answer.info) {
+                        expect(item).to.have.property('protocol');
+                        expect(item).to.have.property('fname');
+                        expect(item).to.have.property('version');
+                        expect(item).to.have.property('req_version');
+                        expect(item).to.have.property('homedir');
+                    }
+                    expect(conn0.isDisconnected).to.be.false;
+                    done();
+                });
+        });
+
         it('Disconnecting', function (done) {
             let conn0 = new Connection;
             conn0.checkConnection(false, connectionTimeout).then(
@@ -63,6 +94,7 @@ function unittests (){
                     conn0.checkConnection(false, connectionTimeout).then(
                         function (result) {
                             expect(result).to.be.false;
+                            expect(conn0.isDisconnected).to.be.true;
                             done();
                         });
                 });