4270be58547cde99692895b129243396dfd3b66d
[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 { GPGMEJS_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             GPGMEJS_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(GPGMEJS_Error('CONN_NO_CONNECT'));
87         }
88         if (!message || !message instanceof GPGME_Message){
89             return Promise.reject(GPGMEJS_Error('PARAM_WRONG'), message);
90         }
91         if (message.isComplete !== true){
92             return Promise.reject(GPGMEJS_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(GPGMEJS_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             let timeout = new Promise(function(resolve, reject){
122                 setTimeout(function(){
123                     reject(GPGMEJS_Error('CONN_TIMEOUT'));
124                 }, 5000);
125             });
126             if (permittedOperations[message.operation].pinentry){
127                 return me._connection.postMessage(message.message);
128             } else {
129                 return Promise.race([timeout,
130                     me._connection.postMessage(message.message)
131                 ]);
132             }
133         });
134      }
135 };
136
137 /**
138  * A class for answer objects, checking and processing the return messages of
139  * the nativeMessaging communication.
140  * @param {String} operation The operation, to look up validity of returning messages
141  */
142 class Answer{
143
144     constructor(operation){
145         this.operation = operation;
146     }
147
148     /**
149      * Add the information to the answer
150      * @param {Object} msg The message as received with nativeMessaging
151      * returns true if successfull, GPGMEJS_Error otherwise
152      */
153     add(msg){
154         if (this._response === undefined){
155             this._response = {};
156         }
157         let messageKeys = Object.keys(msg);
158         let poa = permittedOperations[this.operation].answer;
159         if (messageKeys.length === 0){
160             return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER');
161         }
162         for (let i= 0; i < messageKeys.length; i++){
163             let key = messageKeys[i];
164             switch (key) {
165                 case 'type':
166                     if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
167                         return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER');
168                     }
169                     break;
170                 case 'more':
171                     break;
172                 default:
173                     //data should be concatenated
174                     if (poa.data.indexOf(key) >= 0){
175                         if (!this._response.hasOwnProperty(key)){
176                             this._response[key] = '';
177                         }
178                         this._response[key] = this._response[key].concat(msg[key]);
179                     }
180                     //params should not change through the message
181                     else if (poa.params.indexOf(key) >= 0){
182                         if (!this._response.hasOwnProperty(key)){
183                             this._response[key] = msg[key];
184                         }
185                         else if (this._response[key] !== msg[key]){
186                                 return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER',msg[key]);
187                         }
188                     }
189                     //infos may be json objects etc. Not yet defined.
190                     // Pushing them into arrays for now
191                     else if (poa.infos.indexOf(key) >= 0){
192                         if (!this._response.hasOwnProperty(key)){
193                             this._response[key] = [];
194                         }
195                         this._response.push(msg[key]);
196                     }
197                     else {
198                         return GPGMEJS_Error('CONN_UNEXPECTED_ANSWER', key);
199                     }
200                     break;
201             }
202         }
203         return true;
204     }
205
206     /**
207      * @returns {Object} the assembled message.
208      * TODO: does not care yet if completed.
209      */
210     get message(){
211         return this._response;
212     }
213 }