/ captcha-test.php
captcha-test.php
   1  <?php
   2  
   3  define('DEV_MODE', $_SERVER['REMOTE_ADDR'] === '195.154.113.119');
   4  //define('DEV_MODE', false);
   5  
   6  if (!DEV_MODE) {
   7    http_response_code(404);
   8    echo('File not found.');
   9    die();
  10  }
  11  else {
  12    ini_set('display_errors', 1);
  13    error_reporting(E_ALL & ~E_NOTICE);
  14  }
  15  
  16  require_once 'lib/userpwd-test.php';
  17  require_once 'lib/twister_captcha-test.php';
  18  
  19  define('TWISTER_FONT_PATH', '/usr/local/share/captcha/PCss_8x16_LE.gdf');
  20  
  21  define('TWISTER_USE_TICKET_CAPTCHA', true);
  22  define('TWISTER_HCAPTCHA_SITEKEY', '49d294fa-f15c-41fc-80ba-c2544c52ec2a');
  23  
  24  // Validity period for captcha in seconds
  25  define('TWISTER_TTL', 120);
  26  // Delay in seconds before requesting a new captcha
  27  define('TWISTER_COOLDOWN', 0);
  28  // Long cooldown for unsolved captchas
  29  define('TWISTER_COOLDOWN_LONG', 0);
  30  // Duration of unsolved sessions
  31  define('TWISTER_TTL_UNSOLVED', 600);
  32  // Pre-cooldowns
  33  define('TWISTER_HMAC_SECRET', 'y474c22ugpVtJD5gveDxcmMcL7DQFP+/w1FhVP9CYVM=');
  34  define('TWISTER_TICKET_TTL', 3600); // 60 minutes
  35  define('TWISTER_PRE_CD_THREAD', 300); // 5 minutes
  36  define('TWISTER_PRE_CD_REPLY', 60);
  37  define('TWISTER_PRE_CD_REPORT', 60);
  38  
  39  define('TWISTER_PRE_CD_PENALITY', 60);
  40  
  41  // Default number of characters for captchas
  42  define('TWISTER_CHARS', 5);
  43  define('TWISTER_CHARS_MIN', 4);
  44  define('TWISTER_CHARS_MAX', 6);
  45  
  46  // Userpwd reputation checks:
  47  // Number of posts to get static captchas
  48  define('TWISTER_PWD_STATIC_POSTS', 5);
  49  // Known time in minutes to get static captchas
  50  define('TWISTER_PWD_STATIC_KNOWN', 4320); // 3 days
  51  // Idle time above which slider captchas will be served
  52  define('TWISTER_PWD_SLIDER_IDLE', 86400); // 24 hours
  53  
  54  // Whether or not to check for bypassing credits
  55  define('TWISTER_ALLOW_NOOP', true);
  56  // Number of posts to be eligible for captcha bypassing
  57  define('TWISTER_PWD_NOOP_POSTS', 5);
  58  // Known time in minutes to be eligible for captcha bypassing
  59  define('TWISTER_PWD_NOOP_KNOWN', 4320); // 3 days
  60  
  61  // List of boards where bypassing credits are disabled.
  62  // Corresponds to the CAPTCHA_ALLOW_BYPASS board setting.
  63  const TWISTER_NO_NOOP_BOARDS = [
  64    'bant', 'pol', 'trash'
  65  ];
  66  
  67  // Covers 272814 (84.81 %) unique IPs and 10863 (79.15 %) unique bans
  68  // IPs %: 0.05 | Bans %: 0 | GR1 all: 0 | Any EU: false
  69  const TWISTER_WHITELIST_COUNTRIES = [
  70    'US', 'CA', 'GB', 'DE', 'AU', 'PL', 'IT', 'FR', 'FI', 'SE', 'ID', 'ES', 'NL',
  71    'PH', 'JP', 'MY', 'NO', 'IE', 'VN', 'NZ', 'HR', 'HU', 'AT', 'SG', 'PT', 'BE',
  72    'GR', 'DK', 'RS', 'CZ', 'TH', 'BG', 'CH', 'EE', 'LT', 'SK', 'SI', 'LV', 'TW',
  73    'IS',
  74  ];
  75  
  76  // Covers 73407 (29.95 %) unique IPs and 2501 (16.7 %) unique bans
  77  // IPs %: 2 | Bans %: 0 | GR1 all: 0 | Any EU: false
  78  const TWISTER_WHITELIST_ASNS = [
  79    21928, 6167,
  80  ];
  81  
  82  // Mobile ISPs
  83  const TWISTER_MOBILE_ASNS = [
  84    21928,6167,20057,26599,35228,206067,4775,22085,8359,10139,30689,52876,27895,
  85    31615,11315,28403,6614,25135,10030,45143,4230,20365,6306,12929,21450,264731,
  86    203995,29465,38466,4818,15480,13280,13335,132618,25106,4657,10631,29247,12716,
  87    262210,138384,8953,9146,31213,2497,5578,21575,28036,28469,132061,29975
  88  ];
  89  
  90  define('TWISTER_BAD_UA', '/headless|node-fetch|python-|java\/|jakarta|-perl|http-?client|-resty-|awesomium\//i');
  91  
  92  // Memcached server
  93  define('TWISTER_MEMCACHED_HOST', '127.0.0.1');
  94  define('TWISTER_MEMCACHED_PORT', 11211);
  95  
  96  // ---
  97  
  98  define('TWISTER_DOMAIN', ($_SERVER['HTTP_HOST'] === 'sys.4chan.org') ? '4chan.org' : '4channel.org');
  99  
 100  define('TWISTER_ERR_GENERIC', 'Internal Server Error');
 101  define('TWISTER_ERR_COOLDOWN', 'You have to wait a while before doing this again');
 102  define('TWISTER_ERR_PCD_THREAD', 'Please wait a while before making a thread');
 103  define('TWISTER_ERR_PCD_REPLY', 'Please wait a while before making a post');
 104  define('TWISTER_ERR_PCD_SIGNIN', 'Please wait for the timer<br>or verify your email address before making a post.<br><br><a href="https://sys.4chan.org/signin">Click here</a> for more information<br>or to verify your email.');
 105  
 106  // ---
 107  
 108  function twister_captcha_output_data($data) {
 109    if (isset($_GET['framed'])) {
 110      twister_captcha_output_html($data);
 111    }
 112    else {
 113      header('Content-Type: application/json');
 114      echo json_encode($data);
 115    }
 116  }
 117  
 118  function twister_captcha_output_html($data) {
 119    header('Content-Security-Policy: frame-ancestors https://*.' . TWISTER_DOMAIN . ';');
 120    $now = $_SERVER['REQUEST_TIME'];
 121  ?><!DOCTYPE html><html><head><meta charset="utf-8"><title></title>
 122  <script>window.parent.postMessage(<?php echo json_encode(['twister' => $data]) ?>, '*');</script>
 123  <script>document.cookie = `_tcs=${0|(Date.now()/1000)}.${new window.Intl.DateTimeFormat().resolvedOptions().timeZone}.<?php echo $now ?>.${window.eval.toString().length}; path=/; domain=sys.<?php echo TWISTER_DOMAIN ?>`;</script>
 124  </head><body></body></html><?php
 125  }
 126  
 127  function twister_captcha_output_dummy() {
 128  ?><!DOCTYPE html><html><head><meta charset="utf-8">
 129  <meta name="viewport" content="width=device-width, initial-scale=1.0"><title></title></head><body>
 130  <h3 style="text-align:center">You can now close this page and try getting a captcha again.<h3>
 131  </body></html><?php
 132  }
 133  
 134  function twister_captcha_output_ticket_captcha() {
 135    twister_captcha_output_data([ 'mpcd' => true, 'ticket' => false ]);
 136  }
 137  
 138  function twister_captcha_get_ticket_captcha_response() {
 139    if (isset($_GET['ticket_resp'])) {
 140      return $_GET['ticket_resp'];
 141    }
 142    
 143    return false;
 144  }
 145  
 146  function twister_captcha_get_hcaptcha_private_key() {
 147    $path = '/www/global/yotsuba/config/captcha_config.ini';
 148    
 149    $cfg = file_get_contents($path);
 150    
 151    if (!$cfg) {
 152      return false;
 153    }
 154    
 155    $res = preg_match('/^HCAPTCHA_API_KEY_PRIVATE ?= ?([^\s]+)$/m', $cfg, $m);
 156    
 157    if (!$res || empty($m) || !$m[1]) {
 158      return false;
 159    }
 160    
 161    return $m[1];
 162  }
 163  
 164  function twister_captcha_verify_ticket_captcha() {
 165    $response = twister_captcha_get_ticket_captcha_response();
 166    
 167    if (!$response || strlen($response) > 4096) {
 168      return false;
 169    }
 170    
 171    $captcha_private_key = twister_captcha_get_hcaptcha_private_key();
 172    
 173    if (!$captcha_private_key) {
 174      // Don't block in case of misconfiguration on our end
 175      return true;
 176    }
 177    
 178    $url = 'https://hcaptcha.com/siteverify';
 179    
 180    $post = array(
 181      'secret' => $captcha_private_key,
 182      'response' => $response,
 183      'remoteip' => $_SERVER['REMOTE_ADDR'],
 184      'sitekey' => TWISTER_HCAPTCHA_SITEKEY,
 185    );
 186    
 187    $curl = curl_init();
 188    
 189    curl_setopt($curl, CURLOPT_URL, $url);
 190    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
 191    curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2);
 192    curl_setopt($curl, CURLOPT_TIMEOUT, 4);
 193    curl_setopt($curl, CURLOPT_USERAGENT, '4chan');
 194    curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
 195    
 196    $resp = curl_exec($curl);
 197    
 198    if ($resp === false) {
 199      curl_close($curl);
 200      return false;
 201    }
 202    
 203    $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
 204    
 205    if ($resp_status >= 300) {
 206      curl_close($curl);
 207      return false;
 208    }
 209    
 210    curl_close($curl);
 211    
 212    $json = json_decode($resp, true);
 213    
 214    // BAD
 215    if (json_last_error() !== JSON_ERROR_NONE) {
 216      return false;
 217    }
 218    
 219    // GOOD
 220    if ($json && isset($json['success']) && $json['success']) {
 221      return true;
 222    }
 223    
 224    // BAD
 225    return false;
 226  }
 227  
 228  function twister_captcha_process_ticket_captcha($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) {
 229    // Captcha response was sent, verify it
 230    if (twister_captcha_get_ticket_captcha_response()) {
 231      if (twister_captcha_verify_ticket_captcha() === true) {
 232        // Captcha is valid, return a new ticket
 233        twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg, $bypassable);
 234      }
 235      else {
 236        // Wrong captcha or captcha malfunction
 237        twister_captcha_error(TWISTER_ERR_GENERIC . ' (tcr0)');
 238      }
 239    }
 240    // No captcha reponse provided, tell the frontend to show a ticket captcha
 241    else {
 242      twister_captcha_output_ticket_captcha();
 243    }
 244  }
 245  
 246  function twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) {
 247    $ticket = twister_captcha_generate_ticket($now, $long_ip, $board);
 248    
 249    if (!$ticket) {
 250      twister_captcha_error(TWISTER_ERR_GENERIC . ' (gt1)');
 251    }
 252    
 253    twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable);
 254  }
 255  
 256  function twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable) {
 257    $data = [];
 258    
 259    if ($ticket) {
 260      $data['ticket'] = $ticket;
 261    }
 262    
 263    $data['pcd'] = $pcd;
 264    
 265    if ($msg) {
 266      $data['pcd_msg'] = $msg;
 267    }
 268    
 269    if ($bypassable) {
 270      $data['bpcd'] = true;
 271    }
 272    
 273    twister_captcha_output_data($data);
 274  }
 275  
 276  function twister_captcha_error($msg, $extra = null) {
 277    //http_response_code(500);
 278    $data = [ 'error' => $msg ];
 279    
 280    if ($extra) {
 281      $data = array_merge($data, $extra);
 282    }
 283    
 284    twister_captcha_output_data($data);
 285    
 286    die();
 287  }
 288  
 289  function twister_captcha_is_req_suspicious() {
 290    /*
 291    if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) {
 292      if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) {
 293        return true;
 294      }
 295    }
 296    */
 297    $no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false;
 298    $no_accept = isset($_SERVER['HTTP_ACCEPT']) === false;
 299    
 300    if ($no_lang && $no_accept) {
 301      return true;
 302    }
 303    
 304    if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) {
 305      return true;
 306    }
 307    
 308    if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) {
 309      return true;
 310    }
 311    
 312    if ($no_lang && isset($_SERVER['HTTP_REFERER'])) {
 313      $ref = $_SERVER['HTTP_REFERER'];
 314      
 315      if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) {
 316        return true;
 317      }
 318    }
 319    
 320    return false;
 321  }
 322  
 323  function twister_captcha_need_hcaptcha() {
 324    if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) {
 325      return true;
 326    }
 327    
 328    $ua = $_SERVER['HTTP_USER_AGENT'];
 329    
 330    $score = (int)$_SERVER['HTTP_X_BOT_SCORE'];
 331    
 332    // Skip Android Webviews
 333    if ($score == 1 && strpos($ua, '; wv)') !== false) {
 334      return false;
 335    }
 336    
 337    return $score < 99;
 338  }
 339  
 340  function twister_captcha_check_likely_automated($memcached, $now, $threshold = 29) {
 341    if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) {
 342      return false;
 343    }
 344    
 345    $ua = $_SERVER['HTTP_USER_AGENT'];
 346    
 347    // Skip Android Webviews
 348    if (strpos($ua, '; wv)') !== false) {
 349      return false;
 350    }
 351    
 352    // Skip iPhone Webviews
 353    if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) {
 354      return false;
 355    }
 356    
 357    $score = (int)$_SERVER['HTTP_X_BOT_SCORE'];
 358    
 359    if ($score > 1 && $score <= $threshold) {
 360      $key = 'bmbot' . $_SERVER['REMOTE_ADDR'];
 361      $memcached->set($key, 1, $now + 43200);
 362      return true;
 363    }
 364    
 365    return false;
 366  }
 367  
 368  function twister_captcha_get_pcd_penalty($board, $thread_id) {
 369    $count = 0;
 370    
 371    return 0; // FIXME
 372    
 373    // Reports
 374    if ($thread_id === 1 && $board !== '!') {
 375      return 0;
 376    }
 377    
 378    if (isset($_SERVER['HTTP_X_GEO_ASN'])) {
 379      $asn = (int)$_SERVER['HTTP_X_GEO_ASN'];
 380    }
 381    else {
 382      $asn = 0;
 383    }
 384    
 385    if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) {
 386      $country = $_SERVER['HTTP_X_GEO_COUNTRY'];
 387    }
 388    else {
 389      $country = null;
 390    }
 391    
 392    // Mobile clients
 393    if ($asn > 0 && in_array($asn, TWISTER_MOBILE_ASNS)) {
 394      $count++;
 395    }
 396    else if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Mobile|iPhone|; wv/', $_SERVER['HTTP_USER_AGENT'])) {
 397      $count++;
 398    }
 399    
 400    // Rare countries
 401    if ($country && !in_array($country, TWISTER_WHITELIST_COUNTRIES)) {
 402      $count++;
 403    }
 404    
 405    // Rare ISPs
 406    if ($asn > 0 && !in_array($asn, TWISTER_WHITELIST_ASNS)) {
 407      $count++;
 408    }
 409    
 410    return $count * TWISTER_PRE_CD_PENALITY;
 411  }
 412  
 413  function twister_captcha_store_challenge($memcached, $challenge_uid, $long_ip, $expiration) {
 414    if (!$challenge_uid || !$long_ip || !$expiration) {
 415      return false;
 416    }
 417    
 418    return $memcached->set("ch$long_ip", $challenge_uid, $expiration);
 419  }
 420  
 421  // Cooldowns
 422  function twister_captcha_store_cooldown($memcached, $long_ip, $expiration) {
 423    if (!$long_ip || !$expiration) {
 424      return false;
 425    }
 426    
 427    return $memcached->set("cd$long_ip", $expiration, $expiration);
 428  }
 429  
 430  function twister_captcha_get_cooldown($memcached, $long_ip) {
 431    if (!$long_ip) {
 432      return false;
 433    }
 434    
 435    $val = $memcached->get("cd$long_ip");
 436    
 437    if ($val === false && $memcached->getResultCode() === Memcached::RES_NOTFOUND) {
 438      return 0;
 439    }
 440    
 441    return $val;
 442  }
 443  
 444  function twister_captcha_generate_ticket($now, $long_ip, $board) {
 445    if (!$long_ip || !$now) {
 446      return false;
 447    }
 448    
 449    $hmac_secret = base64_decode(TWISTER_HMAC_SECRET);
 450    
 451    if (!$hmac_secret) {
 452      return false;
 453    }
 454    
 455    $hash = hash_hmac('sha256', "$now.$long_ip.$board", $hmac_secret);
 456    
 457    if (!$hash) {
 458      return false;
 459    }
 460    
 461    return "$now.$hash";
 462  }
 463  
 464  function twister_captcha_decode_ticket($ticket, $long_ip, $board) {
 465    if (!$long_ip || !$ticket) {
 466      return false;
 467    }
 468    
 469    $hmac_secret = base64_decode(TWISTER_HMAC_SECRET);
 470    
 471    if (!$hmac_secret) {
 472      return false;
 473    }
 474    
 475    list($ts, $hash) = explode('.', $ticket);
 476    
 477    $ts = (int)$ts;
 478    
 479    if (!$ts || !$hash) {
 480      return false;
 481    }
 482    
 483    $this_hash = hash_hmac('sha256', "$ts.$long_ip.$board", $hmac_secret);
 484    
 485    if ($this_hash === $hash) {
 486      return $ts;
 487    }
 488    
 489    return false;
 490  }
 491  
 492  function twister_captcha_should_purge_ticket($ticket, $now) {
 493    if (!$ticket || !$now) {
 494      return false;
 495    }
 496    
 497    list($ts, $hash) = explode('.', $ticket);
 498    
 499    if (!$ts) {
 500      return false;
 501    }
 502    
 503    return $ts + TWISTER_TICKET_TTL <= $now;
 504  }
 505  
 506  function twister_captcha_store_session($memcached, $long_ip, $count, $expiration) {
 507    if (!$long_ip || !$expiration || $count < 1) {
 508      return false;
 509    }
 510    
 511    $key = "us$long_ip";
 512    
 513    $res = $memcached->replace($key, $count, $expiration);
 514    
 515    if ($res === false) {
 516      if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) {
 517        return $memcached->set($key, $count, $expiration);
 518      }
 519      else {
 520        return false;
 521      }
 522    }
 523    
 524    return true;
 525  }
 526  
 527  function twister_captcha_get_session($memcached, $long_ip) {
 528    if (!$long_ip) {
 529      return false;
 530    }
 531    
 532    $val = $memcached->get("us$long_ip");
 533    
 534    if ($val === false) {
 535      if ($memcached->getResultCode() === Memcached::RES_NOTFOUND) {
 536        return 0;
 537      }
 538      else {
 539        return false;
 540      }
 541    }
 542    
 543    return $val;
 544  }
 545  
 546  function twister_captcha_get_credits($memcached, $pwd) {
 547    if (!$pwd) {
 548      return false;
 549    }
 550    
 551    $key = "cr-$pwd";
 552    $val = $memcached->get($key);
 553    
 554    if ($val === false) {
 555      return false;
 556    }
 557    
 558    $val = explode('.', $val);
 559    
 560    $count = (int)$val[0];
 561    $ts = (int)$val[1];
 562    
 563    if ($count <= 0 || $ts <= 0) {
 564      $memcached->delete($key);
 565      return false;
 566    }
 567    
 568    return [ $count, $ts ];
 569  }
 570  
 571  function twister_captcha_get_userpwd($user_ip) {
 572    if (isset($_COOKIE['4chan_pass'])) {
 573      $_c = $_COOKIE['4chan_pass'];
 574    }
 575    else {
 576      $_c = null;
 577    }
 578    
 579    return new UserPwd($user_ip, TWISTER_DOMAIN, $_COOKIE['4chan_pass']);
 580  }
 581  
 582  // ---
 583  
 584  // Dummy page for cloudflare challenges
 585  if (isset($_GET['opened'])) {
 586    twister_captcha_output_dummy();
 587    die();
 588  }
 589  
 590  // ---
 591  
 592  // Block TOR immediately
 593  if ($_SERVER['HTTP_X_GEO_CONTINENT'] === 'T1') {
 594    twister_captcha_error(TWISTER_ERR_GENERIC . ' (T1)');
 595  }
 596  
 597  // Check parameters
 598  if (isset($_GET['board']) && preg_match('/^[!0-9a-z]{1,10}$/', $_GET['board'])) {
 599    $param_board = $_GET['board'];
 600  }
 601  else {
 602    $param_board = '!';
 603  }
 604  
 605  if (isset($_GET['thread_id']) && $_GET['thread_id']) {
 606    $param_thread_id = (int)$_GET['thread_id'];
 607  }
 608  else {
 609    $param_thread_id = 0;
 610  }
 611  
 612  $now = $_SERVER['REQUEST_TIME'];
 613  $user_ip = $_SERVER['REMOTE_ADDR'];
 614  $user_long_ip = ip2long($user_ip);
 615  
 616  if (!isset($_SERVER['HTTP_USER_AGENT'])) {
 617    $user_agent = '!';
 618  }
 619  else {
 620    $user_agent = md5($_SERVER['HTTP_USER_AGENT']);
 621  }
 622  
 623  $userpwd = twister_captcha_get_userpwd($user_ip);
 624  
 625  if (!$userpwd) {
 626    twister_captcha_error(TWISTER_ERR_GENERIC . ' (GU1)');
 627  }
 628  
 629  if (!$userpwd->isNew() && $param_board !== '!signin') {
 630    $password = $userpwd->getPwd();
 631  }
 632  else {
 633    $password = '!';
 634  }
 635  
 636  // ---
 637  
 638  $use_static = false;
 639  $difficulty = TwisterCaptcha::LEVEL_NORMAL;
 640  $char_count = TWISTER_CHARS_MAX;
 641  
 642  // ---
 643  
 644  $m = new Memcached();
 645  
 646  // Only call the following once (when getServerList() is empty) if using persistent connections
 647  //$m->setOption(Memcached::OPT_TCP_NODELAY, true);
 648  $m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1);
 649  $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms
 650  $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms
 651  
 652  // Only use one server. Having multiple servers will break the captcha
 653  // as "set" is used instead of "replace + add"
 654  if ($m->addServer(TWISTER_MEMCACHED_HOST, TWISTER_MEMCACHED_PORT) === false) {
 655    twister_captcha_error(TWISTER_ERR_GENERIC . ' (c0)');
 656  }
 657  
 658  // ---
 659  
 660  // If CF's bot score is low, store the information for 1h and use it during posting
 661  twister_captcha_check_likely_automated($m, $now);
 662  
 663  /**
 664   * Pre-cooldowns
 665   */
 666  $pcd = 0;
 667  
 668  // Posting a thread
 669  if ($param_thread_id === 0) {
 670    if ($userpwd->threadCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) {
 671      $pcd = TWISTER_PRE_CD_THREAD;
 672    }
 673  }
 674  // Posting a reply (reporting uses a thread id of 1)
 675  else if ($param_thread_id !== 1) {
 676    if ($userpwd->postCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) {
 677      $pcd = TWISTER_PRE_CD_REPLY;
 678    }
 679  }
 680  // Reporting a post
 681  else if ($param_board !== '!') {
 682    if ($userpwd->reportCount() < 1 && $userpwd->postCount() < 1) {
 683      $pcd = TWISTER_PRE_CD_REPORT;
 684    }
 685  }
 686  
 687  if ($param_thread_id != 1 && !$userpwd->verifiedLevel()) {
 688    // Initial cooldown, bypassable by verifying your email
 689    if ($userpwd->postCount() < 1 || !$userpwd->isUserKnown(15)) {
 690      $pcd = 900;
 691    }
 692    // Cooldown for when the user is still new and the mask changes
 693    else if ($userpwd->postCount() > 0 && $userpwd->pwdLifetime() < 86400 && $userpwd->maskChanged()) { // 24h
 694      $pcd = 180;
 695    }
 696  }
 697  $pcd = 0; // FIXME
 698  // Pre-cooldown needed
 699  if ($pcd > 0 && $param_board !== '!signin') {
 700    // Extra pre-cooldown for unverified users
 701    if (!$userpwd->verifiedLevel()) {
 702      $pcd += twister_captcha_get_pcd_penalty($param_board, $param_thread_id);
 703    }
 704    
 705    $ticket_ts = twister_captcha_decode_ticket($_GET['ticket'], $user_long_ip, $param_board);
 706    
 707    $bypassable = false;
 708    
 709    if ($param_thread_id === 0) {
 710      $pcd_msg = TWISTER_ERR_PCD_THREAD;
 711    }
 712    else if ($param_thread_id !== 1 && $param_board !== '!') {
 713      $pcd_msg = TWISTER_ERR_PCD_REPLY;
 714    }
 715    
 716    if ($param_thread_id !== 1 && $pcd >= 900) {
 717      $pcd_msg = TWISTER_ERR_PCD_SIGNIN;
 718      $bypassable = true;
 719    }
 720    
 721    if (!$ticket_ts) {
 722      //if (TWISTER_USE_TICKET_CAPTCHA && !in_array((int)$_SERVER['HTTP_X_GEO_ASN'], TWISTER_WHITELIST_ASNS)) {
 723      if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) {
 724        twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
 725      }
 726      else {
 727        twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
 728      }
 729      
 730      die();
 731    }
 732    
 733    $ticket_lifetime = $now - $ticket_ts;
 734    
 735    if ($ticket_lifetime < $pcd) {
 736      $pcd = $pcd - $ticket_lifetime;
 737      twister_captcha_output_ticket_pcd(null, $pcd, $pcd_msg, $bypassable);
 738      die();
 739    }
 740    
 741    // Ticket expired
 742    if ($ticket_lifetime >= TWISTER_TICKET_TTL) {
 743      if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) {
 744        twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
 745      }
 746      else {
 747        twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
 748      }
 749      
 750      die();
 751    }
 752  }
 753  
 754  /**
 755   * Adjust difficulty
 756   */
 757  
 758  $ip_ttl_static = TWISTER_PWD_TTL_IP_STATIC;
 759  $mask_ttl_static = TWISTER_PWD_TTL_MASK_STATIC;
 760  
 761  $ip_ttl_min = TWISTER_PWD_TTL_IP_MIN;
 762  $idle_ttl = TWISTER_PWD_TTL_IDLE;
 763  
 764  // Serve max len twister to bad actors
 765  $bad_actor = false;
 766  
 767  if (!isset($_SERVER['HTTP_USER_AGENT']) || !$_SERVER['HTTP_USER_AGENT']) {
 768    $bad_actor = true;
 769  }
 770  else if (preg_match(TWISTER_BAD_UA, $_SERVER['HTTP_USER_AGENT'])) {
 771    $bad_actor = true;
 772  }
 773  else if (twister_captcha_is_req_suspicious()) {
 774    $bad_actor = true;
 775  }
 776  
 777  // Serve static captcha to known users.
 778  // Serve max length captchas for unknown users.
 779  // Only applies to replies. Theads always use slider captchas.
 780  if ($param_thread_id !== 0 && !$bad_actor) {
 781    // Known and post count check
 782    if ($userpwd->isUserKnown(TWISTER_PWD_STATIC_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_STATIC_POSTS) {
 783      // Inactivity and IP change checks for unverified users
 784      if ($userpwd->verifiedLevel()) {
 785        $use_static = true;
 786        $char_count = TWISTER_CHARS_MIN;
 787      }
 788      else if ($userpwd->idleLifetime() <= TWISTER_PWD_SLIDER_IDLE && !$userpwd->ipChanged()) {
 789        $use_static = true;
 790        $char_count = TWISTER_CHARS;
 791      }
 792    }
 793  }
 794  
 795  // ---
 796  
 797  // Debug controls
 798  if (DEV_MODE) {
 799    if (isset($_GET['mc_stats']) && $_GET['mc_stats']) {
 800      $data = [
 801        $m->getServerList(),
 802        $m->getStats()
 803      ];
 804      echo json_encode($data);
 805      die();
 806    }
 807    
 808    if (isset($_GET['del_key']) && $_GET['del_key']) {
 809      $m->delete($_GET['del_key']);
 810      die("ok");
 811    }
 812    
 813    if (isset($_GET['get_key']) && $_GET['get_key']) {
 814      var_dump($m->get($_GET['get_key']));
 815      die();
 816    }
 817    
 818    if (isset($_GET['mode'])) {
 819      if ($_GET['mode'] === 'static') {
 820        $use_static = true;
 821      }
 822      else if ($_GET['mode'] === 'twister') {
 823        $use_static = false;
 824      }
 825    }
 826    
 827    if (isset($_GET['difficulty']) && $_GET['difficulty']) {
 828      $difficulty = (int)$_GET['difficulty'];
 829    }
 830  }
 831  
 832  // ---
 833  
 834  // Check captcha bypassing credits
 835  if (TWISTER_ALLOW_NOOP && !in_array($param_board, TWISTER_NO_NOOP_BOARDS) && !$bad_actor && $param_thread_id !== 0) {
 836    if ($userpwd->isUserKnown(TWISTER_PWD_NOOP_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_NOOP_POSTS) {
 837      $credits = twister_captcha_get_credits($m, $userpwd->getPwd());
 838      
 839      if ($credits !== false && $credits[0] > 0) {
 840        $data = [
 841          'challenge' => 'noop',
 842          'ttl' => min(TWISTER_TTL, $credits[1] - $now),
 843          'cd' => TWISTER_COOLDOWN
 844        ];
 845        
 846        twister_captcha_output_data($data);
 847        
 848        die();
 849      }
 850    }
 851  }
 852  
 853  // Check cooldown
 854  $should_cd_until = twister_captcha_get_cooldown($m, $user_long_ip);
 855  
 856  if ($should_cd_until === false) {
 857    twister_captcha_error(TWISTER_ERR_GENERIC . ' (scdu)');
 858  }
 859  
 860  if ($should_cd_until > 1) {
 861    twister_captcha_error(TWISTER_ERR_COOLDOWN, ['cd' => $should_cd_until - $now]);
 862  }
 863  
 864  // Number of unsolved captchas requested recently
 865  $unsolved_count = twister_captcha_get_session($m, $user_long_ip);
 866  
 867  if ($unsolved_count === false) {
 868    twister_captcha_error(TWISTER_ERR_GENERIC . ' (gus1)');
 869  }
 870  
 871  if ($unsolved_count > 2) {
 872    $cooldown = TWISTER_COOLDOWN_LONG * min($unsolved_count, 20);
 873    
 874    if ($unsolved_count > 10) {
 875      $cooldown = 300;
 876    }
 877  }
 878  else {
 879    $cooldown = TWISTER_COOLDOWN;
 880  }
 881  
 882  if (twister_captcha_store_cooldown($m, $user_long_ip, $now + $cooldown) === false) {
 883    twister_captcha_error(TWISTER_ERR_GENERIC . ' (sc0)');
 884  }
 885  
 886  // Generate images
 887  $c = new TwisterCaptcha(TWISTER_FONT_PATH);
 888  $c->setDifficulty($difficulty);
 889  
 890  // Adjust features
 891  // User is suspicious
 892  $should_harden = $bad_actor && ($userpwd->ipChanged() || !$userpwd->isUserKnownOrVerified(7200) || mt_rand(0, 9) === 9);
 893  
 894  $should_fog = false;
 895  
 896  if ($should_harden) {
 897    $use_static = false;
 898    
 899    if (mt_rand(0, 1)) {
 900      $c->useInkBlot(true);
 901      
 902      if (mt_rand(0, 1)) {
 903        $c->useNegateBlotFilter(true);
 904      }
 905    }
 906    else {
 907      $c->useEdgeBlock(true);
 908    }
 909    
 910    $c->useFakeCharPadding(true);
 911    $c->useJumpyMode(true);
 912    
 913    if (mt_rand(0, 9) === 9) {
 914      $c->setDifficulty(TwisterCaptcha::LEVEL_LUNATIC);
 915    }
 916    else {
 917      $c->setDifficulty(TwisterCaptcha::LEVEL_HARD);
 918    }
 919    
 920    if (mt_rand(0, 9) === 9) {
 921      $char_count = 8;
 922    }
 923    
 924    if (mt_rand(0, 1)) {
 925      $c->useGridLines(true);
 926    }
 927    else {
 928      $c->useScoreLines(true);
 929    }
 930  }
 931  // Other new users
 932  else if (true || $userpwd->postCount() < 5 || $userpwd->maskLifetime() < 3600) {
 933    if (!$use_static) {
 934      $_boards = [ 'a', 'v', 'vg', 'co', 'vp', 'g', 'biz', 'b', 'vt', 'mu', 'pol', 'tv', 'sp', 'int', 'soc', 'test' ];
 935      
 936      if (true || in_array($param_board, $_boards) || $param_thread_id == 0) {
 937        $should_fog = true;
 938      }
 939      else {
 940        $c->useInkBlot(true);
 941        $c->useScoreLines(true);
 942      }
 943    }
 944    else {
 945      $c->useInkBlot(true);
 946      //$c->useFakeCharPadding(true);
 947      
 948      if (mt_rand(0, 1)) {
 949        $c->useJumpyMode(true);
 950      }
 951    }
 952    
 953    $char_count = mt_rand(TWISTER_CHARS, TWISTER_CHARS_MAX);
 954  }
 955  
 956  if ($param_board === '!signin') {
 957    $should_fog = true;
 958    $use_static = false;
 959  }
 960  
 961  if ($use_static) {
 962    if (mt_rand(0, 9) === 9) {
 963      $c->useScoreLines(true);
 964    }
 965    
 966    list($challenge_str, $img, $img_width, $img_height) = $c->generateStatic($char_count);
 967    $img_bg = null;
 968  }
 969  else {
 970    if (true || $should_fog) {
 971      if (false && $param_board == 'co' && $param_thread_id == 0) {
 972        $c->useEdgeDetect(true);
 973        $c->useSpecialRot(true);
 974        //$c->useOverlayId(5, true);
 975        $c->useInvert(true);
 976        //$c->useAltBlackWhite(true);
 977        $c->useGridLines(true);
 978        $c->useSimplexBg(true);
 979        //$c->useEmboss(true);
 980        //list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count);
 981        list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(7, 28, 28);
 982        //list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterV(3);
 983      }
 984      else {
 985        $_olid = mt_rand(0, 6);
 986  
 987        if ($_olid) {
 988          $c->useOverlayId($_olid, (bool)mt_rand(0, 1));
 989        }
 990        
 991        if (mt_rand(0, 1)) {
 992          $c->useInvert(true);
 993        }
 994        
 995        if (mt_rand(0, 3) === 3) {
 996          $c->useFakeCharPadding(true);
 997        }
 998        
 999        if ($param_thread_id == 0) {
1000          $_olid = 2;
1001        }
1002        else {
1003          $_olid = mt_rand(1, 2);
1004        }
1005        
1006        $_olid = 2;
1007        
1008        // Simplex
1009        if ($_olid === 1) {
1010          if (mt_rand(0, 3) === 3) {
1011            $c->useScoreLines(true);
1012          }
1013          
1014          if (mt_rand(0, 1)) {
1015            $c->useInvert(true);
1016          }
1017          
1018          list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHSimplex($char_count);
1019        }
1020        // Fog New
1021        else if ($_olid === 2) {
1022          if (mt_rand(0, 1)) {
1023            $c->useSimplexBg(true);
1024          }
1025          
1026          if (mt_rand(0, 1)) {
1027            $c->useEmboss(true);
1028          }
1029          else {
1030            $c->useEdgeDetect(true);
1031          }
1032          
1033          if (mt_rand(0, 1)) {
1034            $c->useAltBlackWhite(true);
1035          }
1036          
1037          if (mt_rand(0, 1)) {
1038            $c->useInvert(true);
1039          }
1040          
1041          //if (mt_rand(0, 3) === 3) {
1042            //$c->useSpecialRot(true);
1043            //$char_count = 5;
1044          //}
1045          
1046          if (isset($_SERVER['HTTP_X_BOT_SCORE'])) {
1047            $_bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE'];
1048          }
1049          else {
1050            $_bot_score = 100;
1051          }
1052          
1053          if ($param_board === '!signin') {
1054            if ($_bot_score > 95) {
1055              list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50);
1056            }
1057            else {
1058              list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2);
1059            }
1060          }
1061          else {
1062            //$c->useSpecialRot(true);
1063            
1064            if ($_bot_score <= 70 && mt_rand(0, 2) === 0) {
1065              list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2);
1066            }
1067            else {
1068              list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50);
1069            }
1070          }
1071        }
1072        // Default
1073        else {
1074          list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count);
1075        }
1076      }
1077    }
1078    else {
1079      list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterH($char_count);
1080    }
1081  }
1082  
1083  if (!$challenge_str) {
1084    twister_captcha_error(TWISTER_ERR_GENERIC . ' (ncs1)');
1085  }
1086  
1087  list($challenge_uid, $challenge_hash) = TwisterCaptcha::getChallengeHash(
1088    $challenge_str,
1089    [$user_ip, $password, $user_agent, $param_board, $param_thread_id]
1090  );
1091  
1092  if (!$challenge_uid || !$challenge_hash) {
1093    twister_captcha_error(TWISTER_ERR_GENERIC . ' (gch1)');
1094  }
1095  
1096  // Register challenge
1097  if (twister_captcha_store_challenge($m, $challenge_uid, $user_long_ip, $now + TWISTER_TTL) === false) {
1098    twister_captcha_error(TWISTER_ERR_GENERIC . ' (sch1)');
1099  }
1100  
1101  // Register unsolved session
1102  //if (twister_captcha_store_session($m, $user_long_ip, $unsolved_count + 1, $now + TWISTER_TTL_UNSOLVED) === false) {
1103  //  twister_captcha_error(TWISTER_ERR_GENERIC . ' (sus1)');
1104  //}
1105  
1106  // Generate base 64 urls of images
1107  list($img_b64, $img_bg_b64) = TwisterCaptcha::getBase64Images($img, $img_bg);
1108  
1109  $data = [
1110    'challenge' => "$challenge_uid.$challenge_hash",
1111    'ttl' => TWISTER_TTL,
1112    'cd' => $cooldown,
1113    'img' => $img_b64,
1114    'img_width' => $img_width,
1115    'img_height' => $img_height
1116  ];
1117  
1118  if ($img_bg) {
1119    $data['bg'] = $img_bg_b64;
1120    $data['bg_width'] = $bg_width;
1121  }
1122  
1123  if (twister_captcha_should_purge_ticket($_GET['ticket'], $now)) {
1124    $data['ticket'] = false;
1125  }
1126  
1127  twister_captcha_output_data($data);