PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 4.7.0
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v4.7.0
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / team-updraft / common-libs / src / updraft-rpc / class-udrpc3.php

class-udrpc3.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 4.7.0, at vendor/team-updraft/common-libs/src/updraft-rpc/class-udrpc3.php

1,110 lines 40.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreStart
3 /*
4 This class provides methods for encrypting, sending, receiving and decrypting messages of arbitrary length, using standard encryption methods and including protection against replay attacks.
5
6 Example:
7
8 // Set a key and encrypt with it
9 $ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
10 $ud_rpc->set_key_local($our_private_key);
11 $ud_rpc->set_key_remote($their_public_key);
12 $encrypted = $ud_rpc->encrypt_message('blah blah');
13
14 // Use the saved WP site option
15 $ud_rpc = new UpdraftPlus_Remote_Communications($name_indicator); // $name_indicator is a key indicator - indicating which key is being used.
16 $ud_rpc->set_option_name('udrpc_remotekey');
17 if (!$ud_rpc->get_key_remote()) throw new Exception('...');
18 $encrypted = $ud_rpc->encrypt_message('blah blah');
19
20 // Generate a new key
21 $ud_rpc = new UpdraftPlus_Remote_Communications('myindicator.example.com');
22 $ud_rpc->set_option_name('udrpc_localkey'); // Save as a WP site option
23 $new_pair = $ud_rpc->generate_new_keypair();
24 if ($new_pair) {
25 $local_private_key = $ud_rpc->get_key_local();
26 $remote_public_key = $ud_rpc->get_key_remote();
27 // ...
28 } else {
29 throw new Exception('...');
30 }
31
32 // Send a message
33 $ud_rpc->activate_replay_protection();
34 $ud_rpc->set_destination_url('https://example.com/path/to/wp');
35 $ud_rpc->send_message('ping');
36 $ud_rpc->send_message('somecommand', array('param1' => 'data', 'param2' => 'moredata'));
37
38 // N.B. The data sent needs to be something that will pass json_encode(). So, it may be desirable to base64-encode it first.
39
40 // Create a listener for incoming messages
41
42 add_filter('udrpc_command_somecommand', 'my_function', 10, 3);
43 // function my_function($response, $data, $name_indicator) { ... ; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
44 // Or:
45 // add_filter('udrpc_action', 'some_function', 10, 4); // Function must return something other than false to indicate that it handled the specific command. Any returned value will be sent as the reply.
46 // function some_function($response, $command, $data, $name_indicator) { ...; return array('response' => 'my_reply', 'data' => 'any mixed data'); }
47 $ud_rpc->set_option_name('udrpc_local_private_key');
48 $ud_rpc->activate_replay_protection();
49 if ($ud_rpc->get_key_local()) {
50 // Make sure you call this before the wp_loaded action is fired (e.g. at init)
51 $ud_rpc->create_listener();
52 }
53
54 // Instead of using activate_replay_protection(), you can use activate_sequence_protection() (receiving side) and set_next_send_sequence_id(). They are very similar; but, the sequence number code isn't tested, and is problematic if you may have multiple clients that don't share storage (you can use the current time as a sequence number, but if two clients send at the same millisecond (or whatever granularity you use), you may have problems); whereas the replay protection code relies on database storage on the sending side (not just the receiving).
55
56 */
57 // @codingStandardsIgnoreEnd
58 if (!class_exists('UpdraftPlus_Remote_Communications_V3')) :
59 class UpdraftPlus_Remote_Communications_V3 {
60
61 // Version numbers relate to versions of this PHP library only (i.e. it's not a protocol support number, and version numbers of other compatible libraries (e.g. JavaScript) are not comparable)
62 public $version = '3.1';
63
64 private $key_name_indicator;
65
66 private $key_option_name = false;
67
68 private $key_remote = false;
69
70 private $key_local = false;
71
72 private $can_generate = false;
73
74 private $destination_url = false;
75
76 private $maximum_replay_time_difference = 300;
77
78 private $extra_replay_protection = false;
79
80 private $sequence_protection_tolerance;
81
82 private $sequence_protection_table;
83
84 private $sequence_protection_column;
85
86 private $sequence_protection_where_sql;
87
88 // Debug may log confidential data using $this->log() - so only use when you are in a secure environment
89 private $debug = false;
90
91 private $next_send_sequence_id;
92
93 private $allow_cors_from = array();
94
95 private $http_transport = null;
96
97 // Default protocol version - this can be over-ridden with set_message_format
98 // Protocol version 1 (which uses only one RSA key-pair, instead of two) has been removed
99 private $format = 2;
100
101 private $http_credentials = array();
102
103 private $incoming_message = null;
104
105 private $message_random_number = null;
106
107 private $require_message_to_be_understood = false;
108
109 /**
110 * Constructor
111 *
112 * @param string $key_name_indicator
113 */
114 public function __construct($key_name_indicator = 'default') {
115 $this->set_key_name_indicator($key_name_indicator);
116 }
117
118 /**
119 * Set the key name indicator
120 *
121 * @param string $key_name_indicator
122 */
123 public function set_key_name_indicator($key_name_indicator) {
124 $this->key_name_indicator = $key_name_indicator;
125 }
126
127 /**
128 * Set whether generating a new key-pair is allowed
129 *
130 * @param boolean $can_generate
131 */
132 public function set_can_generate($can_generate = true) {
133 $this->can_generate = $can_generate;
134 }
135
136 /**
137 * Which sites to allow CORS requests from
138 *
139 * @param string $allow_cors_from
140 */
141 public function set_allow_cors_from($allow_cors_from) {
142 $this->allow_cors_from = $allow_cors_from;
143 }
144
145 public function set_maximum_replay_time_difference($replay_time_difference) {
146 $this->maximum_replay_time_difference = (int) $replay_time_difference;
147 }
148
149 /**
150 * This will cause more things to be sent to $this->log()
151 *
152 * @param boolean $debug
153 */
154 public function set_debug($debug = true) {
155 $this->debug = (bool) $debug;
156 }
157
158 /**
159 * Supported values: a Guzzle object, or, if not, then WP's HTTP API function siwll be used
160 *
161 * @param string $transport
162 */
163 public function set_http_transport($transport) {
164 $this->http_transport = $transport;
165 }
166
167 /**
168 * Sequence protection and replay protection perform similar functions, and using both is often over-kill; the distinction is that sequence protection can be used without needing to do database writes on the sending side (e.g. use the value of time() as the sequence number).
169 * The only rule of sequences is that the receiving side will reject any sequence number that is less than the last previously seen one, within the bounds of the tolerance (but it may also reject those if they are repeats).
170 * The given table/column will record a comma-separated list of recently seen sequences numbers within the tolerance threshold.
171 *
172 * @param string $table
173 * @param string $column
174 * @param string $where_sql
175 * @param integer $tolerance
176 */
177 public function activate_sequence_protection($table, $column, $where_sql, $tolerance = 5) {
178 $this->sequence_protection_tolerance = (int) $tolerance;
179 $this->sequence_protection_table = (string) $table;
180 $this->sequence_protection_column = (string) $column;
181 $this->sequence_protection_where_sql = (string) $where_sql;
182 }
183
184 /**
185 * Ugly, but necessary to prevent debug output breaking the conversation when the user has debug turned on
186 */
187 private function no_deprecation_warnings_on_php7() {
188 // PHP_MAJOR_VERSION is defined in PHP 5.2.7+
189 // We don't test for PHP > 7 because the specific deprecated element will be removed in PHP 8 - and so no warning should come anyway (and we shouldn't suppress other stuff until we know we need to).
190 // @codingStandardsIgnoreLine
191 if (defined('PHP_MAJOR_VERSION') && PHP_MAJOR_VERSION == 7) {
192 $old_level = error_reporting();
193 // @codingStandardsIgnoreLine
194 $new_level = $old_level & ~E_DEPRECATED;
195 if ($old_level != $new_level) error_reporting($new_level);
196 }
197 }
198
199 public function set_destination_url($destination_url) {
200 $this->destination_url = $destination_url;
201 }
202
203 public function get_destination_url() {
204 return $this->destination_url;
205 }
206
207 public function set_option_name($key_option_name) {
208 $this->key_option_name = $key_option_name;
209 }
210
211 /**
212 * Method to get the remote key
213 *
214 * @return string
215 */
216 public function get_key_remote() {
217 if (empty($this->key_remote) && $this->can_generate) {
218 $this->generate_new_keypair();
219 }
220
221 return empty($this->key_remote) ? false : $this->key_remote;
222 }
223
224 /**
225 * Set the remote key
226 *
227 * @param string $key_remote
228 */
229 public function set_key_remote($key_remote) {
230 $this->key_remote = $key_remote;
231 }
232
233 /**
234 * Used for sending - when receiving, the format is part of the message
235 *
236 * @param integer $format
237 */
238 public function set_message_format($format = 2) {
239 $this->format = $format;
240 }
241
242 /**
243 * Used for sending - when receiving, the format is part of the message
244 *
245 * @return integer
246 */
247 public function get_message_format() {
248 return $this->format;
249 }
250
251 /**
252 * Method to get the local key
253 *
254 * @return string
255 */
256 public function get_key_local() {
257 if (empty($this->key_local)) {
258 if ($this->key_option_name) {
259 $key_local = get_site_option($this->key_option_name);
260 if ($key_local) {
261 $this->key_local = $key_local;
262 }
263 }
264 }
265 if (empty($this->key_local) && $this->can_generate) {
266 $this->generate_new_keypair();
267 }
268
269 return empty($this->key_local) ? false : $this->key_local;
270 }
271
272 /**
273 * Tests whether a supplied string (after trimming) is a valid portable bundle
274 *
275 * @param string $bundle [description]
276 * @param string $format same as get_portable_bundle()
277 * @return array (which the consumer is free to use - e.g. convert into internationalised string), with keys 'code' and (perhaps) 'data'
278 */
279 public function decode_portable_bundle($bundle, $format = 'raw') {
280 $bundle = trim($bundle);
281 if ('base64_with_count' == $format) {
282 if (strlen($bundle) < 5) return array('code' => 'invalid_wrong_length', 'data' => 'too_short');
283 $len = substr($bundle, 0, 4);
284 $bundle = substr($bundle, 4);
285 $len = hexdec($len);
286 if (strlen($bundle) != $len) return array('code' => 'invalid_wrong_length', 'data' => "1,$len,".strlen($bundle));
287 if (false === ($bundle = base64_decode($bundle))) return array('code' => 'invalid_corrupt', 'data' => 'not_base64');
288 if (null === ($bundle = json_decode($bundle, true))) return array('code' => 'invalid_corrupt', 'data' => 'not_json');
289 }
290 if (empty($bundle['key'])) return array('code' => 'invalid_corrupt', 'data' => 'no_key');
291 if (empty($bundle['url'])) return array('code' => 'invalid_corrupt', 'data' => 'no_url');
292 if (empty($bundle['name_indicator'])) return array('code' => 'invalid_corrupt', 'data' => 'no_name_indicator');
293
294 return $bundle;
295 }
296
297 /**
298 * Method 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)
299 *
300 * @param string $format Supported formats: base64_with_count and default)raw
301 * @param array $extra_info needs to be JSON-serialisable, so be careful about what you put into it.
302 * @param array $options [description]
303 * @return array
304 */
305 public function get_portable_bundle($format = 'raw', $extra_info = array(), $options = array()) {
306
307 $bundle = array_merge($extra_info, array(
308 'key' => empty($options['key']) ? $this->get_key_remote() : $options['key'],
309 'name_indicator' => $this->key_name_indicator,
310 'url' => trailingslashit(network_site_url()),
311 'admin_url' => trailingslashit(admin_url()),
312 'network_admin_url' => trailingslashit(network_admin_url()),
313 'format_support' => 2,
314 ));
315
316 if ('base64_with_count' == $format) {
317 $bundle = base64_encode(json_encode($bundle));
318
319 $len = strlen($bundle); // Get the length
320 $len = dechex($len); // The first bytes of the message are the bundle length
321 $len = str_pad($len, 4, '0', STR_PAD_LEFT); // Zero pad
322
323 return $len.$bundle;
324
325 } else {
326 return $bundle;
327 }
328
329 }
330
331 public function set_key_local($key_local) {
332 $this->key_local = $key_local;
333 if ($this->key_option_name) update_site_option($this->key_option_name, $this->key_local);
334 }
335
336 public function generate_new_keypair($key_size = 2048) {
337
338 $rsa = new phpseclib_Crypt_RSA();
339 $keys = $rsa->createKey($key_size);
340
341 if (empty($keys['privatekey']) || (class_exists('phpseclib3_Crypt_RSA_PrivateKey') && !is_a($keys['privatekey'], 'phpseclib3_Crypt_RSA_PrivateKey'))) {
342 $this->set_key_local(false);
343 } else {
344 if (is_a($keys['privatekey'], 'phpseclib3_Crypt_RSA_PrivateKey')) $keys['privatekey'] = $keys['privatekey']->toString('PKCS1');
345 $this->set_key_local($keys['privatekey']);
346 }
347
348 if (empty($keys['publickey']) || (class_exists('phpseclib3_Crypt_RSA_PublicKey') && !is_a($keys['publickey'], 'phpseclib3_Crypt_RSA_PublicKey'))) {
349 $this->set_key_remote(false);
350 } else {
351 if (is_a($keys['publickey'], 'phpseclib3_Crypt_RSA_PublicKey')) $keys['publickey'] = $keys['publickey']->toString('PKCS1');
352 $this->set_key_remote($keys['publickey']);
353 }
354
355 return empty($keys['publickey']) ? false : true;
356 }
357
358 /**
359 * A base-64 encoded RSA hash (PKCS_1) of the message digest
360 *
361 * @param string $message
362 * @param boolean $use_key
363 * @return array
364 */
365 public function signature_for_message($message, $use_key = false) {
366
367 $hash_algorithm = 'sha256';
368
369 // Sign with the private (local) key
370 if (!$use_key) {
371 if (!$this->key_local) throw new Exception('No signing key has been set');
372 $use_key = $this->key_local;
373 }
374
375 $rsa = new phpseclib_Crypt_RSA();
376 $rsa->loadKey($use_key);
377 // This is the older signature mode; phpseclib's default is the preferred CRYPT_RSA_SIGNATURE_PSS; however, Forge JS doesn't yet support this. More info: https://en.wikipedia.org/wiki/PKCS_1
378 $rsa->setSignatureMode(phpseclib_Crypt_RSA::SIGNATURE_PKCS1);
379
380 // Don't do this: Crypt_RSA::sign() already calculates the digest of the hash
381 // $hash = new Crypt_Hash($hash_algorithm);
382 // $hashed = $hash->hash($message);
383
384 // if ($this->debug) $this->log("Message hash (hash=$hash_algorithm) (hex): ".bin2hex($hashed));
385
386 // phpseclib defaults to SHA1
387 $rsa->setHash($hash_algorithm);
388 $encrypted = $rsa->sign($message);
389
390 if ($this->debug) $this->log('Signed hash (mode='.phpseclib_Crypt_RSA::SIGNATURE_PKCS1.') (hex): '.bin2hex($encrypted));
391
392 $signature = base64_encode($encrypted);
393
394 if ($this->debug) $this->log("Message signature (base64): $signature");
395
396 return $signature;
397 }
398
399 /**
400 * Log description
401 *
402 * @param string $message
403 * @param string $level $level is not yet used much
404 */
405 private function log($message, $level = 'notice') {
406 // Allow other plugins to do something with the message
407 do_action('udrpc_log', $message, $level, $this->key_name_indicator, $this->debug, $this);
408 if ('info' != $level) error_log('UDRPC ('.$this->key_name_indicator.", $level): $message");
409 }
410
411 /**
412 * Encrypt the message, using the local key (which needs to exist)
413 *
414 * @param string $plaintext
415 * @param boolean $use_key
416 * @param integer $key_length
417 * @return array
418 */
419 public function encrypt_message($plaintext, $use_key = false, $key_length = 32) {
420
421 if (!$use_key) {
422 if (1 == $this->format) {
423 if (!$this->key_local) throw new Exception('No encryption key has been set');
424 $use_key = $this->key_local;
425 } else {
426 if (!$this->key_remote) throw new Exception('No encryption key has been set');
427 $use_key = $this->key_remote;
428 }
429 }
430
431 $rsa = new phpseclib_Crypt_RSA();
432
433 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
434
435 $rij = new phpseclib_Crypt_Rijndael();
436
437 // Generate Random Symmetric Key
438 $sym_key = phpseclib_Crypt_Random::string($key_length);
439
440 if ($this->debug) $this->log('Unencrypted symmetric key (hex): '.bin2hex($sym_key));
441
442 // Encrypt Message with new Symmetric Key
443 $rij->setKey($sym_key);
444 $ciphertext = $rij->encrypt($plaintext);
445
446 if ($this->debug) $this->log('Encrypted ciphertext (hex): '.bin2hex($ciphertext));
447
448 $ciphertext = base64_encode($ciphertext);
449
450 // Encrypt the Symmetric Key with the Asymmetric Key
451 $rsa->loadKey($use_key);
452 $sym_key = $rsa->encrypt($sym_key);
453
454 if ($this->debug) $this->log('Encrypted symmetric key (hex): '.bin2hex($sym_key));
455
456 // Base 64 encode the symmetric key for transport
457 $sym_key = base64_encode($sym_key);
458
459 if ($this->debug) $this->log('Encrypted symmetric key (b64): '.$sym_key);
460
461 $len = str_pad(dechex(strlen($sym_key)), 3, '0', STR_PAD_LEFT); // Zero pad to be sure.
462
463 // 16 characters of hex is enough for the payload to be to 16 exabytes (giga < tera < peta < exa) of data
464 $cipherlen = str_pad(dechex(strlen($ciphertext)), 16, '0', STR_PAD_LEFT);
465
466 // Concatenate the length, the encrypted symmetric key, and the message
467 return $len.$sym_key.$cipherlen.$ciphertext;
468
469 }
470
471 /**
472 * Decrypt the message, using the local key (which needs to exist)
473 *
474 * @param string $message
475 * @return string|boolean
476 */
477 public function decrypt_message($message) {
478
479 if (!$this->key_local) throw new Exception('No decryption key has been set');
480
481 $rsa = new phpseclib_Crypt_RSA();
482 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
483 // Defaults to CRYPT_AES_MODE_CBC
484 $rij = new phpseclib_Crypt_Rijndael();
485
486 // Extract the Symmetric Key
487 $len = substr($message, 0, 3);
488 $len = hexdec($len);
489 $sym_key = substr($message, 3, $len);
490
491 // Extract the encrypted message
492 $cipherlen = substr($message, ($len + 3), 16);
493 $cipherlen = hexdec($cipherlen);
494
495 $ciphertext = substr($message, ($len + 19), $cipherlen);
496 $ciphertext = base64_decode($ciphertext);
497
498 // Decrypt the encrypted symmetric key
499 $rsa->loadKey($this->key_local);
500 $sym_key = base64_decode($sym_key);
501 $sym_key = $rsa->decrypt($sym_key);
502
503 if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
504 return false;
505 }
506
507 // Decrypt the message
508 $rij->setKey($sym_key);
509
510 return $rij->decrypt($ciphertext);
511
512 }
513
514 /**
515 * Creates a message
516 *
517 * @param string $command
518 * @param string $data
519 * @param boolean $is_response
520 * @param boolean $use_key_remote
521 * @param boolean $use_key_local
522 * @return array which the caller will then format as required (e.g. use as body in post, or JSON-encode, etc.) [description]
523 */
524 public function create_message($command, $data = null, $is_response = false, $use_key_remote = false, $use_key_local = false) {
525
526 if ($is_response) {
527 $send_array = array('response' => $command);
528 } else {
529 $send_array = array('command' => $command);
530 }
531
532 $send_array['time'] = time();
533 // This goes in the encrypted portion as well to prevent replays with a different unencrypted name indicator
534 $send_array['key_name'] = $this->key_name_indicator;
535
536 // 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
537 // The value of PHP_INT_MAX on a 32-bit platform
538 $this->message_random_number = rand(1, 2147483647);
539 $send_array['rand'] = $this->message_random_number;
540
541 if ($this->next_send_sequence_id) {
542 $send_array['sequence_id'] = $this->next_send_sequence_id;
543 ++$this->next_send_sequence_id;
544 }
545
546 if ($is_response && !empty($this->incoming_message) && isset($this->incoming_message['rand'])) {
547 $send_array['incoming_rand'] = $this->incoming_message['rand'];
548 }
549
550 if (null !== $data) $send_array['data'] = $data;
551 $send_data = $this->encrypt_message(json_encode($send_array), $use_key_remote);
552
553 $message = array(
554 'format' => $this->format,
555 'key_name' => $this->key_name_indicator,
556 'udrpc_message' => $send_data,
557 );
558
559 $signature = $this->signature_for_message($send_data, $use_key_local);
560 $message['signature'] = $signature;
561
562 return $message;
563
564 }
565
566 /**
567 * N.B. There's already some time-based replay protection. This can be turned on to beef it up.
568 * This is only for listeners. Replays can only be detection if transients are working on the WP site (which by default only means that the option table is working).
569 *
570 * @param boolean $activate
571 */
572 public function activate_replay_protection($activate = true) {
573 $this->extra_replay_protection = (bool) $activate;
574 }
575
576 public function set_next_send_sequence_id($id) {
577 $this->next_send_sequence_id = $id;
578 }
579
580 /**
581 * Set_http_credentials
582 *
583 * @param string $credentials should be an array with entries for 'username' and 'password'
584 */
585 public function set_http_credentials($credentials) {
586 $this->http_credentials = $credentials;
587 }
588
589 /**
590 * This needs only to return an array with keys body and response - where response is also an array, with key 'code' (the HTTP status code)
591 * The $post_options array support these keys: timeout, body,
592 * Public, to allow short-circuiting of the library's own encoding/decoding (e.g. for acting as a proxy for a message already encrypted elsewhere)
593 *
594 * @param array $post_options
595 * @return array
596 */
597 public function http_post($post_options) {
598 global $wp_version;
599 include ABSPATH.WPINC.'/version.php';
600 $http_credentials = $this->http_credentials;
601
602 if (is_a($this->http_transport, 'GuzzleHttp\Client')) {
603
604 // https://guzzle.readthedocs.org/en/5.3/clients.html
605
606 $client = $this->http_transport;
607
608 $guzzle_options = array(
609 'form_params' => $post_options['body'],
610 'headers' => array(
611 'User-Agent' => 'WordPress/'.$wp_version.'; class-udrpc.php-Guzzle/'.$this->version.'; '.get_bloginfo('url'),
612 ),
613 'exceptions' => false,
614 'timeout' => $post_options['timeout'],
615 );
616
617 if (!class_exists('WP_HTTP_Proxy')) include_once ABSPATH.WPINC.'/class-http.php';
618 $proxy = new WP_HTTP_Proxy();
619 if ($proxy->is_enabled()) {
620 $user = $proxy->username();
621 $pass = $proxy->password();
622 $host = $proxy->host();
623 $port = (int) $proxy->port();
624 if (empty($port)) $port = 8080;
625 if (!empty($host) && $proxy->send_through_proxy($this->destination_url)) {
626 $proxy_auth = '';
627 if (!empty($user)) {
628 $proxy_auth = $user;
629 if (!empty($pass)) $proxy_auth .= ':'.$pass;
630 $proxy_auth .= '@';
631 }
632 $guzzle_options['proxy'] = array(
633 'http' => "http://{$proxy_auth}$host:$port",
634 'https' => "http://{$proxy_auth}$host:$port",
635 );
636 }
637 }
638
639 if (defined('UDRPC_GUZZLE_SSL_VERIFY')) {
640 $verify = UDRPC_GUZZLE_SSL_VERIFY;
641 } elseif (file_exists(ABSPATH.WPINC.'/certificates/ca-bundle.crt')) {
642 $verify = ABSPATH.WPINC.'/certificates/ca-bundle.crt';
643 } else {
644 $verify = true;
645 }
646
647 $guzzle_options['verify'] = apply_filters('udrpc_guzzle_verify', $verify);
648
649 if (!empty($http_credentials['username'])) {
650
651 $authentication_method = empty($http_credentials['authentication_method']) ? 'basic' : $http_credentials['authentication_method'];
652
653 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
654
655 $guzzle_options['auth'] = array(
656 $http_credentials['username'],
657 $password,
658 $authentication_method,
659 );
660
661 }
662
663 $response = $client->post($this->destination_url, apply_filters('udrpc_guzzle_options', $guzzle_options, $this));
664
665 $formatted_response = array(
666 'response' => array(
667 'code' => $response->getStatusCode(),
668 ),
669 'body' => $response->getBody(),
670 );
671
672 return $formatted_response;
673
674 } else {
675
676 $post_options['user-agent'] = 'WordPress/'.$wp_version.'; class-udrpc.php/'.$this->version.'; '.get_bloginfo('url');
677
678 if (!empty($http_credentials['username'])) {
679
680 $authentication_type = empty($http_credentials['authentication_type']) ? 'basic' : $http_credentials['authentication_type'];
681
682 if ('basic' != $authentication_type) {
683 return new WP_Error('unsupported_http_authentication_type', 'Only HTTP basic authentication is supported (for other types, use Guzzle)');
684 }
685
686 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
687 $post_options['headers'] = array(
688 'Authorization' => 'Basic '.base64_encode($http_credentials['username'].':'.$password),
689 );
690 }
691
692 return wp_remote_post(
693 $this->destination_url,
694 $post_options
695 );
696 }
697 }
698
699 public function send_message($command, $data = null, $timeout = 20) {
700
701 if (empty($this->destination_url)) return new WP_Error('not_initialised', 'RPC error: URL not initialised');
702
703 $message = $this->create_message($command, $data);
704
705 $post_options = array(
706 'timeout' => $timeout,
707 'body' => $message,
708 );
709
710 $post_options = apply_filters('udrpc_post_options', $post_options, $command, $data, $timeout, $this);
711
712 // Make the memory available - may be useful if the message was large
713 unset($data);
714
715 try {
716 $post = $this->http_post($post_options);
717 } catch (Exception $e) {
718 // Curl can return an error code 0, which causes WP_Error to return early, without recording the message. So, we prefix the code.
719 return new WP_Error('http_post_'.$e->getCode(), $e->getMessage());
720 }
721
722 if (is_wp_error($post)) return $post;
723
724 $response_code = wp_remote_retrieve_response_code($post);
725
726 if (empty($response_code)) return new WP_Error('empty_http_code', 'Unexpected HTTP response code');
727
728 if ($response_code < 200 || $response_code >= 300) return new WP_Error('unexpected_http_code', 'Unexpected HTTP response code ('.$response_code.')', $post);
729
730 $response_body = wp_remote_retrieve_body($post);
731
732 if (empty($response_body)) return new WP_Error('empty_response', 'Empty response from remote site');
733
734 $decoded = json_decode($response_body, true);
735
736 if (empty($decoded)) {
737
738 if (false != ($found_at = strpos($response_body, '{"format":'))) {
739 $new_body = substr($response_body, $found_at);
740 $decoded = json_decode($new_body, true);
741 }
742
743 if (empty($decoded)) {
744 $this->log('response from remote site ('.$this->destination_url.') could not be understood: '.substr($response_body, 0, 100).' ... ', 'info');
745 return new WP_Error('response_not_understood', 'Response from remote site could not be understood', $response_body);
746 }
747 }
748
749 if (!is_array($decoded) || empty($decoded['udrpc_message'])) return new WP_Error('response_not_understood', 'Response from remote site was not in the expected format ('.$post['body'].')', $decoded);
750
751 $format = $decoded['format'];
752
753 // Don't allow the remote side to downgrade
754 if ($format > 1 || $this->format > 1) {
755 if (empty($decoded['signature'])) {
756 $this->log('Decoding response: no message signature found');
757 die;
758 }
759 if (!$this->key_remote) {
760 $this->log('Decoding response: no signature verification key has been set');
761 die;
762 }
763 if (!$this->verify_signature($decoded['udrpc_message'], $decoded['signature'], $this->key_remote)) {
764 $this->log('Decoding response: signature verification failed; discarding');
765 die;
766 }
767 }
768
769 $decoded = $this->decrypt_message($decoded['udrpc_message']);
770
771 if (!is_string($decoded)) return new WP_Error('not_decrypted', 'Response from remote site was not successfully decrypted');
772
773 $json_decoded = json_decode($decoded, true);
774
775 if (!is_array($json_decoded) || empty($json_decoded['response']) || empty($json_decoded['time']) || !is_numeric($json_decoded['time'])) return new WP_Error('response_corrupt', 'Response from remote site was not in the expected format', $decoded);
776
777 // 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)
778 if ($this->extra_replay_protection) {
779 $message_hash = $this->calculate_message_hash((string) $post['body']);
780 if ($this->message_hash_seen($message_hash)) {
781 return new WP_Error('replay_detected', 'Message refused: replay detected', $message_hash);
782 }
783 }
784
785 $time_difference = absint((time() - $json_decoded['time']));
786 if ($time_difference > $this->maximum_replay_time_difference) return new WP_Error('window_error', 'Message refused: maxium replay time difference exceeded', $time_difference);
787
788 if (isset($json_decoded['incoming_rand']) && !empty($this->message_random_number) && $json_decoded['incoming_rand'] != $this->message_random_number) {
789 // @codingStandardsIgnoreLine
790 $this->log('UDRPC: Message mismatch (possibly MITM) (sent_rand=' + $this->message_random_number + ', returned_rand='.$json_decoded['incoming_rand'].'): dropping', 'error');
791
792 return new WP_Error('message_mismatch_error', 'Message refused: message mismatch (possible MITM)');
793
794 }
795
796 // Should be an array with keys including 'response' and (if relevant) 'data'
797 return $json_decoded;
798
799 }
800
801 /**
802 * Returns a boolean indicating whether a listener was created - which depends on whether one was needed (so, false does not necessarily indicate an error condition)
803 *
804 * @return boolean
805 */
806 public function create_listener() {
807
808 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
809
810 // Create the WP actions to handle incoming commands, handle built-in commands (e.g. ping, create_keys (authenticate with admin creds)), dispatch them to the right place, and die
811 if ((!empty($_POST) && !empty($_POST['udrpc_message']) && !empty($_POST['format'])) || (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin)) {
812 add_action('wp_loaded', array($this, 'wp_loaded'));
813 add_action('wp_loaded', array($this, 'wp_loaded_final'), 10000);
814 return true;
815 }
816
817 return false;
818 }
819
820 public function wp_loaded_final() {
821 if (empty($this->require_message_to_be_understood)) return;
822 $message_for = empty($_POST['key_name']) ? '' : (string) $_POST['key_name'];
823 $this->log("Message was received, but not understood by local site (for: $message_for)");
824 die;
825 }
826
827 public function wp_loaded() {
828
829 /*
830 // What if something else already set some response headers?
831 if (function_exists('apache_response_headers')) {
832 $apache_response_headers = apache_response_headers();
833 // Do something...
834 }
835 */
836
837 // CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
838 // get_http_origin() : since WP 3.4
839 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
840 if (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin) {
841 if (in_array($http_origin, $this->allow_cors_from)) {
842 // @codingStandardsIgnoreLine
843 if (!defined('UDRPC_DO_NOT_SEND_CORS_HEADERS') || !UDRPC_DO_NOT_SEND_CORS_HEADERS) {
844 header("Access-Control-Allow-Origin: $http_origin");
845 header('Access-Control-Allow-Credentials: true');
846 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) header('Access-Control-Allow-Methods: POST, OPTIONS');
847 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) header('Access-Control-Allow-Headers: '.$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
848 }
849 die;
850 } elseif ($this->debug) {
851 $this->log('Non-allowed CORS from: '.$http_origin);
852 }
853 // Having detected that this is a CORS request, there's nothing more to do. We return, because a different listener might pick it up, even though we didn't.
854 return;
855 }
856
857 // Silently return, rather than dying, in case another instance is able to handle this
858 if (empty($_POST['format']) || (1 != $_POST['format'] && 2 != $_POST['format'])) return;
859
860 $this->require_message_to_be_understood = true;
861
862 $format = $_POST['format'];
863
864 /*
865 In format 1 (obsolete/deprecated), the one encrypts (the shared AES key) using one half of the key-pair, and decrypts with the other; whereas the other side of the conversation does the reverse when replying (and uses a different shared AES key). Though this is possible in RSA, this is the wrong thing to do - see https://crypto.stackexchange.com/questions/2123/rsa-encryption-with-private-key-and-decryption-with-a-public-key
866 In format 2, both sides have their own private and public key. The sender encrypts using the other side's public key, and decrypts using its own private key. Messages are signed (the message digest is SHA-256).
867 */
868
869 // Is this for us?
870 if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) {
871 return;
872 }
873
874 // wp_unslash() does not exist until after WP 3.5
875 // $udrpc_message = function_exists('wp_unslash') ? wp_unslash($_POST['udrpc_message']) : stripslashes_deep($_POST['udrpc_message']);
876
877 // Data should not have any slashes - it is base64-encoded
878 $udrpc_message = (string) $_POST['udrpc_message'];
879
880 // Check this now, rather than allow the decrypt method to thrown an Exception
881
882 if ($format > 1) {
883 if (empty($_POST['signature'])) {
884 $this->log('No message signature found', 'error');
885 die;
886 }
887 if (!$this->key_remote) {
888 $this->log('No signature verification key has been set', 'error');
889 die;
890 }
891 if (!$this->verify_signature($udrpc_message, $_POST['signature'], $this->key_remote)) {
892 $this->log('Signature verification failed; discarding', 'error');
893 die;
894 }
895 }
896
897 try {
898 $udrpc_message = $this->decrypt_message($udrpc_message);
899 } catch (Exception $e) {
900 $this->log('Exception ('.get_class($e).'): '.$e->getMessage(), 'error');
901 die;
902 }
903
904 if (!is_string($udrpc_message)) {
905 $this->log('Could not decrypt incoming message', 'error');
906 die;
907 }
908
909 $udrpc_message = json_decode($udrpc_message, true);
910
911 if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
912 $this->log('Could not decode JSON on incoming message', 'error');
913 die;
914 }
915
916 if (empty($udrpc_message['time'])) {
917 $this->log('No time set in incoming message', 'error');
918 die;
919 }
920
921 // Mismatch indicating a replay of the message with a different key name in the unencrypted portion?
922 if (empty($udrpc_message['key_name']) || $_POST['key_name'] != $udrpc_message['key_name']) {
923 $this->log('key_name mismatch between encrypted and unencrypted portions', 'error');
924 die;
925 }
926
927 if ($this->extra_replay_protection) {
928 $message_hash = $this->calculate_message_hash((string) $_POST['udrpc_message']);
929 if ($this->message_hash_seen($message_hash)) {
930 $this->log("Message dropped: apparently a replay (hash: $message_hash)", 'error');
931 die;
932 }
933 }
934
935 // Do this after the extra replay protection, as that checks hashes within the maximum time window - so don't check the maximum time window until afterwards, to avoid a tiny window (race) in between.
936 $time_difference = absint($udrpc_message['time'] - time());
937 if ($time_difference > $this->maximum_replay_time_difference) {
938 $this->log("Time in incoming message is outside of allowed window ($time_difference > ".$this->maximum_replay_time_difference.')', 'error');
939 die;
940 }
941
942 // The sequence number should always be larger than any previously-sent sequence number
943 if ($this->sequence_protection_tolerance) {
944
945 if ($this->debug) $this->log('Sequence protection is active; tolerance: '.$this->sequence_protection_tolerance);
946
947 global $wpdb;
948
949 if (!isset($udrpc_message['sequence_id']) || !is_numeric($udrpc_message['sequence_id'])) {
950 $this->log('a numerical sequence number is required, but none was included in the message - dropping', 'error');
951 die;
952 }
953
954 $message_sequence_id = (int) $udrpc_message['sequence_id'];
955 $recently_seen_sequences_ids = $wpdb->get_var($wpdb->prepare('SELECT %s FROM %s LIMIT 1 WHERE '.$this->sequence_protection_where_sql, $this->sequence_protection_column, $this->sequence_protection_table));
956
957 if ('' === $recently_seen_sequences_ids) $recently_seen_sequences_ids = '0';
958
959 $recently_seen_sequences_ids_as_array = explode($recently_seen_sequences_ids, ',');
960 sort($recently_seen_sequences_ids_as_array);
961
962 // Seen before?
963 if (in_array($message_sequence_id, $recently_seen_sequences_ids_as_array)) {
964 $this->log("message with duplicate sequence number received - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids)");
965 die;
966 }
967
968 // Within the tolerance threshold? That means: a) either bigger than the max, or b) no more than <tolerance> lower than the least
969 if ($message_sequence_id > max($recently_seen_sequences_ids)) {
970 if ($this->debug) $this->log("Sequence id ($message_sequence_id) is greater than any previous (".max($recently_seen_sequences_ids).') - message is thus OK');
971 // All is well
972 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
973 } elseif ((max($recently_seen_sequences_ids) - $message_sequence_id) <= $this->sequence_protection_tolerance) {
974 // All is well - was one of those 'missing' in the sequence
975 if ($this->debug) $this->log("Sequence id ($message_sequence_id) is within tolerance range of previous maximum (".max($recently_seen_sequences_ids).') - message is thus OK');
976 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
977 } else {
978 $this->log("message received outside of allowed sequence window - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids, tolerance=".$this->sequence_protection_tolerance.')', 'error');
979 die;
980 }
981
982 // Remove out-of-bounds seen IDs
983 $max_sequence_id_seen = max($recently_seen_sequences_ids_as_array);
984 foreach ($recently_seen_sequences_ids_as_array as $k => $id) {
985 if ($max_sequence_id_seen - $id > $this->sequence_protection_tolerance) {
986 if ($this->debug) $this->log("Removing no-longer-relevant sequence from list of those recently seen: $id");
987 unset($recently_seen_sequences_ids_as_array[$k]);
988 }
989 }
990
991 // Allow reset
992 if ($message_sequence_id > PHP_INT_MAX - 10) {
993 $recently_seen_sequences_ids_as_array = array(0);
994 }
995
996 // Write them back to the database
997 $sql = $wpdb->prepare('UPDATE %s SET %s=%s WHERE '.$this->sequence_protection_where_sql, $this->sequence_protection_table, $this->sequence_protection_column, implode(',', $recently_seen_sequences_ids_as_array));
998 if ($this->debug) $this->log("SQL to send recent sequence IDs back to the database: $sql");
999 $wpdb->query($sql);
1000
1001 }
1002
1003 $this->incoming_message = $udrpc_message;
1004
1005 $command = (string) $udrpc_message['command'];
1006 $data = empty($udrpc_message['data']) ? null : $udrpc_message['data'];
1007
1008 // @codingStandardsIgnoreLine
1009 if ($http_origin && !empty($udrpc_message['cors_headers_wanted']) && (!defined('UDRPC_DO_NOT_SEND_CORS_HEADERS') || !UDRPC_DO_NOT_SEND_CORS_HEADERS)) {
1010 header("Access-Control-Allow-Origin: $http_origin");
1011 header('Access-Control-Allow-Credentials: true');
1012 }
1013
1014 // Restrict use of format 1
1015 if ($format < 2 && (!in_array($command, array('ping', 'get_file_status', 'send_chunk', 'upload_complete')) || !preg_match('/^([a-f0-9]+)\.migrator.updraftplus.com$/', $this->key_name_indicator))) {
1016 $this->log("Obsolete format used - dropping (command: $command)", 'error');
1017 die;
1018 }
1019
1020 $this->log('Command received: '.$command, 'info');
1021
1022 if ('ping' == $command) {
1023 $response = array('response' => 'pong', 'data' => null);
1024 } else {
1025 if (has_filter('udrpc_command_'.$command)) {
1026 $response = apply_filters('udrpc_command_'.$command, null, $data, $this->key_name_indicator);
1027 } else {
1028 $response = array('response' => 'rpcerror', 'data' => array('code' => 'unknown_rpc_command', 'data' => $command));
1029 }
1030 }
1031
1032 $response = apply_filters('udrpc_action', $response, $command, $data, $this->key_name_indicator, $this);
1033
1034 if (is_array($response)) {
1035
1036 if ($this->debug) {
1037 $this->log('UDRPC response (pre-encoding/encryption): '.serialize($response));
1038 }
1039
1040 $data = isset($response['data']) ? $response['data'] : null;
1041
1042 $final_response = json_encode($this->create_message($response['response'], $data, true));
1043
1044 do_action('udrpc_action_send_response', $final_response, $command);
1045
1046 echo $final_response;
1047 }
1048
1049 die;
1050
1051 }
1052
1053 /**
1054 * The hash needs to be in a format that phpseclib likes. phpseclib uses lower case.
1055 * Pass in a base64-encoded signature (i.e. just as signature_for_message creates)
1056 *
1057 * @param string $message
1058 * @param string $signature
1059 * @param string $key
1060 * @param string $hash_algorithm
1061 * @return boolean
1062 */
1063 public function verify_signature($message, $signature, $key, $hash_algorithm = 'sha256') {
1064 $rsa = new phpseclib_Crypt_RSA();
1065 $rsa->setHash(strtolower($hash_algorithm));
1066 // This is not the default, but is what we use
1067 $rsa->setSignatureMode(phpseclib_Crypt_RSA::SIGNATURE_PKCS1);
1068 $rsa->loadKey($key);
1069
1070 // Don't hash it - Crypt_RSA::verify() already does that
1071 // $hash = new Crypt_Hash($hash_algorithm);
1072 // $hashed = $hash->hash($message);
1073
1074 $verified = $rsa->verify($message, base64_decode($signature));
1075
1076 if ($this->debug) $this->log('Signature verification result: '.serialize($verified));
1077
1078 return $verified;
1079 }
1080
1081 private function calculate_message_hash($message) {
1082 return hash('sha256', $message);
1083 }
1084
1085 private function message_hash_seen($message_hash) {
1086 // 39 characters - less than the WP site transient name limit (40). Though, we use a normal transient, as these don't auto-load at all times.
1087 $transient_name = 'udrpch_'.md5($this->key_name_indicator);
1088 $seen_hashes = get_transient($transient_name);
1089 if (!is_array($seen_hashes)) $seen_hashes = array();
1090 $time_now = time();
1091 // $any_changes = false;
1092 // Prune the old hashes
1093 foreach ($seen_hashes as $hash => $last_seen) {
1094 if ($last_seen < ($time_now - $this->maximum_replay_time_difference)) {
1095 // $any_changes = true;
1096 unset($seen_hashes[$hash]);
1097 }
1098 }
1099 if (isset($seen_hashes[$message_hash])) {
1100 return true;
1101 }
1102 $seen_hashes[$message_hash] = $time_now;
1103 set_transient($transient_name, $seen_hashes, $this->maximum_replay_time_difference);
1104
1105 return false;
1106 }
1107 }
1108
1109 endif;
1110