js: code cleanup (eslint)
[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  * Author(s):
21  *     Maximilian Krambach <mkrambach@intevation.de>
22  */
23
24 /* global chrome */
25
26 import { permittedOperations } from './permittedOperations';
27 import { gpgme_error } from './Errors';
28 import { GPGME_Message, createMessage } from './Message';
29
30 /**
31  * A Connection handles the nativeMessaging interaction.
32  */
33 export class Connection{
34
35     constructor(){
36         this.connect();
37     }
38
39     /**
40      * Retrieves the information about the backend.
41      * @param {Boolean} details (optional) If set to false, the promise will
42      *  just return a connection status
43      * @returns {Promise<Object>}
44      *      {String} The property 'gpgme': Version number of gpgme
45      *      {Array<Object>} 'info' Further information about the backends.
46      *      Example:
47      *          "protocol":     "OpenPGP",
48      *          "fname":        "/usr/bin/gpg",
49      *          "version":      "2.2.6",
50      *          "req_version":  "1.4.0",
51      *          "homedir":      "default"
52      */
53     checkConnection(details = true){
54         if (details === true) {
55             return this.post(createMessage('version'));
56         } else {
57             let me = this;
58             return new Promise(function(resolve) {
59                 Promise.race([
60                     me.post(createMessage('version')),
61                     new Promise(function(resolve, reject){
62                         setTimeout(function(){
63                             reject(gpgme_error('CONN_TIMEOUT'));
64                         }, 500);
65                     })
66                 ]).then(function(){ // success
67                     resolve(true);
68                 }, function(){ // failure
69                     resolve(false);
70                 });
71             });
72         }
73     }
74
75     /**
76      * Immediately closes the open port.
77      */
78     disconnect() {
79         if (this._connection){
80             this._connection.disconnect();
81             this._connection = null;
82         }
83     }
84
85     /**
86      * Opens a nativeMessaging port.
87      */
88     connect(){
89         if (!this._connection){
90             this._connection = chrome.runtime.connectNative('gpgmejson');
91         }
92     }
93
94     /**
95      * Sends a message and resolves with the answer.
96      * @param {GPGME_Message} message
97      * @returns {Promise<Object>} the gnupg answer, or rejection with error
98      * information.
99      */
100     post(message){
101         if (!message || !(message instanceof GPGME_Message)){
102             this.disconnect();
103             return Promise.reject(gpgme_error(
104                 'PARAM_WRONG', 'Connection.post'));
105         }
106         if (message.isComplete !== true){
107             this.disconnect();
108             return Promise.reject(gpgme_error('MSG_INCOMPLETE'));
109         }
110         let me = this;
111         return new Promise(function(resolve, reject){
112             let answer = new Answer(message);
113             let listener = function(msg) {
114                 if (!msg){
115                     me._connection.onMessage.removeListener(listener);
116                     me._connection.disconnect();
117                     reject(gpgme_error('CONN_EMPTY_GPG_ANSWER'));
118                 } else if (msg.type === 'error'){
119                     me._connection.onMessage.removeListener(listener);
120                     me._connection.disconnect();
121                     reject(gpgme_error('GNUPG_ERROR', msg.msg));
122                 } else {
123                     let answer_result = answer.add(msg);
124                     if (answer_result !== true){
125                         me._connection.onMessage.removeListener(listener);
126                         me._connection.disconnect();
127                         reject(answer_result);
128                     } else if (msg.more === true){
129                         me._connection.postMessage({'op': 'getmore'});
130                     } else {
131                         me._connection.onMessage.removeListener(listener);
132                         me._connection.disconnect();
133                         resolve(answer.message);
134                     }
135                 }
136             };
137             me._connection.onMessage.addListener(listener);
138             if (permittedOperations[message.operation].pinentry){
139                 return me._connection.postMessage(message.message);
140             } else {
141                 return Promise.race([
142                     me._connection.postMessage(message.message),
143                     function(resolve, reject){
144                         setTimeout(function(){
145                             me._connection.disconnect();
146                             reject(gpgme_error('CONN_TIMEOUT'));
147                         }, 5000);
148                     }]).then(function(result){
149                     return result;
150                 }, function(reject){
151                     if(!(reject instanceof Error)) {
152                         me._connection.disconnect();
153                         return gpgme_error('GNUPG_ERROR', reject);
154                     } else {
155                         return reject;
156                     }
157                 });
158             }
159         });
160     }
161 }
162
163 /**
164  * A class for answer objects, checking and processing the return messages of
165  * the nativeMessaging communication.
166  * @param {String} operation The operation, to look up validity of returning
167  * messages
168  */
169 class Answer{
170
171     constructor(message){
172         this.operation = message.operation;
173         this.expected = message.expected;
174     }
175
176     /**
177      * Add the information to the answer
178      * @param {Object} msg The message as received with nativeMessaging
179      * returns true if successfull, gpgme_error otherwise
180      */
181     add(msg){
182         if (this._response === undefined){
183             this._response = {};
184         }
185         let messageKeys = Object.keys(msg);
186         let poa = permittedOperations[this.operation].answer;
187         if (messageKeys.length === 0){
188             return gpgme_error('CONN_UNEXPECTED_ANSWER');
189         }
190         for (let i= 0; i < messageKeys.length; i++){
191             let key = messageKeys[i];
192             switch (key) {
193             case 'type':
194                 if ( msg.type !== 'error' && poa.type.indexOf(msg.type) < 0){
195                     return gpgme_error('CONN_UNEXPECTED_ANSWER');
196                 }
197                 break;
198             case 'more':
199                 break;
200             default:
201                 //data should be concatenated
202                 if (poa.data.indexOf(key) >= 0){
203                     if (!this._response.hasOwnProperty(key)){
204                         this._response[key] = '';
205                     }
206                     this._response[key] += msg[key];
207                 }
208                 //params should not change through the message
209                 else if (poa.params.indexOf(key) >= 0){
210                     if (!this._response.hasOwnProperty(key)){
211                         this._response[key] = msg[key];
212                     }
213                     else if (this._response[key] !== msg[key]){
214                         return gpgme_error('CONN_UNEXPECTED_ANSWER',msg[key]);
215                     }
216                 }
217                 //infos may be json objects etc. Not yet defined.
218                 // Pushing them into arrays for now
219                 else if (poa.infos.indexOf(key) >= 0){
220                     if (!this._response.hasOwnProperty(key)){
221                         this._response[key] = [];
222                     }
223
224                     if (Array.isArray(msg[key])) {
225                         for (let i=0; i< msg[key].length; i++) {
226                             this._response[key].push(msg[key][i]);
227                         }
228                     } else {
229                         this._response[key].push(msg[key]);
230                     }
231                 }
232                 else {
233                     return gpgme_error('CONN_UNEXPECTED_ANSWER');
234                 }
235                 break;
236             }
237         }
238         return true;
239     }
240
241     /**
242      * @returns {Object} the assembled message, original data assumed to be
243      * (javascript-) strings
244      */
245     get message(){
246         let keys = Object.keys(this._response);
247         let msg = {};
248         let poa = permittedOperations[this.operation].answer;
249         for (let i=0; i < keys.length; i++) {
250             if (poa.data.indexOf(keys[i]) >= 0
251                 && this._response.base64 === true
252             ) {
253                 msg[keys[i]] = atob(this._response[keys[i]]);
254                 if (this.expected === 'base64'){
255                     msg[keys[i]] = this._response[keys[i]];
256                 } else {
257                     msg[keys[i]] = decodeURIComponent(
258                         atob(this._response[keys[i]]).split('').map(
259                             function(c) {
260                                 return '%' +
261                                 ('00' + c.charCodeAt(0).toString(16)).slice(-2);
262                             }).join(''));
263                 }
264             } else {
265                 msg[keys[i]] = this._response[keys[i]];
266             }
267         }
268         return msg;
269     }
270 }