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