PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-firewall.php

class-firewall.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/component/class-firewall.php

662 lines 18.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles operations related to IP and country-based blacklisting and
4 * whitelisting, as well as managing firewall settings and logs.
5 *
6 * @package WP_Defender\Component
7 */
8
9 namespace WP_Defender\Component;
10
11 use Exception;
12 use WPMUDEV_Dashboard;
13 use WP_Defender\Component;
14 use WP_Defender\Model\Lockout_Ip;
15 use WP_Defender\Behavior\WPMUDEV;
16 use WP_Defender\Model\Lockout_Log;
17 use WP_Defender\Model\Onboard;
18 use WP_DEFENDER_VENDOR\MaxMind\Db\Reader\InvalidDatabaseException;
19 use WP_Defender\Model\Setting\Firewall as Model_Firewall;
20 use WP_Defender\Component\Trusted_Proxy_Preset\Cloudflare_Proxy;
21 use WP_Defender\Component\Trusted_Proxy_Preset\Trusted_Proxy_Preset;
22 use WP_Defender\Controller\Firewall as Firewall_Controller;
23 use WP_Defender\Component\Smart_Ip_Detection;
24
25 /**
26 * Handles operations related to IP and country-based blacklisting and whitelisting,
27 * managing firewall settings, logs, and automatic detection of proxy configurations.
28 */
29 class Firewall extends Component {
30
31 /**
32 * The notice slug if there is switching IP Detection option to Cloudflare (CF).
33 */
34 public const IP_DETECTION_CF_SHOW_SLUG = 'wd_show_ip_detection_cf_notice';
35
36 /**
37 * The notice slug if CF IP Detection notice is rejected.
38 */
39 public const IP_DETECTION_CF_DISMISS_SLUG = 'wd_dismiss_ip_detection_cf_notice';
40
41 /**
42 * The notice slug if there is switching IP Detection option to X-Forwarded-For (XFF).
43 */
44 public const IP_DETECTION_XFF_SHOW_SLUG = 'wd_show_ip_detection_xff_notice';
45
46 /**
47 * The notice slug if CF IP Detection notice is rejected.
48 */
49 public const IP_DETECTION_XFF_DISMISS_SLUG = 'wd_dismiss_ip_detection_xff_notice';
50
51 /**
52 * The option name for the whitelist server public IP.
53 */
54 public const WHITELIST_SERVER_PUBLIC_IP_OPTION = 'wpdef_firewall_whitelist_server_public_ip';
55
56 /**
57 * Check if the first commencing request is proper staff remote access.
58 *
59 * @param array $access The access details including key.
60 *
61 * @return bool
62 */
63 private function is_commencing_staff_access( $access ): bool {
64 $action = defender_get_data_from_request( 'action', 'g' );
65 $wdpunkey = defender_get_data_from_request( 'wdpunkey', 'p' );
66
67 return wp_doing_ajax() && 'wdpunauth' === $action && isset( $access['key'] ) && hash_equals( $wdpunkey, $access['key'] );
68 }
69
70 /**
71 * Check is the access from authenticated staff.
72 *
73 * Validates the `wpmudev_is_staff` cookie token by verifying:
74 * - Token version and structure (v1.<payload>.<signature>)
75 * - HMAC-SHA256 signature against site salt (`wpmudev_remote_access`)
76 * - Token expiration timestamp against current time
77 * - Audience (`aud`) against current site URL (`home_url('/')`)
78 * - Subject (`sub`) against currently authenticated user ID
79 *
80 * @return bool True if staff access token is valid and unexpired, false otherwise.
81 */
82 private function is_authenticated_staff_access(): bool {
83 $cookie = defender_get_data_from_request( 'wpmudev_is_staff', 'c' );
84 if ( ! is_string( $cookie ) ) {
85 return false;
86 }
87
88 $token = explode( '.', $cookie, 3 );
89 if ( 3 !== count( $token ) || 'v1' !== $token[0] ) {
90 return false;
91 }
92
93 $signature = hash_hmac( 'sha256', $token[1], wp_salt( 'wpmudev_remote_access' ) );
94 if ( ! hash_equals( $signature, $token[2] ) ) {
95 return false;
96 }
97 // Improve flow to avoid bypassing staff access authentication via an unsigned cookie. Use legitim payload.
98 $payload = base64_decode( strtr( $token[1], '-_', '+/' ) . str_repeat( '=', ( 4 - strlen( $token[1] ) % 4 ) % 4 ), true );// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
99 $payload = is_string( $payload ) ? json_decode( $payload, true ) : null;
100
101 return is_array( $payload ) &&
102 isset( $payload['aud'], $payload['exp'], $payload['sub'] ) &&
103 is_string( $payload['aud'] ) &&
104 is_numeric( $payload['exp'] ) &&
105 is_numeric( $payload['sub'] ) &&
106 (int) $payload['exp'] > time() &&
107 hash_equals( home_url( '/' ), $payload['aud'] ) &&
108 get_current_user_id() === (int) $payload['sub'];
109 }
110
111 /**
112 * Check if the access is from our staff access.
113 *
114 * @return bool
115 */
116 private function is_a_staff_access(): bool {
117 if ( defined( 'WPMUDEV_DISABLE_REMOTE_ACCESS' ) && true === constant( 'WPMUDEV_DISABLE_REMOTE_ACCESS' ) ) {
118 return false;
119 }
120
121 $wpmu_dev = new WPMUDEV();
122
123 // Remote staff access needs Dashboard mode.
124 if ( 'dashboard' !== $wpmu_dev->resolve_connection_mode() ) {
125 return false;
126 }
127
128 $is_remote_access = is_object( WPMUDEV_Dashboard::$api ) &&
129 true === WPMUDEV_Dashboard::$api->remote_access_details( 'enabled' );
130
131 if ( $is_remote_access ) {
132 $access = $wpmu_dev->get_remote_access();
133 $is_authenticated_staff_access = isset( $access['userid'] ) &&
134 get_current_user_id() === (int) $access['userid'] &&
135 $this->is_authenticated_staff_access();
136 if ( $is_authenticated_staff_access || $this->is_commencing_staff_access( $access ) ) {
137 $this->log( $access, Firewall_Controller::FIREWALL_LOG );
138
139 return true;
140 }
141 }
142
143 return false;
144 }
145
146 /**
147 * Cron for delete old log.
148 *
149 * @throws Exception On failure.
150 */
151 public function firewall_clean_up_logs() {
152 $settings = new Model_Firewall();
153 $storage_days = apply_filters( 'ip_lockout_logs_store_backward', $settings->storage_days );
154 if ( ! is_numeric( $storage_days ) ) {
155 return;
156 }
157 $time_string = '-' . $storage_days . ' days';
158 $timestamp = $this->local_to_utc( $time_string );
159 // Since v5.7.0.
160 $delete_count = apply_filters( 'wpdef_firewall_limit_deleted_logs', 50 );
161 $delete_count = is_int( $delete_count ) ? $delete_count : (int) $delete_count;
162 Lockout_Log::remove_logs( $timestamp, $delete_count );
163 }
164
165 /**
166 * Cron for clean up temporary IP block list.
167 */
168 public function firewall_clean_up_temporary_ip_blocklist() {
169 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED );
170 foreach ( $models as $model ) {
171 $model->status = Lockout_Ip::STATUS_NORMAL;
172 $model->save();
173 }
174 }
175
176 /**
177 * Update temporary IP block list of Firewall, clear cron job.
178 * The interval settings value is updated once.
179 *
180 * @param string $new_interval The new interval to set.
181 */
182 public function update_cron_schedule_interval( $new_interval ) {
183 $settings = new Model_Firewall();
184 // If a new interval is different from the saved value, we need to clear the cron job.
185 if ( $new_interval !== $settings->ip_blocklist_cleanup_interval ) {
186 update_site_option( 'wpdef_clear_schedule_firewall_cleanup_temp_blocklist_ips', true );
187 }
188 }
189
190 /**
191 * Skip priority lockout checks for a given IP.
192 *
193 * @param string $ip The IP address to check.
194 *
195 * @return bool True if the IP should be skipped from lockout checks, false otherwise.
196 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
197 */
198 public function skip_priority_lockout_checks( string $ip ): bool {
199 /**
200 * Retrieve Global_IP instance.
201 *
202 * @var IP\Global_IP $global_ip
203 */
204 $global_ip = wd_di()->get( IP\Global_IP::class );
205
206 if (
207 $global_ip->is_global_ip_enabled() &&
208 $global_ip->is_ip_allowed( $ip )
209 ) {
210 return true;
211 }
212
213 /**
214 * Retrieve Blacklist_Lockout instance.
215 *
216 * @var Blacklist_Lockout $service
217 */
218 $service = wd_di()->get( Blacklist_Lockout::class );
219
220 $model = Lockout_Ip::get( $ip );
221 $is_lockout_ip = is_object( $model ) && $model->is_locked();
222
223 $is_country_whitelisted = ! $service->is_blacklist( $ip ) &&
224 $service->is_country_whitelist( $ip ) && ! $is_lockout_ip;
225
226 // If this IP is whitelisted, so we don't need to blacklist this.
227 if ( $service->is_ip_whitelisted( $ip ) || $is_country_whitelisted ) {
228 return true;
229 }
230 // Green light if access staff is enabled.
231 if ( $this->is_a_staff_access() ) {
232 return true;
233 }
234
235 return false;
236 }
237
238 /**
239 * Check if an IP is blacklisted.
240 *
241 * @param string $ip The IP address to check.
242 *
243 * @return array An array containing the reason and result of the blacklist check.
244 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
245 */
246 public function is_blocklisted_ip( string $ip ): array {
247 $array = array(
248 'reason' => '',
249 'result' => false,
250 );
251 /**
252 * Retrieve Blacklist_Lockout instance.
253 *
254 * @var Blacklist_Lockout $service
255 */
256 $service = wd_di()->get( Blacklist_Lockout::class );
257
258 if ( $service->is_blacklist( $ip ) ) {
259 return array(
260 'reason' => 'local_ip',
261 'result' => true,
262 );
263 }
264
265 if ( $service->is_country_blacklist( $ip ) ) {
266 return array(
267 'reason' => 'country',
268 'result' => true,
269 );
270 }
271
272 /**
273 * Retrieve Global_IP instance.
274 *
275 * @var IP\Global_IP $global_ip
276 */
277 $global_ip = wd_di()->get( IP\Global_IP::class );
278
279 if (
280 $global_ip->is_active() &&
281 $global_ip->is_ip_blocked( $ip )
282 ) {
283 return array(
284 'reason' => IP\Global_IP::REASON_SLUG,
285 'result' => true,
286 );
287 }
288
289 /**
290 * Retrieve Antibot_Global_Firewall instance.
291 *
292 * @var IP\Antibot_Global_Firewall $antibot
293 */
294 $antibot = wd_di()->get( IP\Antibot_Global_Firewall::class );
295
296 if (
297 $antibot->is_active() &&
298 $antibot->is_ip_blocked( $ip ) &&
299 'plugin' === $antibot->get_managed_by()
300 ) {
301 return array(
302 'reason' => IP\Antibot_Global_Firewall::REASON_SLUG,
303 'result' => true,
304 );
305 }
306
307 return $array;
308 }
309
310 /**
311 * Get the limit of Lockout records.
312 *
313 * @return int
314 * @since 3.7.0 Get the limit of Lockout records.
315 */
316 public function get_lockout_record_limit() {
317 $limit = apply_filters( 'wd_lockout_record_limit', 10000 );
318 return is_int( $limit ) ? $limit : (int) $limit;
319 }
320
321 /**
322 * Cron for deleting unwanted lockout records.
323 *
324 * @return void
325 * @since 3.8.0
326 */
327 public function firewall_clean_up_lockout(): void {
328 global $wpdb;
329
330 $current_timestamp = time();
331 $limit = $this->get_lockout_record_limit();
332 $table = $wpdb->base_prefix . ( new Lockout_Ip() )->get_table();
333
334 do {
335 $affected_rows = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
336 $wpdb->prepare(
337 "DELETE FROM {$table} WHERE (release_time = 0 OR release_time < %d) AND meta IN (%s, %s, %s, %s, %s) ORDER BY id LIMIT %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
338 $current_timestamp,
339 '[]',
340 '{"nf":[]}',
341 '{"login":[]}',
342 '{"nf":[],"login":[]}',
343 '{"login":[],"nf":[]}',
344 $limit
345 )
346 );
347
348 } while ( $affected_rows === $limit );
349 }
350
351 /**
352 * Gather IP(s) from headers.
353 *
354 * @since 4.4.2
355 * @deprecated 5.1.0 This method and the 'wpdef_firewall_gathered_ips' filter are no longer in use.
356 * @return array
357 */
358 public function gather_ips(): array {
359 $ip_headers = array(
360 'HTTP_CLIENT_IP',
361 'HTTP_X_REAL_IP',
362 'HTTP_X_FORWARDED_FOR',
363 'HTTP_X_FORWARDED',
364 'HTTP_X_CLUSTER_CLIENT_IP',
365 'HTTP_FORWARDED_FOR',
366 'HTTP_FORWARDED',
367 'HTTP_CF_CONNECTING_IP',
368 'REMOTE_ADDR',
369 );
370
371 $gathered_ips = array();
372 $server = defender_get_data_from_request( null, 's' );
373 foreach ( $ip_headers as $header ) {
374 if ( isset( $server[ $header ] ) && '' !== $server[ $header ] ) {
375 // Handle multiple IP addresses.
376 $ips = array_map( 'trim', explode( ',', $server[ $header ] ) );
377
378 foreach ( $ips as $ip ) {
379 if ( $this->validate_ip( $ip ) ) {
380 $gathered_ips[] = $ip;
381 }
382 }
383 }
384 }
385
386 /**
387 * Filter the gathered IPs before checking the lockout records.
388 *
389 * @param array $gathered_ips IPs gathered from request headers.
390 *
391 * @since 4.5.1
392 * @deprecated 5.1.0 No longer used and will be removed in a future version.
393 */
394 $gathered_ips = apply_filters_deprecated(
395 'wpdef_firewall_gathered_ips',
396 array_unique( $gathered_ips ),
397 '5.1.0',
398 '',
399 'This filter will be removed in a future version. If you are using it, update your implementation.'
400 );
401 $gathered_ips = ! is_array( $gathered_ips ) ? (array) $gathered_ips : $gathered_ips;
402
403 return $this->filter_user_ips( $gathered_ips );
404 }
405
406 /**
407 * Check if the current request is recognized as coming from Cloudflare.
408 *
409 * @return bool
410 */
411 public function is_cloudflare_request(): bool {
412 $is_cloudflare = true;
413
414 $server = defender_get_data_from_request( null, 's' );
415 if ( ! (
416 isset( $server['HTTP_CF_CONNECTING_IP'] ) ||
417 isset( $server['HTTP_CF_IPCOUNTRY'] ) ||
418 isset( $server['HTTP_CF_RAY'] ) ||
419 isset( $server['HTTP_CF_VISITOR'] )
420 ) ) {
421 $is_cloudflare = false;
422 }
423
424 return $is_cloudflare;
425 }
426
427 /**
428 * Auto-detect proxy server and switch to appropriate IP Detection option.
429 *
430 * @return void
431 */
432 public function auto_switch_ip_detection_option(): void {
433 $model = wd_di()->get( Model_Firewall::class );
434
435 if ( $this->is_cloudflare_request() ) {
436 if (
437 'HTTP_CF_CONNECTING_IP' !== $model->http_ip_header &&
438 ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
439 ) {
440 $model->http_ip_header = 'HTTP_CF_CONNECTING_IP';
441 update_site_option( self::IP_DETECTION_CF_SHOW_SLUG, true );
442 }
443
444 $model->trusted_proxy_preset = Cloudflare_Proxy::PROXY_SLUG;
445 $model->save();
446
447 // Fetch trusted proxy ips.
448 $this->update_trusted_proxy_preset_ips();
449 }
450 }
451
452 /**
453 * Update trusted proxy preset IPs.
454 *
455 * @return void
456 */
457 public function update_trusted_proxy_preset_ips(): void {
458 $model = wd_di()->get( Model_Firewall::class );
459 if ( '' !== $model->trusted_proxy_preset ) {
460 /**
461 * Retrieve Trusted_Proxy_Preset instance.
462 *
463 * @var Trusted_Proxy_Preset $trusted_proxy_preset
464 */
465 $trusted_proxy_preset = wd_di()->get( Trusted_Proxy_Preset::class );
466 $trusted_proxy_preset->set_proxy_preset( $model->trusted_proxy_preset );
467 $trusted_proxy_preset->update_ips();
468 }
469 }
470
471 /**
472 * Show a notice if wrong IP Detection option is configured for the site.
473 *
474 * @return void
475 */
476 public function maybe_show_misconfigured_ip_detection_option_notice(): void {
477 $model = wd_di()->get( Model_Firewall::class );
478 $xff = defender_get_data_from_request( 'HTTP_X_FORWARDED_FOR', 's' );
479
480 if (
481 'HTTP_X_FORWARDED_FOR' !== $model->http_ip_header &&
482 ( is_string( $xff ) && 0 < strlen( $xff ) ) &&
483 ! $this->is_cloudflare_request() &&
484 ! self::is_xff_notice_ready()
485 ) {
486 update_site_option( self::IP_DETECTION_XFF_SHOW_SLUG, true );
487 }
488 }
489
490 /**
491 * Check if a switched IP detection notice has been shown.
492 *
493 * @param string $key The notice key to check.
494 *
495 * @return bool True if the notice has been shown, false otherwise.
496 */
497 public static function is_switched_ip_detection_notice( string $key ): bool {
498 return (bool) get_site_option( $key );
499 }
500
501 /**
502 * Check if the XFF notice is ready to be shown.
503 *
504 * @return bool
505 */
506 public static function is_xff_notice_ready(): bool {
507 return ! Onboard::maybe_show_onboarding() &&
508 self::is_switched_ip_detection_notice( self::IP_DETECTION_XFF_SHOW_SLUG )
509 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_XFF_DISMISS_SLUG )
510 && ! wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled()
511 && 'HTTP_X_FORWARDED_FOR' !== wd_di()->get( Model_Firewall::class )->http_ip_header;
512 }
513
514 /**
515 * Check if the CF notice is ready to be shown.
516 *
517 * @return bool
518 */
519 public static function is_cf_notice_ready(): bool {
520 return self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
521 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_DISMISS_SLUG )
522 && ! wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled();
523 }
524
525 /**
526 * Delete all notice slugs related to IP detection switching.
527 */
528 public static function delete_slugs(): void {
529 delete_site_option( self::IP_DETECTION_CF_SHOW_SLUG );
530 delete_site_option( self::IP_DETECTION_CF_DISMISS_SLUG );
531 delete_site_option( self::IP_DETECTION_XFF_SHOW_SLUG );
532 delete_site_option( self::IP_DETECTION_XFF_DISMISS_SLUG );
533 }
534
535 /**
536 * Get the first blocked IP.
537 *
538 * @param array $ips The array of IPs to check.
539 *
540 * @return string
541 * @throws InvalidDatabaseException Thrown for unexpected data is found in DB.
542 */
543 public function get_blocked_ip( $ips ): string {
544 $blocked_ip = '';
545 foreach ( $ips as $ip ) {
546 $is_blocklisted = $this->is_blocklisted_ip( $ip );
547 if ( $is_blocklisted['result'] ) {
548 $blocked_ip = $ip;
549 break;
550 }
551 }
552 // Do not continue if there is not a single blocked IP.
553 if ( '' === $blocked_ip ) {
554 // Maybe IP(-s) in Active lockouts?
555 if ( count( $ips ) > 1 ) {
556 $models = Lockout_Ip::get_bulk( Lockout_Ip::STATUS_BLOCKED, $ips );
557 foreach ( $models as $model ) {
558 $blocked_ip = $model->ip;
559 break;
560 }
561 } elseif ( null !== Lockout_Ip::is_blocklisted_ip( $ips[0] ) ) {
562 $blocked_ip = $ips[0];
563 }
564 }
565
566 return $blocked_ip;
567 }
568
569 /**
570 * Get custom HTTP headers used for IP detection.
571 *
572 * @return array List of custom HTTP headers.
573 */
574 public static function custom_http_headers(): array {
575 return array(
576 'HTTP_X_FORWARDED_FOR',
577 'HTTP_X_REAL_IP',
578 'HTTP_CF_CONNECTING_IP',
579 );
580 }
581
582 /**
583 * Get trusted proxy presets.
584 *
585 * @return array List of trusted proxy presets.
586 */
587 public static function trusted_proxy_presets(): array {
588 return array(
589 Cloudflare_Proxy::PROXY_SLUG => esc_html__( 'Cloudflare', 'defender-security' ),
590 );
591 }
592
593 /**
594 * Dismiss the CF notice if the IP Detection is set to automatic.
595 */
596 public function maybe_dismiss_cf_notice(): void {
597 if (
598 wd_di()->get( Smart_Ip_Detection::class )->is_smart_ip_detection_enabled()
599 && self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_SHOW_SLUG )
600 && ! self::is_switched_ip_detection_notice( self::IP_DETECTION_CF_DISMISS_SLUG )
601 ) {
602 update_site_option( self::IP_DETECTION_CF_DISMISS_SLUG, true );
603 }
604 }
605
606 /**
607 * Is whitelist server public IP enabled.
608 *
609 * @return bool
610 * @since 5.0.2
611 */
612 public function is_whitelist_server_public_ip_enabled(): bool {
613 /**
614 * Filter to enable/disable fetching server public IP.
615 *
616 * @param bool $enable True to enable whitelist server public IP, false otherwise.
617 *
618 * @since 5.0.2
619 */
620 $enabled = apply_filters( 'wpdef_firewall_whitelist_server_public_ip_enabled', true );
621 return is_bool( $enabled ) ? $enabled : (bool) $enabled;
622 }
623
624 /**
625 * Set whitelist server public IP.
626 *
627 * @return bool
628 * @since 5.0.2
629 */
630 public function set_whitelist_server_public_ip(): bool {
631 if ( ! $this->is_whitelist_server_public_ip_enabled() ) {
632 return false;
633 }
634
635 $ip = wd_di()->get( Smart_Ip_Detection::class )->get_server_public_ip();
636 if ( '' === $ip ) {
637 $this->log( 'Failed to whitelist server public IP.', Firewall_Controller::FIREWALL_LOG );
638 return false;
639 }
640
641 $stored_ip = $this->get_whitelist_server_public_ip();
642 if ( $stored_ip !== $ip ) {
643 update_site_option( self::WHITELIST_SERVER_PUBLIC_IP_OPTION, $ip );
644 }
645
646 $this->log( 'Server public IP whitelisted successfully.', Firewall_Controller::FIREWALL_LOG );
647 return true;
648 }
649
650 /**
651 * Get whitelist server public IP.
652 *
653 * @return string
654 * @since 5.0.2
655 */
656 public function get_whitelist_server_public_ip(): string {
657 return $this->is_whitelist_server_public_ip_enabled() ?
658 get_site_option( self::WHITELIST_SERVER_PUBLIC_IP_OPTION, '' ) :
659 '';
660 }
661 }
662