/ signin.php
signin.php
1 <?php 2 /* 3 define('DEV_MODE', $_SERVER['REMOTE_ADDR'] === '51.159.28.165'); 4 5 if (DEV_MODE) { 6 ini_set('display_errors', 1); 7 error_reporting(E_ALL & ~E_NOTICE); 8 $mysql_suppress_err = false; 9 } 10 */ 11 require_once 'lib/db.php'; 12 require_once 'lib/userpwd.php'; 13 require_once 'lib/geoip2.php'; 14 require_once 'lib/ini.php'; 15 16 // --- 17 18 load_ini_file('captcha_config.ini'); 19 20 class App { 21 protected 22 // Routes 23 $actions = array( 24 'index', 25 'request', 26 'verify', 27 'signout' 28 ) 29 ; 30 31 const WEB_PATH = '/signin'; 32 33 const TPL_ROOT = 'views/'; 34 35 const TBL = 'email_signins'; 36 const TBL_QUEUE = 'email_signins_queue'; 37 const TBL_BLACKLIST = 'email_signins_blacklist'; 38 39 const PWD_DOMAIN = '4chan.org'; 40 41 const HMAC_KEY_PATH = '/www/keys/2024_key64.key'; 42 43 const CSRF_ARG = 'csrf'; 44 45 const 46 PWD_BYTES = 16, 47 TOKEN_BYTES = 16, 48 TOKEN_MAX_USAGES = 5, 49 VERIFY_TOKEN_TTL = 86400, // 24 hours 50 PRUNE_DAYS = 7, 51 MAX_CAPTCHA_FAILURES = 6, 52 MAX_CAPTCHA_FAILURES_SUSP = 3, 53 BOT_SCORE_SUSP = 80 54 ; 55 56 // Cooldowns 57 const 58 REQ_CD = 300, // 5 minutes, base minimum cooldown 59 REQ_CD_MAIL_PER_DAY = 3, // lockdown kicks in after this number of attempts 60 REQ_CD_IP_PER_HOUR = 2 // lockdown kicks in after this number of attempts 61 ; 62 63 const VERIFIED_LEVEL = 1; 64 65 const CAPTCHA_MODE = 2; // 1 for reCaptcha, 2 for hCaptcha 66 67 const 68 ERR_BAD_REQUEST = 'Bad Request.', 69 ERR_BAD_CAPTCHA = 'Invalid captcha.', 70 ERR_COOKIES = 'Cookies need to be enabled before continuing.', 71 ERR_GENERIC = 'Internal Server Error (%s)', 72 ERR_BAD_LINK = 'Invalid or expired link.', 73 ERR_RANGEBAN = 'This ISP has been blocked due to abuse.', 74 ERR_BAD_EMAIL = 'Invalid email address.', 75 ERR_BAD_EMAIL_DOMAIN = 'This email provider is not allowed.', 76 ERR_BAD_EMAIL_PLUS = 'Email subaddressing is not allowed', 77 ERR_CD = 'You have to wait %d more %s before attempting this again.', 78 ERR_EMAIL_QUEUED = 'You have to wait a while before attempting this again.', 79 ERR_PASS_USER = 'Email verification is not required for 4chan Pass users.', 80 ERR_DB = 'We are currently having database issues. Please try again later.' 81 ; 82 83 const 84 EVT_BAD_COUNTRY = 1, 85 EVT_MAX_USED = 2, 86 EVT_BLACKLISTED = 3, 87 EVT_IGNORED = 4 88 ; 89 90 const VERIFY_EMAIL_DOMAIN = true; 91 92 static $allowed_domains = [ 93 'gmail.com', 94 'hotmail.com', 95 'yahoo.com', 96 'proton.me', 97 'protonmail.com', 98 'outlook.com', 99 'live.com', 100 'icloud.com', 101 'yandex.com', 102 'tutanota.com', 103 'tutamail.com', 104 'tuta.io' 105 ]; 106 107 private function renderHTML($view) { 108 include(self::TPL_ROOT . $view . '.tpl.php'); 109 } 110 111 final protected function error($msg) { 112 $this->mode = 'error'; 113 $this->msg = $msg; 114 $this->renderHTML('signin'); 115 die(); 116 } 117 118 final protected function error_generic($code) { 119 $this->error(sprintf(self::ERR_GENERIC, $code)); 120 } 121 122 final protected function error_cooldown($units_left, $units = 'minute') { 123 if ($units_left > 1) { 124 $units .= 's'; 125 } 126 127 $this->error(sprintf(self::ERR_CD, $units_left, $units)); 128 } 129 130 private function log_event($event_id, $token) { 131 $sql = <<<SQL 132 INSERT INTO event_log(`type`, ip, arg_num, arg_str) 133 VALUES('signin_evt', '%s', '%d', '%s') 134 SQL; 135 136 return mysql_global_call($sql, $_SERVER['REMOTE_ADDR'], $event_id, $token); 137 } 138 139 private function get_csrf_token() { 140 return bin2hex(openssl_random_pseudo_bytes(8)); 141 } 142 143 private function validate_csrf() { 144 $arg = self::CSRF_ARG; 145 146 if (!isset($_COOKIE[$arg]) || !isset($_POST[$arg]) 147 || $_COOKIE[$arg] === '' || $_POST[$arg] === '') { 148 $this->error(self::ERR_COOKIES); 149 } 150 151 if ($_COOKIE[$arg] !== $_POST[$arg]) { 152 $this->error(self::ERR_COOKIES); 153 } 154 } 155 156 private function is_email_blacklisted($email) { 157 $tbl = self::TBL_BLACKLIST; 158 159 $sql = "SELECT 1 FROM `$tbl` WHERE email = '%s' LIMIT 1"; 160 161 $res = mysql_global_call($sql, $email); 162 163 if (!$res) { 164 return false; 165 } 166 167 return mysql_num_rows($res) === 1; 168 } 169 170 private function should_ignore_request($email) { 171 if (!preg_match('/@hotmail\.com/', $email)) { 172 return false; 173 } 174 175 if (!isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) || $_SERVER['HTTP_SEC_CH_UA_MODEL'] != 'SM-S928U') { 176 return false; 177 } 178 179 if ($this->get_bot_score() <= 70) { 180 return true; 181 } 182 183 return false; 184 } 185 186 private function get_bot_score() { 187 if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { 188 return 100; 189 } 190 191 return (int)$_SERVER['HTTP_X_BOT_SCORE']; 192 } 193 194 private function get_token_bot_score($token) { 195 $tbl = self::TBL; 196 197 $sql =<<<SQL 198 SELECT bot_score FROM `$tbl` WHERE token = '%s' 199 LIMIT 1 200 SQL; 201 202 $res = mysql_global_call($sql, $token); 203 204 if (!$res) { 205 return 100; 206 } 207 208 $score = mysql_fetch_row($res); 209 210 if (!$score) { 211 return 100; 212 } 213 214 $score = (int)$score[0]; 215 216 if ($score <= 0) { 217 return 100; 218 } 219 220 return $score; 221 } 222 223 private function update_usage_count($token) { 224 $tbl = self::TBL; 225 226 $max_usages = (int)self::TOKEN_MAX_USAGES; 227 228 if ($max_usages <= 0) { 229 return true; 230 } 231 232 $max_usages += 1; 233 234 $sql =<<<SQL 235 UPDATE `$tbl` SET used = LEAST(used + 1, $max_usages) 236 WHERE token = '%s' LIMIT 1 237 SQL; 238 239 return !!mysql_global_call($sql, $token); 240 } 241 242 private function validate_cooldowns($email, $hashed_email) { 243 $ip = $_SERVER['REMOTE_ADDR']; 244 $now = $_SERVER['REQUEST_TIME']; 245 246 if (!$ip || !$now || !$email || !$hashed_email) { 247 $this->error_generic('vf'); 248 } 249 250 $tbl = self::TBL; 251 $tbl_queue = self::TBL_QUEUE; 252 253 // --- 254 // Base cooldown for IP 255 // --- 256 $query =<<<SQL 257 SELECT UNIX_TIMESTAMP(created_on) FROM `$tbl` 258 WHERE ip = '%s' 259 ORDER BY created_on DESC 260 LIMIT 1 261 SQL; 262 263 $res = mysql_global_call($query, $ip); 264 265 if (!$res) { 266 $this->error(self::ERR_DB); 267 } 268 269 $last_ts = (int)mysql_fetch_row($res)[0]; 270 271 $delta = $now - $last_ts; 272 273 if ($delta < self::REQ_CD) { 274 $cd = ceil($delta / 60.0); 275 $this->error_cooldown($cd); 276 } 277 278 // --- 279 // Check if the email is already in the sending queue 280 // --- 281 $query = "SELECT 1 FROM `$tbl_queue` WHERE email = '%s' LIMIT 1"; 282 283 $res = mysql_global_call($query, $email); 284 285 if (!$res) { 286 $this->error(self::ERR_DB); 287 } 288 289 if (mysql_num_rows($res) > 0) { 290 $this->error(self::ERR_EMAIL_QUEUED); 291 } 292 293 // --- 294 // Check repeated requests for IP 295 // --- 296 $_ip_per_hour = (int)self::REQ_CD_IP_PER_HOUR; 297 $cd = 3600; 298 299 $query =<<<SQL 300 SELECT UNIX_TIMESTAMP(created_on) FROM `$tbl` 301 WHERE ip = '%s' 302 AND created_on > DATE_SUB(NOW(), INTERVAL 1 HOUR) 303 ORDER BY created_on ASC 304 LIMIT $_ip_per_hour 305 SQL; 306 307 $res = mysql_global_call($query, $ip); 308 309 if (!$res) { 310 $this->error(self::ERR_DB); 311 } 312 313 if (mysql_num_rows($res) == $_ip_per_hour) { 314 $last_ts = (int)mysql_fetch_row($res)[0]; 315 $cd = ceil(($last_ts - $now + $cd) / 60.0); 316 $this->error_cooldown($cd); 317 } 318 319 // --- 320 // Check repeated requests for email 321 // --- 322 $_email_per_day = (int)self::REQ_CD_MAIL_PER_DAY; 323 $cd = 86400; 324 325 $query =<<<SQL 326 SELECT UNIX_TIMESTAMP(created_on) FROM `$tbl` 327 WHERE hashed_email = '%s' 328 AND created_on > DATE_SUB(NOW(), INTERVAL 1 DAY) 329 ORDER BY created_on ASC 330 LIMIT $_email_per_day 331 SQL; 332 333 $res = mysql_global_call($query, $hashed_email); 334 335 if (!$res) { 336 $this->error(self::ERR_DB); 337 } 338 339 if (mysql_num_rows($res) == $_email_per_day) { 340 $last_ts = (int)mysql_fetch_row($res)[0]; 341 $cd = ($last_ts - $now + $cd) / 60.0; 342 343 if ($cd > 60) { 344 $cd = $cd / 60.0; 345 $units = 'hour'; 346 } 347 else { 348 $units = 'minute'; 349 } 350 351 $cd = ceil($cd); 352 353 $this->error_cooldown($cd, $units); 354 } 355 } 356 357 private function is_valid_captcha_t() { 358 require_once 'lib/captcha.php'; 359 360 $m = new Memcached(); 361 //$m->setOption(Memcached::OPT_TCP_NODELAY, true); 362 $m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1); 363 $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms 364 $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms 365 $m->addServer('localhost', 11211); 366 367 return is_twister_captcha_valid($m, $_SERVER['REMOTE_ADDR'], null, '!signin', 1, $_uc); 368 } 369 370 private function validate_captcha($force_recaptcha = false) { 371 if (!defined('RECAPTCHA_API_KEY_PRIVATE')) { 372 $this->error_generic('nck'); 373 } 374 375 if (!isset($_POST["g-recaptcha-response"])) { 376 $this->error(self::ERR_BAD_CAPTCHA); 377 } 378 379 $response = $_POST["g-recaptcha-response"]; 380 381 if (!$response || strlen($response) > 4096) { 382 $this->error(self::ERR_BAD_CAPTCHA); 383 } 384 385 if (self::CAPTCHA_MODE === 2 && !$force_recaptcha) { 386 $url = 'https://hcaptcha.com/siteverify'; 387 $captcha_private_key = HCAPTCHA_API_KEY_PRIVATE; 388 $captcha_public_key = HCAPTCHA_API_KEY_PUBLIC; 389 } 390 else { 391 $url = 'https://www.google.com/recaptcha/api/siteverify'; 392 $captcha_private_key = RECAPTCHA_API_KEY_PRIVATE; 393 $captcha_public_key = null; 394 } 395 396 $post = array( 397 'secret' => $captcha_private_key, 398 'response' => $response, 399 'remoteip' => $_SERVER['REMOTE_ADDR'] 400 ); 401 402 if ($captcha_public_key) { 403 $post['sitekey'] = $captcha_public_key; 404 } 405 406 $curl = curl_init(); 407 408 curl_setopt($curl, CURLOPT_URL, $url); 409 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 410 curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2); 411 curl_setopt($curl, CURLOPT_TIMEOUT, 4); 412 curl_setopt($curl, CURLOPT_USERAGENT, '4chan'); 413 curl_setopt($curl, CURLOPT_POSTFIELDS, $post); 414 415 $resp = curl_exec($curl); 416 417 if ($resp === false) { 418 curl_close($curl); 419 $this->error_generic('cne0'); 420 } 421 422 $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); 423 424 if ($resp_status >= 300) { 425 curl_close($curl); 426 $this->error_generic('cne1'); 427 } 428 429 curl_close($curl); 430 431 $json = json_decode($resp, true); 432 433 // BAD 434 if (json_last_error() !== JSON_ERROR_NONE) { 435 $this->error(self::ERR_BAD_CAPTCHA); 436 } 437 438 // GOOD 439 if ($json && isset($json['success']) && $json['success']) { 440 return true; 441 } 442 443 // BAD 444 $this->error(self::ERR_BAD_CAPTCHA); 445 } 446 447 private function get_random_hex_bytes($length = 64) { 448 $data = openssl_random_pseudo_bytes($length); 449 450 if (!$data) { 451 return false; 452 } 453 454 return bin2hex($data); 455 } 456 457 private function generate_token() { 458 return $this->get_random_hex_bytes(self::TOKEN_BYTES); 459 } 460 461 private function set_ev1_cookie($flag = true) { 462 $cookie_name = '_ev1'; 463 464 if ($flag) { 465 setcookie($cookie_name, '1', $_SERVER['REQUEST_TIME'] + 60, '/', '.' . self::PWD_DOMAIN, true, false); 466 } 467 else { 468 setcookie($cookie_name, '', -1, '/', '.' . self::PWD_DOMAIN, true, false); 469 } 470 } 471 472 private function prune_old_requests() { 473 $tbl = self::TBL; 474 $ttl = (int)self::PRUNE_DAYS; 475 $sql = "DELETE FROM `$tbl` WHERE created_on <= DATE_SUB(NOW(), INTERVAL $ttl DAY)"; 476 return mysql_global_call($sql); 477 } 478 479 private function validate_email($email) { 480 if (!preg_match('/^[^@]+@[^@]+\.[a-z]+$/', $email)) { 481 $this->error(self::ERR_BAD_EMAIL); 482 } 483 484 if (strpos($email, '+') !== false) { 485 $this->error(self::ERR_BAD_EMAIL_PLUS); 486 } 487 488 if (self::VERIFY_EMAIL_DOMAIN) { 489 $flag = false; 490 491 $domain = explode('@', $email)[1]; 492 493 if (!$domain) { 494 $this->error(self::ERR_BAD_EMAIL); 495 } 496 497 foreach (self::$allowed_domains as $allowed_domain) { 498 if ($allowed_domain === $domain) { 499 $flag = true; 500 break; 501 } 502 } 503 504 if (!$flag) { 505 $this->error(self::ERR_BAD_EMAIL_DOMAIN); 506 } 507 } 508 509 return true; 510 } 511 512 private function is_pwd_banned($pwd) { 513 if (!$pwd) { 514 return false; 515 } 516 517 $sql =<<<SQL 518 SELECT 1 FROM banned_users 519 WHERE active = 1 AND password = '%s' AND length > NOW() 520 LIMIT 1 521 SQL; 522 523 $res = mysql_global_call($sql, $pwd); 524 525 if (!$res) { 526 return false; 527 } 528 529 return (int)mysql_num_rows($res) > 0; 530 } 531 532 private function validate_rangeban() { 533 $long_ip = ip2long($_SERVER['REMOTE_ADDR']); 534 535 if (!$long_ip) { 536 $this->error_generic('vri'); 537 } 538 539 $asn = 0; 540 541 if (isset($_SERVER['HTTP_X_GEO_ASN'])) { 542 $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; 543 } 544 else { 545 $_asninfo = GeoIP2::get_asn($ip); 546 547 if ($_asninfo) { 548 $asn = (int)$_asninfo['asn']; 549 } 550 } 551 552 $now = (int)$_SERVER['REQUEST_TIME']; 553 554 $perma_clause =<<<SQL 555 expires_on = 0 AND boards = '' AND ops_only = 0 AND img_only = 0 556 AND lenient = 0 AND report_only = 0 AND ua_ids = '' 557 SQL; 558 559 $query = <<<SQL 560 (SELECT SQL_NO_CACHE 1 FROM iprangebans 561 WHERE range_start <= $long_ip AND range_end >= $long_ip AND active = 1 562 AND $perma_clause) 563 SQL; 564 565 if ($asn > 0) { 566 $query .= <<<SQL 567 UNION (SELECT 1 FROM iprangebans 568 WHERE asn = $asn AND active = 1 AND $perma_clause) 569 SQL; 570 } 571 572 $res = mysql_global_call($query); 573 574 if (!$res) { 575 $this->error(self::ERR_DB); 576 } 577 578 if ((int)mysql_num_rows($res) > 0) { 579 $this->error(self::ERR_RANGEBAN); 580 } 581 } 582 583 private function get_country() { 584 static $country = null; 585 586 if ($country !== null) { 587 return $country; 588 } 589 590 if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) { 591 $country = $_SERVER['HTTP_X_GEO_COUNTRY']; 592 } 593 else { 594 $geo_data = GeoIP2::get_country($_SERVER['REMOTE_ADDR']); 595 596 if ($geo_data && isset($geo_data['country_code'])) { 597 $country = $geo_data['country_code']; 598 } 599 else { 600 $country = 'XX'; 601 } 602 } 603 604 return $country; 605 } 606 607 private function get_user_agent() { 608 $ua = $_SERVER['HTTP_USER_AGENT']; 609 610 if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && $_SERVER['HTTP_SEC_CH_UA_MODEL'] && $_SERVER['HTTP_SEC_CH_UA_MODEL'] != '""') { 611 $model = $_SERVER['HTTP_SEC_CH_UA_MODEL']; 612 $ua .= " ~[$model]"; 613 } 614 615 return $ua; 616 } 617 618 private function normalize_gmail_address($email) { 619 list($user, $domain) = explode('@', $email); 620 $user = preg_replace('/\./', '', $user); 621 return "$user@$domain"; 622 } 623 624 private function hash_email($email) { 625 $hmac_key = file_get_contents(self::HMAC_KEY_PATH); 626 627 if (!$hmac_key) { 628 return false; 629 } 630 631 $hashed_email = substr(hash_hmac('sha256', $email, $hmac_key, true), 0, self::PWD_BYTES); 632 633 return bin2hex($hashed_email); 634 } 635 636 private function get_domain($email) { 637 $parts = explode('@', $email); 638 639 if (count($parts) !== 2) { 640 return ''; 641 } 642 643 return $parts[1]; 644 } 645 646 private function create_request($email, $hashed_email) { 647 $tbl = self::TBL; 648 $tbl_queue = self::TBL_QUEUE; 649 650 $token = $this->generate_token(); 651 652 if (!$token) { 653 $this->error_generic('gt'); 654 } 655 656 if (!$email || !$hashed_email) { 657 $this->error_generic('crne'); 658 } 659 660 $ip = $_SERVER['REMOTE_ADDR']; 661 662 $ua = $this->get_user_agent(); 663 664 $country = $this->get_country(); 665 666 if ($country === 'T1') { 667 $this->error(self::ERR_RANGEBAN); 668 } 669 670 $domain = $this->get_domain($email); 671 672 if (isset($_SERVER['HTTP_X_BOT_SCORE'])) { 673 $bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE']; 674 } 675 else { 676 $bot_score = 0; 677 } 678 679 // Insert the request 680 $sql =<<<SQL 681 INSERT INTO `$tbl` (token, hashed_email, ip, domain, ua, country, bot_score) 682 VALUES ('%s', '%s', '%s', '%s', '%s', '%s', %d) 683 SQL; 684 685 $res = mysql_global_call($sql, $token, $hashed_email, $ip, $domain, $ua, $country, $bot_score); 686 687 if (!$res) { 688 $this->error_generic('cr2'); 689 } 690 691 // Add the email to the mailer job queue 692 $sql =<<<SQL 693 INSERT INTO `$tbl_queue` (email, token) 694 VALUES ('%s', '%s') 695 SQL; 696 697 $res = mysql_global_call($sql, $email, $token); 698 699 if (!$res) { 700 $this->error_generic('crq'); 701 } 702 703 return $token; 704 } 705 706 private function register_captcha_failure($token) { 707 $tbl = self::TBL; 708 709 if (!$token) { 710 return false; 711 } 712 713 $sql = "UPDATE `$tbl` SET failed_challenges = failed_challenges + 1 WHERE token = '%s' LIMIT 1"; 714 715 mysql_global_call($sql, $token); 716 } 717 718 private function get_captcha_failures($token) { 719 $tbl = self::TBL; 720 721 if (!$token) { 722 return 0; 723 } 724 725 $sql = "SELECT failed_challenges FROM `$tbl` WHERE token = '%s' LIMIT 1"; 726 727 $res = mysql_global_call($sql, $token); 728 729 if (!$res) { 730 return 0; 731 } 732 733 return (int)mysql_fetch_row($res)[0]; 734 } 735 736 public function request() { 737 $this->mode = 'request'; 738 739 $this->validate_csrf(); 740 741 if (!isset($_POST['email']) || !$_POST['email']) { 742 $this->error(self::ERR_BAD_EMAIL); 743 } 744 745 $email = strtolower(trim($_POST['email'])); 746 747 $this->validate_email($email); 748 749 if ($this->get_domain($email) == 'gmail.com') { 750 $clean_email = $this->normalize_gmail_address($email); 751 } 752 else { 753 $clean_email = $email; 754 } 755 756 $hashed_email = $this->hash_email($clean_email); 757 758 if (!$hashed_email) { 759 $this->error_generic('nhe'); 760 } 761 762 $this->validate_cooldowns($email, $hashed_email); 763 764 $this->validate_captcha(); 765 766 $this->validate_rangeban(); 767 768 if (isset($_COOKIE['4chan_pass'])) { 769 $pwd = UserPwd::decodePwd($_COOKIE['4chan_pass']); 770 771 // Password has a ban, show fake success screen 772 if ($pwd && $this->is_pwd_banned($pwd)) { 773 return $this->renderHTML('signin'); 774 } 775 } 776 777 // Email is blacklisted, show fake success screen 778 if ($this->is_email_blacklisted($email)) { 779 $this->log_event(self::EVT_BLACKLISTED, $email); 780 return $this->renderHTML('signin'); 781 } 782 783 // Try to ignore bot requests 784 if ($this->should_ignore_request($email)) { 785 $this->log_event(self::EVT_IGNORED, $email); 786 return $this->renderHTML('signin'); 787 } 788 789 $token = $this->create_request($email, $hashed_email); 790 791 $this->renderHTML('signin'); 792 } 793 794 private function pre_verify() { 795 if (!isset($_GET['tkn'])) { 796 $this->error(self::ERR_BAD_REQUEST); 797 } 798 799 $this->token = trim($_GET['tkn']); 800 801 if (!$this->token) { 802 $this->error(self::ERR_BAD_REQUEST); 803 } 804 805 $_token_bot_score = $this->get_token_bot_score($this->token); 806 807 if (true && $_token_bot_score < 90) { 808 $this->use_recaptcha = true; 809 } 810 else { 811 $this->use_recaptcha = false; 812 } 813 814 $this->csrf_token = $this->get_csrf_token(); 815 816 setcookie(self::CSRF_ARG, $this->csrf_token, 0, '/', $_SERVER['HTTP_HOST'], true, true); 817 818 $this->renderHTML('signin'); 819 } 820 821 public function verify() { 822 if ($_SERVER['REQUEST_METHOD'] == 'GET') { 823 $this->mode = 'verify'; 824 return $this->pre_verify(); 825 } 826 else { 827 $this->mode = 'verify-done'; 828 } 829 830 $this->validate_csrf(); 831 832 if (!isset($_POST['tkn'])) { 833 $this->error(self::ERR_BAD_REQUEST); 834 } 835 836 $token = trim($_POST['tkn']); 837 838 if (!$token) { 839 $this->error(self::ERR_BAD_REQUEST); 840 } 841 842 if (isset($_COOKIE['pass_enabled']) && $_COOKIE['pass_enabled']) { 843 $this->error(self::ERR_PASS_USER); 844 } 845 846 $_bot_score = $this->get_bot_score(); 847 $_token_bot_score = $this->get_token_bot_score($token); 848 849 if ($_bot_score < self::BOT_SCORE_SUSP || $_token_bot_score < self::BOT_SCORE_SUSP) { 850 $_max_fails = self::MAX_CAPTCHA_FAILURES_SUSP; 851 } 852 else { 853 $_max_fails = self::MAX_CAPTCHA_FAILURES; 854 } 855 856 if (true && $_token_bot_score < 90) { 857 $this->use_recaptcha = true; 858 } 859 else { 860 $this->use_recaptcha = false; 861 } 862 863 if ($this->get_captcha_failures($token) >= $_max_fails) { 864 $this->error(self::ERR_BAD_LINK); 865 } 866 867 if ($this->use_recaptcha == false) { 868 if ($this->is_valid_captcha_t() !== true) { 869 $this->register_captcha_failure($token); 870 $this->mode = 'verify-captcha-failed'; 871 $this->token = $token; 872 $this->renderHTML('signin'); 873 die(); 874 } 875 } 876 else { 877 $this->validate_captcha(true); 878 } 879 880 $this->prune_old_requests(); 881 882 $this->update_usage_count($token); 883 884 $tbl = self::TBL; 885 886 $ttl = (int)self::VERIFY_TOKEN_TTL; 887 888 $sql =<<<SQL 889 SELECT * FROM `$tbl` WHERE token = '%s' 890 AND created_on > DATE_SUB(NOW(), INTERVAL $ttl SECOND) 891 SQL; 892 893 $res = mysql_global_call($sql, $token); 894 895 if (!$res) { 896 $this->error(self::ERR_DB); 897 } 898 899 $request = mysql_fetch_assoc($res); 900 901 if (!$request) { 902 $this->error(self::ERR_BAD_LINK); 903 } 904 905 if (!$request['hashed_email']) { 906 $this->error_generic('hee'); 907 } 908 909 // Validate usage count 910 if ((int)$request['used'] > self::TOKEN_MAX_USAGES) { 911 $this->log_event(self::EVT_MAX_USED, $request['token']); 912 $this->error(self::ERR_BAD_LINK); 913 } 914 915 // Country must match the requester's country 916 $country = $this->get_country(); 917 918 if ($country !== $request['country']) { 919 $this->log_event(self::EVT_BAD_COUNTRY, $request['token']); 920 $this->error(self::ERR_BAD_LINK); 921 } 922 923 $ip = $_SERVER['REMOTE_ADDR']; 924 925 $userpwd = null; 926 927 if (isset($_COOKIE['4chan_pass'])) { 928 $userpwd = new UserPwd($ip, self::PWD_DOMAIN, $_COOKIE['4chan_pass']); 929 930 if (!$userpwd) { 931 $this->error_generic('nup'); 932 } 933 934 // Password has a ban, show fake success screen 935 if ($this->is_pwd_banned($userpwd->getPwd())) { 936 return $this->renderHTML('signin'); 937 } 938 939 // If already verified, make a brand new pwd 940 if ($userpwd->verifiedLevel() > 0) { 941 $userpwd = null; 942 } 943 } 944 945 if (!$userpwd) { 946 $userpwd = new UserPwd($ip, self::PWD_DOMAIN); 947 948 if (!$userpwd) { 949 $this->error_generic('nupn'); 950 } 951 } 952 953 $userpwd->setPwd($request['hashed_email']); 954 $userpwd->setVerifiedLevel(self::VERIFIED_LEVEL); 955 956 $userpwd->setCookie('.' . self::PWD_DOMAIN); 957 958 // This is to let currently running cooldowns that the email was verified 959 $this->set_ev1_cookie(true); 960 961 $this->renderHTML('signin'); 962 } 963 964 /** 965 * Signout - deletes the password cookie 966 */ 967 public function signout() { 968 $this->mode = 'signout'; 969 970 $this->validate_csrf(); 971 972 $userpwd = null; 973 974 if (isset($_COOKIE['4chan_pass'])) { 975 $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], self::PWD_DOMAIN, $_COOKIE['4chan_pass']); 976 } 977 978 // If already verified, make a brand new pwd 979 if ($userpwd && $userpwd->verifiedLevel() > 0) { 980 setcookie(UserPwd::COOKIE_NAME, '', -1, '/', '.' . self::PWD_DOMAIN, true, true); 981 } 982 983 $this->set_ev1_cookie(false); 984 985 $this->renderHTML('signin'); 986 } 987 988 /** 989 * Index 990 */ 991 public function index() { 992 $this->mode = 'index'; 993 994 if (isset($_COOKIE['pass_enabled']) && $_COOKIE['pass_enabled']) { 995 $this->pass_user = true; 996 return $this->renderHTML('signin'); 997 } 998 999 $this->pass_user = false; 1000 1001 $this->csrf_token = $this->get_csrf_token(); 1002 1003 $domain = $_SERVER['HTTP_HOST']; 1004 1005 $userpwd = null; 1006 1007 if (isset($_COOKIE['4chan_pass'])) { 1008 $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], self::PWD_DOMAIN, $_COOKIE['4chan_pass']); 1009 } 1010 1011 $this->authed = $userpwd && $userpwd->verifiedLevel() > 0; 1012 1013 setcookie(self::CSRF_ARG, $this->csrf_token, 0, '/', $domain, true, true); 1014 1015 $this->renderHTML('signin'); 1016 } 1017 1018 /** 1019 * Main 1020 */ 1021 public function run() { 1022 $method = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET; 1023 1024 if (isset($method['action'])) { 1025 $action = $method['action']; 1026 } 1027 else { 1028 $action = 'index'; 1029 } 1030 1031 if (in_array($action, $this->actions)) { 1032 $this->$action(); 1033 } 1034 else { 1035 $this->error('Bad request'); 1036 } 1037 } 1038 } 1039 1040 $ctrl = new App(); 1041 $ctrl->run();