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