1127 lines
28 KiB
PHP
1127 lines
28 KiB
PHP
<?php
|
|
|
|
define('DEV_MODE', $_SERVER['REMOTE_ADDR'] === '195.154.113.119');
|
|
//define('DEV_MODE', false);
|
|
|
|
if (!DEV_MODE) {
|
|
http_response_code(404);
|
|
echo('File not found.');
|
|
die();
|
|
}
|
|
else {
|
|
ini_set('display_errors', 1);
|
|
error_reporting(E_ALL & ~E_NOTICE);
|
|
}
|
|
|
|
require_once 'lib/userpwd-test.php';
|
|
require_once 'lib/twister_captcha-test.php';
|
|
|
|
define('TWISTER_FONT_PATH', '/usr/local/share/captcha/PCss_8x16_LE.gdf');
|
|
|
|
define('TWISTER_USE_TICKET_CAPTCHA', true);
|
|
define('TWISTER_HCAPTCHA_SITEKEY', '49d294fa-f15c-41fc-80ba-c2544c52ec2a');
|
|
|
|
// Validity period for captcha in seconds
|
|
define('TWISTER_TTL', 120);
|
|
// Delay in seconds before requesting a new captcha
|
|
define('TWISTER_COOLDOWN', 0);
|
|
// Long cooldown for unsolved captchas
|
|
define('TWISTER_COOLDOWN_LONG', 0);
|
|
// Duration of unsolved sessions
|
|
define('TWISTER_TTL_UNSOLVED', 600);
|
|
// Pre-cooldowns
|
|
define('TWISTER_HMAC_SECRET', 'y474c22ugpVtJD5gveDxcmMcL7DQFP+/w1FhVP9CYVM=');
|
|
define('TWISTER_TICKET_TTL', 3600); // 60 minutes
|
|
define('TWISTER_PRE_CD_THREAD', 300); // 5 minutes
|
|
define('TWISTER_PRE_CD_REPLY', 60);
|
|
define('TWISTER_PRE_CD_REPORT', 60);
|
|
|
|
define('TWISTER_PRE_CD_PENALITY', 60);
|
|
|
|
// Default number of characters for captchas
|
|
define('TWISTER_CHARS', 5);
|
|
define('TWISTER_CHARS_MIN', 4);
|
|
define('TWISTER_CHARS_MAX', 6);
|
|
|
|
// Userpwd reputation checks:
|
|
// Number of posts to get static captchas
|
|
define('TWISTER_PWD_STATIC_POSTS', 5);
|
|
// Known time in minutes to get static captchas
|
|
define('TWISTER_PWD_STATIC_KNOWN', 4320); // 3 days
|
|
// Idle time above which slider captchas will be served
|
|
define('TWISTER_PWD_SLIDER_IDLE', 86400); // 24 hours
|
|
|
|
// Whether or not to check for bypassing credits
|
|
define('TWISTER_ALLOW_NOOP', true);
|
|
// Number of posts to be eligible for captcha bypassing
|
|
define('TWISTER_PWD_NOOP_POSTS', 5);
|
|
// Known time in minutes to be eligible for captcha bypassing
|
|
define('TWISTER_PWD_NOOP_KNOWN', 4320); // 3 days
|
|
|
|
// List of boards where bypassing credits are disabled.
|
|
// Corresponds to the CAPTCHA_ALLOW_BYPASS board setting.
|
|
const TWISTER_NO_NOOP_BOARDS = [
|
|
'bant', 'pol', 'trash'
|
|
];
|
|
|
|
// Covers 272814 (84.81 %) unique IPs and 10863 (79.15 %) unique bans
|
|
// IPs %: 0.05 | Bans %: 0 | GR1 all: 0 | Any EU: false
|
|
const TWISTER_WHITELIST_COUNTRIES = [
|
|
'US', 'CA', 'GB', 'DE', 'AU', 'PL', 'IT', 'FR', 'FI', 'SE', 'ID', 'ES', 'NL',
|
|
'PH', 'JP', 'MY', 'NO', 'IE', 'VN', 'NZ', 'HR', 'HU', 'AT', 'SG', 'PT', 'BE',
|
|
'GR', 'DK', 'RS', 'CZ', 'TH', 'BG', 'CH', 'EE', 'LT', 'SK', 'SI', 'LV', 'TW',
|
|
'IS',
|
|
];
|
|
|
|
// Covers 73407 (29.95 %) unique IPs and 2501 (16.7 %) unique bans
|
|
// IPs %: 2 | Bans %: 0 | GR1 all: 0 | Any EU: false
|
|
const TWISTER_WHITELIST_ASNS = [
|
|
21928, 6167,
|
|
];
|
|
|
|
// Mobile ISPs
|
|
const TWISTER_MOBILE_ASNS = [
|
|
21928,6167,20057,26599,35228,206067,4775,22085,8359,10139,30689,52876,27895,
|
|
31615,11315,28403,6614,25135,10030,45143,4230,20365,6306,12929,21450,264731,
|
|
203995,29465,38466,4818,15480,13280,13335,132618,25106,4657,10631,29247,12716,
|
|
262210,138384,8953,9146,31213,2497,5578,21575,28036,28469,132061,29975
|
|
];
|
|
|
|
define('TWISTER_BAD_UA', '/headless|node-fetch|python-|java\/|jakarta|-perl|http-?client|-resty-|awesomium\//i');
|
|
|
|
// Memcached server
|
|
define('TWISTER_MEMCACHED_HOST', '127.0.0.1');
|
|
define('TWISTER_MEMCACHED_PORT', 11211);
|
|
|
|
// ---
|
|
|
|
define('TWISTER_DOMAIN', ($_SERVER['HTTP_HOST'] === 'sys.4chan.org') ? '4chan.org' : '4channel.org');
|
|
|
|
define('TWISTER_ERR_GENERIC', 'Internal Server Error');
|
|
define('TWISTER_ERR_COOLDOWN', 'You have to wait a while before doing this again');
|
|
define('TWISTER_ERR_PCD_THREAD', 'Please wait a while before making a thread');
|
|
define('TWISTER_ERR_PCD_REPLY', 'Please wait a while before making a post');
|
|
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.');
|
|
|
|
// ---
|
|
|
|
function twister_captcha_output_data($data) {
|
|
if (isset($_GET['framed'])) {
|
|
twister_captcha_output_html($data);
|
|
}
|
|
else {
|
|
header('Content-Type: application/json');
|
|
echo json_encode($data);
|
|
}
|
|
}
|
|
|
|
function twister_captcha_output_html($data) {
|
|
header('Content-Security-Policy: frame-ancestors https://*.' . TWISTER_DOMAIN . ';');
|
|
$now = $_SERVER['REQUEST_TIME'];
|
|
?><!DOCTYPE html><html><head><meta charset="utf-8"><title></title>
|
|
<script>window.parent.postMessage(<?php echo json_encode(['twister' => $data]) ?>, '*');</script>
|
|
<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>
|
|
</head><body></body></html><?php
|
|
}
|
|
|
|
function twister_captcha_output_dummy() {
|
|
?><!DOCTYPE html><html><head><meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"><title></title></head><body>
|
|
<h3 style="text-align:center">You can now close this page and try getting a captcha again.<h3>
|
|
</body></html><?php
|
|
}
|
|
|
|
function twister_captcha_output_ticket_captcha() {
|
|
twister_captcha_output_data([ 'mpcd' => true, 'ticket' => false ]);
|
|
}
|
|
|
|
function twister_captcha_get_ticket_captcha_response() {
|
|
if (isset($_GET['ticket_resp'])) {
|
|
return $_GET['ticket_resp'];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function twister_captcha_get_hcaptcha_private_key() {
|
|
$path = '/www/global/yotsuba/config/captcha_config.ini';
|
|
|
|
$cfg = file_get_contents($path);
|
|
|
|
if (!$cfg) {
|
|
return false;
|
|
}
|
|
|
|
$res = preg_match('/^HCAPTCHA_API_KEY_PRIVATE ?= ?([^\s]+)$/m', $cfg, $m);
|
|
|
|
if (!$res || empty($m) || !$m[1]) {
|
|
return false;
|
|
}
|
|
|
|
return $m[1];
|
|
}
|
|
|
|
function twister_captcha_verify_ticket_captcha() {
|
|
$response = twister_captcha_get_ticket_captcha_response();
|
|
|
|
if (!$response || strlen($response) > 4096) {
|
|
return false;
|
|
}
|
|
|
|
$captcha_private_key = twister_captcha_get_hcaptcha_private_key();
|
|
|
|
if (!$captcha_private_key) {
|
|
// Don't block in case of misconfiguration on our end
|
|
return true;
|
|
}
|
|
|
|
$url = 'https://hcaptcha.com/siteverify';
|
|
|
|
$post = array(
|
|
'secret' => $captcha_private_key,
|
|
'response' => $response,
|
|
'remoteip' => $_SERVER['REMOTE_ADDR'],
|
|
'sitekey' => TWISTER_HCAPTCHA_SITEKEY,
|
|
);
|
|
|
|
$curl = curl_init();
|
|
|
|
curl_setopt($curl, CURLOPT_URL, $url);
|
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
|
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2);
|
|
curl_setopt($curl, CURLOPT_TIMEOUT, 4);
|
|
curl_setopt($curl, CURLOPT_USERAGENT, '4chan');
|
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
|
|
|
|
$resp = curl_exec($curl);
|
|
|
|
if ($resp === false) {
|
|
curl_close($curl);
|
|
return false;
|
|
}
|
|
|
|
$resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
|
|
if ($resp_status >= 300) {
|
|
curl_close($curl);
|
|
return false;
|
|
}
|
|
|
|
curl_close($curl);
|
|
|
|
$json = json_decode($resp, true);
|
|
|
|
// BAD
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return false;
|
|
}
|
|
|
|
// GOOD
|
|
if ($json && isset($json['success']) && $json['success']) {
|
|
return true;
|
|
}
|
|
|
|
// BAD
|
|
return false;
|
|
}
|
|
|
|
function twister_captcha_process_ticket_captcha($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) {
|
|
// Captcha response was sent, verify it
|
|
if (twister_captcha_get_ticket_captcha_response()) {
|
|
if (twister_captcha_verify_ticket_captcha() === true) {
|
|
// Captcha is valid, return a new ticket
|
|
twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg, $bypassable);
|
|
}
|
|
else {
|
|
// Wrong captcha or captcha malfunction
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (tcr0)');
|
|
}
|
|
}
|
|
// No captcha reponse provided, tell the frontend to show a ticket captcha
|
|
else {
|
|
twister_captcha_output_ticket_captcha();
|
|
}
|
|
}
|
|
|
|
function twister_captcha_output_new_ticket($pcd, $now, $long_ip, $board, $msg = null, $bypassable = false) {
|
|
$ticket = twister_captcha_generate_ticket($now, $long_ip, $board);
|
|
|
|
if (!$ticket) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (gt1)');
|
|
}
|
|
|
|
twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable);
|
|
}
|
|
|
|
function twister_captcha_output_ticket_pcd($ticket, $pcd, $msg, $bypassable) {
|
|
$data = [];
|
|
|
|
if ($ticket) {
|
|
$data['ticket'] = $ticket;
|
|
}
|
|
|
|
$data['pcd'] = $pcd;
|
|
|
|
if ($msg) {
|
|
$data['pcd_msg'] = $msg;
|
|
}
|
|
|
|
if ($bypassable) {
|
|
$data['bpcd'] = true;
|
|
}
|
|
|
|
twister_captcha_output_data($data);
|
|
}
|
|
|
|
function twister_captcha_error($msg, $extra = null) {
|
|
//http_response_code(500);
|
|
$data = [ 'error' => $msg ];
|
|
|
|
if ($extra) {
|
|
$data = array_merge($data, $extra);
|
|
}
|
|
|
|
twister_captcha_output_data($data);
|
|
|
|
die();
|
|
}
|
|
|
|
function twister_captcha_is_req_suspicious() {
|
|
/*
|
|
if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) {
|
|
if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
*/
|
|
$no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false;
|
|
$no_accept = isset($_SERVER['HTTP_ACCEPT']) === false;
|
|
|
|
if ($no_lang && $no_accept) {
|
|
return true;
|
|
}
|
|
|
|
if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) {
|
|
return true;
|
|
}
|
|
|
|
if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) {
|
|
return true;
|
|
}
|
|
|
|
if ($no_lang && isset($_SERVER['HTTP_REFERER'])) {
|
|
$ref = $_SERVER['HTTP_REFERER'];
|
|
|
|
if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function twister_captcha_need_hcaptcha() {
|
|
if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) {
|
|
return true;
|
|
}
|
|
|
|
$ua = $_SERVER['HTTP_USER_AGENT'];
|
|
|
|
$score = (int)$_SERVER['HTTP_X_BOT_SCORE'];
|
|
|
|
// Skip Android Webviews
|
|
if ($score == 1 && strpos($ua, '; wv)') !== false) {
|
|
return false;
|
|
}
|
|
|
|
return $score < 99;
|
|
}
|
|
|
|
function twister_captcha_check_likely_automated($memcached, $now, $threshold = 29) {
|
|
if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) {
|
|
return false;
|
|
}
|
|
|
|
$ua = $_SERVER['HTTP_USER_AGENT'];
|
|
|
|
// Skip Android Webviews
|
|
if (strpos($ua, '; wv)') !== false) {
|
|
return false;
|
|
}
|
|
|
|
// Skip iPhone Webviews
|
|
if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) {
|
|
return false;
|
|
}
|
|
|
|
$score = (int)$_SERVER['HTTP_X_BOT_SCORE'];
|
|
|
|
if ($score > 1 && $score <= $threshold) {
|
|
$key = 'bmbot' . $_SERVER['REMOTE_ADDR'];
|
|
$memcached->set($key, 1, $now + 43200);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function twister_captcha_get_pcd_penalty($board, $thread_id) {
|
|
$count = 0;
|
|
|
|
return 0; // FIXME
|
|
|
|
// Reports
|
|
if ($thread_id === 1 && $board !== '!') {
|
|
return 0;
|
|
}
|
|
|
|
if (isset($_SERVER['HTTP_X_GEO_ASN'])) {
|
|
$asn = (int)$_SERVER['HTTP_X_GEO_ASN'];
|
|
}
|
|
else {
|
|
$asn = 0;
|
|
}
|
|
|
|
if (isset($_SERVER['HTTP_X_GEO_COUNTRY'])) {
|
|
$country = $_SERVER['HTTP_X_GEO_COUNTRY'];
|
|
}
|
|
else {
|
|
$country = null;
|
|
}
|
|
|
|
// Mobile clients
|
|
if ($asn > 0 && in_array($asn, TWISTER_MOBILE_ASNS)) {
|
|
$count++;
|
|
}
|
|
else if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Mobile|iPhone|; wv/', $_SERVER['HTTP_USER_AGENT'])) {
|
|
$count++;
|
|
}
|
|
|
|
// Rare countries
|
|
if ($country && !in_array($country, TWISTER_WHITELIST_COUNTRIES)) {
|
|
$count++;
|
|
}
|
|
|
|
// Rare ISPs
|
|
if ($asn > 0 && !in_array($asn, TWISTER_WHITELIST_ASNS)) {
|
|
$count++;
|
|
}
|
|
|
|
return $count * TWISTER_PRE_CD_PENALITY;
|
|
}
|
|
|
|
function twister_captcha_store_challenge($memcached, $challenge_uid, $long_ip, $expiration) {
|
|
if (!$challenge_uid || !$long_ip || !$expiration) {
|
|
return false;
|
|
}
|
|
|
|
return $memcached->set("ch$long_ip", $challenge_uid, $expiration);
|
|
}
|
|
|
|
// Cooldowns
|
|
function twister_captcha_store_cooldown($memcached, $long_ip, $expiration) {
|
|
if (!$long_ip || !$expiration) {
|
|
return false;
|
|
}
|
|
|
|
return $memcached->set("cd$long_ip", $expiration, $expiration);
|
|
}
|
|
|
|
function twister_captcha_get_cooldown($memcached, $long_ip) {
|
|
if (!$long_ip) {
|
|
return false;
|
|
}
|
|
|
|
$val = $memcached->get("cd$long_ip");
|
|
|
|
if ($val === false && $memcached->getResultCode() === Memcached::RES_NOTFOUND) {
|
|
return 0;
|
|
}
|
|
|
|
return $val;
|
|
}
|
|
|
|
function twister_captcha_generate_ticket($now, $long_ip, $board) {
|
|
if (!$long_ip || !$now) {
|
|
return false;
|
|
}
|
|
|
|
$hmac_secret = base64_decode(TWISTER_HMAC_SECRET);
|
|
|
|
if (!$hmac_secret) {
|
|
return false;
|
|
}
|
|
|
|
$hash = hash_hmac('sha256', "$now.$long_ip.$board", $hmac_secret);
|
|
|
|
if (!$hash) {
|
|
return false;
|
|
}
|
|
|
|
return "$now.$hash";
|
|
}
|
|
|
|
function twister_captcha_decode_ticket($ticket, $long_ip, $board) {
|
|
if (!$long_ip || !$ticket) {
|
|
return false;
|
|
}
|
|
|
|
$hmac_secret = base64_decode(TWISTER_HMAC_SECRET);
|
|
|
|
if (!$hmac_secret) {
|
|
return false;
|
|
}
|
|
|
|
list($ts, $hash) = explode('.', $ticket);
|
|
|
|
$ts = (int)$ts;
|
|
|
|
if (!$ts || !$hash) {
|
|
return false;
|
|
}
|
|
|
|
$this_hash = hash_hmac('sha256', "$ts.$long_ip.$board", $hmac_secret);
|
|
|
|
if ($this_hash === $hash) {
|
|
return $ts;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function twister_captcha_should_purge_ticket($ticket, $now) {
|
|
if (!$ticket || !$now) {
|
|
return false;
|
|
}
|
|
|
|
list($ts, $hash) = explode('.', $ticket);
|
|
|
|
if (!$ts) {
|
|
return false;
|
|
}
|
|
|
|
return $ts + TWISTER_TICKET_TTL <= $now;
|
|
}
|
|
|
|
function twister_captcha_store_session($memcached, $long_ip, $count, $expiration) {
|
|
if (!$long_ip || !$expiration || $count < 1) {
|
|
return false;
|
|
}
|
|
|
|
$key = "us$long_ip";
|
|
|
|
$res = $memcached->replace($key, $count, $expiration);
|
|
|
|
if ($res === false) {
|
|
if ($memcached->getResultCode() === Memcached::RES_NOTSTORED) {
|
|
return $memcached->set($key, $count, $expiration);
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function twister_captcha_get_session($memcached, $long_ip) {
|
|
if (!$long_ip) {
|
|
return false;
|
|
}
|
|
|
|
$val = $memcached->get("us$long_ip");
|
|
|
|
if ($val === false) {
|
|
if ($memcached->getResultCode() === Memcached::RES_NOTFOUND) {
|
|
return 0;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $val;
|
|
}
|
|
|
|
function twister_captcha_get_credits($memcached, $pwd) {
|
|
if (!$pwd) {
|
|
return false;
|
|
}
|
|
|
|
$key = "cr-$pwd";
|
|
$val = $memcached->get($key);
|
|
|
|
if ($val === false) {
|
|
return false;
|
|
}
|
|
|
|
$val = explode('.', $val);
|
|
|
|
$count = (int)$val[0];
|
|
$ts = (int)$val[1];
|
|
|
|
if ($count <= 0 || $ts <= 0) {
|
|
$memcached->delete($key);
|
|
return false;
|
|
}
|
|
|
|
return [ $count, $ts ];
|
|
}
|
|
|
|
function twister_captcha_get_userpwd($user_ip) {
|
|
if (isset($_COOKIE['4chan_pass'])) {
|
|
$_c = $_COOKIE['4chan_pass'];
|
|
}
|
|
else {
|
|
$_c = null;
|
|
}
|
|
|
|
return new UserPwd($user_ip, TWISTER_DOMAIN, $_COOKIE['4chan_pass']);
|
|
}
|
|
|
|
// ---
|
|
|
|
// Dummy page for cloudflare challenges
|
|
if (isset($_GET['opened'])) {
|
|
twister_captcha_output_dummy();
|
|
die();
|
|
}
|
|
|
|
// ---
|
|
|
|
// Block TOR immediately
|
|
if ($_SERVER['HTTP_X_GEO_CONTINENT'] === 'T1') {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (T1)');
|
|
}
|
|
|
|
// Check parameters
|
|
if (isset($_GET['board']) && preg_match('/^[!0-9a-z]{1,10}$/', $_GET['board'])) {
|
|
$param_board = $_GET['board'];
|
|
}
|
|
else {
|
|
$param_board = '!';
|
|
}
|
|
|
|
if (isset($_GET['thread_id']) && $_GET['thread_id']) {
|
|
$param_thread_id = (int)$_GET['thread_id'];
|
|
}
|
|
else {
|
|
$param_thread_id = 0;
|
|
}
|
|
|
|
$now = $_SERVER['REQUEST_TIME'];
|
|
$user_ip = $_SERVER['REMOTE_ADDR'];
|
|
$user_long_ip = ip2long($user_ip);
|
|
|
|
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
|
|
$user_agent = '!';
|
|
}
|
|
else {
|
|
$user_agent = md5($_SERVER['HTTP_USER_AGENT']);
|
|
}
|
|
|
|
$userpwd = twister_captcha_get_userpwd($user_ip);
|
|
|
|
if (!$userpwd) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (GU1)');
|
|
}
|
|
|
|
if (!$userpwd->isNew() && $param_board !== '!signin') {
|
|
$password = $userpwd->getPwd();
|
|
}
|
|
else {
|
|
$password = '!';
|
|
}
|
|
|
|
// ---
|
|
|
|
$use_static = false;
|
|
$difficulty = TwisterCaptcha::LEVEL_NORMAL;
|
|
$char_count = TWISTER_CHARS_MAX;
|
|
|
|
// ---
|
|
|
|
$m = new Memcached();
|
|
|
|
// Only call the following once (when getServerList() is empty) if using persistent connections
|
|
//$m->setOption(Memcached::OPT_TCP_NODELAY, true);
|
|
$m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1);
|
|
$m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms
|
|
$m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms
|
|
|
|
// Only use one server. Having multiple servers will break the captcha
|
|
// as "set" is used instead of "replace + add"
|
|
if ($m->addServer(TWISTER_MEMCACHED_HOST, TWISTER_MEMCACHED_PORT) === false) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (c0)');
|
|
}
|
|
|
|
// ---
|
|
|
|
// If CF's bot score is low, store the information for 1h and use it during posting
|
|
twister_captcha_check_likely_automated($m, $now);
|
|
|
|
/**
|
|
* Pre-cooldowns
|
|
*/
|
|
$pcd = 0;
|
|
|
|
// Posting a thread
|
|
if ($param_thread_id === 0) {
|
|
if ($userpwd->threadCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) {
|
|
$pcd = TWISTER_PRE_CD_THREAD;
|
|
}
|
|
}
|
|
// Posting a reply (reporting uses a thread id of 1)
|
|
else if ($param_thread_id !== 1) {
|
|
if ($userpwd->postCount() < 1 || ($userpwd->ipChanged() && $userpwd->postCount() < 2)) {
|
|
$pcd = TWISTER_PRE_CD_REPLY;
|
|
}
|
|
}
|
|
// Reporting a post
|
|
else if ($param_board !== '!') {
|
|
if ($userpwd->reportCount() < 1 && $userpwd->postCount() < 1) {
|
|
$pcd = TWISTER_PRE_CD_REPORT;
|
|
}
|
|
}
|
|
|
|
if ($param_thread_id != 1 && !$userpwd->verifiedLevel()) {
|
|
// Initial cooldown, bypassable by verifying your email
|
|
if ($userpwd->postCount() < 1 || !$userpwd->isUserKnown(15)) {
|
|
$pcd = 900;
|
|
}
|
|
// Cooldown for when the user is still new and the mask changes
|
|
else if ($userpwd->postCount() > 0 && $userpwd->pwdLifetime() < 86400 && $userpwd->maskChanged()) { // 24h
|
|
$pcd = 180;
|
|
}
|
|
}
|
|
$pcd = 0; // FIXME
|
|
// Pre-cooldown needed
|
|
if ($pcd > 0 && $param_board !== '!signin') {
|
|
// Extra pre-cooldown for unverified users
|
|
if (!$userpwd->verifiedLevel()) {
|
|
$pcd += twister_captcha_get_pcd_penalty($param_board, $param_thread_id);
|
|
}
|
|
|
|
$ticket_ts = twister_captcha_decode_ticket($_GET['ticket'], $user_long_ip, $param_board);
|
|
|
|
$bypassable = false;
|
|
|
|
if ($param_thread_id === 0) {
|
|
$pcd_msg = TWISTER_ERR_PCD_THREAD;
|
|
}
|
|
else if ($param_thread_id !== 1 && $param_board !== '!') {
|
|
$pcd_msg = TWISTER_ERR_PCD_REPLY;
|
|
}
|
|
|
|
if ($param_thread_id !== 1 && $pcd >= 900) {
|
|
$pcd_msg = TWISTER_ERR_PCD_SIGNIN;
|
|
$bypassable = true;
|
|
}
|
|
|
|
if (!$ticket_ts) {
|
|
//if (TWISTER_USE_TICKET_CAPTCHA && !in_array((int)$_SERVER['HTTP_X_GEO_ASN'], TWISTER_WHITELIST_ASNS)) {
|
|
if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) {
|
|
twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
|
|
}
|
|
else {
|
|
twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
|
|
}
|
|
|
|
die();
|
|
}
|
|
|
|
$ticket_lifetime = $now - $ticket_ts;
|
|
|
|
if ($ticket_lifetime < $pcd) {
|
|
$pcd = $pcd - $ticket_lifetime;
|
|
twister_captcha_output_ticket_pcd(null, $pcd, $pcd_msg, $bypassable);
|
|
die();
|
|
}
|
|
|
|
// Ticket expired
|
|
if ($ticket_lifetime >= TWISTER_TICKET_TTL) {
|
|
if (TWISTER_USE_TICKET_CAPTCHA && twister_captcha_need_hcaptcha()) {
|
|
twister_captcha_process_ticket_captcha($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
|
|
}
|
|
else {
|
|
twister_captcha_output_new_ticket($pcd, $now, $user_long_ip, $param_board, $pcd_msg, $bypassable);
|
|
}
|
|
|
|
die();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adjust difficulty
|
|
*/
|
|
|
|
$ip_ttl_static = TWISTER_PWD_TTL_IP_STATIC;
|
|
$mask_ttl_static = TWISTER_PWD_TTL_MASK_STATIC;
|
|
|
|
$ip_ttl_min = TWISTER_PWD_TTL_IP_MIN;
|
|
$idle_ttl = TWISTER_PWD_TTL_IDLE;
|
|
|
|
// Serve max len twister to bad actors
|
|
$bad_actor = false;
|
|
|
|
if (!isset($_SERVER['HTTP_USER_AGENT']) || !$_SERVER['HTTP_USER_AGENT']) {
|
|
$bad_actor = true;
|
|
}
|
|
else if (preg_match(TWISTER_BAD_UA, $_SERVER['HTTP_USER_AGENT'])) {
|
|
$bad_actor = true;
|
|
}
|
|
else if (twister_captcha_is_req_suspicious()) {
|
|
$bad_actor = true;
|
|
}
|
|
|
|
// Serve static captcha to known users.
|
|
// Serve max length captchas for unknown users.
|
|
// Only applies to replies. Theads always use slider captchas.
|
|
if ($param_thread_id !== 0 && !$bad_actor) {
|
|
// Known and post count check
|
|
if ($userpwd->isUserKnown(TWISTER_PWD_STATIC_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_STATIC_POSTS) {
|
|
// Inactivity and IP change checks for unverified users
|
|
if ($userpwd->verifiedLevel()) {
|
|
$use_static = true;
|
|
$char_count = TWISTER_CHARS_MIN;
|
|
}
|
|
else if ($userpwd->idleLifetime() <= TWISTER_PWD_SLIDER_IDLE && !$userpwd->ipChanged()) {
|
|
$use_static = true;
|
|
$char_count = TWISTER_CHARS;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
// Debug controls
|
|
if (DEV_MODE) {
|
|
if (isset($_GET['mc_stats']) && $_GET['mc_stats']) {
|
|
$data = [
|
|
$m->getServerList(),
|
|
$m->getStats()
|
|
];
|
|
echo json_encode($data);
|
|
die();
|
|
}
|
|
|
|
if (isset($_GET['del_key']) && $_GET['del_key']) {
|
|
$m->delete($_GET['del_key']);
|
|
die("ok");
|
|
}
|
|
|
|
if (isset($_GET['get_key']) && $_GET['get_key']) {
|
|
var_dump($m->get($_GET['get_key']));
|
|
die();
|
|
}
|
|
|
|
if (isset($_GET['mode'])) {
|
|
if ($_GET['mode'] === 'static') {
|
|
$use_static = true;
|
|
}
|
|
else if ($_GET['mode'] === 'twister') {
|
|
$use_static = false;
|
|
}
|
|
}
|
|
|
|
if (isset($_GET['difficulty']) && $_GET['difficulty']) {
|
|
$difficulty = (int)$_GET['difficulty'];
|
|
}
|
|
}
|
|
|
|
// ---
|
|
|
|
// Check captcha bypassing credits
|
|
if (TWISTER_ALLOW_NOOP && !in_array($param_board, TWISTER_NO_NOOP_BOARDS) && !$bad_actor && $param_thread_id !== 0) {
|
|
if ($userpwd->isUserKnown(TWISTER_PWD_NOOP_KNOWN) && $userpwd->postCount() >= TWISTER_PWD_NOOP_POSTS) {
|
|
$credits = twister_captcha_get_credits($m, $userpwd->getPwd());
|
|
|
|
if ($credits !== false && $credits[0] > 0) {
|
|
$data = [
|
|
'challenge' => 'noop',
|
|
'ttl' => min(TWISTER_TTL, $credits[1] - $now),
|
|
'cd' => TWISTER_COOLDOWN
|
|
];
|
|
|
|
twister_captcha_output_data($data);
|
|
|
|
die();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check cooldown
|
|
$should_cd_until = twister_captcha_get_cooldown($m, $user_long_ip);
|
|
|
|
if ($should_cd_until === false) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (scdu)');
|
|
}
|
|
|
|
if ($should_cd_until > 1) {
|
|
twister_captcha_error(TWISTER_ERR_COOLDOWN, ['cd' => $should_cd_until - $now]);
|
|
}
|
|
|
|
// Number of unsolved captchas requested recently
|
|
$unsolved_count = twister_captcha_get_session($m, $user_long_ip);
|
|
|
|
if ($unsolved_count === false) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (gus1)');
|
|
}
|
|
|
|
if ($unsolved_count > 2) {
|
|
$cooldown = TWISTER_COOLDOWN_LONG * min($unsolved_count, 20);
|
|
|
|
if ($unsolved_count > 10) {
|
|
$cooldown = 300;
|
|
}
|
|
}
|
|
else {
|
|
$cooldown = TWISTER_COOLDOWN;
|
|
}
|
|
|
|
if (twister_captcha_store_cooldown($m, $user_long_ip, $now + $cooldown) === false) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (sc0)');
|
|
}
|
|
|
|
// Generate images
|
|
$c = new TwisterCaptcha(TWISTER_FONT_PATH);
|
|
$c->setDifficulty($difficulty);
|
|
|
|
// Adjust features
|
|
// User is suspicious
|
|
$should_harden = $bad_actor && ($userpwd->ipChanged() || !$userpwd->isUserKnownOrVerified(7200) || mt_rand(0, 9) === 9);
|
|
|
|
$should_fog = false;
|
|
|
|
if ($should_harden) {
|
|
$use_static = false;
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useInkBlot(true);
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useNegateBlotFilter(true);
|
|
}
|
|
}
|
|
else {
|
|
$c->useEdgeBlock(true);
|
|
}
|
|
|
|
$c->useFakeCharPadding(true);
|
|
$c->useJumpyMode(true);
|
|
|
|
if (mt_rand(0, 9) === 9) {
|
|
$c->setDifficulty(TwisterCaptcha::LEVEL_LUNATIC);
|
|
}
|
|
else {
|
|
$c->setDifficulty(TwisterCaptcha::LEVEL_HARD);
|
|
}
|
|
|
|
if (mt_rand(0, 9) === 9) {
|
|
$char_count = 8;
|
|
}
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useGridLines(true);
|
|
}
|
|
else {
|
|
$c->useScoreLines(true);
|
|
}
|
|
}
|
|
// Other new users
|
|
else if (true || $userpwd->postCount() < 5 || $userpwd->maskLifetime() < 3600) {
|
|
if (!$use_static) {
|
|
$_boards = [ 'a', 'v', 'vg', 'co', 'vp', 'g', 'biz', 'b', 'vt', 'mu', 'pol', 'tv', 'sp', 'int', 'soc', 'test' ];
|
|
|
|
if (true || in_array($param_board, $_boards) || $param_thread_id == 0) {
|
|
$should_fog = true;
|
|
}
|
|
else {
|
|
$c->useInkBlot(true);
|
|
$c->useScoreLines(true);
|
|
}
|
|
}
|
|
else {
|
|
$c->useInkBlot(true);
|
|
//$c->useFakeCharPadding(true);
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useJumpyMode(true);
|
|
}
|
|
}
|
|
|
|
$char_count = mt_rand(TWISTER_CHARS, TWISTER_CHARS_MAX);
|
|
}
|
|
|
|
if ($param_board === '!signin') {
|
|
$should_fog = true;
|
|
$use_static = false;
|
|
}
|
|
|
|
if ($use_static) {
|
|
if (mt_rand(0, 9) === 9) {
|
|
$c->useScoreLines(true);
|
|
}
|
|
|
|
list($challenge_str, $img, $img_width, $img_height) = $c->generateStatic($char_count);
|
|
$img_bg = null;
|
|
}
|
|
else {
|
|
if (true || $should_fog) {
|
|
if (false && $param_board == 'co' && $param_thread_id == 0) {
|
|
$c->useEdgeDetect(true);
|
|
$c->useSpecialRot(true);
|
|
//$c->useOverlayId(5, true);
|
|
$c->useInvert(true);
|
|
//$c->useAltBlackWhite(true);
|
|
$c->useGridLines(true);
|
|
$c->useSimplexBg(true);
|
|
//$c->useEmboss(true);
|
|
//list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count);
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(7, 28, 28);
|
|
//list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterV(3);
|
|
}
|
|
else {
|
|
$_olid = mt_rand(0, 6);
|
|
|
|
if ($_olid) {
|
|
$c->useOverlayId($_olid, (bool)mt_rand(0, 1));
|
|
}
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useInvert(true);
|
|
}
|
|
|
|
if (mt_rand(0, 3) === 3) {
|
|
$c->useFakeCharPadding(true);
|
|
}
|
|
|
|
if ($param_thread_id == 0) {
|
|
$_olid = 2;
|
|
}
|
|
else {
|
|
$_olid = mt_rand(1, 2);
|
|
}
|
|
|
|
$_olid = 2;
|
|
|
|
// Simplex
|
|
if ($_olid === 1) {
|
|
if (mt_rand(0, 3) === 3) {
|
|
$c->useScoreLines(true);
|
|
}
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useInvert(true);
|
|
}
|
|
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHSimplex($char_count);
|
|
}
|
|
// Fog New
|
|
else if ($_olid === 2) {
|
|
if (mt_rand(0, 1)) {
|
|
$c->useSimplexBg(true);
|
|
}
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useEmboss(true);
|
|
}
|
|
else {
|
|
$c->useEdgeDetect(true);
|
|
}
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useAltBlackWhite(true);
|
|
}
|
|
|
|
if (mt_rand(0, 1)) {
|
|
$c->useInvert(true);
|
|
}
|
|
|
|
//if (mt_rand(0, 3) === 3) {
|
|
//$c->useSpecialRot(true);
|
|
//$char_count = 5;
|
|
//}
|
|
|
|
if (isset($_SERVER['HTTP_X_BOT_SCORE'])) {
|
|
$_bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE'];
|
|
}
|
|
else {
|
|
$_bot_score = 100;
|
|
}
|
|
|
|
if ($param_board === '!signin') {
|
|
if ($_bot_score > 95) {
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50);
|
|
}
|
|
else {
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2);
|
|
}
|
|
}
|
|
else {
|
|
//$c->useSpecialRot(true);
|
|
|
|
if ($_bot_score <= 70 && mt_rand(0, 2) === 0) {
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateSimpleTask(2);
|
|
}
|
|
else {
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew(5, 30, 50);
|
|
}
|
|
}
|
|
}
|
|
// Default
|
|
else {
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterHFogNew($char_count);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
list($challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width) = $c->generateTwisterH($char_count);
|
|
}
|
|
}
|
|
|
|
if (!$challenge_str) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (ncs1)');
|
|
}
|
|
|
|
list($challenge_uid, $challenge_hash) = TwisterCaptcha::getChallengeHash(
|
|
$challenge_str,
|
|
[$user_ip, $password, $user_agent, $param_board, $param_thread_id]
|
|
);
|
|
|
|
if (!$challenge_uid || !$challenge_hash) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (gch1)');
|
|
}
|
|
|
|
// Register challenge
|
|
if (twister_captcha_store_challenge($m, $challenge_uid, $user_long_ip, $now + TWISTER_TTL) === false) {
|
|
twister_captcha_error(TWISTER_ERR_GENERIC . ' (sch1)');
|
|
}
|
|
|
|
// Register unsolved session
|
|
//if (twister_captcha_store_session($m, $user_long_ip, $unsolved_count + 1, $now + TWISTER_TTL_UNSOLVED) === false) {
|
|
// twister_captcha_error(TWISTER_ERR_GENERIC . ' (sus1)');
|
|
//}
|
|
|
|
// Generate base 64 urls of images
|
|
list($img_b64, $img_bg_b64) = TwisterCaptcha::getBase64Images($img, $img_bg);
|
|
|
|
$data = [
|
|
'challenge' => "$challenge_uid.$challenge_hash",
|
|
'ttl' => TWISTER_TTL,
|
|
'cd' => $cooldown,
|
|
'img' => $img_b64,
|
|
'img_width' => $img_width,
|
|
'img_height' => $img_height
|
|
];
|
|
|
|
if ($img_bg) {
|
|
$data['bg'] = $img_bg_b64;
|
|
$data['bg_width'] = $bg_width;
|
|
}
|
|
|
|
if (twister_captcha_should_purge_ticket($_GET['ticket'], $now)) {
|
|
$data['ticket'] = false;
|
|
}
|
|
|
|
twister_captcha_output_data($data);
|