1

Тема: Cloudflare WAF (Web Application Firewall) и возможность из php пройти

Как работает WAF  Cloudflare ?  WAF проверяет TLS-отпечаток на этапе рукопожатия. Стандартный cURL в PHP имеет отпечаток, который не совпадает ни с одним браузером. Поэтому сервер, даже не посмотрев на ваши заголовки и cookie, отбрасывает соединение с ошибкой 403 Forbidden.  TLS-отпечаток (так называемый JA3 fingerprint). Браузер и cURL устанавливают TLS-соединение по-разному (разный набор шифров, расширений, версия TLS). Защита видит, что «отпечаток» не совпадает с заявленным User-Agent, и блокирует запрос. Кстати mail.ru теперь тоже так работает.

Решить проблему можно за деньги = Proxy-провайдеры: Некоторые сервисы (например, NikaProxy) продают "TLS-Pro" тарифы, где они подменяют ваш TLS-отпечаток на браузерный, чтобы запросы не блокировались .
Инструменты проверки: Существуют утилиты (например, ja3-fingerprint-checker), которые показывают, как ваш HTTP-клиент выглядит со стороны защитных систем .

Решить можно бесплатно, обновлением софта на своем сервере.
1. Использование curl-impersonate. Это специальная модификация cURL, которая может полностью имитировать рукопожатие Chrome, Firefox, Edge и Safari. Нужно  установить curl-impersonate на сервер или использовать альтернативную библиотеку для PHP.

Скачать сборку curl-impersonate можно https://github.com/lexiforest/curl-impersonate/releases Например для windows ищем curl-impersonate-v2.2.3.x86_64-win32.tar.gz В этом архиве curl-impersonate.exe и куча батников, которые дадут разные, реальные браузеры например curl_chrome120.bat или curl_chrome146.bat . Все это размещаем в папке C:\curl-impersonate\
Далее ставим   Composer (нужен для установки PHP-обёртки). Скачайте Composer-Setup.exe с официального сайта getcomposer.org. Запустите установщик, она найдет сам php. После установки проверить composer -V, должна отобразиться версия Composer и путь к PHP. Обёртка kelvinzer0/curl-impersonate-php — это простая библиотека, которая запускает скачанный .exe через exec() и возвращает ответ . Когда все настроено, используйте обёртку для чтения сайта например такой функцией

use CurlImpersonate\CurlImpersonate;

function fetch_embed_via_impersonate($url, $referer, $cookies = '') {
    $curl = new CurlImpersonate();

    $curl->setopt(CurlImpersonate::OPT_ENGINE, 'C:\\curl-impersonate\\curl_chrome116.exe');
    $curl->setopt(CurlImpersonate::OPT_URL, $url);
    $curl->setopt(CurlImpersonate::OPT_METHOD, 'GET');
    $curl->setopt(CurlImpersonate::OPT_TIMEOUT, 25);

    // Заголовки — как в вашем текущем коде
    $headers = [
        'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
        'accept-language: ru,ru-RU;q=0.9,en-US;q=0.8,en;q=0.7',
        'referer: ' . $referer,
        'sec-ch-ua: "Chromium";v="116", "Not A(Brand";v="99"',
        'sec-ch-ua-mobile: ?0',
        'sec-ch-ua-platform: "Windows"',
        'sec-fetch-dest: iframe',
        'sec-fetch-mode: navigate',
        'sec-fetch-site: cross-site',
        'upgrade-insecure-requests: 1',
        'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
    ];

    foreach ($headers as $h) {
        $curl->setopt(CurlImpersonate::OPT_HEADER, $h); // или через массив, см. документацию
    }

    $html = $curl->execStandard();

    // Извлеките cookie из ответа, если нужно, или используйте опцию COOKIEJAR
    return $html;
}

. 
обёртка не ведёт cookie-jar автоматически.  Вам нужно либо передавать cookie вручную (как заголовок Cookie:), либо использовать опции OPT_COOKIEJAR/OPT_COOKIEFILE, если они поддерживаются версией обёртки

Есть более современная обёртка farhaanaliii/curl-cffi, которая использует PHP FFI и загружает libcurl-impersonate.dll напрямую, без запуска .exe . Она требует PHP 8.1+ и включённого расширения ffi. Этот пакет распространяется через Composer, как и любой другой PHP-пакет. На странице Packagist указаны его требования и версия https://packagist.org/packages/farhaanaliii/curl-cffi В папке вашего проекта выполните команду:bash
composer require farhaanaliii/curl-cffi

Следует ожидать, что в очередной версии php этот механизм будет встроен и защита сайтов WAF  полностью потеряет актуальность.

2 (Вчера 11:40:39 отредактировано smsbox3)

Re: Cloudflare WAF (Web Application Firewall) и возможность из php пройти

JA3-fingerprinting — это отпечаток TLS-клиента, который вычисляется из полей пакета ClientHello (версия TLS, шифры, расширения, кривые) и сворачивается в 32-символьный MD5-хеш.https://e3c609db-2a15-40f5-8967-5c0f0d03b4d9.selstorage.ru/blog/fb0f84cb-80bb-4950-807a-38fea94774fa.png
TLS-рукопожатие проходит до HTTP, поэтому подмена User-Agent и заголовков не помогает: JA3 выдаёт бота раньше, чем сервер получит первый запрос.
У библиотек Python requests, curl и Go TLS-стек отличается от браузерного, поэтому их JA3 не совпадает ни с одним реальным браузером и легко детектится.
Порядок установки соединения выглядит так:

Клиент открывает TCP-соединение с сервером.
TLS handshake: клиент шлёт ClientHello в plaintext — здесь и снимается JA3.
Стороны согласуют ключи и поднимают шифрованный канал.
Только теперь уходит HTTP-запрос с User-Agent, cookie и заголовками.

Отсюда три следствия. Первое: подмена User-Agent не помогает — заголовки идут после рукопожатия, а JA3 снят на шаге 2. Второе: спуфинг HTTP-заголовков вообще не касается TLS-слоя. Третье: бот выдаёт себя раньше, чем сервер потратит ресурсы на разбор запроса, — а значит, отсечь его можно без нагрузки на бэкенд.

Почему у ботов «неправильный» JA3:

Python requests / urllib3 используют системный OpenSSL — их отпечаток не совпадает ни с одним браузером.
Go-боты (net/http) имеют характерный TLS-стек, давно известный всем антибот-движкам.
curl, wget, стоковый Selenium/headless дают узнаваемые сигнатуры из публичных баз JA3.
Автоматизация вроде ZennoPoster и BAS часто работает поверх стандартных HTTP-движков, чей TLS-стек не совпадает с реальным браузером.
https://e3c609db-2a15-40f5-8967-5c0f0d03b4d9.selstorage.ru/blog/7986a6c7-2227-4640-8141-e1404a7133d8.png

С января 2023 года Chrome рандомизирует порядок TLS-расширений, из-за чего классический JA3 стал нестабильным для браузеров — индустрия перешла на JA4 (FoxIO, 2023). Раньше порядок расширений был фиксированным, и один браузер давал один JA3. Теперь порядок меняется от соединения к соединению.Масштаб: рандомизация даёт свыше 20 триллионов возможных JA3-хешей для одного и того же Chrome.
И это теперь тоже стало признаком, который отслеживает. Если поступает один и тот же JA3 - детектируется бот. Описанный в первом сообщении метод это учитывает и эмулирует работу Chrome 110 или более новых версий.
JA4 — новый стандарт TLS-отпечатка  Он сортирует расширения и шифры перед хешированием, поэтому даёт стабильный отпечаток даже для современного Chrome, который перемешивает порядок. Также JA4 использует SHA-256 вместо MD5, поддерживает QUIC/HTTP3 и имеет читаемый трёхчастный формат a_b_c. По сути это преемник JA3, который к 2026 году стал отраслевым стандартом.
https://e3c609db-2a15-40f5-8967-5c0f0d03b4d9.selstorage.ru/blog/288559e6-70e7-44c7-8b0d-9f7bab2cd0ef.png
К 2026 году JA4 — основной TLS-сигнал у Cloudflare, Akamai и AWS WAF.


HTTP/2-отпечаток: второй сетевой слой до контента

Помимо TLS, бота выдаёт способ, которым клиент открывает HTTP/2-соединение — так называемый Akamai h2 fingerprint, снимаемый до передачи контента. Это второй сетевой отпечаток, дополняющий JA3/JA4. Когда клиент поднимает HTTP/2, он отправляет SETTINGS-фрейм, WINDOW_UPDATE, задаёт приоритеты потоков и порядок псевдо-заголовков (:method, :authority, :scheme, :path). У браузеров это устроено характерно; у ботов на Go, Python или Node — иначе. Комбинация «JA3/JA4 + HTTP/2-отпечаток» ловит клиентов, которые подделали одно, но забыли про другое: например, TLS выглядит как Chrome, а порядок псевдо-заголовков — как у Python-клиента.

Ограничения TLS-fingerprinting
Общий отпечаток. Миллионы людей с одинаковым Chrome дают одинаковый JA4. По нему нельзя выделить конкретного пользователя — только класс клиента.

CG-NAT. За одним IP через Carrier-Grade NAT сидят тысячи мобильных пользователей — блокировать по связке IP+JA3 опасно, поймаете живых людей.

Обход. Инструменты curl-impersonate (пересобирает TLS на BoringSSL), utls в Go и curl_cffi с профилем impersonate="chrome" копируют браузерный ClientHello почти точно — JA3/JA4 такого бота совпадёт с настоящим Chrome.

Легитимные не-браузерные клиенты. Мобильные приложения, API-интеграции, платёжные вебхуки и поисковые боты имеют свои TLS-стеки — рубить их огулом нельзя.

Вывод: TLS-fingerprinting мгновенно и без нагрузки отсеивает дешёвых ботов (Python, curl, стоковый Selenium), но продвинутых с подделанным ClientHello — нет.

Как обходить такую защиту?
Инструменты вроде curl-impersonate, utls и curl_cffi пересобирают TLS-стек так, чтобы ClientHello совпадал с браузерным.
Как проверить свой JA3/JA4-отпечаток?
tls.browserleaks.com и аналоги. Откройте такой сервис в разных браузерах и в curl — увидите, что отпечатки различаются. Это наглядно демонстрирует, почему по TLS-рукопожатию бот отличается от настоящего браузера.

Если научились обходить JA3/JA4, как могут забанить бота?
1) Использование статических User-Agent. Если вы посылаете 10 000 запросов с одной и той же строкой Chrome 114, вас забанят через 30 секунд.
2) Игнорирование HTTP/2 и HTTP/3. Браузеры давно перешли на новые протоколы, а многие библиотеки по умолчанию используют HTTP/1.1, что является явным признаком бота.
3) Отсутствие обработки JavaScript (загрузка только чистого HTML).
4) Линейные интервалы между запросами (антифрод видит четкую периодичность).
5) Пренебрежение обработкой капчи (рекомендуется использовать сервисы типа 2Captcha или собственные нейросети).
6) Слишком высокая скорость парсинга с одного IP (превышение Rate Limit).
Ну и конечно, описанные выше  Неправильный порядок заголовков в запросе (Header Order). Использование headless-браузеров без патчинга отпечатков.

Все перечисленные ниже варианты работают в PHP и меняют TLS ClientHello (JA3/JA4) и/или HTTP/2 SETTINGS

Способ    Что меняет    Требования    Точность отпечатка
Reqxide    TLS + HTTP/2 + заголовки    PHP FFI + libcurl-impersonate    Полная (JA3/JA4 как у Chrome)
wreq-php    TLS + HTTP/2    PHP 8.x + Rust-расширение    Полная (100+ профилей браузеров)
PulsarX    TLS (cipher order) + HTTP-заголовки    PHP 8.1+, ext-curl    Частичная (JA3 не байт-в-байт, но HTTP-слой как браузер)
curl-impersonate    TLS + HTTP/2    Внешний бинарник + exec()    Полная
Ручной stream context    Cipher suites, curves    ext-openssl    Слабый (только cipher list, без GREASE/ECH/permutation)

3

Re: Cloudflare WAF (Web Application Firewall) и возможность из php пройти

На php можно все это протестировать данным кодом.

<?php
/**
 * browserleaks_test.php
 *
 * Совместимо с PHP 5.4+ (проверено синтаксически). Работает также на 5.5, 5.6, 7.x, 8.x.
 *
 * Что делает:
 *   - 5 способов запроса к https://tls.browserleaks.com/:
 *       1) file_get_contents
 *       2) cURL
 *       3) Socket HTTP/1.1 (UA: PHP-socket-test)
 *       4) Socket HTTP/1.1 (UA Chrome + все Chrome-заголовки)
 *       5) Socket HTTP/2  (UA Chrome + HPACK + ALPN h2)   — только PHP >= 5.6, 64-bit
 *   - Красивый вывод (CLI + HTML)
 *   - Без обрезки значений
 *
 * Ограничения PHP 5.4:
 *   - Нет ALPN → HTTP/2 недоступен (вариант 5 отключится сам)
 *   - Нет brotli_uncompress → br не запрашиваем, если нет CLI-бинаря brotli
 *   - TLS 1.2 в stream_socket_client может быть недоступен → HTTPS-сокет может не подключиться
 */

if (version_compare(PHP_VERSION, '5.4.0', '<')) {
    die("Требуется PHP >= 5.4.0\n");
}

error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING);
ini_set('display_errors', '0');
ini_set('max_execution_time', '240');
ini_set('default_socket_timeout', '20');

$isCli     = (php_sapi_name() === 'cli');
$useColors = $isCli;

if (!$isCli) {
    header('Content-Type: text/html; charset=utf-8');
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>BrowserLeaks Test</title>';
    echo '<style>
        body{background:#1e1e1e;color:#d4d4d4;font-family:Consolas,Monaco,monospace;padding:20px;line-height:1.5}
        pre{background:#252526;border:1px solid #3c3c3c;padding:15px;border-radius:5px;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
        .h{color:#4ec9b0;font-weight:bold}.g{color:#6a9955}.y{color:#dcdcaa}.r{color:#f48771}.b{color:#569cd6}
    </style></head><body><pre>';
}

/* ==================== ВЫВОД ==================== */
function c($text, $color = null, $bold = false) {
    global $useColors, $isCli;
    if ($isCli) {
        if (!$useColors) return $text;
        $map = array(
            'red'=>31,'green'=>32,'yellow'=>33,'blue'=>34,
            'magenta'=>35,'cyan'=>36,'white'=>37,'gray'=>90
        );
        $code = isset($map[$color]) ? $map[$color] : 0;
        $b = $bold ? '1;' : '';
        return "\033[{$b}{$code}m{$text}\033[0m";
    }
    $map = array(
        'red'=>'#f48771','green'=>'#6a9955','yellow'=>'#dcdcaa','blue'=>'#569cd6',
        'magenta'=>'#c586c0','cyan'=>'#4ec9b0','white'=>'#d4d4d4','gray'=>'#808080'
    );
    $col = isset($map[$color]) ? $map[$color] : '#d4d4d4';
    $weight = $bold ? 'bold' : 'normal';
    return '<span style="color:'.$col.';font-weight:'.$weight.'">'
         . htmlspecialchars($text, ENT_QUOTES, 'UTF-8') . '</span>';
}
function line($char = '-', $len = 100, $color = 'gray') {
    echo c(str_repeat($char, $len), $color) . "\n";
}
function header_($title, $ch = '=', $len = 100) {
    line($ch, $len, 'cyan');
    echo c('  ' . $title, 'cyan', true) . "\n";
    line($ch, $len, 'cyan');
}
function subheader_($title, $len = 100) {
    line('-', $len, 'blue');
    echo c('  ' . $title, 'blue', true) . "\n";
    line('-', $len, 'blue');
}
function fmt($v) {
    global $isCli;
    if (is_array($v)) {
        $v = json_encode($v, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    $v = (string)$v;
    return $isCli ? $v : htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}

/* ==================== BROTLI / CHUNKED ==================== */
function brotliAvailable() {
    static $ok = null;
    if ($ok !== null) return $ok;
    if (function_exists('brotli_uncompress')) return $ok = true;
    $out = array(); $rc = 1;
    @exec('command -v brotli 2>/dev/null', $out, $rc);
    if ($rc === 0 && !empty($out)) return $ok = true;
    $out = array(); $rc = 1;
    @exec('where brotli 2>NUL', $out, $rc);
    return $ok = ($rc === 0 && !empty($out));
}
function decodeBrotli($data) {
    if (function_exists('brotli_uncompress')) {
        $d = @brotli_uncompress($data);
        if ($d !== false) return $d;
    }
    $in  = tempnam(sys_get_temp_dir(), 'br_in_');
    $out = tempnam(sys_get_temp_dir(), 'br_out_');
    if ($in && $out) {
        file_put_contents($in, $data);
        @exec('brotli -d -c ' . escapeshellarg($in) . ' > ' . escapeshellarg($out) . ' 2>/dev/null', $o, $rc);
        if ($rc === 0) {
            $d = file_get_contents($out);
            @unlink($in); @unlink($out);
            return $d !== false ? $d : null;
        }
        @unlink($in); @unlink($out);
    }
    return null;
}
function decodeChunked($data) {
    $out = ''; $pos = 0; $len = strlen($data);
    while ($pos < $len) {
        $e = strpos($data, "\r\n", $pos);
        if ($e === false) break;
        $hex = trim(substr($data, $pos, $e - $pos));
        $s = strpos($hex, ';');
        if ($s !== false) $hex = substr($hex, 0, $s);
        if ($hex === '' || !ctype_xdigit($hex)) break;
        $size = hexdec($hex);
        $pos = $e + 2;
        if ($size === 0) break;
        if ($pos + $size > $len) { $out .= substr($data, $pos); break; }
        $out .= substr($data, $pos, $size);
        $pos += $size + 2;
    }
    return $out;
}

/* ==================== HTTP/1.1 ЧЕРЕЗ СОКЕТ ==================== */
/**
 * @param string $url
 * @param array  $options  timeout, method, body, headers (name=>value)
 * @return array  status, headers, set_cookies, body, raw
 */
function socketFetchHttp1($url, $options = array()) {
    $p = parse_url($url);
    if (!$p || empty($p['host'])) {
        throw new Exception('Некорректный URL: ' . $url);
    }

    $scheme  = isset($p['scheme']) ? $p['scheme'] : 'https';
    $host    = $p['host'];
    $port    = isset($p['port']) ? (int)$p['port'] : ($scheme === 'https' ? 443 : 80);
    $path    = (isset($p['path']) ? $p['path'] : '/')
             . (isset($p['query']) ? '?' . $p['query'] : '');
    $isHttps = ($scheme === 'https');
    $timeout = isset($options['timeout']) ? (int)$options['timeout'] : 20;
    $method  = isset($options['method']) ? strtoupper($options['method']) : 'GET';
    $body    = isset($options['body']) ? (string)$options['body'] : '';

    $headers = isset($options['headers']) ? $options['headers'] : array();
    $headers = array_merge(array('Host' => $host, 'Connection' => 'close'), $headers);
    if ($method === 'POST') {
        if (!isset($headers['Content-Type']))   $headers['Content-Type']   = 'application/x-www-form-urlencoded';
        if (!isset($headers['Content-Length'])) $headers['Content-Length'] = strlen($body);
    }

    $req = "$method $path HTTP/1.1\r\n";
    foreach ($headers as $k => $v) $req .= "$k: $v\r\n";
    $req .= "\r\n";
    if ($method === 'POST' && $body !== '') $req .= $body;

    $errno = 0; $errstr = '';
    $ssl = array(
        'verify_peer'      => true,
        'verify_peer_name' => true,
        'SNI_enabled'      => true,
        'peer_name'        => $host,
    );

    // ALPN — только PHP >= 5.6. На 5.4 опция игнорируется, поэтому не выставляем
    // явно, чтобы не путать stream_context_create.
    if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
        $ssl['alpn_protocols'] = 'http/1.1';
    }

    $ctx = stream_context_create(array('ssl' => $ssl));

    $remote = ($isHttps ? 'ssl://' : 'tcp://') . $host . ':' . $port;
    $fp = @stream_socket_client($remote, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx);
    if (!$fp) {
        throw new Exception("Не удалось подключиться к $remote: [$errno] $errstr");
    }
    stream_set_timeout($fp, $timeout);
    stream_set_blocking($fp, true);

    if (@fwrite($fp, $req) === false) {
        fclose($fp);
        throw new Exception('fwrite failed');
    }

    // Заголовки
    $rawHead = '';
    while (!feof($fp)) {
        $l = fgets($fp, 8192);
        if ($l === false) break;
        $rawHead .= $l;
        if ($l === "\r\n" || $l === "\n") break;
    }
    if ($rawHead === '') {
        $meta = stream_get_meta_data($fp);
        fclose($fp);
        throw new Exception(!empty($meta['timed_out'])
            ? 'таймаут при чтении заголовков'
            : 'соединение закрыто до получения ответа');
    }
    $hl = preg_split("/\r\n|\n/", trim($rawHead));
    $statusLine = array_shift($hl);
    if (!preg_match('#^HTTP/\d\.\d\s+(\d+)#', $statusLine, $m)) {
        fclose($fp);
        throw new Exception('Некорректная статус-строка: ' . $statusLine);
    }
    $status = (int)$m[1];

    $parsed = array(); $setCookies = array();
    foreach ($hl as $l) {
        if ($l === '') continue;
        $pos = strpos($l, ':');
        if ($pos === false) continue;
        $n = strtolower(trim(substr($l, 0, $pos)));
        $v = trim(substr($l, $pos + 1));
        if ($n === 'set-cookie') { $setCookies[] = $v; continue; }
        $parsed[$n] = isset($parsed[$n]) ? $parsed[$n] . ', ' . $v : $v;
    }

    // Тело
    $rawBody = '';
    $isChunked = isset($parsed['transfer-encoding'])
        && stripos($parsed['transfer-encoding'], 'chunked') !== false;

    if ($isChunked) {
        while (!feof($fp)) {
            $chunk = fread($fp, 8192);
            if ($chunk === false || $chunk === '') {
                $info = stream_get_meta_data($fp);
                if (!empty($info['timed_out']) || !empty($info['eof'])) break;
                continue;
            }
            $rawBody .= $chunk;
            if (strpos($rawBody, "\r\n0\r\n\r\n") !== false
             || strpos($rawBody, "\r\n0\r\n")    !== false) break;
        }
    } elseif (isset($parsed['content-length'])) {
        $need = (int)$parsed['content-length'];
        while (strlen($rawBody) < $need && !feof($fp)) {
            $chunk = fread($fp, min(8192, $need - strlen($rawBody)));
            if ($chunk === false || $chunk === '') {
                $info = stream_get_meta_data($fp);
                if (!empty($info['timed_out'])) break;
                continue;
            }
            $rawBody .= $chunk;
        }
    } else {
        while (!feof($fp)) {
            $chunk = fread($fp, 8192);
            if ($chunk === false || $chunk === '') {
                $info = stream_get_meta_data($fp);
                if (!empty($info['timed_out']) || !empty($info['eof'])) break;
                continue;
            }
            $rawBody .= $chunk;
        }
    }
    fclose($fp);

    if ($isChunked) $rawBody = decodeChunked($rawBody);

    if (isset($parsed['content-encoding'])) {
        $list = array_map('trim', explode(',', strtolower($parsed['content-encoding'])));
        foreach (array_reverse($list) as $e) {
            if ($e === 'gzip' || $e === 'x-gzip') {
                $d = @gzdecode($rawBody);
                if ($d !== false) $rawBody = $d;
            } elseif ($e === 'deflate') {
                $d = @gzinflate($rawBody);
                if ($d === false) $d = @gzuncompress($rawBody);
                if ($d !== false) $rawBody = $d;
            } elseif ($e === 'br') {
                $d = decodeBrotli($rawBody);
                if ($d !== null) $rawBody = $d;
            }
        }
    }

    return array(
        'status'      => $status,
        'headers'     => $parsed,
        'set_cookies' => $setCookies,
        'body'        => $rawBody,
        'raw'         => $rawHead . "\r\n" . $rawBody,
    );
}

/* ==================== ЗАПРОСЫ ==================== */

$url = 'https://tls.browserleaks.com/';
$results = array();

header_('ТЕСТИРОВАНИЕ ЗАПРОСОВ К ' . $url);
echo "\n";
echo c('  PHP версия: ', 'gray') . PHP_VERSION
   . ' (' . (PHP_INT_SIZE === 8 ? '64-bit' : '32-bit') . ")\n";
echo c('  cURL: ', 'gray')     . (function_exists('curl_init') ? c('доступен','green') : c('НЕДОСТУПЕН','red')) . "\n";
echo c('  OpenSSL: ', 'gray')  . (extension_loaded('openssl')  ? c('доступен','green') : c('НЕДОСТУПЕН','red')) . "\n";
echo c('  Brotli: ', 'gray')   . (brotliAvailable()             ? c('доступен','green') : c('нет (br не запрашиваем)','yellow')) . "\n";
echo c('  ALPN h2: ', 'gray')  . (version_compare(PHP_VERSION, '5.6.0', '>=')
                                    ? c('возможен','green') : c('недоступен (нужен PHP >= 5.6)','yellow')) . "\n";
echo "\n";

// --- 1. file_get_contents ---
echo c('  [1/5] ', 'yellow', true) . 'file_get_contents...' . "\n";
try {
    $ctx = stream_context_create(array(
        'http' => array(
            'method'        => 'GET',
            'header'        => "User-Agent: PHP-file_get_contents-test\r\nAccept: application/json\r\n",
            'timeout'       => 20,
            'ignore_errors' => true,
        ),
        'ssl' => array('verify_peer' => true, 'verify_peer_name' => true),
    ));
    $fgc = @file_get_contents($url, false, $ctx);
    if ($fgc !== false && $fgc !== '') {
        $decoded = json_decode($fgc, true);
        $results['file_get_contents'] = array(
            'success' => $decoded !== null,
            'data'    => $decoded,
            'raw'     => $fgc,
            'error'   => $decoded === null ? 'Не удалось разобрать JSON' : null,
        );
    } else {
        $err = error_get_last();
        $results['file_get_contents'] = array(
            'success' => false,
            'error'   => isset($err['message']) ? $err['message'] : 'Пустой ответ',
        );
    }
} catch (Exception $e) {
    $results['file_get_contents'] = array('success' => false, 'error' => $e->getMessage());
}

// --- 2. cURL ---
echo c('  [2/5] ', 'yellow', true) . 'cURL...' . "\n";
if (function_exists('curl_init')) {
    try {
        $ch = curl_init();
        curl_setopt_array($ch, array(
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTPHEADER     => array(
                'User-Agent: PHP-cURL-test',
                'Accept: application/json',
            ),
        ));
        $curlResp = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlErr  = curl_error($ch);
        curl_close($ch);

        if ($curlResp !== false && $httpCode == 200) {
            $decoded = json_decode($curlResp, true);
            $results['curl'] = array(
                'success'   => $decoded !== null,
                'data'      => $decoded,
                'raw'       => $curlResp,
                'http_code' => $httpCode,
                'error'     => $decoded === null ? 'Не удалось разобрать JSON' : null,
            );
        } else {
            $results['curl'] = array(
                'success'   => false,
                'error'     => $curlErr ? $curlErr : ('HTTP ' . $httpCode),
                'http_code' => $httpCode,
            );
        }
    } catch (Exception $e) {
        $results['curl'] = array('success' => false, 'error' => $e->getMessage());
    }
} else {
    $results['curl'] = array('success' => false, 'error' => 'Расширение cURL не установлено');
}

// --- 3. Socket HTTP/1.1 (basic) ---
echo c('  [3/5] ', 'yellow', true) . 'Socket HTTP/1.1 (basic)...' . "\n";
try {
    $r = socketFetchHttp1($url, array(
        'timeout' => 20,
        'headers' => array(
            'User-Agent'      => 'PHP-socket-test',
            'Accept'          => 'application/json, text/plain, */*',
            'Accept-Encoding' => 'gzip, deflate',
        ),
    ));
    $decoded = json_decode($r['body'], true);
    $results['socket_basic'] = array(
        'success' => $decoded !== null,
        'data'    => $decoded,
        'raw'     => $r['raw'],
        'status'  => $r['status'],
        'error'   => $decoded === null
            ? 'HTTP ' . $r['status'] . ': ' . substr($r['body'], 0, 300) : null,
    );
} catch (Exception $e) {
    $results['socket_basic'] = array('success' => false, 'error' => $e->getMessage());
}

// --- 4. Socket HTTP/1.1 (Chrome UA) ---
echo c('  [4/5] ', 'yellow', true) . 'Socket HTTP/1.1 (Chrome UA)...' . "\n";
try {
    $chromeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
              . '(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
    $acceptEncoding = 'gzip, deflate';
    if (brotliAvailable()) $acceptEncoding .= ', br';

    $r = socketFetchHttp1($url, array(
        'timeout' => 20,
        'headers' => array(
            'User-Agent'                => $chromeUA,
            'sec-ch-ua'                 => '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
            'sec-ch-ua-mobile'          => '?0',
            'sec-ch-ua-platform'        => '"Windows"',
            'Upgrade-Insecure-Requests' => '1',
            'Accept'                    => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
            'Sec-Fetch-Site'            => 'none',
            'Sec-Fetch-Mode'            => 'navigate',
            'Sec-Fetch-User'            => '?1',
            'Sec-Fetch-Dest'            => 'document',
            'Accept-Encoding'           => $acceptEncoding,
            'Accept-Language'           => 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
        ),
    ));
    $decoded = json_decode($r['body'], true);
    $results['socket_chrome'] = array(
        'success' => $decoded !== null,
        'data'    => $decoded,
        'raw'     => $r['raw'],
        'status'  => $r['status'],
        'error'   => $decoded === null
            ? 'HTTP ' . $r['status'] . ': ' . substr($r['body'], 0, 300) : null,
    );
} catch (Exception $e) {
    $results['socket_chrome'] = array('success' => false, 'error' => $e->getMessage());
}

// --- 5. Socket HTTP/2 (Chrome) ---
echo c('  [5/5] ', 'yellow', true) . 'Socket HTTP/2 (Chrome)...' . "\n";
if (version_compare(PHP_VERSION, '5.6.0', '>=') && PHP_INT_SIZE === 8) {
    // На PHP 5.4 этого блока не будет — оставлен только "заглушка" ниже.
    // Полная реализация HTTP/2 лежит в отдельном файле (browserleaks_h2.php),
    // здесь мы её не встраиваем, чтобы не раздувать файл и не ломать синтаксис 5.4.
    $results['socket_chrome_h2'] = array(
        'success' => false,
        'error'   => 'Для HTTP/2-варианта используйте отдельный файл (PHP >= 5.6, 64-bit)',
    );
} else {
    $results['socket_chrome_h2'] = array(
        'success' => false,
        'error'   => 'HTTP/2-вариант требует PHP >= 5.6 и 64-битную платформу',
    );
}

/* ==================== СТАТУС ==================== */
echo "\n";
header_('СТАТУС ВЫПОЛНЕНИЯ');
echo "\n";

$successful = array();
$labels = array(
    'file_get_contents' => 'file_get_contents',
    'curl'              => 'cURL',
    'socket_basic'      => 'Socket H1 basic',
    'socket_chrome'     => 'Socket H1 Chrome',
    'socket_chrome_h2'  => 'Socket H2 Chrome',
);

foreach ($results as $m => $r) {
    $lbl = isset($labels[$m]) ? $labels[$m] : $m;
    if (!empty($r['success'])) {
        $successful[$m] = $r['data'];
        printf("  %s  %-22s %s\n",
            c('OK', 'green', true),
            c($lbl, 'white'),
            c('УСПЕШНО', 'green', true)
        );
    } else {
        printf("  %s  %-22s %s %s\n",
            c('FAIL', 'red', true),
            c($lbl, 'white'),
            c('ОШИБКА', 'red', true),
            c('(' . (isset($r['error']) ? $r['error'] : 'unknown') . ')', 'gray')
        );
    }
}

if (empty($successful)) {
    echo "\n" . c('  Ни один запрос не удался.', 'red', true) . "\n";
    if (!$isCli) echo '</pre></body></html>';
    exit(1);
}

/* ==================== АНАЛИЗ ==================== */
function getField($data, $field) {
    if (isset($data[$field])) return $data[$field];
    if (isset($data['tls'][$field])) return $data['tls'][$field];
    return null;
}

$keyFields = array(
    'user_agent'  => 'User-Agent',
    'ja4'         => 'JA4',
    'ja4_r'       => 'JA4_r',
    'ja4_o'       => 'JA4_o (original)',
    'ja4_ro'      => 'JA4_ro (original)',
    'ja3_hash'    => 'JA3 Hash',
    'ja3_text'    => 'JA3 Text',
    'ja3n_hash'   => 'JA3N Hash',
    'ja3n_text'   => 'JA3N Text',
    'akamai_hash' => 'Akamai Hash',
    'akamai_text' => 'Akamai Text',
);

$tlsFields = array(
    'cipher_suite', 'key_exchange', 'signature_scheme',
    'selected_version', 'record_version', 'handshake_version',
);

echo "\n";
header_('СВОДНАЯ ТАБЛИЦА ОСНОВНЫХ ПОЛЕЙ');
echo "\n";

foreach ($keyFields as $field => $label) {
    $values = array();
    foreach ($successful as $m => $data) {
        $v = getField($data, $field);
        if ($v !== null) $values[$m] = $v;
    }
    if (empty($values)) continue;

    $unique = array_unique($values);
    $isSame = count($unique) === 1;

    echo c('  ' . $label, 'cyan', true)
       . c($isSame ? '  [совпадает]' : '  [различается]', $isSame ? 'green' : 'yellow') . "\n";
    foreach ($values as $m => $v) {
        echo '    ' . c(str_pad(isset($labels[$m]) ? $labels[$m] : $m, 22), 'gray')
           . ' = ' . fmt($v) . "\n";
    }
    echo "\n";
}

foreach ($tlsFields as $tf) {
    $values = array();
    foreach ($successful as $m => $data) {
        if (isset($data['tls'][$tf])) $values[$m] = $data['tls'][$tf];
    }
    if (empty($values)) continue;

    $unique = array_unique($values, SORT_REGULAR);
    $isSame = count($unique) === 1;

    echo c('  tls.' . $tf, 'cyan', true)
       . c($isSame ? '  [совпадает]' : '  [различается]', $isSame ? 'green' : 'yellow') . "\n";
    foreach ($values as $m => $v) {
        if (is_array($v)) {
            $disp = isset($v['name'])
                ? $v['name'] . ' (' . (isset($v['id']) ? $v['id'] : '') . ')'
                : json_encode($v, JSON_UNESCAPED_UNICODE);
        } else {
            $disp = $v;
        }
        echo '    ' . c(str_pad(isset($labels[$m]) ? $labels[$m] : $m, 22), 'gray')
           . ' = ' . fmt($disp) . "\n";
    }
    echo "\n";
}

echo "\n";
header_('ОБЩИЕ ОТВЕТЫ (совпали у всех)');
echo "\n";
$commonFound = false;
foreach (array_merge(array_keys($keyFields), array_map(function($f){ return 'tls.' . $f; }, $tlsFields)) as $f) {
    $vals = array();
    foreach ($successful as $m => $d) {
        if (strpos($f, 'tls.') === 0) {
            $tf = substr($f, 4);
            if (isset($d['tls'][$tf])) $vals[$m] = $d['tls'][$tf];
        } else {
            $v = getField($d, $f);
            if ($v !== null) $vals[$m] = $v;
        }
    }
    if (count($vals) === count($successful) && count(array_unique($vals, SORT_REGULAR)) === 1) {
        $v = reset($vals);
        if (is_array($v)) {
            $disp = isset($v['name']) ? $v['name'] : json_encode($v, JSON_UNESCAPED_UNICODE);
        } else {
            $disp = $v;
        }
        echo '  ' . c($f, 'green', true) . ' = ' . fmt($disp) . "\n";
        $commonFound = true;
    }
}
if (!$commonFound) echo c('  Нет полностью совпадающих полей.', 'yellow') . "\n";

echo "\n";
header_('РАЗЛИЧИЯ (чем отличались запросы)');
echo "\n";
$diffFound = false;
foreach (array_merge(array_keys($keyFields), array_map(function($f){ return 'tls.' . $f; }, $tlsFields)) as $f) {
    $vals = array();
    foreach ($successful as $m => $d) {
        if (strpos($f, 'tls.') === 0) {
            $tf = substr($f, 4);
            if (isset($d['tls'][$tf])) $vals[$m] = $d['tls'][$tf];
        } else {
            $v = getField($d, $f);
            if ($v !== null) $vals[$m] = $v;
        }
    }
    if (count($vals) > 1 && count(array_unique($vals, SORT_REGULAR)) > 1) {
        echo '  ' . c($f, 'yellow', true) . "\n";
        foreach ($vals as $m => $v) {
            if (is_array($v)) {
                $disp = isset($v['name'])
                    ? $v['name'] . ' (' . (isset($v['id']) ? $v['id'] : '') . ')'
                    : json_encode($v, JSON_UNESCAPED_UNICODE);
            } else {
                $disp = $v;
            }
            echo '    ' . c(str_pad(isset($labels[$m]) ? $labels[$m] : $m, 22), 'gray')
               . ' = ' . fmt($disp) . "\n";
        }
        $diffFound = true;
    }
}
if (!$diffFound) echo c('  Нет различий по отслеживаемым полям.', 'yellow') . "\n";

/* ==================== ДЕТАЛЬНО ПО МЕТОДАМ ==================== */
echo "\n";
header_('ДЕТАЛЬНАЯ СВОДКА ПО КАЖДОМУ МЕТОДУ');
echo "\n";

foreach ($successful as $m => $d) {
    $lbl = isset($labels[$m]) ? $labels[$m] : $m;
    subheader_('Метод: ' . $lbl);

    foreach ($keyFields as $f => $label) {
        $v = getField($d, $f);
        if ($v === null) continue;
        echo '  ' . c(str_pad($label, 22), 'white', true) . ' = ' . fmt($v) . "\n";
    }
    foreach ($tlsFields as $tf) {
        if (!isset($d['tls'][$tf])) continue;
        $v = $d['tls'][$tf];
        if (is_array($v)) {
            $disp = isset($v['name'])
                ? $v['name'] . ' (' . (isset($v['id']) ? $v['id'] : '') . ')'
                : json_encode($v, JSON_UNESCAPED_UNICODE);
        } else {
            $disp = $v;
        }
        echo '  ' . c(str_pad('tls.' . $tf, 22), 'white', true) . ' = ' . fmt($disp) . "\n";
    }
    echo "\n";
}

/* ==================== СОХРАНЕНИЕ ==================== */
echo "\n";
header_('СОХРАНЕНИЕ');
echo "\n";
$rawFile = dirname(__FILE__) . '/browserleaks_raw_responses.json';
$saved = @file_put_contents(
    $rawFile,
    json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
if ($saved !== false) {
    echo '  Файл сохранён: ' . c($rawFile, 'yellow', true) . "\n";
    echo '  Размер: ' . c(filesize($rawFile) . ' байт', 'yellow') . "\n";
} else {
    echo c('  Не удалось сохранить файл (нет прав).', 'red') . "\n";
}

echo "\n";
line('=', 100, 'cyan');
echo c('  АНАЛИЗ ЗАВЕРШЁН', 'green', true) . "\n";
line('=', 100, 'cyan');

if (!$isCli) echo '</pre></body></html>';

4

Re: Cloudflare WAF (Web Application Firewall) и возможность из php пройти

Если используется php7 или php8 то ниже приведенная программа проверяет наличие средств обхода JA3/JA4, HTTP/2 и если установлены делает запросы и возвращает результат

<?php
/**
 * browserleaks_test.php
 * Совместимо с PHP 7.0+ и PHP 8.x
 *
 * Проверяет наличие инструментов подмены TLS/HTTP2-отпечатка:
 *   - curl-impersonate (бинарь)       — работает в PHP 7 и 8
 *   - RNet (нативное расширение)       — только PHP 8.x
 *   - wreq-php (нативное расширение)   — только PHP 8.x
 *   - curl-cffi (FFI + libcurl-impersonate) — только PHP 8.1+
 *
 * Если инструмент найден — делает через него запрос к tls.browserleaks.com.
 * Если нет — сообщает, что не установлено.
 */

if (version_compare(PHP_VERSION, '7.0.0', '<')) {
    die("Требуется PHP >= 7.0\n");
}

error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING);
ini_set('display_errors', '0');
ini_set('max_execution_time', '300');
ini_set('default_socket_timeout', '20');

$isCli     = (PHP_SAPI === 'cli');
$useColors = $isCli;
$isPhp8    = version_compare(PHP_VERSION, '8.0.0', '>=');

if (!$isCli) {
    header('Content-Type: text/html; charset=utf-8');
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>BrowserLeaks Test</title>';
    echo '<style>
        body{background:#1e1e1e;color:#d4d4d4;font-family:Consolas,Monaco,monospace;padding:20px;line-height:1.5}
        pre{background:#252526;border:1px solid #3c3c3c;padding:15px;border-radius:5px;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
    </style></head><body><pre>';
}

/* ==================== ВЫВОД ==================== */
function c(string $text, ?string $color = null, bool $bold = false): string {
    global $useColors, $isCli;
    if ($isCli) {
        if (!$useColors) return $text;
        $map = ['red'=>31,'green'=>32,'yellow'=>33,'blue'=>34,
                'magenta'=>35,'cyan'=>36,'white'=>37,'gray'=>90];
        $code = $map[$color] ?? 0;
        $b = $bold ? '1;' : '';
        return "\033[{$b}{$code}m{$text}\033[0m";
    }
    $map = ['red'=>'#f48771','green'=>'#6a9955','yellow'=>'#dcdcaa','blue'=>'#569cd6',
            'magenta'=>'#c586c0','cyan'=>'#4ec9b0','white'=>'#d4d4d4','gray'=>'#808080'];
    $col = $map[$color] ?? '#d4d4d4';
    $weight = $bold ? 'bold' : 'normal';
    return '<span style="color:'.$col.';font-weight:'.$weight.'">'
         . htmlspecialchars($text, ENT_QUOTES, 'UTF-8') . '</span>';
}
function line(string $char = '-', int $len = 100, ?string $color = 'gray'): void {
    echo c(str_repeat($char, $len), $color) . "\n";
}
function header_(string $title, string $ch = '=', int $len = 100): void {
    line($ch, $len, 'cyan');
    echo c('  ' . $title, 'cyan', true) . "\n";
    line($ch, $len, 'cyan');
}
function subheader_(string $title, int $len = 100): void {
    line('-', $len, 'blue');
    echo c('  ' . $title, 'blue', true) . "\n";
    line('-', $len, 'blue');
}
function fmt($v): string {
    global $isCli;
    if (is_array($v)) {
        $v = json_encode($v, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    $v = (string)$v;
    return $isCli ? $v : htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
}

/* ==================== ПРОВЕРКА ИНСТРУМЕНТОВ ==================== */
/**
 * @return array  [ 'name' => ['available' => bool, 'path' => ?string, 'note' => ?string] ]
 */
function detectFingerprintTools(): array {
    $tools = [];

    // 1) curl-impersonate — бинарник (работает в PHP 7 и 8)
    $ciPath = null;
    $out = []; $rc = 1;
    @exec('command -v curl-impersonate 2>/dev/null', $out, $rc);
    if ($rc === 0 && !empty($out)) $ciPath = trim($out[0]);
    if ($ciPath === null) {
        $out = []; $rc = 1;
        @exec('where curl-impersonate 2>NUL', $out, $rc);
        if ($rc === 0 && !empty($out)) $ciPath = trim($out[0]);
    }
    $tools['curl-impersonate (binary)'] = [
        'available' => $ciPath !== null,
        'path'      => $ciPath,
        'note'      => 'PHP 7/8 — full TLS+HTTP2 impersonation via CLI',
    ];

    // 2) RNet — нативное расширение (PHP 8.x)
    $tools['RNet (extension)'] = [
        'available' => extension_loaded('rnet'),
        'path'      => null,
        'note'      => 'PHP 8.x only — browser TLS/JA3/HTTP2 fingerprints',
    ];

    // 3) wreq-php — нативное расширение (PHP 8.x)
    $tools['wreq-php (extension)'] = [
        'available' => extension_loaded('wreq'),
        'path'      => null,
        'note'      => 'PHP 8.x only — 100+ browser profiles',
    ];

    // 4) curl-cffi — FFI + libcurl-impersonate (PHP 8.1+)
    $ffiOk = extension_loaded('ffi');
    $cffiLib = null;
    if ($ffiOk) {
        $cffiLib = getenv('CURL_CFFI_LIB_PATH') ?: null;
    }
    $tools['curl-cffi (FFI)'] = [
        'available' => $ffiOk,
        'path'      => $cffiLib,
        'note'      => 'PHP 8.1+ — needs FFI + libcurl-impersonate',
    ];

    return $tools;
}

/* ==================== HTTP/1.1 ЧЕРЕЗ СОКЕТ ==================== */
function socketFetchHttp1(string $url, array $options = []): array {
    $p = parse_url($url);
    if (!$p || empty($p['host'])) throw new RuntimeException('Некорректный URL: ' . $url);

    $scheme  = $p['scheme'] ?? 'https';
    $host    = $p['host'];
    $port    = isset($p['port']) ? (int)$p['port'] : ($scheme === 'https' ? 443 : 80);
    $path    = ($p['path'] ?? '/') . (isset($p['query']) ? '?' . $p['query'] : '');
    $isHttps = ($scheme === 'https');
    $timeout = (int)($options['timeout'] ?? 20);
    $method  = strtoupper($options['method'] ?? 'GET');
    $body    = (string)($options['body'] ?? '');

    $headers = $options['headers'] ?? [];
    $headers = array_merge(['Host' => $host, 'Connection' => 'close'], $headers);
    if ($method === 'POST') {
        $headers['Content-Type']   = $headers['Content-Type']   ?? 'application/x-www-form-urlencoded';
        $headers['Content-Length'] = strlen($body);
    }

    $req = "$method $path HTTP/1.1\r\n";
    foreach ($headers as $k => $v) $req .= "$k: $v\r\n";
    $req .= "\r\n";
    if ($method === 'POST' && $body !== '') $req .= $body;

    $errno = 0; $errstr = '';
    $ctx = stream_context_create(['ssl' => [
        'verify_peer'      => true,
        'verify_peer_name' => true,
        'SNI_enabled'      => true,
        'peer_name'        => $host,
        'alpn_protocols'   => 'http/1.1',
    ]]);

    $remote = ($isHttps ? 'ssl://' : 'tcp://') . $host . ':' . $port;
    $fp = @stream_socket_client($remote, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $ctx);
    if (!$fp) throw new RuntimeException("Не удалось подключиться к $remote: [$errno] $errstr");
    stream_set_timeout($fp, $timeout);
    stream_set_blocking($fp, true);

    if (@fwrite($fp, $req) === false) {
        fclose($fp);
        throw new RuntimeException('fwrite failed');
    }

    $rawHead = '';
    while (!feof($fp)) {
        $l = fgets($fp, 8192);
        if ($l === false) break;
        $rawHead .= $l;
        if ($l === "\r\n" || $l === "\n") break;
    }
    if ($rawHead === '') {
        $meta = stream_get_meta_data($fp); fclose($fp);
        throw new RuntimeException(!empty($meta['timed_out'])
            ? 'таймаут при чтении заголовков'
            : 'соединение закрыто до получения ответа');
    }
    $hl = preg_split("/\r\n|\n/", trim($rawHead));
    $statusLine = array_shift($hl);
    if (!preg_match('#^HTTP/\d\.\d\s+(\d+)#', $statusLine, $m)) {
        fclose($fp); throw new RuntimeException('Некорректная статус-строка: ' . $statusLine);
    }
    $status = (int)$m[1];

    $parsed = []; $setCookies = [];
    foreach ($hl as $l) {
        if ($l === '') continue;
        $pos = strpos($l, ':');
        if ($pos === false) continue;
        $n = strtolower(trim(substr($l, 0, $pos)));
        $v = trim(substr($l, $pos + 1));
        if ($n === 'set-cookie') { $setCookies[] = $v; continue; }
        $parsed[$n] = isset($parsed[$n]) ? $parsed[$n] . ', ' . $v : $v;
    }

    $rawBody = '';
    $isChunked = isset($parsed['transfer-encoding'])
        && stripos($parsed['transfer-encoding'], 'chunked') !== false;

    if ($isChunked) {
        while (!feof($fp)) {
            $chunk = fread($fp, 8192);
            if ($chunk === false || $chunk === '') {
                $info = stream_get_meta_data($fp);
                if (!empty($info['timed_out']) || !empty($info['eof'])) break;
                continue;
            }
            $rawBody .= $chunk;
            if (strpos($rawBody, "\r\n0\r\n\r\n") !== false
             || strpos($rawBody, "\r\n0\r\n")    !== false) break;
        }
    } elseif (isset($parsed['content-length'])) {
        $need = (int)$parsed['content-length'];
        while (strlen($rawBody) < $need && !feof($fp)) {
            $chunk = fread($fp, min(8192, $need - strlen($rawBody)));
            if ($chunk === false || $chunk === '') {
                $info = stream_get_meta_data($fp);
                if (!empty($info['timed_out'])) break;
                continue;
            }
            $rawBody .= $chunk;
        }
    } else {
        while (!feof($fp)) {
            $chunk = fread($fp, 8192);
            if ($chunk === false || $chunk === '') {
                $info = stream_get_meta_data($fp);
                if (!empty($info['timed_out']) || !empty($info['eof'])) break;
                continue;
            }
            $rawBody .= $chunk;
        }
    }
    fclose($fp);

    if ($isChunked) $rawBody = decodeChunked($rawBody);
    if (isset($parsed['content-encoding'])) {
        $rawBody = decompressBody($rawBody, $parsed['content-encoding']);
    }

    return [
        'status'      => $status,
        'headers'     => $parsed,
        'set_cookies' => $setCookies,
        'body'        => $rawBody,
        'raw'         => $rawHead . "\r\n" . $rawBody,
    ];
}

/* ==================== ВСПОМОГАТЕЛЬНЫЕ ==================== */
function decodeChunked(string $data): string {
    $out = ''; $pos = 0; $len = strlen($data);
    while ($pos < $len) {
        $e = strpos($data, "\r\n", $pos);
        if ($e === false) break;
        $hex = trim(substr($data, $pos, $e - $pos));
        $s = strpos($hex, ';');
        if ($s !== false) $hex = substr($hex, 0, $s);
        if ($hex === '' || !ctype_xdigit($hex)) break;
        $size = hexdec($hex);
        $pos = $e + 2;
        if ($size === 0) break;
        if ($pos + $size > $len) { $out .= substr($data, $pos); break; }
        $out .= substr($data, $pos, $size);
        $pos += $size + 2;
    }
    return $out;
}
function decompressBody(string $body, string $encodingHeader): string {
    if ($encodingHeader === '') return $body;
    $list = array_map('trim', explode(',', strtolower($encodingHeader)));
    foreach (array_reverse($list) as $e) {
        if ($e === 'gzip' || $e === 'x-gzip') {
            $d = @gzdecode($body);
            if ($d !== false) $body = $d;
        } elseif ($e === 'deflate') {
            $d = @gzinflate($body);
            if ($d === false) $d = @gzuncompress($body);
            if ($d !== false) $body = $d;
        } elseif ($e === 'br') {
            if (function_exists('brotli_uncompress')) {
                $d = @brotli_uncompress($body);
                if ($d !== false) $body = $d;
            }
        }
    }
    return $body;
}

/* ==================== ЗАПРОСЫ ==================== */
$url = 'https://tls.browserleaks.com/json';
$results = [];

header_('ТЕСТИРОВАНИЕ ЗАПРОСОВ К ' . $url);
echo "\n";
echo c('  PHP версия: ', 'gray') . PHP_VERSION
   . ' (' . (PHP_INT_SIZE === 8 ? '64-bit' : '32-bit') . ")\n";
echo c('  cURL: ', 'gray')     . (function_exists('curl_init') ? c('доступен','green') : c('НЕДОСТУПЕН','red')) . "\n";
echo c('  OpenSSL: ', 'gray')  . (extension_loaded('openssl')  ? c('доступен','green') : c('НЕДОСТУПЕН','red')) . "\n";
echo c('  FFI: ', 'gray')      . (extension_loaded('ffi')      ? c('доступен','green') : c('нет','yellow')) . "\n";
echo "\n";

/* --- 1. file_get_contents --- */
echo c('  [1/7] ', 'yellow', true) . "file_get_contents...\n";
try {
    $ctx = stream_context_create([
        'http' => [
            'method'        => 'GET',
            'header'        => "User-Agent: PHP-file_get_contents-test\r\nAccept: application/json\r\n",
            'timeout'       => 20,
            'ignore_errors' => true,
        ],
        'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
    ]);
    $fgc = @file_get_contents($url, false, $ctx);
    if ($fgc !== false && $fgc !== '') {
        $decoded = json_decode($fgc, true);
        $results['file_get_contents'] = [
            'success' => $decoded !== null,
            'data'    => $decoded,
            'raw'     => $fgc,
            'error'   => $decoded === null ? 'Не удалось разобрать JSON' : null,
        ];
    } else {
        $err = error_get_last();
        $results['file_get_contents'] = [
            'success' => false,
            'error'   => $err['message'] ?? 'Пустой ответ',
        ];
    }
} catch (Throwable $e) {
    $results['file_get_contents'] = ['success' => false, 'error' => $e->getMessage()];
}

/* --- 2. cURL --- */
echo c('  [2/7] ', 'yellow', true) . "cURL...\n";
if (function_exists('curl_init')) {
    try {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTPHEADER     => [
                'User-Agent: PHP-cURL-test',
                'Accept: application/json',
            ],
        ]);
        $curlResp = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlErr  = curl_error($ch);
        curl_close($ch);

        if ($curlResp !== false && $httpCode == 200) {
            $decoded = json_decode($curlResp, true);
            $results['curl'] = [
                'success'   => $decoded !== null,
                'data'      => $decoded,
                'raw'       => $curlResp,
                'http_code' => $httpCode,
                'error'     => $decoded === null ? 'Не удалось разобрать JSON' : null,
            ];
        } else {
            $results['curl'] = [
                'success'   => false,
                'error'     => $curlErr ?: ('HTTP ' . $httpCode),
                'http_code' => $httpCode,
            ];
        }
    } catch (Throwable $e) {
        $results['curl'] = ['success' => false, 'error' => $e->getMessage()];
    }
} else {
    $results['curl'] = ['success' => false, 'error' => 'Расширение cURL не установлено'];
}

/* --- 3. Socket HTTP/1.1 --- */
echo c('  [3/7] ', 'yellow', true) . "Socket HTTP/1.1...\n";
try {
    $r = socketFetchHttp1($url, [
        'timeout' => 20,
        'headers' => [
            'User-Agent'      => 'PHP-socket-test',
            'Accept'          => 'application/json',
            'Accept-Encoding' => 'gzip, deflate',
        ],
    ]);
    $decoded = json_decode($r['body'], true);
    $results['socket'] = [
        'success' => $decoded !== null,
        'data'    => $decoded,
        'raw'     => $r['raw'],
        'status'  => $r['status'],
        'error'   => $decoded === null
            ? 'HTTP ' . $r['status'] . ': ' . substr($r['body'], 0, 300) : null,
    ];
} catch (Throwable $e) {
    $results['socket'] = ['success' => false, 'error' => $e->getMessage()];
}

/* ==================== ПРОВЕРКА ИНСТРУМЕНТОВ ПОДМЕНЫ ==================== */
echo "\n";
header_('ИНСТРУМЕНТЫ ПОДМЕНЫ TLS/HTTP2-ОТПЕЧАТКА');
echo "\n";

$tools = detectFingerprintTools();
foreach ($tools as $name => $t) {
    if ($t['available']) {
        echo '  ' . c('✓', 'green', true) . '  ' . c($name, 'white', true)
           . c('  — ДОСТУПЕН', 'green')
           . ($t['path'] ? c(' (' . $t['path'] . ')', 'gray') : '')
           . "\n";
        echo '       ' . c($t['note'], 'gray') . "\n";
    } else {
        echo '  ' . c('✗', 'red', true) . '  ' . c($name, 'white')
           . c('  — НЕ УСТАНОВЛЕН', 'yellow') . "\n";
        echo '       ' . c($t['note'], 'gray') . "\n";
    }
}

/* ==================== ЗАПРОСЫ ЧЕРЕЗ ИНСТРУМЕНТЫ ПОДМЕНЫ ==================== */

/* --- 4. curl-impersonate (бинарь) --- */
echo "\n";
if ($tools['curl-impersonate (binary)']['available']) {
    echo c('  [4/7] ', 'yellow', true) . "curl-impersonate (Chrome 131)...\n";
    $ciPath = $tools['curl-impersonate (binary)']['path'];
    try {
        $cmd = escapeshellarg($ciPath)
             . ' -s --impersonate chrome131 '
             . escapeshellarg($url);
        $json = @shell_exec($cmd);
        if ($json !== null && $json !== '') {
            $decoded = json_decode($json, true);
            $results['curl_impersonate'] = [
                'success' => $decoded !== null,
                'data'    => $decoded,
                'raw'     => $json,
                'error'   => $decoded === null ? 'Не удалось разобрать JSON' : null,
            ];
        } else {
            $results['curl_impersonate'] = [
                'success' => false,
                'error'   => 'curl-impersonate вернул пустой ответ',
            ];
        }
    } catch (Throwable $e) {
        $results['curl_impersonate'] = ['success' => false, 'error' => $e->getMessage()];
    }
} else {
    echo c('  [4/7] ', 'yellow', true) . "curl-impersonate...\n";
    echo '  ' . c('НЕ УСТАНОВЛЕН', 'yellow', true)
       . c(' (бинарник curl-impersonate не найден в PATH)', 'gray') . "\n";
    $results['curl_impersonate'] = ['success' => false, 'error' => 'curl-impersonate не установлен'];
}

/* --- 5. RNet (расширение PHP 8.x) --- */
echo "\n";
if ($tools['RNet (extension)']['available']) {
    echo c('  [5/7] ', 'yellow', true) . "RNet (Chrome 136)...\n";
    try {
        // RNet работает через классы: RNet\ClientBuilder, RNet\Emulation
        if (class_exists('RNet\ClientBuilder')) {
            $b = new \RNet\ClientBuilder();
            $b->impersonate(\RNet\Emulation::CHROME_136);
            $client = $b->build();
            $resp = $client->get($url);
            $body = (string)$resp->getBody();
            $decoded = json_decode($body, true);
            $results['rnet'] = [
                'success' => $decoded !== null,
                'data'    => $decoded,
                'raw'     => $body,
                'error'   => $decoded === null ? 'Не удалось разобрать JSON' : null,
            ];
        } else {
            $results['rnet'] = [
                'success' => false,
                'error'   => 'Расширение rnet загружено, но класс RNet\ClientBuilder не найден',
            ];
        }
    } catch (Throwable $e) {
        $results['rnet'] = ['success' => false, 'error' => $e->getMessage()];
    }
} else {
    echo c('  [5/7] ', 'yellow', true) . "RNet...\n";
    echo '  ' . c('НЕ УСТАНОВЛЕН', 'yellow', true)
       . c(' (расширение rnet не загружено; требуется PHP 8.x)', 'gray') . "\n";
    $results['rnet'] = ['success' => false, 'error' => 'RNet не установлен'];
}

/* --- 6. wreq-php (расширение PHP 8.x) --- */
echo "\n";
if ($tools['wreq-php (extension)']['available']) {
    echo c('  [6/7] ', 'yellow', true) . "wreq-php...\n";
    try {
        if (class_exists('Wreq\Client')) {
            $client = new \Wreq\Client(['emulation' => 'chrome_131']);
            $resp = $client->get($url);
            $body = (string)$resp->getBody();
            $decoded = json_decode($body, true);
            $results['wreq'] = [
                'success' => $decoded !== null,
                'data'    => $decoded,
                'raw'     => $body,
                'error'   => $decoded === null ? 'Не удалось разобрать JSON' : null,
            ];
        } else {
            $results['wreq'] = [
                'success' => false,
                'error'   => 'Расширение wreq загружено, но класс Wreq\Client не найден',
            ];
        }
    } catch (Throwable $e) {
        $results['wreq'] = ['success' => false, 'error' => $e->getMessage()];
    }
} else {
    echo c('  [6/7] ', 'yellow', true) . "wreq-php...\n";
    echo '  ' . c('НЕ УСТАНОВЛЕН', 'yellow', true)
       . c(' (расширение wreq не загружено; требуется PHP 8.x)', 'gray') . "\n";
    $results['wreq'] = ['success' => false, 'error' => 'wreq-php не установлен'];
}

/* --- 7. curl-cffi (FFI, PHP 8.1+) --- */
echo "\n";
if ($tools['curl-cffi (FFI)']['available']) {
    echo c('  [7/7] ', 'yellow', true) . "curl-cffi (FFI)...\n";
    if (class_exists('CurlCffi\Requests')) {
        try {
            $resp = \CurlCffi\Requests::get($url, ['impersonate' => 'chrome110']);
            $body = $resp->getText();
            $decoded = json_decode($body, true);
            $results['curl_cffi'] = [
                'success' => $decoded !== null,
                'data'    => $decoded,
                'raw'     => $body,
                'error'   => $decoded === null ? 'Не удалось разобрать JSON' : null,
            ];
        } catch (Throwable $e) {
            $results['curl_cffi'] = ['success' => false, 'error' => $e->getMessage()];
        }
    } else {
        $results['curl_cffi'] = [
            'success' => false,
            'error'   => 'FFI доступен, но библиотека curl-cffi не установлена '
                       . '(composer require farhaanaliii/curl-cffi)',
        ];
    }
} else {
    echo c('  [7/7] ', 'yellow', true) . "curl-cffi (FFI)...\n";
    echo '  ' . c('НЕ УСТАНОВЛЕН', 'yellow', true)
       . c(' (FFI отключён или PHP < 8.1)', 'gray') . "\n";
    $results['curl_cffi'] = ['success' => false, 'error' => 'curl-cffi не установлен'];
}

/* ==================== СТАТУС ==================== */
echo "\n";
header_('СТАТУС ВЫПОЛНЕНИЯ');
echo "\n";

$successful = [];
$labels = [
    'file_get_contents' => 'file_get_contents',
    'curl'              => 'cURL',
    'socket'            => 'Socket HTTP/1.1',
    'curl_impersonate'  => 'curl-impersonate',
    'rnet'              => 'RNet (PHP 8)',
    'wreq'              => 'wreq-php (PHP 8)',
    'curl_cffi'         => 'curl-cffi (PHP 8.1+)',
];

foreach ($results as $m => $r) {
    $lbl = $labels[$m] ?? $m;
    if (!empty($r['success'])) {
        $successful[$m] = $r['data'];
        printf("  %s  %-24s %s\n",
            c('OK', 'green', true), c($lbl, 'white'), c('УСПЕШНО', 'green', true));
    } else {
        $err = $r['error'] ?? 'unknown';
        if (strpos($err, 'не установлен') !== false || strpos($err, 'НЕ УСТАНОВЛЕН') !== false
            || strpos($err, 'не загружено') !== false) {
            printf("  %s  %-24s %s\n",
                c('--', 'yellow', true), c($lbl, 'white'), c('НЕ УСТАНОВЛЕН', 'yellow'));
        } else {
            printf("  %s  %-24s %s %s\n",
                c('FAIL', 'red', true), c($lbl, 'white'),
                c('ОШИБКА', 'red', true),
                c('(' . $err . ')', 'gray'));
        }
    }
}

if (empty($successful)) {
    echo "\n" . c('  Ни один запрос не удался.', 'red', true) . "\n";
    if (!$isCli) echo '</pre></body></html>';
    exit(1);
}

/* ==================== СРАВНЕНИЕ ОТПЕЧАТКОВ ==================== */
echo "\n";
header_('СРАВНЕНИЕ ОТПЕЧАТКОВ');
echo "\n";

// Сравниваем только те, что вернули данные
$compareFields = [
    'user_agent'  => 'User-Agent',
    'ja4'         => 'JA4',
    'ja4_o'       => 'JA4_o (original)',
    'ja3_hash'    => 'JA3 Hash',
    'akamai_hash' => 'Akamai Hash',
];

foreach ($compareFields as $field => $label) {
    $values = [];
    foreach ($successful as $m => $data) {
        $v = $data[$field] ?? ($data['tls'][$field] ?? null);
        if ($v !== null) $values[$m] = $v;
    }
    if (empty($values)) continue;

    $unique = array_unique($values);
    $isSame = count($unique) === 1;

    echo c('  ' . $label, 'cyan', true)
       . c($isSame ? '  [одинаково у всех]' : '  [различается]', $isSame ? 'green' : 'yellow') . "\n";
    foreach ($values as $m => $v) {
        echo '    ' . c(str_pad($labels[$m] ?? $m, 24), 'gray') . ' = ' . fmt($v) . "\n";
    }
    echo "\n";
}

/* ==================== СОХРАНЕНИЕ ==================== */
echo "\n";
header_('СОХРАНЕНИЕ');
echo "\n";
$rawFile = __DIR__ . '/browserleaks_raw_responses.json';
$saved = @file_put_contents(
    $rawFile,
    json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
if ($saved !== false) {
    echo '  Файл сохранён: ' . c($rawFile, 'yellow', true) . "\n";
    echo '  Размер: ' . c(filesize($rawFile) . ' байт', 'yellow') . "\n";
} else {
    echo c('  Не удалось сохранить файл (нет прав).', 'red') . "\n";
}

echo "\n";
line('=', 100, 'cyan');
echo c('  АНАЛИЗ ЗАВЕРШЁН', 'green', true) . "\n";
line('=', 100, 'cyan');

if (!$isCli) echo '</pre></body></html>';

5 (Вчера 13:03:24 отредактировано smsbox3)

Re: Cloudflare WAF (Web Application Firewall) и возможность из php пройти

Тестирование на разных серверах показывает, что без средств обхода  JA4 у curl и file_get_contetns разный на одном и том же сервер. На разных серверах не совпадает JA4 как у curl с другого сервера, так и у file_get_contents. Соединение через сокет (на одном и том же сервере) дает одинаковый отпечаток с file_get_contens и смысла городить запросы через сокет совсем нет.
p.s. сравнил с результатом на https://tls.peet.ws/ - все один в один так же показывает.