PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.13
Patchstack – WordPress & Plugins Security v2.1.13
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
patchstack / includes / firewall.php

firewall.php in Patchstack – WordPress & Plugins Security 2.1.13, at includes/firewall.php

753 lines 20.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class provides the firewall functionality.
10 */
11 class P_Firewall extends P_Core {
12
13 /**
14 * Parse the firewall and whitelist rules and determine if it's valid.
15 * Then set the types with all server/client variables and launch the processor.
16 *
17 * @param bool $from_main Whether or not the firewall is loaded from the main script or not.
18 * @param Patchstack $core
19 * @param bool $skip Whether or not to process and execute the rules.
20 * @return void
21 */
22 public function __construct( $from_main = false, $core = null, $skip = false ) {
23 if ( ! $from_main || ! $core ) {
24 if ( $core ) {
25 parent::__construct( $core );
26 }
27 return;
28 }
29
30 parent::__construct( $core );
31
32 // If we only want to initialize the firewall but not execute the rules.
33 if ( $skip ) {
34 return;
35 }
36
37 // Process the firewall rules.
38 $this->processor();
39 }
40
41 /**
42 * Check the custom whitelist rules defined in the backend of WordPress
43 * and attempt to match it with the request.
44 *
45 * @return boolean
46 */
47 private function is_custom_whitelisted() {
48 $whitelist = str_replace( '<?php exit; ?>', '', get_option( 'patchstack_custom_whitelist_rules', '' ) );
49 if ( empty( $whitelist ) ) {
50 return false;
51 }
52
53 // Loop through all lines.
54 $lines = explode( "\n", $whitelist );
55 $ip = $this->get_ip();
56
57 foreach ( $lines as $line ) {
58 $t = explode( ':', $line );
59
60 if ( count( $t ) == 2 ) {
61 $val = strtolower( trim( $t[1] ) );
62 switch ( strtolower( $t[0] ) ) {
63 // IP address match.
64 case 'ip':
65 if ( $ip == $val ) {
66 return true;
67 }
68 break;
69 // Payload match.
70 case 'payload':
71 if ( count( $_POST ) > 0 && strpos( strtolower( print_r( $_POST, true ) ), $val ) !== false ) {
72 return true;
73 }
74
75 if ( count( $_GET ) > 0 && strpos( strtolower( print_r( $_GET, true ) ), $val ) !== false ) {
76 return true;
77 }
78 break;
79 // URL match.
80 case 'url':
81 if ( strpos( strtolower( $_SERVER['REQUEST_URI'] ), $val ) !== false ) {
82 return true;
83 }
84 break;
85 }
86 }
87 }
88
89 return false;
90 }
91
92 /**
93 * Determine if the request should be whitelisted.
94 *
95 * @return boolean
96 */
97 private function is_whitelisted() {
98 // First check if the user has custom whitelist rules configured.
99 if ( $this->is_custom_whitelisted() ) {
100 return true;
101 }
102
103 // Load the whitelist.
104 $whitelists = get_option( 'patchstack_whitelist_rules', '' );
105 if ( $whitelists == null || $whitelists == '' ) {
106 return false;
107 }
108
109 // Parse the whitelist.
110 $whitelists = json_decode( str_replace( '<?php exit; ?>', '', $whitelists ), true );
111
112 // Grab visitor's IP address and request data.
113 $client_ip = $this->get_ip();
114 $requests = $this->capture_request();
115
116 foreach ( $whitelists as $whitelist ) {
117 $whitelist_rule = json_decode( $whitelist['rule'] );
118 $matched_rules = 0;
119
120 // If matches on all request methods, only 1 rule match is required to whitelist.
121 if ( $whitelist_rule->method === 'ALL' ) {
122 $count_rules = 1;
123 } else {
124 if ( ! is_null( $whitelist_rule ) ) {
125 $count_rules = $whitelist_rule->rules;
126 $count_rules = $this->count_rules( $count_rules );
127 }
128 }
129
130 // If an IP address match is given, determine if it matches.
131 $ip = isset( $whitelist_rule->rules, $whitelist_rule->rules->ip_address ) ? $whitelist_rule->rules->ip_address : null;
132 if ( ! is_null( $ip ) ) {
133 if ( strpos( $ip, '*' ) !== false ) {
134 $whitelisted_ip = $this->plugin->ban->check_wildcard_rule( $client_ip, $ip );
135 } elseif ( strpos( $ip, '-' ) !== false ) {
136 $whitelisted_ip = $this->plugin->ban->check_range_rule( $client_ip, $ip );
137 } elseif ( strpos( $ip, '/' ) !== false ) {
138 $whitelisted_ip = $this->plugin->ban->check_subnet_mask_rule( $client_ip, $ip );
139 } elseif ( $client_ip == $ip ) {
140 $whitelisted_ip = true;
141 } else {
142 $whitelisted_ip = false;
143 }
144 } else {
145 $whitelisted_ip = true;
146 }
147
148 foreach ( $requests as $key => $request ) {
149
150 // Treat the raw POST data string as the body contents of all values combined.
151 if ( $key == 'rulesRawPost' ) {
152 $key = 'rulesBodyAll';
153 }
154
155 if ( $whitelist_rule->method == $requests['method'] || $whitelist_rule->method == 'ALL' ) {
156 $test = strtolower( preg_replace( '/(?!^)[A-Z]{2,}(?=[A-Z][a-z])|[A-Z][a-z]/', '->$0', $key ) );
157 $rule = array_reduce(
158 explode( '->', $test ),
159 function ( $o, $p ) {
160 if ( ! isset( $o->$p ) ) {
161 return null;
162 }
163
164 return $o->$p;
165 },
166 $whitelist_rule
167 );
168
169 if ( ! is_null( $rule ) && substr( $key, 0, 4 ) == 'rule' && $this->is_rule_match( $rule, $request ) ) {
170 $matched_rules++;
171 }
172 }
173 }
174
175 if ( $matched_rules >= $count_rules && $whitelisted_ip ) {
176 return true;
177 }
178 }
179
180 return false;
181 }
182
183 /**
184 * Retrieve all HTTP headers that start with HTTP_.
185 *
186 * @return array
187 */
188 private function get_headers() {
189 $headers = array();
190 foreach ( $_SERVER as $name => $value ) {
191 if ( substr( $name, 0, 5 ) == 'HTTP_' ) {
192 $headers[ str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) ) ] = $value;
193 }
194 }
195
196 return $headers;
197 }
198
199 /**
200 * Retrieve information about any file uploads.
201 *
202 * @return array
203 */
204 private function get_file_upload_data() {
205 if ( ! is_array( $_FILES ) || count( $_FILES ) == 0 ) {
206 return '';
207 }
208
209 // Extract the information we need from $_FILES.
210 $return = array();
211 foreach ( $_FILES as $key => $data ) {
212 foreach ( $data as $key2 => $data2 ) {
213
214 // We only want the name and type.
215 if ( ! in_array( $key2, array( 'name', 'type' ) ) ) {
216 continue;
217 }
218
219 if ( ! is_array( $data2 ) ) {
220 $return[] = $key2 . '=' . $data2;
221 } else {
222 $return[] = $key2 . '=' . @$this->multi_implode( $data2, '&' . $key2 . '=' );
223 }
224 }
225 }
226
227 return implode( '&', $return );
228 }
229
230
231 /**
232 * Returns all request methods and parameters
233 *
234 * @return string
235 */
236 private function capture_request() {
237 $data = $this->capture_keys();
238
239 // Get the method and URL.
240 $method = $_SERVER['REQUEST_METHOD'];
241 $rulesUri = $_SERVER['REQUEST_URI'];
242
243 // Store the header values in different formats.
244 $rulesHeadersKeys = array();
245 $rulesHeadersValues = array();
246 $rulesHeadersCombinations = array();
247
248 // Retrieve the headers.
249 $headers = $this->get_headers();
250 $rulesHeadersAll = implode( ' ', $headers );
251 foreach ( $headers as $name => $value ) {
252 $rulesHeadersKeys[] = $name;
253 $rulesHeadersValues[] = $value;
254 $rulesHeadersCombinations[] = $name . ': ' . $value;
255 }
256
257 // Store the $_POST values in different formats.
258 $rulesBodyKeys = array();
259 $rulesBodyValues = array();
260 $rulesBodyCombinations = array();
261
262 // Retrieve the $_POST values.
263 $rulesBodyAll = urldecode( http_build_query( $data['POST'] ) );
264 foreach ( $data['POST'] as $key => $value ) {
265 if ( is_array( $value ) ) {
266 $value = @$this->multi_implode( $value, ' ' );
267 }
268 $rulesBodyKeys[] = $key;
269 $rulesBodyValues[] = $value;
270 $rulesBodyCombinations[] = $key . '=' . $value;
271 }
272
273 // Store the $_GET values in different formats.
274 $rulesParamsKeys = array();
275 $rulesParamsValues = array();
276 $rulesParamsCombinations = array();
277
278 // Retrieve the $_GET values.
279 $rulesParamsAll = urldecode( http_build_query( $data['GET'] ) );
280 foreach ( $data['GET'] as $key => $value ) {
281 if ( is_array( $value ) ) {
282 $value = @$this->multi_implode( $value, ' ' );
283 }
284 $rulesParamsKeys[] = $key;
285 $rulesParamsValues[] = $value;
286 $rulesParamsCombinations[] = $key . '=' . $value;
287 }
288
289 // Raw POST data.
290 $rulesRawPost = @file_get_contents( 'php://input' );
291
292 // Data about file uploads.
293 $rulesFile = $this->get_file_upload_data();
294
295 // Return each value as its own array.
296 return compact(
297 'method',
298 'rulesFile',
299 'rulesRawPost',
300 'rulesUri',
301 'rulesHeadersAll',
302 'rulesHeadersKeys',
303 'rulesHeadersValues',
304 'rulesHeadersCombinations',
305 'rulesBodyAll',
306 'rulesBodyKeys',
307 'rulesBodyValues',
308 'rulesBodyCombinations',
309 'rulesParamsAll',
310 'rulesParamsKeys',
311 'rulesParamsValues',
312 'rulesParamsCombinations'
313 );
314 }
315
316 /**
317 * Capture the keys of the request.
318 *
319 * @return array
320 */
321 private function capture_keys() {
322 // Data we want to go through.
323 $data = array(
324 'POST' => $_POST,
325 'GET' => $_GET,
326 );
327
328 // Determine if there are any keys we should remove from the data set.
329 if ( get_option( 'patchstack_whitelist_keys_rules', '' ) == '' ) {
330 return $data;
331 }
332
333 // Must be valid JSON and decodes to at least 2 primary data arrays.
334 $keys = json_decode( get_option( 'patchstack_whitelist_keys_rules' ), true );
335 if ( ! $keys || ! is_array( $keys ) || $keys && count( $keys ) < 2 ) {
336 return $data;
337 }
338
339 // Remove the keys where necessary, go through all data types (GET, POST).
340 foreach ( $keys as $type => $entries ) {
341
342 // Go through all whitelisted actions.
343 foreach ( $entries as $entry ) {
344 $t = explode( '.', $entry );
345
346 // For non-multidimensional array checks.
347 if ( count( $t ) == 1 ) {
348 // If the value itself exists.
349 if ( isset( $data[ $type ][ $t[0] ] ) ) {
350 unset( $data[ $type ][ $t[0] ] );
351 }
352
353 // For pattern checking.
354 if ( strpos( $t[0], '*' ) !== false ) {
355 $star = explode( '*', $t[0] );
356
357 // Loop through all $_POST, $_GET values.
358 foreach ( $data as $method => $values ) {
359 foreach ( $values as $key => $value ) {
360 if ( ! is_array( $value ) && strpos( $key, $star[0] ) !== false ) {
361 unset( $data[ $method ][ $key ] );
362 }
363 }
364 }
365 }
366 continue;
367 }
368
369 // For multidimensional array checks.
370 $end =& $data[ $type ];
371 $skip = false;
372 foreach ( $t as $var ) {
373 if ( ! isset( $end[ $var ] ) ) {
374 $skip = true;
375 break;
376 }
377 $end =& $end[ $var ];
378 }
379
380 // Since we cannot unset it due to it being a reference variable,
381 // we just set it to an empty string instead.
382 if ( ! $skip ) {
383 $end = '';
384 }
385 }
386 }
387
388 return $data;
389 }
390
391 /**
392 * Implode array recursively.
393 *
394 * @param $array
395 * @param $glue
396 * @return bool|string
397 */
398 private function multi_implode( $array, $glue ) {
399 $ret = '';
400
401 foreach ( $array as $item ) {
402 if ( is_array( $item ) ) {
403 $ret .= $this->multi_implode( $item, $glue ) . $glue;
404 } else {
405 $ret .= $item . $glue;
406 }
407 }
408
409 return substr( $ret, 0, 0 - strlen( $glue ) );
410 }
411
412 /**
413 * Determine if the request matches the given firewall or whitelist rule.
414 *
415 * @param string $rule
416 * @param string|array $request
417 * @return bool
418 */
419 private function is_rule_match( $rule, $request ) {
420 $is_matched = false;
421 if ( is_array( $request ) ) {
422 foreach ( $request as $key => $value ) {
423 $is_matched = $this->is_rule_match( $rule, $value );
424 if ( $is_matched ) {
425 return $is_matched;
426 }
427 }
428 } else {
429 return preg_match( $rule, urldecode( $request ) );
430 }
431
432 return $is_matched;
433 }
434
435 /**
436 * Count the number of rules.
437 *
438 * @param array $array
439 * @return integer
440 */
441 private function count_rules( $array ) {
442 $counter = 0;
443 if ( is_object( $array ) ) {
444 $array = (array) $array;
445 }
446
447 if ( $array['uri'] ) {
448 $counter++;
449 }
450
451 foreach ( array( 'body', 'params', 'headers' ) as $type ) {
452 foreach ( $array[ $type ] as $key => $value ) {
453 if ( ! is_null( $value ) ) {
454 $counter++;
455 }
456 }
457 }
458
459 return $counter;
460 }
461
462 /**
463 * Runs the firewall rules processor.
464 *
465 * @return void
466 */
467 private function processor() {
468 // Load the firewall rules.
469 $rules = json_decode( get_option( 'patchstack_firewall_rules', '' ), true );
470 if ( $rules == '' || is_null( $rules ) ) {
471 return;
472 }
473
474 // Determine if the user is temporarily blocked from the site.
475 if ( $this->is_auto_ip_blocked() > $this->get_option( 'patchstack_autoblock_attempts', 10 ) && ! $this->is_authenticated() ) {
476 $this->display_error_page( 22 );
477 }
478
479 // Check for whitelist.
480 $is_whitelisted = $this->is_whitelisted();
481
482 // Obtain the IP address and request data.
483 $client_ip = $this->get_ip();
484 $requests = $this->capture_request();
485
486 // Iterate through all root objects.
487 foreach ( $rules as $firewall_rule ) {
488 $blocked_count = 0;
489 $firewall_rule['bypass_whitelist'] = isset( $firewall_rule['bypass_whitelist'] ) ? $firewall_rule['bypass_whitelist'] : false;
490
491 // Do we need to skip the whitelist for a particular rule?
492 if ( isset( $firewall_rule['bypass_whitelist'] ) && ! $firewall_rule['bypass_whitelist'] && $is_whitelisted ) {
493 continue;
494 }
495
496 $rule_terms = json_decode( $firewall_rule['rule'] );
497
498 // Determine if we should match the IP address.
499 $ip = isset( $rule_terms->rules->ip_address ) ? $rule_terms->rules->ip_address : null;
500 if ( ! is_null( $ip ) ) {
501 $matched_ip = false;
502 if ( strpos( $ip, '*' ) !== false ) {
503 $matched_ip = $this->plugin->ban->check_wildcard_rule( $client_ip, $ip );
504 } elseif ( strpos( $ip, '-' ) !== false ) {
505 $matched_ip = $this->plugin->ban->check_range_rule( $client_ip, $ip );
506 } elseif ( strpos( $ip, '/' ) !== false ) {
507 $matched_ip = $this->plugin->ban->check_subnet_mask_rule( $client_ip, $ip );
508 } elseif ( $client_ip == $ip ) {
509 $matched_ip = true;
510 }
511
512 if ( ! $matched_ip ) {
513 continue;
514 }
515 }
516
517 // If matches on all request methods, only 1 rule match is required to block
518 if ( $rule_terms->method === 'ALL' ) {
519 $count_rules = 1;
520 } else {
521 $count_rules = json_decode( json_encode( $rule_terms->rules ), true );
522 $count_rules = $this->count_rules( $count_rules );
523 }
524
525 // Loop through all request data that we captured.
526 foreach ( $requests as $key => $request ) {
527
528 // Treat the raw POST data string as the body contents of all values combined.
529 if ( $key == 'rulesRawPost' ) {
530 $key = 'rulesBodyAll';
531 }
532
533 // Determine if the requesting method matches.
534 if ( $rule_terms->method == $requests['method'] || $rule_terms->method == 'ALL' || $rule_terms->method == 'GET' || ( $rule_terms->method == 'FILES' && $this->is_file_upload() ) ) {
535 $test = strtolower( preg_replace( '/(?!^)[A-Z]{2,}(?=[A-Z][a-z])|[A-Z][a-z]/', '->$0', $key ) );
536 $exp = explode( '->', $test );
537
538 // Determine if a rule exists for this request.
539 $rule = array_reduce(
540 $exp,
541 function ( $o, $p ) {
542 if ( ! isset( $o->$p ) ) {
543 return null;
544 }
545
546 return $o->$p;
547 },
548 $rule_terms
549 );
550
551 // Determine if the rule matches the request.
552 if ( ! is_null( $rule ) && substr( $key, 0, 4 ) == 'rule' && $this->is_rule_match( $rule, $request ) ) {
553 $blocked_count++;
554 }
555 }
556 }
557
558 // Determine if the user should be blocked.
559 if ( $blocked_count >= $count_rules ) {
560 if ( $rule_terms->type == 'BLOCK' ) {
561 $this->block_user( $firewall_rule['id'], (bool) $firewall_rule['bypass_whitelist'] );
562 } elseif ( $rule_terms->type == 'LOG' ) {
563 $this->log_user( $firewall_rule['id'] );
564 } elseif ( $rule_terms->type == 'REDIRECT' ) {
565 $this->redirect_user( $firewall_rule['id'], $rule_terms->type_params );
566 }
567 }
568 }
569 }
570
571 /**
572 * Determine if the current request is a file upload.
573 *
574 * @return boolean
575 */
576 private function is_file_upload() {
577 return isset( $_FILES ) && count( $_FILES ) > 0;
578 }
579
580 /**
581 * Automatically block the user if there are many blocked requests in a short period of time.
582 *
583 * @return integer
584 */
585 public function is_auto_ip_blocked() {
586 // Calculate block time.
587 $minutes = (int) $this->get_option( 'patchstack_autoblock_minutes', 30 );
588 $timeout = (int) $this->get_option( 'patchstack_autoblock_blocktime', 60 );
589 if ( empty( $minutes ) || empty( $timeout ) ) {
590 $time = 30 + 60;
591 } else {
592 $time = $minutes + $timeout;
593 }
594
595 // Determine if the user should be blocked.
596 global $wpdb;
597 $results = $wpdb->get_results(
598 $wpdb->prepare( 'SELECT COUNT(*) as numIps FROM ' . $wpdb->prefix . "patchstack_firewall_log WHERE block_type = 'BLOCK' AND apply_ban = 1 AND ip = '%s' AND log_date >= ('" . current_time( 'mysql' ) . "' - INTERVAL %d MINUTE)", array( $this->get_ip(), $time ) ),
599 OBJECT
600 );
601
602 if ( ! isset( $results, $results[0], $results[0]->numIps ) ) {
603 return 0;
604 }
605 return $results[0]->numIps;
606 }
607
608 /**
609 * Block the user, and log, do whatever is necessary.
610 *
611 * @param string $rule
612 * @param bool $bypass
613 * @return void
614 */
615 private function block_user( $rule, $bypass = false ) {
616 if ( ! $this->is_authenticated( $bypass ) ) {
617 $this->display_error_page( '55' . intval( $rule ) );
618 }
619 }
620
621 /**
622 * Log the user action.
623 *
624 * @param string $rule
625 * @return void
626 */
627 private function log_user( $rule ) {
628 $this->log_hacker( $rule, '', 'LOG' );
629 }
630
631 /**
632 * Log the user action and redirect.
633 *
634 * @param integer $rule_id
635 * @param string $redirect
636 * @return void
637 */
638 private function redirect_user( $rule_id, $redirect ) {
639 $this->log_hacker( $rule_id, '', 'REDIRECT' );
640
641 // Don't redirect an invalid URL.
642 if ( ! $redirect || stripos( $redirect, 'http' ) === false ) {
643 return;
644 }
645
646 ob_start();
647 header( 'Location: ' . $redirect );
648 ob_end_flush();
649 exit;
650 }
651
652 /**
653 * Determine if the user is authenticated and in the list of whitelisted roles.
654 *
655 * @param bool $bypass
656 * @return bool
657 */
658 public function is_authenticated( $bypass = false ) {
659 if ( $bypass || ! is_user_logged_in() ) {
660 return false;
661 }
662
663 // Get the whitelisted roles.
664 $roles = $this->get_option( 'patchstack_basic_firewall_roles', array( 'administrator', 'editor', 'author' ) );
665 if ( ! is_array ( $roles ) ) {
666 return false;
667 }
668
669 // Special scenario for super admins on a multisite environment.
670 if ( in_array( 'administrator', $roles ) && is_multisite() && is_super_admin() ) {
671 return true;
672 }
673
674 // User is logged in, determine the role.
675 $user = wp_get_current_user();
676 if ( ! isset( $user->roles ) || count( (array) $user->roles ) == 0 ) {
677 return false;
678 }
679
680 // Is the user in the whitelist roles list?
681 $role_count = array_intersect( $user->roles, $roles );
682 return count( $role_count ) != 0;
683 }
684
685 /**
686 * Log the blocked request.
687 *
688 * @param integer $fid firewall
689 * @param array $query_vars
690 * @param string $block_type
691 * @param array $block_params
692 * @return void
693 */
694 private function log_hacker( $fid = 1, $post_data = '', $block_type = 'BLOCK' ) {
695 global $wpdb;
696 if ( ! $wpdb || $fid == 22 || $fid == 23 ) {
697 return;
698 }
699
700 // Insert into the logs.
701 $wpdb->insert(
702 $wpdb->prefix . 'patchstack_firewall_log',
703 array(
704 'ip' => $this->get_ip(),
705 'request_uri' => isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '',
706 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '',
707 'method' => isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : '',
708 'fid' => $fid,
709 'flag' => '',
710 'post_data' => $post_data != '' ? json_encode( $post_data ) : $this->get_post_data(),
711 'block_type' => $block_type,
712 )
713 );
714 }
715
716 /**
717 * Get POST data.
718 *
719 * @return string|NULL
720 */
721 private function get_post_data() {
722 if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || $_SERVER['REQUEST_METHOD'] != 'POST' ) {
723 return null;
724 }
725
726 return json_encode( $_POST );
727 }
728
729 /**
730 * Display error page.
731 *
732 * @param integer $fid
733 * @return void
734 */
735 public function display_error_page( $fid = 1 ) {
736 if ( $fid != 22 && $fid != 23 && $fid != 'login' ) {
737 $this->log_hacker( $fid );
738 }
739
740 header( 'Cache-Control: no-store' );
741 header( 'Pragma: no-cache' );
742 http_response_code( 403 );
743
744 if ($fid == 'login' ) {
745 require_once dirname( __FILE__ ) . '/views/access-denied-login.php';
746 } else {
747 require_once dirname( __FILE__ ) . '/views/access-denied.php';
748 }
749
750 exit;
751 }
752 }
753