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