Использование curl-олицетворения в Node.js чтобы избежать блокировок

Одна из самых больших проблем для скриптов автоматизации Node.js заключается в том, что они блокируются с помощью мер по борьбе с ботами. Ни один сайт не любит ботов, поэтому многие веб-серверы внедряют решения для защиты от них.

Ключом к выявлению ботов является изучение сетевых данных низкого уровня, поскольку большинство HTTP-клиентов не используют те же базовые библиотеки подключений, что и браузеры. Вот тут-то и пригодится curl-олицетворение.

Как кастомизированная сборка curl, она использует те же низкоуровневые сетевые библиотеки, что и популярные браузеры, что делает ее запросы практически идентичными запросам обычных пользователей.

В этой статье вы узнаете, что такое curl-олицетворение и как его использовать в Node.js для обхода обнаружения ботов в скриптах веб-скрейпинга и автоматизации. Если вы хотите узнать больше о том, как обычно работает curl, прежде чем переходить к этой части, ознакомьтесь с нашим вводным руководством.

Как использовать curl-олицетворение в Node.js

curl-impersonate - это специализированная версия curl, которая может выдавать себя за браузеры реального мира. В отличие от стандартного curl, она настраивает заголовки запросов, отпечатки TLS и другие параметры, чтобы сделать свои запросы похожими на запросы из таких браузеров, как Chrome, Firefox и Safari.

Таким образом, curl-impersonate помогает обмануть механизмы защиты от ботов, заставив их думать, что ваш автоматический запрос поступает из обычного браузера, а не из HTTP-клиента. Это делает проект полезным для таких сценариев, как очистка веб-страниц, или в любой ситуации, когда сайт может ограничить или заблокировать доступ с помощью автоматизированных средств.

curl-impersonate доступен через Docker images, так что вы можете использовать его в качестве команды в своем терминале на уровне операционной системы. Кроме того, проект предоставляет библиотеку libcurl-impersonate, которая открывает доступ к определенным привязкам на нескольких языках программирования, включая Node.js.

Давайте теперь посмотрим, как использовать curl-олицетворение в скрипте Node.js!

Установить curl-олицетворение

В реестре npm перечислены несколько привязок Node.js для проекта curl-impersonate:

Search results on npm for curl-impersonate showing different TypeScript and Node.js packages for bypassing TLS fingerprinting with popularity and maintenance stats.

 

Хотя ни один из этих вариантов явно не отличается от других, node-curl-impersonate является одним из наиболее надежных вариантов. Он написан на TypeScript, активно поддерживается, часто обновляется и находится в стадии постоянной разработки уже более года.

Добавьте node-curl-impersonate в зависимости вашего проекта с помощью следующей команды:

npm install node-curl-impersonate

Примечание: node-curl-impersonate совместим только с операционными системами на базе Unix, такими как Linux и macOS. Если вы работаете в Windows и не можете использовать WSL (Windows Subsystem for Linux), рассмотрите возможность использования ts-curl-impersonate в качестве альтернативы, поскольку он поставляется со встроенной поддержкой Windows.

Настройка и использование клиента

Сначала импортируйте node-curl-impersonate в свой скрипт JavaScript или TypeScript:

import CurlImpersonate from "node-curl-impersonate";

Имейте в виду, что node-curl-impersonate - это модуль ES, поэтому вы не можете импортировать его с помощью require(), как пакет CommonJS. Если вы не знаете, что это значит, прочитайте нашу статью о модулях CommonJS и ES в Node.js.

CurlImpersonate - это конструктор, который вы можете использовать для инициализации запроса curl-олицетворения, как в примере ниже:

const curlImpersonate = new CurlImpersonate("https://example.com", {
  method: "GET",
  impersonate: "chrome-116",
  headers: {},
});

Конструктор принимает URL-адрес и необязательный объект options. Вот список доступных опций:

    method — метод HTTP, который будет использоваться для запроса. В настоящее время поддерживаются только "GET" и "POST" . impersonate — строка, идентифицирующая браузер для олицетворения. Поддерживаются следующие параметры: "chrome-110", "chrome-116", "firefox-109" и "firefox-117" headers — объект типа ключ-значение, содержащий пользовательские HTTP-заголовки, которые объединяются с заголовками, автоматически устанавливаемыми curl-impersonate. Обратите внимание, что это необязательное тело — необязательный объект, используемый в качестве тела JSON для запроса POST. verbose — Необязательный логический флаг для включения режима verbose, который регистрирует то, что клиент делает за кулисами flags — Необязательный набор дополнительных флагов для передачи в базовую библиотеку libcurl-impersonate

Чтобы отправить запрос, вызовите makeRequest() для возвращаемого экземпляра:

await curlImpersonate.makeRequest();

В качестве альтернативы вы можете создать экземпляр без URL-адреса и передать его позже в makeRequest():

const curlImpersonate = new CurlImpersonate(undefined, {
  method: "GET",
  impersonate: "chrome-116",
  headers: {},
});

curlImpersonate.makeRequest("https://example.com")
// ...
// curlImpersonate.makeRequest(...)

Это позволяет вам повторно использовать один и тот же экземпляр CurlImpersonate для нескольких запросов, особенно для запросов GET, поскольку для запросов POST обычно требуется тело, которое может быть задано только в конструкторе.

Не забывайте, что node-curl-impersonate работает только с системами на базе Unix. Попытка использовать его в Windows приведет к следующей ошибке:

Error: Unsupported Platform! win32

Если вы являетесь пользователем Windows, вы можете обойти эту проблему с помощью WSL.

Выполнить запрос на сайт, защищенный от ботов

Kick - популярный стриминговый сервис, особенно среди молодежной аудитории, и его популярность быстро растет. Если вы попытаетесь выполнить очистку веб-страниц с помощью Kick, вы, скорее всего, столкнетесь со следующей страницей обнаружения ботов, которая блокирует автоматические запросы:

Verification screen on kick.com requiring users to confirm they are human via a Cloudflare CAPTCHA check before proceeding.

 

С помощью node-curl-impersonate вы можете обойти меры защиты от ботов Kick и получить доступ к HTML-контенту сайта. Вот как это можно сделать:

import CurlImpersonate from "node-curl-impersonate";

(async () => {
  // initialize a curl-impersonate request with the specified options
  const curlImpersonate = new CurlImpersonate("https://kick.com/", {
    method: "GET",
    impersonate: "chrome-116",
    headers: {},
  });

  // perform the request
  const curlResponse = await curlImpersonate.makeRequest();

  // extract the response data
  const response = curlResponse.response;
  const responseStatusCode = curlResponse.statusCode;

  // if the server responded with a 4xx or 5xx error
  if (responseStatusCode && ["4", "5"].includes(responseStatusCode.toString()[0])) {
    // error handling logic...
    console.error("Error response:", response);
  } else {
    // handle the response...
    console.log(response);
  }
})();

Если вы запустите описанный выше скрипт, результатом будет HTML-содержимое домашней страницы Kick:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charSet="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
    />
    <link rel="preload" as="image" href="/img/kick-logo.svg" />
    <!-- omitted for brevity... -->
    <title>Kick</title>
    <meta
      name="description"
      content="Kick is a streaming platform that makes it easy for you to find and watch your favorite content."
    />
    <!-- omitted for brevity... -->
  </head>
</html>

Потрясающе, результат подтверждает, что вы смогли получить доступ к целевой странице без блокировки!

Настройки HTTP-заголовков, подобных браузерным, недостаточно, чтобы избежать блокировок

curl-олицетворение, безусловно, интересная технология, но вы можете задаться вопросом, что делает ее такой мощной и уникальной.

Распространенное предположение, когда дело доходит до обмана систем защиты от ботов, заключается в том, что все, что вам нужно сделать, это повторить запросы браузера. Это не совсем неверно, но сделать это далеко не просто. Давайте разберемся, почему!

Откройте свой браузер в режиме инкогнито и перейдите на домашнюю страницу Kick — целевую веб-страницу этой статьи. На вкладке “Сеть” в DevTools вы увидите запрос, который отправляет браузер:

Browser DevTools showing network activity for kick.com, including request and response headers for a GET request with status code 200.

 

Обратите внимание, что Chrome включает в запрос специальные HTTP-заголовки. По-видимому, это единственное отличие от запроса, выполненного с помощью обычного HTTP-клиента.

Щелкните правой кнопкой мыши на запросе и выберите "Копировать" > "Копировать как выборку" (Node.js). Вот что вы получите:

fetch("https://kick.com/", {
  "headers": {
    "sec-ch-ua": ""Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": ""Windows"",
    "upgrade-insecure-requests": "1"
  },
  "referrerPolicy": "strict-origin-when-cross-origin",
  "body": null,
  "method": "GET"
});

fetch() - это функция, которая поступает из Node.js Fetch API. Смотрите, почему для приведенного выше кода не требуется внешняя библиотека, в нашей статье о Fetch API в Node.js.

Скопируйте запрос в JavaScript-скрипт и выполните его. Вы получите следующую запрещенную страницу 403:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <title>Just a moment...</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="robots" content="noindex,nofollow">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- omitted for brevity... -->
  </head>
</html>

В этом случае Kick смог распознать ваш запрос как исходящий от автоматического скрипта и заблокировать его. Как это вообще возможно? Читайте дальше!

Почему curl-олицетворение эффективно против большинства решений по борьбе с ботами

То, что мы сделали выше, имитирует поведение браузера на прикладном уровне, отправляя запрос HTTPS, эквивалентный запросу вашего браузера. Но помните, что Интернет работает на нескольких уровнях!

 

Чтобы добраться до сервера, ваш HTTPS-запрос должен пройти через канал TLS, созданный на транспортном уровне, затем через IP-уровень и так далее.

Как веб-разработчик, вы проводите большую часть дня, работая с технологиями на прикладном уровне. Однако важно не упускать из виду базовые уровни, которые обеспечивают функционирование прикладного уровня.

Решения для защиты от ботов анализируют все аспекты входящих запросов, от высокоуровневых сведений о приложении до элементов более низкого уровня. Чтобы определить, является ли запрос подлинным, они не могут полагаться исключительно на данные прикладного уровня на уровне HTTPS. В противном случае избежать обнаружения ботов было бы проще простого!

Таким образом, самые передовые системы защиты от ботов на рынке, такие как Cloudflare, сосредоточены на низкоуровневых сетевых аспектах, таких как TLS-отпечаток запроса.

TLS-дактилоскопия как ключ к обнаружению ботов

Когда клиент, такой как ваш браузер или сканирующий бот, инициирует безопасное соединение с сервером, для этого требуется подтверждение связи по протоколу TLS.

 

Во время этого процесса клиент и сервер согласовывают параметры шифрования. Это подтверждение включает в себя такие сведения, как версия TLS, наборы шифров и расширения, поддерживаемые клиентом.

На основе информации, которой обмениваются во время рукопожатия, можно сгенерировать “отпечаток пальца”, который помогает отличить одного клиента от другого.

Именно так большинство систем обнаружения ботов могут определить, используете ли вы реальный браузер или нет. Браузеры используют хорошо известные библиотеки TLS, которые, как правило, отличаются от тех, что используются HTTP-клиентами.

Следствием этого является то, что TLS—отпечаток запроса, отправляемого браузером, сильно отличается от отпечатка HTTP-клиента, даже если они используют одни и те же HTTP-заголовки.

Вы можете убедиться в этом, запустив в своем браузере Scrapingly TLS Fingerprinting API и сравнив результат с такими клиентами, как node-curl-impersonate и Fetch API.

Chrome 130 возвращает:

{
  "ja3": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,35-27-43-11-16-65281-10-13-65037-5-18-23-45-0-17513-51,25497-29-23-24,0",
  "ja3n": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-35-43-45-51-17513-65037-65281,25497-29-23-24,0",
  "ja3_digest": "370fa7191028e260eac290c51745d8f8",
  "ja3n_digest": "eb5a4e1d21094c5caf044c8f3117f306",
  "scrapfly_fp": "version:772|ch_ciphers:GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|ch_extensions:GREASE-0-5-10-11-13-16-18-23-27-35-43-45-51-17513-65037-65281-GREASE|groups:GREASE-25497-29-23-24|points:0|compression:0|supported_versions:GREASE-772-771|supported_protocols:h2-http11|key_shares:GREASE-25497-29|psk:1|signature_algs:1027-2052-1025-1283-2053-1281-2054-1537|early_data:0|",
  "scrapfly_fp_digest": "58e05a62bade1452454ea0b0cc49c971",
  "tls": {
    "version": "0x0303 - TLS 1.2",
    "ciphers": [
      "0x3A3A",
      "TLS_AES_128_GCM_SHA256",
      "TLS_AES_256_GCM_SHA384",
      "TLS_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
      "TLS_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_128_CBC_SHA",
      "TLS_RSA_WITH_AES_256_CBC_SHA"
    ],
    "curves": [
      "TLS_GREASE (0x1A1A)",
      "Unknown curve 0x6399",
      "X25519 (29)",
      "secp256r1 (23)",
      "secp384r1 (24)"
    ],
    "extensions": [
      "GREASE (0x4A4A)",
      "session_ticket (35) (IANA)",
      "compress_certificate (27) (IANA)",
      "supported_versions (43) (IANA)",
      "ec_point_formats (11) (IANA)",
      "application_layer_protocol_negotiation (16) (IANA)",
      "extensionRenegotiationInfo (boringssl) (65281) (IANA)",
      "supported_groups (10) (IANA)",
      "signature_algorithms (13) (IANA)",
      "extensionEncryptedClientHello (65037) (boringssl)",
      "status_request (5) (IANA)",
      "signed_certificate_timestamp (18) (IANA)",
      "extended_master_secret (23) (IANA)",
      "psk_key_exchange_modes (45) (IANA)",
      "server_name (0) (IANA)",
      "extensionApplicationSettings (17513) (boringssl)",
      "key_share (51) (IANA)",
      "GREASE (0x8A8A)"
    ],
    "points": [
      "0x00"
    ],
    "protocols": [
      "h2",
      "http/1.1"
    ],
    "versions": [
      "43690",
      "772",
      "771"
    ],
    "handshake_duration": "184.049664ms",
    "is_session_resumption": false,
    "session_ticket_supported": true,
    "support_secure_renegotiation": true,
    "supported_tls_versions": [
      43690,
      772,
      771
    ],
    "supported_protocols": [
      "h2",
      "http11"
    ],
    "signature_algorithms": [
      1027,
      2052,
      1025,
      1283,
      2053,
      1281,
      2054,
      1537
    ],
    "psk_key_exchange_mode": "AQ==",
    "cert_compression_algorithms": "AA==",
    "early_data": false,
    "using_psk": false,
    "selected_protocol": "h2",
    "selected_curve_group": 29,
    "selected_cipher_suite": 4865,
    "key_shares": [
      6682,
      25497,
      29
    ]
  }
}

возвращает значение node-curl-impersonate:

{
  "ja3": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,35-43-65281-45-51-5-16-0-27-13-23-11-10-17513-18,29-23-24,0",
  "ja3n": "772,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-5-10-11-13-16-18-23-27-35-43-45-51-17513-65281,29-23-24,0",
  "ja3_digest": "d737eab1c0aba59b4b466cf91d42a47a",
  "ja3n_digest": "0fb2c926015957b7e56038e269a7c58a",
  "scrapfly_fp": "version:772|ch_ciphers:GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|ch_extensions:GREASE-0-5-10-11-13-16-18-23-27-35-43-45-51-17513-65281-GREASE|groups:GREASE-29-23-24|points:0|compression:0|supported_versions:GREASE-772-771|supported_protocols:h2-http11|key_shares:GREASE-29|psk:1|signature_algs:1027-2052-1025-1283-2053-1281-2054-1537|early_data:0|",
  "scrapfly_fp_digest": "81fbc443bb8cb67310e62d982c1e4c98",
  "tls": {
    "version": "0x0303 - TLS 1.2",
    "ciphers": [
      "0x6A6A",
      "TLS_AES_128_GCM_SHA256",
      "TLS_AES_256_GCM_SHA384",
      "TLS_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
      "TLS_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_128_CBC_SHA",
      "TLS_RSA_WITH_AES_256_CBC_SHA"
    ],
    "curves": [
      "TLS_GREASE (0xBABA)",
      "X25519 (29)",
      "secp256r1 (23)",
      "secp384r1 (24)"
    ],
    "extensions": [
      "GREASE (0x0A0A)",
      "session_ticket (35) (IANA)",
      "supported_versions (43) (IANA)",
      "extensionRenegotiationInfo (boringssl) (65281) (IANA)",
      "psk_key_exchange_modes (45) (IANA)",
      "key_share (51) (IANA)",
      "status_request (5) (IANA)",
      "application_layer_protocol_negotiation (16) (IANA)",
      "server_name (0) (IANA)",
      "compress_certificate (27) (IANA)",
      "signature_algorithms (13) (IANA)",
      "extended_master_secret (23) (IANA)",
      "ec_point_formats (11) (IANA)",
      "supported_groups (10) (IANA)",
      "extensionApplicationSettings (17513) (boringssl)",
      "signed_certificate_timestamp (18) (IANA)",
      "GREASE (0x5A5A)",
      "padding (21) (IANA)"
    ],
    "points": [
      "0x00"
    ],
    "protocols": [
      "h2",
      "http/1.1"
    ],
    "versions": [
      "23130",
      "772",
      "771"
    ],
    "handshake_duration": "221.314783ms",
    "is_session_resumption": false,
    "session_ticket_supported": true,
    "support_secure_renegotiation": true,
    "supported_tls_versions": [
      23130,
      772,
      771
    ],
    "supported_protocols": [
      "h2",
      "http11"
    ],
    "signature_algorithms": [
      1027,
      2052,
      1025,
      1283,
      2053,
      1281,
      2054,
      1537
    ],
    "psk_key_exchange_mode": "AQ==",
    "cert_compression_algorithms": "AA==",
    "early_data": false,
    "using_psk": false,
    "selected_protocol": "h2",
    "selected_curve_group": 29,
    "selected_cipher_suite": 4865,
    "key_shares": [
      47802,
      29
    ]
  }
}

функция fetc() возвращает:

{
  "ja3": "772,4866-4867-4865-49199-49195-49200-49196-158-49191-103-49192-107-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-162-49326-49324-49314-49310-49244-49248-49238-49234-49188-106-49187-64-49162-49172-57-56-49161-49171-51-50-157-49313-49309-49233-156-49312-49308-49232-61-60-53-47-255,0-11-10-35-16-22-23-13-43-45-51,29-23-30-25-24-256-257-258-259-260,0-1-2",
  "ja3n": "772,4866-4867-4865-49199-49195-49200-49196-158-49191-103-49192-107-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-162-49326-49324-49314-49310-49244-49248-49238-49234-49188-106-49187-64-49162-49172-57-56-49161-49171-51-50-157-49313-49309-49233-156-49312-49308-49232-61-60-53-47-255,0-10-11-13-16-22-23-35-43-45-51,29-23-30-25-24-256-257-258-259-260,0-1-2",
  "ja3_digest": "f376ddf05a7a38d2fb080069329ce2a2",
  "ja3n_digest": "7b70814919c3f12abb0b7d0b603462aa",
  "scrapfly_fp": "version:772|ch_ciphers:4866-4867-4865-49199-49195-49200-49196-158-49191-103-49192-107-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-162-49326-49324-49314-49310-49244-49248-49238-49234-49188-106-49187-64-49162-49172-57-56-49161-49171-51-50-157-49313-49309-49233-156-49312-49308-49232-61-60-53-47-255|ch_extensions:0-10-11-13-16-22-23-35-43-45-51|groups:29-23-30-25-24-256-257-258-259-260|points:0-1-2|compression:0|supported_versions:772-771|supported_protocols:http11|key_shares:29|psk:1|signature_algs:1027-1283-1539-2055-2056-2057-2058-2059-2052-2053-2054-1025-1281-1537-771-769-770-1026-1282-1538|early_data:0|",
  "scrapfly_fp_digest": "8b2bf560717049d7bb701693d9f0d90b",
  "tls": {
    "version": "0x0303 - TLS 1.2",
    "ciphers": [
      "TLS_AES_256_GCM_SHA384",
      "TLS_CHACHA20_POLY1305_SHA256",
      "TLS_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
      "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
      "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256",
      "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384",
      "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8",
      "TLS_ECDHE_ECDSA_WITH_AES_256_CCM",
      "TLS_DHE_RSA_WITH_AES_256_CCM_8",
      "TLS_DHE_RSA_WITH_AES_256_CCM",
      "TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384",
      "TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384",
      "TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384",
      "TLS_DHE_DSS_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
      "TLS_ECDHE_ECDSA_WITH_AES_128_CCM",
      "TLS_DHE_RSA_WITH_AES_128_CCM_8",
      "TLS_DHE_RSA_WITH_AES_128_CCM",
      "TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256",
      "TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256",
      "TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
      "TLS_DHE_DSS_WITH_AES_256_CBC_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
      "TLS_DHE_DSS_WITH_AES_128_CBC_SHA256",
      "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
      "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
      "TLS_DHE_DSS_WITH_AES_256_CBC_SHA",
      "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
      "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
      "TLS_DHE_DSS_WITH_AES_128_CBC_SHA",
      "TLS_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_256_CCM_8",
      "TLS_RSA_WITH_AES_256_CCM",
      "TLS_RSA_WITH_ARIA_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_RSA_WITH_AES_128_CCM_8",
      "TLS_RSA_WITH_AES_128_CCM",
      "TLS_RSA_WITH_ARIA_128_GCM_SHA256",
      "TLS_RSA_WITH_AES_256_CBC_SHA256",
      "TLS_RSA_WITH_AES_128_CBC_SHA256",
      "TLS_RSA_WITH_AES_256_CBC_SHA",
      "TLS_RSA_WITH_AES_128_CBC_SHA",
      "TLS_EMPTY_RENEGOTIATION_INFO"
    ],
    "curves": [
      "X25519 (29)",
      "secp256r1 (23)",
      "X448 (30)",
      "secp521r1 (25)",
      "secp384r1 (24)",
      "ffdhe2048 (256)",
      "ffdhe3072 (257)",
      "ffdhe4096 (258)",
      "ffdhe6144 (259)",
      "ffdhe8192 (260)"
    ],
    "extensions": [
      "server_name (0) (IANA)",
      "ec_point_formats (11) (IANA)",
      "supported_groups (10) (IANA)",
      "session_ticket (35) (IANA)",
      "application_layer_protocol_negotiation (16) (IANA)",
      "encrypt_then_mac (22) (IANA)",
      "extended_master_secret (23) (IANA)",
      "signature_algorithms (13) (IANA)",
      "supported_versions (43) (IANA)",
      "psk_key_exchange_modes (45) (IANA)",
      "key_share (51) (IANA)"
    ],
    "points": [
      "0x00",
      "0x01",
      "0x02"
    ],
    "protocols": [
      "http/1.1"
    ],
    "versions": [
      "772",
      "771"
    ],
    "handshake_duration": "195.733862ms",
    "is_session_resumption": false,
    "session_ticket_supported": true,
    "support_secure_renegotiation": true,
    "supported_tls_versions": [
      772,
      771
    ],
    "supported_protocols": [
      "http11"
    ],
    "signature_algorithms": [
      1027,
      1283,
      1539,
      2055,
      2056,
      2057,
      2058,
      2059,
      2052,
      2053,
      2054,
      1025,
      1281,
      1537,
      771,
      769,
      770,
      1026,
      1282,
      1538
    ],
    "psk_key_exchange_mode": "AQ==",
    "cert_compression_algorithms": "AA==",
    "early_data": false,
    "using_psk": false,
    "selected_protocol": "http/1.1",
    "selected_curve_group": 29,
    "selected_cipher_suite": 4865,
    "key_shares": [
      29
    ]
  }
}

Как вы можете заметить, отпечаток TLS, сгенерированный Chrome, и node-curl-impersonate намного ближе друг к другу, чем отпечаток, сгенерированный fetch().

Скорее всего, единственное различие между отпечатками TLS в Chrome и node-curl-impersonate заключается в том, что они основаны на разных версиях браузера. Это играет ключевую роль в обнаружении ботов и объясняет, почему node-curl-impersonate смог получить HTML-содержимое домашней страницы Kick, в то время как Fetch API не удалось выполнить.

Как работает curl-олицетворение

Чтобы достичь результата, о котором говорилось ранее, команде, создавшей curl-impersonate, пришлось внести изменения в curl, чтобы он максимально напоминал браузер. В частности, они внесли следующие изменения:

    Компиляция curl с использованием BoringSSL, библиотеки TLS, используемой Google Chrome, вместо OpenSSL. Для версии Firefox curl был скомпилирован с использованием NSS, библиотеки TLS Firefox Изменен способ настройки curl нескольких параметров SSL и расширений TLS Добавлена поддержка новых расширений TLS Настройка параметров HTTP/2-соединений curl Запуск curl с флагами, отличными от стандартных, такими как --ciphers, --curves и специальные заголовки -H (например, User-Agent), для дальнейшей имитации поведения браузера.

Эти изменения позволяют запросам, выполняемым curl-impersonate, быть идентичными с точки зрения сети запросам реального браузера.

Вы можете найти все подробности реализации в руководствах в официальном блоге, которые объясняют, как им удалось полностью выдать себя за Chrome и имитировать Firefox.

Преимущества curl-олицетворения перед средствами автоматизации браузера

Если вы являетесь экспертом в Node.js веб-автоматизация, вы могли бы предположить, что использование безголовых браузеров, управляемых такими технологиями, как Playground или Puppeteer, более эффективно, чем использование curl-олицетворения.

Неудивительно, что эти две библиотеки включены в наш список лучших Node.js технологии веб-очистки.

В конце концов, средства автоматизации браузера также позволяют вам взаимодействовать с элементами страницы. Однако curl-impersonate - это всего лишь HTTP-клиент, который может только извлекать веб-страницы.

Тем не менее, есть Node.js сценарии веб-автоматизации, в которых такая библиотека, как node-curl-impersonate, может оказаться лучшим выбором, чем Playstation или Puppeteer.

Причина этого заключается в том, что системы защиты от ботов часто используют двухэтапный подход для обнаружения и блокировки ботов. На первом этапе проверяется, поступает ли запрос из легального браузера, как объяснялось ранее в этой статье. Если запрос кажется подозрительным, он блокируется. В противном случае сервер доставит HTML-документ страницы.

Страница содержит специальные скрипты JavaScript, которые проверяют настройки и конфигурацию браузера, чтобы сгенерировать отпечаток браузера. Затем он отправляется обратно в систему защиты от ботов, чтобы определить, является ли пользователь легитимным.

Второй шаг работает, потому что средства автоматизации, как правило, настраивают браузеры таким образом, который отличается от обычных браузеров. Этих различий достаточно, чтобы решения для защиты от ботов поняли, что они имеют дело с автоматическим запросом. Для получения дополнительной информации ознакомьтесь с нашим руководством по Playstation Extra.

В отличие от этого, curl-impersonate не может отображать JavaScript, полностью пропуская второй шаг. Если второй шаг не требуется для того, чтобы считаться законным пользователем, node-curl-impersonate может продолжать эффективно отправлять запросы на целевой сервер без затрат ресурсов и медлительности, характерных для браузеров без заголовков, даже в режиме headles.

Вывод

В этой статье мы рассмотрели, что такое curl-олицетворение, как его использовать в Node.js и почему оно может быть более эффективным, чем средства автоматизации браузера, для обхода систем защиты от ботов. Мы узнали, что ключ к его успеху лежит в низкоуровневых сетевых данных, таких как снятие отпечатков пальцев по протоколу TLS. С помощью этой специальной сборки curl вы можете использовать свои сценарии автоматизации в Node.js переходим на следующий уровень!

Если у вас возникнут какие-либо дополнительные вопросы об использовании curl-impersonate в Node.js, не стесняйтесь оставлять комментарии ниже.