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-udrpc2.php

class-udrpc2.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-udrpc2.php

1,108 lines 40.1 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_V2')) :
59 class UpdraftPlus_Remote_Communications_V2 {
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 = '2.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'])) {
342 $this->set_key_local(false);
343 } else {
344 $this->set_key_local($keys['privatekey']);
345 }
346
347 if (empty($keys['publickey'])) {
348 $this->set_key_remote(false);
349 } else {
350 $this->set_key_remote($keys['publickey']);
351 }
352
353 return empty($keys['publickey']) ? false : true;
354 }
355
356 /**
357 * A base-64 encoded RSA hash (PKCS_1) of the message digest
358 *
359 * @param string $message
360 * @param boolean $use_key
361 * @return array
362 */
363 public function signature_for_message($message, $use_key = false) {
364
365 $hash_algorithm = 'sha256';
366
367 // Sign with the private (local) key
368 if (!$use_key) {
369 if (!$this->key_local) throw new Exception('No signing key has been set');
370 $use_key = $this->key_local;
371 }
372
373 $rsa = new phpseclib_Crypt_RSA();
374 $rsa->loadKey($use_key);
375 // 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
376 $rsa->setSignatureMode(phpseclib_Crypt_RSA::SIGNATURE_PKCS1);
377
378 // Don't do this: Crypt_RSA::sign() already calculates the digest of the hash
379 // $hash = new Crypt_Hash($hash_algorithm);
380 // $hashed = $hash->hash($message);
381
382 // if ($this->debug) $this->log("Message hash (hash=$hash_algorithm) (hex): ".bin2hex($hashed));
383
384 // phpseclib defaults to SHA1
385 $rsa->setHash($hash_algorithm);
386 $encrypted = $rsa->sign($message);
387
388 if ($this->debug) $this->log('Signed hash (mode='.phpseclib_Crypt_RSA::SIGNATURE_PKCS1.') (hex): '.bin2hex($encrypted));
389
390 $signature = base64_encode($encrypted);
391
392 if ($this->debug) $this->log("Message signature (base64): $signature");
393
394 return $signature;
395 }
396
397 /**
398 * Log description
399 *
400 * @param string $message
401 * @param string $level $level is not yet used much
402 */
403 private function log($message, $level = 'notice') {
404 // Allow other plugins to do something with the message
405 do_action('udrpc_log', $message, $level, $this->key_name_indicator, $this->debug, $this);
406 if ('info' != $level) error_log('UDRPC ('.$this->key_name_indicator.", $level): $message");
407 }
408
409 /**
410 * Encrypt the message, using the local key (which needs to exist)
411 *
412 * @param string $plaintext
413 * @param boolean $use_key
414 * @param integer $key_length
415 * @return array
416 */
417 public function encrypt_message($plaintext, $use_key = false, $key_length = 32) {
418
419 if (!$use_key) {
420 if (1 == $this->format) {
421 if (!$this->key_local) throw new Exception('No encryption key has been set');
422 $use_key = $this->key_local;
423 } else {
424 if (!$this->key_remote) throw new Exception('No encryption key has been set');
425 $use_key = $this->key_remote;
426 }
427 }
428
429 $rsa = new phpseclib_Crypt_RSA();
430
431 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
432
433 $rij = new phpseclib_Crypt_Rijndael();
434
435 // Generate Random Symmetric Key
436 $sym_key = phpseclib_Crypt_Random::string($key_length);
437
438 if ($this->debug) $this->log('Unencrypted symmetric key (hex): '.bin2hex($sym_key));
439
440 // Encrypt Message with new Symmetric Key
441 $rij->setKey($sym_key);
442 $ciphertext = $rij->encrypt($plaintext);
443
444 if ($this->debug) $this->log('Encrypted ciphertext (hex): '.bin2hex($ciphertext));
445
446 $ciphertext = base64_encode($ciphertext);
447
448 // Encrypt the Symmetric Key with the Asymmetric Key
449 $rsa->loadKey($use_key);
450 $sym_key = $rsa->encrypt($sym_key);
451
452 if ($this->debug) $this->log('Encrypted symmetric key (hex): '.bin2hex($sym_key));
453
454 // Base 64 encode the symmetric key for transport
455 $sym_key = base64_encode($sym_key);
456
457 if ($this->debug) $this->log('Encrypted symmetric key (b64): '.$sym_key);
458
459 $len = str_pad(dechex(strlen($sym_key)), 3, '0', STR_PAD_LEFT); // Zero pad to be sure.
460
461 // 16 characters of hex is enough for the payload to be to 16 exabytes (giga < tera < peta < exa) of data
462 $cipherlen = str_pad(dechex(strlen($ciphertext)), 16, '0', STR_PAD_LEFT);
463
464 // Concatenate the length, the encrypted symmetric key, and the message
465 return $len.$sym_key.$cipherlen.$ciphertext;
466
467 }
468
469 /**
470 * Decrypt the message, using the local key (which needs to exist)
471 *
472 * @param string $message
473 * @return string|boolean
474 */
475 public function decrypt_message($message) {
476
477 if (!$this->key_local) throw new Exception('No decryption key has been set');
478
479 $rsa = new phpseclib_Crypt_RSA();
480 if (defined('UDRPC_PHPSECLIB_ENCRYPTION_MODE')) $rsa->setEncryptionMode(UDRPC_PHPSECLIB_ENCRYPTION_MODE);
481 // Defaults to CRYPT_AES_MODE_CBC
482 $rij = new phpseclib_Crypt_Rijndael();
483
484 // Extract the Symmetric Key
485 $len = substr($message, 0, 3);
486 $len = hexdec($len);
487 $sym_key = substr($message, 3, $len);
488
489 // Extract the encrypted message
490 $cipherlen = substr($message, ($len + 3), 16);
491 $cipherlen = hexdec($cipherlen);
492
493 $ciphertext = substr($message, ($len + 19), $cipherlen);
494 $ciphertext = base64_decode($ciphertext);
495
496 // Decrypt the encrypted symmetric key
497 $rsa->loadKey($this->key_local);
498 $sym_key = base64_decode($sym_key);
499 $sym_key = $rsa->decrypt($sym_key);
500
501 if (false === $sym_key || !is_string($sym_key) || strlen($sym_key) < 16) {
502 return false;
503 }
504
505 // Decrypt the message
506 $rij->setKey($sym_key);
507
508 return $rij->decrypt($ciphertext);
509
510 }
511
512 /**
513 * Creates a message
514 *
515 * @param string $command
516 * @param string $data
517 * @param boolean $is_response
518 * @param boolean $use_key_remote
519 * @param boolean $use_key_local
520 * @return array which the caller will then format as required (e.g. use as body in post, or JSON-encode, etc.) [description]
521 */
522 public function create_message($command, $data = null, $is_response = false, $use_key_remote = false, $use_key_local = false) {
523
524 if ($is_response) {
525 $send_array = array('response' => $command);
526 } else {
527 $send_array = array('command' => $command);
528 }
529
530 $send_array['time'] = time();
531 // This goes in the encrypted portion as well to prevent replays with a different unencrypted name indicator
532 $send_array['key_name'] = $this->key_name_indicator;
533
534 // 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
535 // The value of PHP_INT_MAX on a 32-bit platform
536 $this->message_random_number = rand(1, 2147483647);
537 $send_array['rand'] = $this->message_random_number;
538
539 if ($this->next_send_sequence_id) {
540 $send_array['sequence_id'] = $this->next_send_sequence_id;
541 ++$this->next_send_sequence_id;
542 }
543
544 if ($is_response && !empty($this->incoming_message) && isset($this->incoming_message['rand'])) {
545 $send_array['incoming_rand'] = $this->incoming_message['rand'];
546 }
547
548 if (null !== $data) $send_array['data'] = $data;
549 $send_data = $this->encrypt_message(json_encode($send_array), $use_key_remote);
550
551 $message = array(
552 'format' => $this->format,
553 'key_name' => $this->key_name_indicator,
554 'udrpc_message' => $send_data,
555 );
556
557 $signature = $this->signature_for_message($send_data, $use_key_local);
558 $message['signature'] = $signature;
559
560 return $message;
561
562 }
563
564 /**
565 * N.B. There's already some time-based replay protection. This can be turned on to beef it up.
566 * 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).
567 *
568 * @param boolean $activate
569 */
570 public function activate_replay_protection($activate = true) {
571 $this->extra_replay_protection = (bool) $activate;
572 }
573
574 public function set_next_send_sequence_id($id) {
575 $this->next_send_sequence_id = $id;
576 }
577
578 /**
579 * Set_http_credentials
580 *
581 * @param string $credentials should be an array with entries for 'username' and 'password'
582 */
583 public function set_http_credentials($credentials) {
584 $this->http_credentials = $credentials;
585 }
586
587 /**
588 * 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)
589 * The $post_options array support these keys: timeout, body,
590 * 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)
591 *
592 * @param array $post_options
593 * @return array
594 */
595 public function http_post($post_options) {
596 global $wp_version;
597 include ABSPATH.WPINC.'/version.php';
598 $http_credentials = $this->http_credentials;
599
600 if (is_a($this->http_transport, 'GuzzleHttp\Client')) {
601
602 // https://guzzle.readthedocs.org/en/5.3/clients.html
603
604 $client = $this->http_transport;
605
606 $guzzle_options = array(
607 'form_params' => $post_options['body'],
608 'headers' => array(
609 'User-Agent' => 'WordPress/'.$wp_version.'; class-udrpc.php-Guzzle/'.$this->version.'; '.get_bloginfo('url'),
610 ),
611 'exceptions' => false,
612 'timeout' => $post_options['timeout'],
613 );
614
615 if (!class_exists('WP_HTTP_Proxy')) include_once ABSPATH.WPINC.'/class-http.php';
616 $proxy = new WP_HTTP_Proxy();
617 if ($proxy->is_enabled()) {
618 $user = $proxy->username();
619 $pass = $proxy->password();
620 $host = $proxy->host();
621 $port = (int) $proxy->port();
622 if (empty($port)) $port = 8080;
623 if (!empty($host) && $proxy->send_through_proxy($this->destination_url)) {
624 $proxy_auth = '';
625 if (!empty($user)) {
626 $proxy_auth = $user;
627 if (!empty($pass)) $proxy_auth .= ':'.$pass;
628 $proxy_auth .= '@';
629 }
630 $guzzle_options['proxy'] = array(
631 'http' => "http://{$proxy_auth}$host:$port",
632 'https' => "http://{$proxy_auth}$host:$port",
633 );
634 }
635 }
636
637 if (defined('UDRPC_GUZZLE_SSL_VERIFY')) {
638 $verify = UDRPC_GUZZLE_SSL_VERIFY;
639 } elseif (file_exists(ABSPATH.WPINC.'/certificates/ca-bundle.crt')) {
640 $verify = ABSPATH.WPINC.'/certificates/ca-bundle.crt';
641 } else {
642 $verify = true;
643 }
644
645 $guzzle_options['verify'] = apply_filters('udrpc_guzzle_verify', $verify);
646
647 if (!empty($http_credentials['username'])) {
648
649 $authentication_method = empty($http_credentials['authentication_method']) ? 'basic' : $http_credentials['authentication_method'];
650
651 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
652
653 $guzzle_options['auth'] = array(
654 $http_credentials['username'],
655 $password,
656 $authentication_method,
657 );
658
659 }
660
661 $response = $client->post($this->destination_url, apply_filters('udrpc_guzzle_options', $guzzle_options, $this));
662
663 $formatted_response = array(
664 'response' => array(
665 'code' => $response->getStatusCode(),
666 ),
667 'body' => $response->getBody(),
668 );
669
670 return $formatted_response;
671
672 } else {
673
674 $post_options['user-agent'] = 'WordPress/'.$wp_version.'; class-udrpc.php/'.$this->version.'; '.get_bloginfo('url');
675
676 if (!empty($http_credentials['username'])) {
677
678 $authentication_type = empty($http_credentials['authentication_type']) ? 'basic' : $http_credentials['authentication_type'];
679
680 if ('basic' != $authentication_type) {
681 return new WP_Error('unsupported_http_authentication_type', 'Only HTTP basic authentication is supported (for other types, use Guzzle)');
682 }
683
684 $password = empty($http_credentials['password']) ? '' : $http_credentials['password'];
685 $post_options['headers'] = array(
686 'Authorization' => 'Basic '.base64_encode($http_credentials['username'].':'.$password),
687 );
688 }
689
690 return wp_remote_post(
691 $this->destination_url,
692 $post_options
693 );
694 }
695 }
696
697 public function send_message($command, $data = null, $timeout = 20) {
698
699 if (empty($this->destination_url)) return new WP_Error('not_initialised', 'RPC error: URL not initialised');
700
701 $message = $this->create_message($command, $data);
702
703 $post_options = array(
704 'timeout' => $timeout,
705 'body' => $message,
706 );
707
708 $post_options = apply_filters('udrpc_post_options', $post_options, $command, $data, $timeout, $this);
709
710 // Make the memory available - may be useful if the message was large
711 unset($data);
712
713 try {
714 $post = $this->http_post($post_options);
715 } catch (Exception $e) {
716 // Curl can return an error code 0, which causes WP_Error to return early, without recording the message. So, we prefix the code.
717 return new WP_Error('http_post_'.$e->getCode(), $e->getMessage());
718 }
719
720 if (is_wp_error($post)) return $post;
721
722 $response_code = wp_remote_retrieve_response_code($post);
723
724 if (empty($response_code)) return new WP_Error('empty_http_code', 'Unexpected HTTP response code');
725
726 if ($response_code < 200 || $response_code >= 300) return new WP_Error('unexpected_http_code', 'Unexpected HTTP response code ('.$response_code.')', $post);
727
728 $response_body = wp_remote_retrieve_body($post);
729
730 if (empty($response_body)) return new WP_Error('empty_response', 'Empty response from remote site');
731
732 $decoded = json_decode($response_body, true);
733
734 if (empty($decoded)) {
735
736 if (false != ($found_at = strpos($response_body, '{"format":'))) {
737 $new_body = substr($response_body, $found_at);
738 $decoded = json_decode($new_body, true);
739 }
740
741 if (empty($decoded)) {
742 $this->log('response from remote site ('.$this->destination_url.') could not be understood: '.substr($response_body, 0, 100).' ... ', 'info');
743 return new WP_Error('response_not_understood', 'Response from remote site could not be understood', $response_body);
744 }
745 }
746
747 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);
748
749 $format = $decoded['format'];
750
751 // Don't allow the remote side to downgrade
752 if ($format > 1 || $this->format > 1) {
753 if (empty($decoded['signature'])) {
754 $this->log('Decoding response: no message signature found');
755 die;
756 }
757 if (!$this->key_remote) {
758 $this->log('Decoding response: no signature verification key has been set');
759 die;
760 }
761 if (!$this->verify_signature($decoded['udrpc_message'], $decoded['signature'], $this->key_remote)) {
762 $this->log('Decoding response: signature verification failed; discarding');
763 die;
764 }
765 }
766
767 $decoded = $this->decrypt_message($decoded['udrpc_message']);
768
769 if (!is_string($decoded)) return new WP_Error('not_decrypted', 'Response from remote site was not successfully decrypted');
770
771 $json_decoded = json_decode($decoded, true);
772
773 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);
774
775 // 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)
776 if ($this->extra_replay_protection) {
777 $message_hash = $this->calculate_message_hash((string) $post['body']);
778 if ($this->message_hash_seen($message_hash)) {
779 return new WP_Error('replay_detected', 'Message refused: replay detected', $message_hash);
780 }
781 }
782
783 $time_difference = absint((time() - $json_decoded['time']));
784 if ($time_difference > $this->maximum_replay_time_difference) return new WP_Error('window_error', 'Message refused: maxium replay time difference exceeded', $time_difference);
785
786 if (isset($json_decoded['incoming_rand']) && !empty($this->message_random_number) && $json_decoded['incoming_rand'] != $this->message_random_number) {
787 // @codingStandardsIgnoreLine
788 $this->log('UDRPC: Message mismatch (possibly MITM) (sent_rand=' + $this->message_random_number + ', returned_rand='.$json_decoded['incoming_rand'].'): dropping', 'error');
789
790 return new WP_Error('message_mismatch_error', 'Message refused: message mismatch (possible MITM)');
791
792 }
793
794 // Should be an array with keys including 'response' and (if relevant) 'data'
795 return $json_decoded;
796
797 }
798
799 /**
800 * 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)
801 *
802 * @return boolean
803 */
804 public function create_listener() {
805
806 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
807
808 // 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
809 if ((!empty($_POST) && !empty($_POST['udrpc_message']) && !empty($_POST['format'])) || (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin)) {
810 add_action('wp_loaded', array($this, 'wp_loaded'));
811 add_action('wp_loaded', array($this, 'wp_loaded_final'), 10000);
812 return true;
813 }
814
815 return false;
816 }
817
818 public function wp_loaded_final() {
819 if (empty($this->require_message_to_be_understood)) return;
820 $message_for = empty($_POST['key_name']) ? '' : (string) $_POST['key_name'];
821 $this->log("Message was received, but not understood by local site (for: $message_for)");
822 die;
823 }
824
825 public function wp_loaded() {
826
827 /*
828 // What if something else already set some response headers?
829 if (function_exists('apache_response_headers')) {
830 $apache_response_headers = apache_response_headers();
831 // Do something...
832 }
833 */
834
835 // CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
836 // get_http_origin() : since WP 3.4
837 $http_origin = function_exists('get_http_origin') ? get_http_origin() : (empty($_SERVER['HTTP_ORIGIN']) ? '' : $_SERVER['HTTP_ORIGIN']);
838 if (!empty($_SERVER['REQUEST_METHOD']) && 'OPTIONS' == $_SERVER['REQUEST_METHOD'] && $http_origin) {
839 if (in_array($http_origin, $this->allow_cors_from)) {
840 // @codingStandardsIgnoreLine
841 if (!defined('UDRPC_DO_NOT_SEND_CORS_HEADERS') || !UDRPC_DO_NOT_SEND_CORS_HEADERS) {
842 header("Access-Control-Allow-Origin: $http_origin");
843 header('Access-Control-Allow-Credentials: true');
844 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) header('Access-Control-Allow-Methods: POST, OPTIONS');
845 if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) header('Access-Control-Allow-Headers: '.$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
846 }
847 die;
848 } elseif ($this->debug) {
849 $this->log('Non-allowed CORS from: '.$http_origin);
850 }
851 // 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.
852 return;
853 }
854
855 // Silently return, rather than dying, in case another instance is able to handle this
856 if (empty($_POST['format']) || (1 != $_POST['format'] && 2 != $_POST['format'])) return;
857
858 $this->require_message_to_be_understood = true;
859
860 $format = $_POST['format'];
861
862 /*
863 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
864 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).
865 */
866
867 // Is this for us?
868 if (empty($_POST['key_name']) || $_POST['key_name'] != $this->key_name_indicator) {
869 return;
870 }
871
872 // wp_unslash() does not exist until after WP 3.5
873 // $udrpc_message = function_exists('wp_unslash') ? wp_unslash($_POST['udrpc_message']) : stripslashes_deep($_POST['udrpc_message']);
874
875 // Data should not have any slashes - it is base64-encoded
876 $udrpc_message = (string) $_POST['udrpc_message'];
877
878 // Check this now, rather than allow the decrypt method to thrown an Exception
879
880 if ($format > 1) {
881 if (empty($_POST['signature'])) {
882 $this->log('No message signature found', 'error');
883 die;
884 }
885 if (!$this->key_remote) {
886 $this->log('No signature verification key has been set', 'error');
887 die;
888 }
889 if (!$this->verify_signature($udrpc_message, $_POST['signature'], $this->key_remote)) {
890 $this->log('Signature verification failed; discarding', 'error');
891 die;
892 }
893 }
894
895 try {
896 $udrpc_message = $this->decrypt_message($udrpc_message);
897 } catch (Exception $e) {
898 $this->log('Exception ('.get_class($e).'): '.$e->getMessage(), 'error');
899 die;
900 }
901
902 if (!is_string($udrpc_message)) {
903 $this->log('Could not decrypt incoming message', 'error');
904 die;
905 }
906
907 $udrpc_message = json_decode($udrpc_message, true);
908
909 if (empty($udrpc_message) || !is_array($udrpc_message) || empty($udrpc_message['command']) || !is_string($udrpc_message['command'])) {
910 $this->log('Could not decode JSON on incoming message', 'error');
911 die;
912 }
913
914 if (empty($udrpc_message['time'])) {
915 $this->log('No time set in incoming message', 'error');
916 die;
917 }
918
919 // Mismatch indicating a replay of the message with a different key name in the unencrypted portion?
920 if (empty($udrpc_message['key_name']) || $_POST['key_name'] != $udrpc_message['key_name']) {
921 $this->log('key_name mismatch between encrypted and unencrypted portions', 'error');
922 die;
923 }
924
925 if ($this->extra_replay_protection) {
926 $message_hash = $this->calculate_message_hash((string) $_POST['udrpc_message']);
927 if ($this->message_hash_seen($message_hash)) {
928 $this->log("Message dropped: apparently a replay (hash: $message_hash)", 'error');
929 die;
930 }
931 }
932
933 // 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.
934 $time_difference = absint($udrpc_message['time'] - time());
935 if ($time_difference > $this->maximum_replay_time_difference) {
936 $this->log("Time in incoming message is outside of allowed window ($time_difference > ".$this->maximum_replay_time_difference.')', 'error');
937 die;
938 }
939
940 // The sequence number should always be larger than any previously-sent sequence number
941 if ($this->sequence_protection_tolerance) {
942
943 if ($this->debug) $this->log('Sequence protection is active; tolerance: '.$this->sequence_protection_tolerance);
944
945 global $wpdb;
946
947 if (!isset($udrpc_message['sequence_id']) || !is_numeric($udrpc_message['sequence_id'])) {
948 $this->log('a numerical sequence number is required, but none was included in the message - dropping', 'error');
949 die;
950 }
951
952 $message_sequence_id = (int) $udrpc_message['sequence_id'];
953 $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));
954
955 if ('' === $recently_seen_sequences_ids) $recently_seen_sequences_ids = '0';
956
957 $recently_seen_sequences_ids_as_array = explode($recently_seen_sequences_ids, ',');
958 sort($recently_seen_sequences_ids_as_array);
959
960 // Seen before?
961 if (in_array($message_sequence_id, $recently_seen_sequences_ids_as_array)) {
962 $this->log("message with duplicate sequence number received - dropping (received=$message_sequence_id, seen=$recently_seen_sequences_ids)");
963 die;
964 }
965
966 // Within the tolerance threshold? That means: a) either bigger than the max, or b) no more than <tolerance> lower than the least
967 if ($message_sequence_id > max($recently_seen_sequences_ids)) {
968 if ($this->debug) $this->log("Sequence id ($message_sequence_id) is greater than any previous (".max($recently_seen_sequences_ids).') - message is thus OK');
969 // All is well
970 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
971 } elseif ((max($recently_seen_sequences_ids) - $message_sequence_id) <= $this->sequence_protection_tolerance) {
972 // All is well - was one of those 'missing' in the sequence
973 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');
974 $recently_seen_sequences_ids_as_array[] = $message_sequence_id;
975 } else {
976 $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');
977 die;
978 }
979
980 // Remove out-of-bounds seen IDs
981 $max_sequence_id_seen = max($recently_seen_sequences_ids_as_array);
982 foreach ($recently_seen_sequences_ids_as_array as $k => $id) {
983 if ($max_sequence_id_seen - $id > $this->sequence_protection_tolerance) {
984 if ($this->debug) $this->log("Removing no-longer-relevant sequence from list of those recently seen: $id");
985 unset($recently_seen_sequences_ids_as_array[$k]);
986 }
987 }
988
989 // Allow reset
990 if ($message_sequence_id > PHP_INT_MAX - 10) {
991 $recently_seen_sequences_ids_as_array = array(0);
992 }
993
994 // Write them back to the database
995 $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));
996 if ($this->debug) $this->log("SQL to send recent sequence IDs back to the database: $sql");
997 $wpdb->query($sql);
998
999 }
1000
1001 $this->incoming_message = $udrpc_message;
1002
1003 $command = (string) $udrpc_message['command'];
1004 $data = empty($udrpc_message['data']) ? null : $udrpc_message['data'];
1005
1006 // @codingStandardsIgnoreLine
1007 if ($http_origin && !empty($udrpc_message['cors_headers_wanted']) && (!defined('UDRPC_DO_NOT_SEND_CORS_HEADERS') || !UDRPC_DO_NOT_SEND_CORS_HEADERS)) {
1008 header("Access-Control-Allow-Origin: $http_origin");
1009 header('Access-Control-Allow-Credentials: true');
1010 }
1011
1012 // Restrict use of format 1
1013 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))) {
1014 $this->log("Obsolete format used - dropping (command: $command)", 'error');
1015 die;
1016 }
1017
1018 $this->log('Command received: '.$command, 'info');
1019
1020 if ('ping' == $command) {
1021 $response = array('response' => 'pong', 'data' => null);
1022 } else {
1023 if (has_filter('udrpc_command_'.$command)) {
1024 $response = apply_filters('udrpc_command_'.$command, null, $data, $this->key_name_indicator);
1025 } else {
1026 $response = array('response' => 'rpcerror', 'data' => array('code' => 'unknown_rpc_command', 'data' => $command));
1027 }
1028 }
1029
1030 $response = apply_filters('udrpc_action', $response, $command, $data, $this->key_name_indicator, $this);
1031
1032 if (is_array($response)) {
1033
1034 if ($this->debug) {
1035 $this->log('UDRPC response (pre-encoding/encryption): '.serialize($response));
1036 }
1037
1038 $data = isset($response['data']) ? $response['data'] : null;
1039
1040 $final_response = json_encode($this->create_message($response['response'], $data, true));
1041
1042 do_action('udrpc_action_send_response', $final_response, $command);
1043
1044 echo $final_response;
1045 }
1046
1047 die;
1048
1049 }
1050
1051 /**
1052 * The hash needs to be in a format that phpseclib likes. phpseclib uses lower case.
1053 * Pass in a base64-encoded signature (i.e. just as signature_for_message creates)
1054 *
1055 * @param string $message
1056 * @param string $signature
1057 * @param string $key
1058 * @param string $hash_algorithm
1059 * @return boolean
1060 */
1061 public function verify_signature($message, $signature, $key, $hash_algorithm = 'sha256') {
1062 $rsa = new phpseclib_Crypt_RSA();
1063 $rsa->setHash(strtolower($hash_algorithm));
1064 // This is not the default, but is what we use
1065 $rsa->setSignatureMode(phpseclib_Crypt_RSA::SIGNATURE_PKCS1);
1066 $rsa->loadKey($key);
1067
1068 // Don't hash it - Crypt_RSA::verify() already does that
1069 // $hash = new Crypt_Hash($hash_algorithm);
1070 // $hashed = $hash->hash($message);
1071
1072 $verified = $rsa->verify($message, base64_decode($signature));
1073
1074 if ($this->debug) $this->log('Signature verification result: '.serialize($verified));
1075
1076 return $verified;
1077 }
1078
1079 private function calculate_message_hash($message) {
1080 return hash('sha256', $message);
1081 }
1082
1083 private function message_hash_seen($message_hash) {
1084 // 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.
1085 $transient_name = 'udrpch_'.md5($this->key_name_indicator);
1086 $seen_hashes = get_transient($transient_name);
1087 if (!is_array($seen_hashes)) $seen_hashes = array();
1088 $time_now = time();
1089 // $any_changes = false;
1090 // Prune the old hashes
1091 foreach ($seen_hashes as $hash => $last_seen) {
1092 if ($last_seen < ($time_now - $this->maximum_replay_time_difference)) {
1093 // $any_changes = true;
1094 unset($seen_hashes[$hash]);
1095 }
1096 }
1097 if (isset($seen_hashes[$message_hash])) {
1098 return true;
1099 }
1100 $seen_hashes[$message_hash] = $time_now;
1101 set_transient($transient_name, $seen_hashes, $this->maximum_replay_time_difference);
1102
1103 return false;
1104 }
1105 }
1106
1107 endif;
1108