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