PluginProbe
UpdraftCentral Dashboard / trunk
UpdraftCentral Dashboard vtrunk
0.8.33 0.7.2 0.7.3 0.7.4 0.8.0 0.8.1 0.8.10 0.8.11 0.8.12 0.8.13 0.8.14 0.8.15 0.8.16 0.8.17 0.8.18 0.8.19 0.8.2 0.8.20 0.8.21 0.8.22 0.8.23 0.8.24 0.8.25 0.8.26 0.8.27 All 51 releases
updraftcentral / js / class-udrpc.js

class-udrpc.js in UpdraftCentral Dashboard trunk, at js/class-udrpc.js

944 lines 33.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Ported from the PHP version, class-udrpc.php
3 *
4 * (c) David Anderson 2015-
5 * http://david.dw-perspective.org.uk
6 *
7 * Various things, which are mostly relevant to the server-side, are not ported (i.e. not included in this version). They will emit a message on console.log() if called. It is better to see this as a related library, rather than an exact port.
8 */
9
10 var UpdraftPlus_Remote_Communications = function(key_name_indicator, can_generate) {
11
12 // Version numbers are internal to this library only - e.g. not comparable with the PHP port
13 this.version = '0.3.3 (27/July/2016)';
14
15 // Local storage
16 this.key_name_indicator = '';
17
18 this.key_remote = false;
19 this.key_local = false;
20
21 this.can_generate = false;
22
23 this.destination_url = false;
24
25 this.maximum_replay_time_difference = 300;
26 this.extra_replay_protection = false;
27
28 // Levels: 1 (basic debugging), 2 (deep - e.g. cryptographic internals)
29 this.debug_level = 0;
30
31 this.seen_hashes = {};
32
33 this.cors_headers_wanted = 1;
34
35 // The legacy message format (1) is not supported
36 this.format = 2;
37
38 this.time_correction_sec = 0;
39
40 this.http_credentials = {};
41
42 this.auth_method = 'jquery';
43
44 this.message_wrapper = false;
45 this.message_unwrapper = false;
46
47 this.message_random_number = false;
48
49 // Default parameters
50 key_name_indicator = typeof key_name_indicator !== 'undefined' ? key_name_indicator : 'default';
51 can_generate = typeof can_generate !== 'undefined' ? can_generate : false;
52
53 /**
54 * Get the time now, in seconds, according to the browser, as corrected by any previously supplied correction
55 *
56 * @returns {int} the time in seconds since the UNIX epoch
57 *
58 * @see register_time
59 */
60 this.time_now = function() {
61 var time_now = Math.floor(Date.now() / 1000) + this.time_correction_sec;
62 return time_now;
63 }
64
65 /**
66 * Pass in the time from the server, on the supposition that it is more likely to be accurate than the time on the client. The replay window protections in the library rely upon accurate, agreed time between the two ends. The point is not to get an absolutely accurate time; but one close enough to the reality such that both ends of the conversation are likely to be able to agree on it.
67 *
68 * @param {number} server_time - the time in seconds (anything more accurate is pointless, given that it came over the network, and given the algorithm below)
69 * @returns {void}
70 */
71 this.register_time = function(server_time) {
72 if (typeof server_time == 'undefined') return;
73 var time_now = Math.floor(Date.now() / 1000);
74 var diff = server_time - time_now;
75 if (Math.abs(diff) > 120) {
76 console.log("UDRPC: Time difference detected; time_now="+time_now+", server_time="+server_time+", store_diff="+diff);
77 this.time_correction_sec = diff;
78 }
79 }
80
81 /**
82 * Left-pads a string with the supplied padding
83 *
84 * @param {string} padwith - a string of what to left-pad with, equal in length to the final desired string
85 * @param {string} string - the string to be padded
86 * @returns {string} the left padded string
87 */
88 this.paddingleft = function(padwith, string) {
89 return String(padwith + string).slice(-padwith.length);
90 };
91
92 /**
93 * Set the key name indicator for all future communications. The key name indicator is sent, unencrypted to the remote side, to enable the remote side to know which decryption key to use (analogous to SNI in the TLS protocol).
94 *
95 * @param {string} key_name_indicator -
96 * @returns {void}
97 */
98 this.set_key_name_indicator = function(key_name_indicator) {
99 this.key_name_indicator = key_name_indicator;
100 }
101
102 /**
103 * Indicate whether the remote application should set CORS headers in its reply.
104 *
105 * @param {boolean} [wanted=true] - whether to request that the remote application should include CORS headers in its reply
106 * @returns {void}
107 */
108 this.set_cors_headers_wanted = function(wanted) {
109 wanted = ('undefined' === typeof wanted) ? true : wanted;
110 this.cors_headers_wanted = wanted;
111 }
112
113 /**
114 * Indicate that if a key is needed and not set by the caller, then one can be generated
115 *
116 * @param {boolean} [can_generate=true] - indicate whether or not a key can be generated
117 * @returns {void}
118 */
119 this.set_can_generate = function(can_generate) {
120 this.can_generate = typeof can_generate !== 'undefined' ? can_generate : true;
121 }
122
123 /**
124 * Sets the maximum time that can pass before a message will be rejected as too old (and likely a replay)
125 *
126 * @param {number} replay_time_difference - the number of seconds
127 * @returns {void}
128 */
129 this.set_maximum_replay_time_difference = function(replay_time_difference) {
130 this.maximum_replay_time_difference = parseInt(replay_time_difference);
131 }
132
133 /**
134 * Set the debugging level - controlling how much info is logged in the JS console. This may include keys and other cryptographic information. Though, it your security depends on the user not reading their JavaScript console, then you need to re-think.
135 *
136 * @param {number} [debug_level=1] - an integer, with higher numbers indicating more debugging; zero to indicate no debugging
137 * @returns {void}
138 */
139 this.set_debug_level = function(debug_level) {
140 this.debug_level = typeof debug_level !== 'undefined' ? debug_level : 1;
141 if (this.debug_level) {
142 console.log("UDRPC: Debug mode activated (level: "+debug_level+")");
143 }
144 }
145
146 /**
147 * Log an error if the cryptography library is not loaded. This function doesn't actually ensure it's loaded - that is left up to the composer of the application - but, we do log in case it's not, to assist debugging.
148 *
149 * @returns {void}
150 */
151 this.ensure_crypto_loaded = function() {
152 if ('undefined' == typeof forge) {
153 console.log("UDRPC JS: No loaded forge library found (you should make sure it has loaded before calling UDRPC functions)");
154 throw "No loaded forge library found (you should make sure it has loaded before calling UDRPC functions)";
155 }
156 }
157
158 /**
159 * Sets the destination URL for any remote calls to be made
160 *
161 * @param {string} destination_url - the destination URL to send messages to
162 * @returns {void}
163 */
164 this.set_destination_url = function(destination_url) {
165 this.destination_url = destination_url;
166 }
167
168 /**
169 * Sets a wrapper (container) for any remote calls to be made
170 *
171 * @param {Object|boolean} message_wrapper - if set, then the actual message will be placed in this object as the property 'wrapped_message', before being sent (instead of being sent directly). To unset, set it to false.
172 * @returns {void}
173 */
174 this.set_message_wrapper = function(message_wrapper) {
175 this.message_wrapper = message_wrapper;
176 }
177
178 /**
179 * Unwrap the call back - unwrapperCallback
180 *
181 * @param {*} data - The response data, which the unwrapper should unwrap
182 *
183 * @returns {Object|Boolean} - the unwrapped data, or false if there was a problem and execution should return
184 */
185
186 /**
187 * Sets a function to be used to unwrap the results of any remote calls made
188 *
189 * @param {unwrapperCallback} message_unwrapper - the function to be called to unwrap the message
190 * @returns {void}
191 */
192 this.set_message_unwrapper = function(message_unwrapper) {
193 this.message_unwrapper = message_unwrapper;
194 }
195
196 /**
197 * This is an unused function in the JavaScript port, and will simply log a message to the console
198 *
199 * @param {*} [key_option_name] - unused parameter
200 * @returns {void}
201 */
202 this.set_option_name = function(key_option_name) {
203 console.log("UDRPC: set_option_name() called - which is wrong/unnecessary in the JavaScript port");
204 }
205
206 /**
207 * Get the remote site's (public) key
208 *
209 * @returns {string|boolean} - the public key; or false if none has been set
210 */
211 this.get_key_remote = function() {
212 if (!this.key_remote && this.can_generate) {
213 this.generate_new_keypair();
214 }
215 return this.key_remote ? this.key_remote : false;
216 }
217
218 /**
219 * Get the remote site's (public) key
220 *
221 * @param {string} key_remote - the remote site's (public) key, in PEM format
222 * @returns {void}
223 */
224 this.set_key_remote = function(key_remote) {
225 this.key_remote = key_remote;
226 }
227
228 /**
229 * Get the local site's (private) key
230 *
231 * @returns {string|boolean} - the local site's (private) key, in PEM format; or false if none has been set
232 */
233 this.get_key_local = function() {
234 if (!this.key_local && this.can_generate) {
235 this.generate_new_keypair();
236 }
237 return this.key_local ? this.key_local : false;
238 }
239
240 /**
241 * Unimplemented function in the JavaScript library
242 *
243 * @param {*} bundle - unused parameter
244 * @param {*} format - unused parameter
245 * @returns {boolean} - unimplemented function, always returns false
246 */
247 this.decode_portable_bundle = function(bundle, format) {
248 format = typeof format !== 'undefined' ? format : 'raw';
249 this.unimplemented_function('decode_portable_bundle');
250 return false;
251 }
252
253 /**
254 * An unimplemented function to get a portable bundle sufficient to contact this site (i.e. remote site - so you need to have generated a key-pair, or stored the remote key somewhere and restored it)
255 *
256 * @param {string} [format='raw'] - either 'raw' or 'base64_with-count' - the output format to use
257 *
258 * @returns {boolean} - unimplemented function, always returns false
259 */
260 this.get_portable_bundle = function(format) {
261 format = typeof format !== 'undefined' ? format : 'raw';
262 this.unimplemented_function('get_portable_bundle');
263 return false;
264 }
265
266 /**
267 * Set the local (private) key to be used for decryption
268 *
269 * @param {string} key_local - the key to be used, in PEM format
270 * @returns {void}
271 */
272 this.set_key_local = function(key_local) {
273 this.key_local = key_local;
274 }
275
276 /**
277 * Logs the fact that an unimplemented function has been called
278 *
279 * @param {string} funcname - A name of a function that is undefined
280 * @returns {void}
281 */
282 this.unimplemented_function = function(funcname) {
283 console.log("UDRPC: Unimplemented function in JavaScript port called: "+funcname);
284 }
285
286 /**
287 * Unimplemented function - merely logs the fact that it was called
288 *
289 * @returns {boolean} - unimplemented function, always returns false
290 */
291 this.generate_new_keypair = function() {
292 this.unimplemented_function('generate_new_keypair');
293 return false;
294 }
295
296 /**
297 * Encrypts the given message
298 *
299 * @param {string} plaintext - the message to be encrypted
300 * @param {string|boolean} [use_key=false] - the RSA public key to use to encrypt the message (in PEM format); if false, then the remote key (set with set_key_remote()) will be used.
301 * @param {numeric} [key_length=32] - the length to use for the random symmetric key that is generated (AES-CBC)
302 *
303 * @returns {string} - the encrypted message, in the UDRPC format
304 */
305 this.encrypt_message = function(plaintext, use_key, key_length) {
306
307 use_key = typeof use_key !== 'undefined' ? use_key : false;
308
309 if (!use_key && !this.key_remote) throw 'No encryption key has been set';
310
311 if (!use_key) use_key = this.key_remote;
312
313 this.ensure_crypto_loaded();
314
315 var pub_key = forge.pki.publicKeyFromPem(use_key);
316
317 if (typeof key_length === 'undefined') {
318 var pub_key_length = Math.ceil(pub_key.n.bitLength() / 8);
319 // Catch the case of 512-bit private keys - which otherwise cause an error from the encrypt call below, because only 22 bytes can be encrypted to a 512-bit private key.
320 if (pub_key_length < 65) {
321 key_length = 16;
322 } else {
323 key_length = 32;
324 }
325 }
326
327 // Generate Random Symmetric Key
328 var sym_key = forge.random.getBytesSync(key_length);
329 if (this.debug_level > 1) console.log("Generated symmetric key, in hex: "+forge.util.bytesToHex(sym_key));
330
331 // Encrypt Message with new Symmetric Key
332 var cipher = forge.cipher.createCipher('AES-CBC', sym_key);
333
334 cipher.start({iv: ''});
335 cipher.update(forge.util.createBuffer(forge.util.encodeUtf8(plaintext)));
336 cipher.finish();
337 var ciphertext = cipher.output;
338
339 if (this.debug_level > 1) console.log("Ciphertext, in hex (PHP equiv: bin2hex): "+forge.util.bytesToHex(ciphertext));
340
341 ciphertext = forge.util.encode64(ciphertext.bytes());
342 if (this.debug_level > 1) console.log("Ciphertext, in base64: "+ciphertext);
343
344 // Encrypt the Symmetric Key with the Asymmetric Key
345 sym_key = pub_key.encrypt(sym_key, 'RSA-OAEP');
346 if (this.debug_level > 1) console.log("Symmetric key, after being encrypted with the assymetric key, in hex: "+forge.util.bytesToHex(sym_key));
347
348 // Base 64 encode the symmetric key for transport
349 sym_key = forge.util.encode64(sym_key);
350 if (this.debug_level > 1) console.log("Encrypted symmetric key, in base 64: "+sym_key);
351
352 var len = sym_key.length;
353 if (this.debug_level > 1) console.log("Key length (decimal): "+len);
354 // This converts to hex
355 len = len.toString(16);
356 len = this.paddingleft('000', len);
357
358 // 16 characters of hex is enough for the payload to be to 16 exabytes (giga < tera < peta < exa) of data
359 var cipherlen = ciphertext.length;
360 if (this.debug_level > 1) console.log("Cipher length (decimal): "+cipherlen);
361 cipherlen = cipherlen.toString(16);
362 cipherlen = this.paddingleft('0000000000000000', cipherlen);
363
364 if (this.debug_level > 1) {
365 console.log("Length, hexed + padded (3): "+len);
366 console.log("Cipherlength, hexed + padded (16): "+cipherlen);
367 }
368
369 // Concatenate the length, the encrypted symmetric key, and the message
370 return len+sym_key+cipherlen+ciphertext;
371
372 }
373
374 /**
375 * Decrypts the passed message, using the private key (which needs to be previous set with set_key_local())
376 *
377 * @param {string} message - the message to decrypt, in UDRPC format
378 *
379 * @returns {string} - the decrypted message
380 */
381 this.decrypt_message = function(message) {
382
383 if (!this.key_local) throw new Exception('No decryption key has been set');
384
385 this.ensure_crypto_loaded();
386
387 // Extract the Symmetric Key
388 var len = message.substr(0, 3);
389 if (this.debug_level > 1) console.log("Key length (hex + passed): "+len);
390
391 // Convert to decimal
392 len = parseInt(len, 16);
393 if (this.debug_level > 1) console.log("Key length (decimal): "+len);
394
395 var sym_key = message.substr(3, len);
396 if (this.debug_level > 1) console.log("Encrypted symmetric key, base64-encoded: "+sym_key);
397 sym_key = forge.util.decode64(sym_key);
398 if (this.debug_level > 1) console.log("Encrypted symmetric key, in hex: "+forge.util.bytesToHex(sym_key));
399
400 // Extract the encrypted message
401 var cipherlen = message.substr(len+3, 16);
402 if (this.debug_level > 1) console.log("Ciphertext length (hex + passed): "+cipherlen);
403
404 // Convert to decimal
405 cipherlen = parseInt(cipherlen, 16);
406 if (this.debug_level > 1) console.log("Ciphertext length (decimal): "+cipherlen);
407
408 var ciphertext = message.substr(len+19, cipherlen);
409 if (this.debug_level > 1) console.log("Ciphertext (base64): "+ciphertext);
410
411 ciphertext = forge.util.decode64(ciphertext);
412
413 if (this.debug_level > 1) console.log("Ciphertext, in hex (PHP equiv: bin2hex): "+forge.util.bytesToHex(ciphertext));
414
415 var privKey = forge.pki.privateKeyFromPem(this.key_local);
416 // var pubKey = forge.pki.rsa.setPublicKey(privKey.n, privKey.e);
417
418 if (this.debug_level > 1) console.log("Attempting to decrypt symmetric key");
419
420 // Decrypt the RSA-encrypted symmetric key
421 var sym_key = privKey.decrypt(sym_key, 'RSA-OAEP');
422 if (this.debug_level > 1) console.log("Generated symmetric key, after being decrypted, in hex: "+forge.util.bytesToHex(sym_key));
423
424 var decipher = forge.cipher.createDecipher('AES-CBC', sym_key);
425 // Keys are only used once - no unique IV is needed
426 decipher.start({iv: ''});
427 decipher.update(forge.util.createBuffer(ciphertext));
428 decipher.finish();
429
430 // Return the plaintext
431 return forge.util.decodeUtf8(decipher.output);
432
433 }
434
435 /**
436 * Creates a message which the caller can then format and send as required (e.g. use as body in post, or JSON-encode, etc.)
437 *
438 * @param {string} command - the command to pass to the receiving side
439 * @param {*} [data=null] - any data associated with the command
440 * @param {boolean} [is_response=false] - whether this message is a response to an incoming message (in which case, the format slightly varies)
441 * @param {string|boolean} [use_key_remote=false] - the RSA public key to encrypt to, in PEM format; if false, then the key set with set_key_remote() will be used
442 * @param {string|boolean} [use_key_local=false] - the RSA private key to sign with, in PEM format; if false, then the key set with set_key_local() will be used
443 *
444 * @returns {Array} - the message
445 */
446 this.create_message = function(command, data, is_response, use_key_remote, use_key_local) {
447
448 use_key_remote = typeof use_key_remote !== 'undefined' ? use_key_remote : false;
449 use_key_local = typeof use_key_local !== 'undefined' ? use_key_local : false;
450 data = typeof data !== 'undefined' ? data : null;
451 is_response = typeof is_response !== 'undefined' ? is_response : false;
452
453 var send_array = {};
454 if (is_response) {
455 send_array.response = command;
456 } else {
457 send_array.command = command;
458 }
459
460 if (this.cors_headers_wanted) { send_array.cors_headers_wanted = 1; }
461
462 // UNIX time
463 send_array.time = this.time_now();
464 // This goes in the encrypted portion as well to prevent replays with a different unencrypted name indicator
465 send_array.key_name = this.key_name_indicator;
466
467 // This random element means that if the site needs to send two identical commands or responses in the same second, then it can, and still use replay protection
468 // We store it so that the caller can access it, if wanted
469 // The maximum is the value of PHP_INT_MAX on a 32-bit platform
470 this.message_random_number = Math.round(Math.random() * 2147483647);
471 send_array.rand = this.message_random_number;
472
473 // Not implemented
474 // if (this.next_send_sequence_id) {
475 // $send_array['sequence_id'] = this.next_send_sequence_id;
476 // this.next_send_sequence_id++;
477 // }
478
479 if (null !== data) send_array.data = data;
480
481 var send_data = this.encrypt_message(JSON.stringify(send_array), use_key_remote);
482
483 // This library only supports format 2. So, we don't need to compare the format before adding the signed hash.
484 var raw_message = {
485 format: this.format,
486 key_name: this.key_name_indicator,
487 udrpc_message: send_data,
488 signature: this.signature_for_message(send_data, use_key_local)
489 };
490
491 var message = raw_message;
492
493 // Optional - if the request is being proxied and needs a wrapper
494 if (this.message_wrapper !== false) {
495 message = this.message_wrapper;
496 message.wrapped_message = raw_message;
497 }
498
499 return message;
500
501 }
502
503 /**
504 * Returns a base-64 encoded RSA hash (PKCS_1) of the message digest
505 *
506 * @param {string} message - the message to be hashed
507 * @param {string|boolean} [use_key=false] - the RSA key to use for calculating the hash, in PEM format. If false, then the value of this.key_local will be used.
508 *
509 * @returns {string} the signature
510 */
511 this.signature_for_message = function(message, use_key) {
512
513 use_key = typeof use_key !== 'undefined' ? use_key : false;
514
515 if (!use_key && !this.key_local) throw 'No encryption key (local) has been set';
516
517 if (!use_key) use_key = this.key_local;
518
519 this.ensure_crypto_loaded();
520
521 // This isn't yet variable
522 var hash_algorithm = 'sha256';
523
524 var md = forge.md.sha256.create();
525 md.update(message, 'utf8');
526
527 var privateKey = forge.pki.privateKeyFromPem(use_key);
528 // Defaults to RSASSA PKCS#1 v1.5, but it doesn't hurt to specify
529
530 var signature = privateKey.sign(md, 'RSASSA-PKCS1-V1_5');
531
532 var encoded = forge.util.encode64(signature);
533
534 if (this.debug_level > 0) {
535 console.log("Hash ("+hash_algorithm+") (follows)");
536 console.log(md);
537 if (this.debug_level > 1) {
538 console.log("Signature (raw): "+signature);
539 console.log("Signature (base64-ed): "+encoded);
540 }
541 }
542
543 return encoded;
544 }
545
546 /**
547 * Verify that a message's signature is correct. Note that Only SHA256 is supported; the passed hash_algorithm parameter is ignored.
548 *
549 * @param {string} message - the message whose signature is to be validated
550 * @param {string} signature - the SHA256 signature for the message.
551 * @param {string} key - the public key, in PEM format, for verifying the signature with
552 * @param {string} hash_algorithm - ignored (the signature is always treated as being a SHA256 hash)
553 *
554 * @returns {boolean} - the result
555 */
556 this.verify_signature = function(message, signature, key, hash_algorithm) {
557
558 hash_algorithm = typeof hash_algorithm !== 'undefined' ? 'sha256' : false;
559 this.ensure_crypto_loaded();
560
561 var md = forge.md.sha256.create();
562 md.update(message, 'utf8');
563 var digest = md.digest().bytes();
564
565 var publicKey = forge.pki.publicKeyFromPem(key);
566
567 if (this.debug_level > 1) {
568 // console.log("UDRPC: publicKey for verifying with: "+key);
569 console.log("UDRPC: verify_signature: message hash (hex): "+forge.util.bytesToHex(digest));
570 // console.log("UDRPC: verify_signature: message hash (base64): "+forge.util.encode64(digest));
571 // console.log("UDRPC: verify_signature: signature (len="+signature.length+") (hex): "+forge.util.bytesToHex(signature));
572 console.log("UDRPC: verify_signature: signature (len="+signature.length+") (existing base64): "+signature);
573 }
574
575 var verified = publicKey.verify(digest, forge.util.decode64(signature));
576
577 if (this.debug_level > 0) console.log("UDRPC: verify signature: result: "+verified);
578
579 return verified;
580 }
581
582 /**
583 * Activate, or de-activate, additional replay protection. This stores message hashes, and compares new messages with previous hashes to detect replays. (Since each message contains both a random and a time-based element, no clashes are expected).
584 *
585 * @param {boolean} [activate=true] - whether or not to activate replay protection
586 * @returns {void}
587 */
588 this.activate_replay_protection = function(activate) {
589 this.extra_replay_protection = typeof activate !== 'undefined' ? activate : true;
590 }
591
592 /**
593 * Set HTTP credentials, for all future HTTP calls
594 *
595 * @param {Object} http_credentials - a object for which useful properties are properties 'username' and 'password'
596 * @returns {void}
597 */
598 this.set_http_credentials = function(http_credentials) {
599 this.http_credentials = http_credentials;
600 }
601
602 /**
603 * Set the authorisation method - whether jQuery, or manual (set the Authorization header explicitly)
604 *
605 * @param {string} auth_method - either 'jquery' or 'manual', according to how you want the authorisation to be carried out
606 * @returns {void}
607 */
608 this.set_auth_method = function(auth_method) {
609 this.auth_method = auth_method;
610 }
611
612 // this.set_next_send_sequence_id = function(id) {}
613
614 /**
615 * Post reponse callback - PostResponseCallback
616 *
617 * @param {String} body - The body of the HTTP response
618 * @param {String} status - The status description returned from jQuery, e.g. 'success'|'error'
619 * @param {*} data - Data accompanying the response. In the case of status being 'error', this will be a string giving the error code.
620 */
621
622 /**
623 * Sends an HTTP POST request to the specified destination with the specified data
624 *
625 * @param {string} url - the URL to send the POST request to
626 * @param {Object|null} spinner_where - where to place a spinner object in the DOM during the request
627 * @param {*} data - the data to send to as the data for the POST request
628 * @param {postResponseCallback} response_callback - callback function to be called with the response
629 * @param {number} [timeout=30] - the number of seconds after which the HTTP request whould time-out
630 * @returns {void}
631 */
632 this.send_post = function(url, spinner_where, data, response_callback, timeout) {
633 timeout = typeof timeout !== 'undefined' ? timeout : 30;
634 if (spinner_where) {
635 jQuery(spinner_where).addClass('updraftcentral_spinner');
636 }
637 try {
638 if (this.debug_level > 0) console.log("send_post: "+url);
639
640 // https://api.jquery.com/jquery.ajax/
641 var ajax_options = {
642 type: 'POST',
643 url: url,
644 data: data,
645 dataType: 'text',
646 // This header ensures that it won't be a "simple" CORS request, which has the bonus of making the CORS path predictable (rather than some 'simple', some not)
647 headers: {
648 'X-Secondary-User-Agent': 'class-udrpc.js/'+this.version
649 },
650 timeout: timeout * 1000, // In ms
651 success: function(response) {
652 if (spinner_where) {
653 jQuery(spinner_where).removeClass('updraftcentral_spinner');
654 }
655 response_callback.call(this, response);
656 },
657 error: function(request, status, error_thrown) {
658 // We don't actually need these errors if the browser was reloaded because users can no longer
659 // take any action since it's already been too late. These error info will only be shown if the
660 // browser reload action is not the one causing the error due to an abrupt halting of a current AJAX process.
661 if (!UpdraftCentral.reloaded) {
662 console.log("UDRPC: Error in send_post (url="+url+")");
663 console.log(request);
664 console.log(status);
665 // https://api.jquery.com/jquery.ajax/ says: 'When an HTTP error occurs, (this parameter) receives the textual portion of the HTTP status, such as "Not Found" or "Internal Server Error."'
666 // "Unauthorized" is what you get when HTTP authentication is required. "Timeout" when there's a timeout.
667 console.log(error_thrown);
668 }
669
670 if (spinner_where) {
671 jQuery(spinner_where).removeClass('updraftcentral_spinner');
672 }
673 if ('' == error_thrown) { error_thrown = 'http_post_fail'; }
674
675 if (error_thrown.hasOwnProperty('statusText')) {
676 error_thrown = error_thrown.statusText.toString();
677 }
678
679 if ('function' === typeof error_thrown.toLowerCase) {
680 error_thrown = error_thrown.toLowerCase();
681 } else {
682 try {
683 var tmp = error_thrown.toString().toLowerCase();
684 if (tmp) { error_thrown = tmp; }
685 } catch (e) {
686 }
687 }
688 response_callback.call(this, request, 'error', error_thrown);
689 }
690 }
691
692 if (this.http_credentials.hasOwnProperty('username')) {
693
694 var password = (this.http_credentials.hasOwnProperty('password')) ? this.http_credentials.password : '';
695
696 if (this.auth_method == 'manual') {
697
698 ajax_options.headers.Authorization = 'Basic '+forge.util.encode64(this.http_credentials.username+':'+password);
699
700 } else {
701 // Default: jquery
702 ajax_options.xhrFields = {
703 withCredentials: true
704 }
705 ajax_options.username = this.http_credentials.username;
706 if (this.http_credentials.hasOwnProperty('password')) {
707 ajax_options.password = this.http_credentials.password;
708 }
709 }
710 }
711
712 if (this.debug_level > 1) {
713 console.log("UDPRC: jQuery POST: options follow:");
714 console.log(ajax_options);
715 }
716
717 jQuery.ajax(ajax_options);
718
719 } catch (e) {
720 // Not sure if anything more needs doing here
721 console.log("UDRPC: Exception in send_post (url="+url+")");
722 console.log(e);
723 throw e;
724 }
725 }
726
727 /**
728 * Response Call back
729 *
730 * @callable responseCallback
731 * @param {*} response - the response from the call. The format depends upon what was sent, and upon the error status. In the case of an HTTP error from jQuery, this will be the jqXHR object.
732 * @param {String} code - the basic response status; either 'error', or the data sent from the remote side
733 * @param {String} [error_code] - if code was 'error', then an error code
734 */
735
736 /**
737 * Send a message to the remote site
738 *
739 * @param {string} command - the command to be sent
740 * @param {*} [data=null] - accompanying data associated with the command (if any)
741 * @param {number} [timeout=30] - the number of seconds for the timeout on the resulting HTTP call
742 * @param {responseCallback} response_callback - callback function which is called with the results of the call
743 * @returns {void}
744 */
745 this.send_message = function(command, data, timeout, response_callback) {
746
747 data = typeof data !== 'undefined' ? data : null;
748 timeout = typeof timeout !== 'undefined' ? timeout : 30;
749
750 if (!this.destination_url) {
751 console.log("UDRPC: send_message: no destination URL has been initialised");
752 throw 'RPC error: destination URL not initialised';
753 }
754
755 message = this.create_message(command, data, false);
756
757 var message_random_number = this.message_random_number;
758
759 var ud_rpc = this;
760
761 this.send_post(this.destination_url, false, message, function(body, status, data) {
762
763 if ('error' == status) {
764 response_callback.call(this, body, 'error', data);
765 return;
766 }
767
768 if ('' === body) {
769 console.log("UDRPC: the response from the remote site was empty");
770 response_callback.call(this, body, 'error', 'response_empty');
771 return;
772 }
773
774 try {
775 var response = JSON.parse(body);
776 } catch (e) {
777 console.log(e);
778 response_callback.call(this, body, 'error', 'json_parse_fail');
779 return;
780 }
781
782 try {
783
784 if (!response) {
785 console.log("UDRPC: the response from the remote site was empty");
786 console.log(body);
787 response_callback.call(this, body, 'error', 'parsed_response_not_understood');
788 return;
789 }
790
791 if (false !== ud_rpc.message_unwrapper) {
792 var unwrapped_response = ud_rpc.message_unwrapper.call(this, response);
793 if (false === unwrapped_response) {
794 response_callback.call(this, response, 'error', 'unwrapper_failure');
795 return;
796 }
797 response = unwrapped_response;
798 }
799
800 if (!response.hasOwnProperty('udrpc_message')) {
801 console.log("UDRPC: the response from the remote site could not be understood (follows)");
802 console.log(body);
803 response_callback.call(this, body, 'error', 'parsed_response_not_understood');
804 return;
805 }
806
807 if (!response.hasOwnProperty('signature') || !response.signature) {
808 console.log("UDRPC: No signature found on response from remote site - message dropped");
809 response_callback.call(this, response, 'error', 'response_no_signature');
810 return;
811 }
812
813 try {
814 if (!ud_rpc.verify_signature(response.udrpc_message, response.signature, ud_rpc.key_remote)) {
815 console.log("UDRPC: Verify signature on response: failed - message dropped");
816 response_callback.call(this, response, 'error', 'response_signature_invalid');
817 return;
818 } else if (ud_rpc.debug) {
819 console.log("UDRPC: Verify signature on response: OK");
820 }
821 } catch (e) {
822 console.log(e);
823 response_callback.call(this, response, 'error', 'signature_verify_exception');
824 return;
825 }
826
827 try {
828 var decoded = ud_rpc.decrypt_message(response.udrpc_message);
829 } catch (e) {
830 console.log(e);
831 response_callback.call(this, e, 'error', 'decryption_error');
832 return;
833 }
834
835 var json_decoded = JSON.parse(decoded);
836
837 if (!json_decoded.hasOwnProperty('response') || !json_decoded.hasOwnProperty('time')) {
838 console.log('response_corrupt: Response from remote site was not in the expected format (follows)');
839 console.log(response);
840 console.log(json_decoded);
841 // throw 'Response from remote site was not in the expected format';
842 response_callback.call(this, decoded, 'error', 'parsed_response_bad_format');
843 return;
844 }
845
846 // Don't do the reply detection until now, because post['body'] may not be a message that originated from the remote component at all (e.g. an HTTP error)
847 if (ud_rpc.extra_replay_protection) {
848 message_hash = ud_rpc.calculate_message_hash(body);
849 if (ud_rpc.message_hash_seen(message_hash)) {
850 console.log("Message refused: replay detected");
851 console.log(message_hash);
852 response_callback.call(this, json_decoded.response, 'error', 'response_replay_detected');
853 return;
854 }
855 }
856
857 var time_now = ud_rpc.time_now();
858
859 time_difference = (time_now - json_decoded.time);
860 if (time_difference > ud_rpc.maximum_replay_time_difference) {
861 console.log("UDRPC: Message refused: too late - diff="+time_difference+", maximum_difference="+ud_rpc.maximum_replay_time_difference);
862 response_callback.call(this, json_decoded.response, 'error', 'response_refused_too_late');
863 return;
864 // throw 'Message refused: too late';
865 }
866
867 if (json_decoded.hasOwnProperty('incoming_rand') && message_random_number && json_decoded.incoming_rand != message_random_number) {
868 console.log("UDRPC: Message mismatch (possibly MITM) (sent_rand="+message_random_number+", returned_rand="+json_decoded.incoming_rand+"): dropping");
869 response_callback.call(this, json_decoded.response, 'error', 'response_mismatch');
870 return;
871 }
872
873 } catch (e) {
874 console.log(e);
875 response_callback.call(this, response, 'error', 'js_exception');
876 return;
877 }
878
879 // Should be an object with keys including 'response' and (if relevant) 'data'
880 response_callback.call(this, json_decoded, 'ok');
881 }, timeout);
882
883 }
884
885 /**
886 * Unimplmented function - merely logs the fact that it was called
887 *
888 * @returns {boolean} - unimplemented function, always returns false
889 */
890 this.create_listener = function() {
891 this.unimplemented_function('create_listener');
892 return false;
893 }
894
895 /**
896 * Calculates a SHA256 hash for the passed message
897 *
898 * @param {string} message - the message to create a hash for
899 *
900 * @returns {string} - the SHA256 hash, in hex
901 */
902 this.calculate_message_hash = function(message) {
903 this.ensure_crypto_loaded();
904 var md = forge.md.sha256.create();
905 md.update(message);
906 return md.digest().toHex();
907 }
908
909 /**
910 * Indicate whether the message hash has been previously seen. As this class is designed to run within a browser, that means "within this browser session".
911 *
912 * @param {string} message_hash - the message hash. The result of the function will be true if and only if this has been passed to the function previously.
913 *
914 * @returns {boolean} - whether or not the hash has been previously seen.
915 */
916 this.message_hash_seen = function(message_hash) {
917 var seen_hashes = this.seen_hashes;
918 var time_now = this.time_now();
919
920 var any_changes = false;
921 var seen_it = false;
922
923 // Prune the old hashes
924 jQuery.each(seen_hashes, function(i, hash) {
925 var last_seen = seen_hashes.hash;
926 if (last_seen < (time_now - this.maximum_replay_time_difference)) {
927 any_changes = true;
928 delete seen_hashes.hash;
929 } else if (hash == message_hash) {
930 seen_it = true;
931 any_changes = true;
932 seen_hashes.hash = time_now;
933 }
934 });
935
936 return seen_it;
937 }
938
939 // Setup
940 this.set_key_name_indicator(key_name_indicator);
941
942 return this;
943 }
944