" } // // Эти функции: // - rt_shield_chal_verify(): проверяет подпись/exp/host/rt_cid (если есть) // - rt_shield_chal_seen_update(): хранит "видели ли этот токен" + сколько разных IP/rt_cid // чтобы ловить повторное использование токена (replay) бот-сетями. // ---------------------------- if (!function_exists('rt_shield__hash_equals')) { function rt_shield__hash_equals($a, $b) { if (!is_string($a)) $a = (string)$a; if (!is_string($b)) $b = (string)$b; if (function_exists('hash_equals')) { return hash_equals($a, $b); } $len = strlen($a); if ($len !== strlen($b)) return false; $res = $a ^ $b; $ret = 0; for ($i = 0; $i < $len; $i++) { $ret |= ord($res[$i]); } return $ret === 0; } } if (!function_exists('rt_shield__b64url_decode')) { function rt_shield__b64url_decode($s) { $s = (string)$s; $s = strtr($s, '-_', '+/'); $pad = strlen($s) % 4; if ($pad) $s .= str_repeat('=', 4 - $pad); $out = base64_decode($s, true); return ($out === false) ? '' : $out; } } if (!function_exists('rt_shield_chal_secret')) { function rt_shield_chal_secret($clientId) { $clientId = (int)$clientId; if (!defined('GLOBAL_TOKEN_SALT')) return ''; return 'shield_chal|' . $clientId . '|' . GLOBAL_TOKEN_SALT; } } // Verify challenge token (format + signature + payload checks) // Returns array: // ok(bool), err(string), payload(array|null), sig_ok(bool) if (!function_exists('rt_shield_chal_verify')) { function rt_shield_chal_verify($chal, $clientId, $hostExpected, $rtCidExpected) { $out = array( 'ok' => false, 'sig_ok' => false, 'err' => '', 'payload' => null, ); $chal = trim((string)$chal); if ($chal === '') { $out['err'] = 'missing'; return $out; } // basic format b64.sig $dot = strrpos($chal, '.'); if ($dot === false) { $out['err'] = 'format'; return $out; } $b64 = substr($chal, 0, $dot); $sig = substr($chal, $dot + 1); if ($b64 === '' || $sig === '') { $out['err'] = 'format'; return $out; } $secret = rt_shield_chal_secret($clientId); if ($secret === '') { $out['err'] = 'no_secret'; return $out; } $expSig = hash_hmac('sha256', $b64, $secret); if (!rt_shield__hash_equals($expSig, $sig)) { $out['err'] = 'sig_bad'; return $out; } $out['sig_ok'] = true; $raw = rt_shield__b64url_decode($b64); if ($raw === '') { $out['err'] = 'b64_bad'; return $out; } $payload = json_decode($raw, true); if (!is_array($payload)) { $out['err'] = 'json_bad'; return $out; } $out['payload'] = $payload; // cid must match client_id $pcid = isset($payload['cid']) ? (int)$payload['cid'] : 0; if ($pcid !== (int)$clientId) { $out['err'] = 'cid_mismatch'; return $out; } // host match (soft, but if mismatch -> bad) $ph = isset($payload['h']) ? strtolower(trim((string)$payload['h'])) : ''; $hostExpected = strtolower(trim((string)$hostExpected)); if ($hostExpected !== '' && $ph !== '' && $hostExpected !== $ph) { $out['err'] = 'host_mismatch'; return $out; } // exp check $pexp = isset($payload['exp']) ? (int)$payload['exp'] : 0; if ($pexp <= 0) { $out['err'] = 'exp_bad'; return $out; } $now = time(); if ($pexp <= ($now + 1)) { $out['err'] = 'expired'; return $out; } // optional rc binding $prc = isset($payload['rc']) ? trim((string)$payload['rc']) : ''; $rtCidExpected = trim((string)$rtCidExpected); if ($prc !== '' && $rtCidExpected !== '' && $prc !== $rtCidExpected) { $out['err'] = 'rc_mismatch'; return $out; } $out['ok'] = true; $out['err'] = ''; return $out; } } // Ensure tables for chal replay stats if (!function_exists('rt_shield_chal_seen_init')) { function rt_shield_chal_seen_init($pdo) { if (!($pdo instanceof PDO)) return false; // main $pdo->exec( "CREATE TABLE IF NOT EXISTS shield_chal_seen ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, client_id INT NOT NULL, chal_hash CHAR(64) NOT NULL, first_ts INT NOT NULL, last_ts INT NOT NULL, hits_total INT NOT NULL DEFAULT 1, bucket_10m INT NOT NULL, hits_10m INT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uniq_client_hash (client_id, chal_hash), KEY idx_client_last (client_id, last_ts) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); // ip $pdo->exec( "CREATE TABLE IF NOT EXISTS shield_chal_seen_ip ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, client_id INT NOT NULL, chal_hash CHAR(64) NOT NULL, ip VARCHAR(64) NOT NULL, last_ts INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uniq_client_hash_ip (client_id, chal_hash, ip), KEY idx_client_hash_last (client_id, chal_hash, last_ts) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); // cid $pdo->exec( "CREATE TABLE IF NOT EXISTS shield_chal_seen_cid ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, client_id INT NOT NULL, chal_hash CHAR(64) NOT NULL, rt_cid VARCHAR(64) NOT NULL, last_ts INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uniq_client_hash_cid (client_id, chal_hash, rt_cid), KEY idx_client_hash_last (client_id, chal_hash, last_ts) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); return true; } } // Update replay stats and return counters: // hits_10m, uniq_ip_1h, uniq_cid_1h if (!function_exists('rt_shield_chal_seen_update')) { function rt_shield_chal_seen_update($pdo, $clientId, $chal, $ip, $rtCid) { $out = array('hits_10m' => 0, 'uniq_ip_1h' => 0, 'uniq_cid_1h' => 0); if (!($pdo instanceof PDO)) return $out; $chal = trim((string)$chal); if ($chal === '') return $out; $clientId = (int)$clientId; $ip = rt_shield__valid_ip($ip); $rtCid = trim((string)$rtCid); if (strlen($rtCid) > 64) $rtCid = substr($rtCid, 0, 64); try { rt_shield_chal_seen_init($pdo); } catch (Exception $e0) { return $out; } $hash = hash('sha256', $chal); $now = time(); $bucket10 = (int)floor($now / 600); try { // main upsert $stmt = $pdo->prepare( "INSERT INTO shield_chal_seen (client_id, chal_hash, first_ts, last_ts, hits_total, bucket_10m, hits_10m, created_at, updated_at) VALUES (:cid, :h, :now, :now, 1, :b10, 1, NOW(), NOW()) ON DUPLICATE KEY UPDATE last_ts = VALUES(last_ts), hits_total = hits_total + 1, hits_10m = IF(bucket_10m = VALUES(bucket_10m), hits_10m + 1, 1), bucket_10m = VALUES(bucket_10m), updated_at = NOW()" ); $stmt->execute(array(':cid' => $clientId, ':h' => $hash, ':now' => $now, ':b10' => $bucket10)); // ip upsert if ($ip !== '') { $stmt = $pdo->prepare( "INSERT INTO shield_chal_seen_ip (client_id, chal_hash, ip, last_ts, created_at, updated_at) VALUES (:cid, :h, :ip, :now, NOW(), NOW()) ON DUPLICATE KEY UPDATE last_ts = VALUES(last_ts), updated_at = NOW()" ); $stmt->execute(array(':cid' => $clientId, ':h' => $hash, ':ip' => $ip, ':now' => $now)); } // cid upsert if ($rtCid !== '') { $stmt = $pdo->prepare( "INSERT INTO shield_chal_seen_cid (client_id, chal_hash, rt_cid, last_ts, created_at, updated_at) VALUES (:cid, :h, :rc, :now, NOW(), NOW()) ON DUPLICATE KEY UPDATE last_ts = VALUES(last_ts), updated_at = NOW()" ); $stmt->execute(array(':cid' => $clientId, ':h' => $hash, ':rc' => $rtCid, ':now' => $now)); } // fetch hits_10m $stmt = $pdo->prepare("SELECT hits_10m FROM shield_chal_seen WHERE client_id=:cid AND chal_hash=:h LIMIT 1"); $stmt->execute(array(':cid' => $clientId, ':h' => $hash)); $out['hits_10m'] = (int)$stmt->fetchColumn(); $since = $now - 3600; // uniq ip 1h $stmt = $pdo->prepare("SELECT COUNT(*) FROM shield_chal_seen_ip WHERE client_id=:cid AND chal_hash=:h AND last_ts >= :since"); $stmt->execute(array(':cid' => $clientId, ':h' => $hash, ':since' => $since)); $out['uniq_ip_1h'] = (int)$stmt->fetchColumn(); // uniq cid 1h $stmt = $pdo->prepare("SELECT COUNT(*) FROM shield_chal_seen_cid WHERE client_id=:cid AND chal_hash=:h AND last_ts >= :since"); $stmt->execute(array(':cid' => $clientId, ':h' => $hash, ':since' => $since)); $out['uniq_cid_1h'] = (int)$stmt->fetchColumn(); // occasional cleanup try { if (mt_rand(1, 500) === 1) { $cut = $now - 7 * 86400; $pdo->prepare("DELETE FROM shield_chal_seen WHERE last_ts < :cut LIMIT 5000")->execute(array(':cut' => $cut)); $pdo->prepare("DELETE FROM shield_chal_seen_ip WHERE last_ts < :cut LIMIT 5000")->execute(array(':cut' => $cut)); $pdo->prepare("DELETE FROM shield_chal_seen_cid WHERE last_ts < :cut LIMIT 5000")->execute(array(':cut' => $cut)); } } catch (Exception $e9) {} } catch (Exception $e1) { // ignore } return $out; } } console.error("Retach loader: domain not allowed for this client (good.retach.ru)");