js: more testing of nativeMessaging connection
[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_DISCONNECTED'));
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(gpgme_error('GNUPG_ERROR', msg.msg));
104                 } else {
105                     let answer_result = answer.add(msg);
106                     if (answer_result !== true){
107                         me._connection.onMessage.removeListener(listener);
108                         reject(answer_result);
109                     }
110                     if (msg.more === true){
111                         me._connection.postMessage({'op': 'getmore'});
112                     } else {
113                         me._connection.onMessage.removeListener(listener)
114                         resolve(answer.message);
115                     }
116                 }
117             };
118
119             me._connection.onMessage.addListener(listener);
120             if (permittedOperations[message.operation].pinentry){
121                 return me._connection.postMessage(message.message);
122             } else {
123                 return Promise.race([
124                     me._connection.postMessage(message.message),
125                     function(resolve, reject){
126                         setTimeout(function(){
127                             reject(gpgme_error('CONN_TIMEOUT'));
128                         }, 5000);
129                     }]).then(function(result){
130                     return result;
131                 }, function(reject){
132                     if(!reject instanceof Error) {
133                         return gpgme_error('GNUPG_ERROR', reject);
134                     } else {
135                         return reject;
136                     }
137                 });
138             }
139         });
140      }
141 };
142
143 /**
144  * A class for answer objects, checking and processing the return messages of
145  * the nativeMessaging communication.
146  * @param {String} operation The operation, to look up validity of returning messages
147  */
148 class Answer{
149
150     constructor(operation){
151         this.operation = operation;
152     }
153
154     /**
155      * Add the information to the answer
156      * @param {Object} msg The message as received with nativeMessaging
157      * returns true if successfull, gpgme_error otherwise
158      */
159     add(msg){
160         if (this._response === undefined){
161             this._response = {};
162         }
163         let messageKeys = Object.keys(msg);
164         let poa = permittedOperations[this.operation].answer;
165         if (messageKeys.length === 0){
166             return gpgme_error('CONN_UNEXPECTED_ANSWER');
167         }
168         for (let i= 0; i < messageKeys.length; i++){
169             let key = messageKeys[i];
170             switch (key) {
171                 case 'type':
172                     if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
173                         return gpgme_error('CONN_UNEXPECTED_ANSWER');
174                     }
175                     break;
176                 case 'more':
177                     break;
178                 default:
179                     //data should be concatenated
180                     if (poa.data.indexOf(key) >= 0){
181                         if (!this._response.hasOwnProperty(key)){
182                             this._response[key] = '';
183                         }
184                         // console.log(msg[key]);
185                         this._response[key] += msg[key];
186                     }
187                     //params should not change through the message
188                     else if (poa.params.indexOf(key) >= 0){
189                         if (!this._response.hasOwnProperty(key)){
190                             this._response[key] = msg[key];
191                         }
192                         else if (this._response[key] !== msg[key]){
193                                 return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]);
194                         }
195                     }
196                     //infos may be json objects etc. Not yet defined.
197                     // Pushing them into arrays for now
198                     else if (poa.infos.indexOf(key) >= 0){
199                         if (!this._response.hasOwnProperty(key)){
200                             this._response[key] = [];
201                         }
202                         this._response.push(msg[key]);
203                     }
204                     else {
205                         return gpgme_error('CONN_UNEXPECTED_ANSWER');
206                     }
207                     break;
208             }
209         }
210         return true;
211     }
212
213     /**
214      * @returns {Object} the assembled message.
215      * TODO: does not care yet if completed.
216      */
217     get message(){
218         let keys = Object.keys(this._response);
219         let poa = permittedOperations[this.operation].answer;
220         for (let i=0; i < keys.length; i++) {
221             if (poa.data.indexOf(keys[i]) >= 0){
222                 if (this._response.base64 == true){
223                     let respatob =  atob(this._response[keys[i]]);
224
225                     let result = decodeURIComponent(
226                         respatob.split('').map(function(c) {
227                             return '%' +
228                             ('00' + c.charCodeAt(0).toString(16)).slice(-2);
229                         }).join(''));
230                     this._response[keys[i]] = result;
231                 }
232             }
233         }
234         return this._response;
235     }
236 }