/ 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();