js: change in initialization ancd connection handling
[gpgme.git] / lang / js / src / Connection.js
1 import { GPGME_Message } from "./Message";
2
3 /* gpgme.js - Javascript integration for gpgme
4  * Copyright (C) 2018 Bundesamt für Sicherheit in der Informationstechnik
5  *
6  * This file is part of GPGME.
7  *
8  * GPGME is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU Lesser General Public License as
10  * published by the Free Software Foundation; either version 2.1 of
11  * the License, or (at your option) any later version.
12  *
13  * GPGME is distributed in the hope that it will be useful, but
14  * WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this program; if not, see <http://www.gnu.org/licenses/>.
20  * SPDX-License-Identifier: LGPL-2.1+
21  */
22
23 /**
24  * A connection port will be opened for each communication between gpgmejs and
25  * gnupg. It should be alive as long as there are additional messages to be
26  * expected.
27  */
28 import { permittedOperations } from './permittedOperations'
29 import { GPGMEJS_Error} from "./Errors"
30
31 /**
32  * A Connection handles the nativeMessaging interaction.
33  */
34 export class Connection{
35
36     constructor(){
37         this.connect();
38         let me = this;
39     }
40
41     /**
42      * (Simple) Connection check.
43      * @returns {Boolean} true if the onDisconnect event has not been fired.
44      * Please note that the event listener of the port takes some time
45      * (5 ms seems enough) to react after the port is created. Then this will
46      * return undefined
47      */
48     get isConnected(){
49         return this._isConnected;
50     }
51
52     /**
53      * Immediately closes the open port.
54      */
55     disconnect() {
56         if (this._connection){
57             this._connection.disconnect();
58         }
59     }
60
61     /**
62      * Opens a nativeMessaging port.
63      * TODO: Error handling ALREADY_CONNECTED
64      */
65     connect(){
66         if (this._isConnected === true){
67             return new GPGMEJS_Error('ALREADY_CONNECTED');
68         }
69         this._isConnected = true;
70         this._connection = chrome.runtime.connectNative('gpgmejson');
71         let me = this;
72         this._connection.onDisconnect.addListener(
73             function(){
74                 me._isConnected = false;
75             }
76         );
77     }
78
79     /**
80      * Sends a message and resolves with the answer.
81      * @param {GPGME_Message} message
82      * @returns {Promise<Object>} the gnupg answer, or rejection with error
83      * information.
84      */
85     post(message){
86         if (!this.isConnected){
87             return Promise.reject(new GPGMEJS_Error('NO_CONNECT'));
88         }
89         if (!message || !message instanceof GPGME_Message){
90             return Promise.reject(new GPGMEJS_Error('WRONGPARAM'));
91         }
92         if (message.isComplete !== true){
93             return Promise.reject(new GPGMEJS_Error('MSG_INCOMPLETE'));
94         }
95         // let timeout = 5000; //TODO config
96         let me = this;
97         return new Promise(function(resolve, reject){
98             let answer = new Answer(message.operation);
99             let listener = function(msg) {
100                 if (!msg){
101                     me._connection.onMessage.removeListener(listener)
102                     reject(new GPGMEJS_Error('EMPTY_GPG_ANSWER'));
103                 } else if (msg.type === "error"){
104                     me._connection.onMessage.removeListener(listener)
105                     //TODO: GPGMEJS_Error?
106                     reject(msg.msg);
107                 } else {
108                     answer.add(msg);
109                     if (msg.more === true){
110                         me._connection.postMessage({'op': 'getmore'});
111                     } else {
112                         me._connection.onMessage.removeListener(listener)
113                         resolve(answer.message);
114                     }
115                 }
116             };
117
118             me._connection.onMessage.addListener(listener);
119             me._connection.postMessage(message.message);
120             //TBD: needs to be aware if there is a pinentry pending
121             // setTimeout(
122             //     function(){
123             //         me.disconnect();
124             //         reject(new GPGMEJS_Error('TIMEOUT', 5000));
125             //     }, timeout);
126         });
127      }
128 };
129
130 /**
131  * A class for answer objects, checking and processing the return messages of
132  * the nativeMessaging communication.
133  * @param {String} operation The operation, to look up validity of returning messages
134  */
135 class Answer{
136
137     constructor(operation){
138         this.operation = operation;
139     }
140
141     /**
142      * Add the information to the answer
143      * @param {Object} msg The message as received with nativeMessaging
144      */
145     add(msg){
146         if (this._response === undefined){
147             this._response = {};
148         }
149         let messageKeys = Object.keys(msg);
150         let poa = permittedOperations[this.operation].answer;
151         for (let i= 0; i < messageKeys.length; i++){
152             let key = messageKeys[i];
153             switch (key) {
154                 case 'type':
155                     if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
156                         return new GPGMEJS_Error('UNEXPECTED_ANSWER');
157                     }
158                     break;
159                 case 'more':
160                     break;
161                 default:
162                     //data should be concatenated
163                     if (poa.data.indexOf(key) >= 0){
164                         if (!this._response.hasOwnProperty(key)){
165                             this._response[key] = '';
166                         }
167                         this._response[key] = this._response[key].concat(msg[key]);
168                     }
169                     //params should not change through the message
170                     else if (poa.params.indexOf(key) >= 0){
171                         if (!this._response.hasOwnProperty(key)){
172                             this._response[key] = msg[key];
173                         }
174                         else if (this._response[key] !== msg[key]){
175                                 return new GPGMEJS_Error('UNEXPECTED_ANSWER',msg[key]);
176                         }
177                     }
178                     //infos may be json objects etc. Not yet defined.
179                     // Pushing them into arrays for now
180                     else if (poa.infos.indexOf(key) >= 0){
181                         if (!this._response.hasOwnProperty(key)){
182                             this._response[key] = [];
183                         }
184                         this._response.push(msg[key]);
185                     }
186                     else {
187                         return new GPGMEJS_Error('UNEXPECTED_ANSWER', key);
188                     }
189                     break;
190             }
191         }
192     }
193
194     /**
195      * @returns {Object} the assembled message.
196      * TODO: does not care yet if completed.
197      */
198     get message(){
199         return this._response;
200     }
201 }