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