0%

Реверс-инжиниринг Long Poll ВКонтакте: сниффер, скрытые события и WebSocket

13 мин
Rianvy
Эта запись также доступна на 🇬🇧 English
Реверс-инжиниринг Long Poll ВКонтакте: сниффер, скрытые события и WebSocket

Разбираюсь как работает Long Poll ВКонтакте, пишу сниффер для перехвата событий и нахожу недокументированные коды.

Введение

Когда я разрабатывал браузерное расширение VKify для улучшения функционала ВКонтакте, передо мной встала интересная задача: отслеживать действия пользователей в реальном времени — кто печатает, кто прочитал сообщение, кто удалил его.

Официальная документация VK API по Long Poll существует, но она неполная и местами устарела. Многие коды событий не документированы, а структура данных может отличаться от описанной. Пришлось заниматься реверс-инжинирингом протокола самостоятельно.

В этой статье расскажу:

  • Что такое Long Poll и зачем он нужен
  • Как перехватывать сетевые запросы в браузере
  • Как я написал сниффер для анализа событий
  • Какие подводные камни встретил (спойлер: онлайн-статус, возможно, идёт через WebSocket)
  • Готовый код, который вы можете использовать

Что такое Long Poll?

Long Polling — это техника получения обновлений от сервера в реальном времени. В отличие от обычных запросов, клиент отправляет запрос и сервер держит соединение открытым, пока не появятся новые данные или не истечёт таймаут.

ВКонтакте использует Long Poll для мгновенной доставки событий:

  • Новые сообщения
  • Статусы “печатает…”
  • Прочтение сообщений
  • Удаление сообщений
  • И многое другое

Запросы идут на endpoint вида:

https://api.vk.com/ruimXXXXXXXXX?version=21&mode=682

Где XXXXXXXXX — ваш user ID.


Шаг 1: Простой перехват fetch

Начнём с базового перехвата. Идея простая: подменяем глобальную функцию fetch, анализируем ответы и пропускаем их дальше.

/**
 * Базовый перехватчик Long Poll
 * Вставьте в консоль браузера на странице VK
 */
(function() {
  const originalFetch = window.fetch;

  window.fetch = async function(url, options) {
    const response = await originalFetch.apply(this, arguments);
    const urlStr = url?.toString() || '';

    // Ловим только Long Poll запросы
    if (/api\.vk\.com\/ruim/.test(urlStr)) {
      try {
        const clone = response.clone();
        const data = await clone.json();

        if (data.updates && data.updates.length > 0) {
          console.log('[Long Poll] Получено событий:', data.updates.length);
          data.updates.forEach(update => {
            console.log('  Код:', update[0], '| Данные:', update.slice(1));
          });
        }
      } catch (e) {}
    }

    return response;
  };

  console.log('✅ Перехватчик Long Poll активирован');
})();

Вставьте этот код в консоль браузера (F12 → Console) на любой странице ВКонтакте и начните переписываться. Вы увидите поток событий.


Шаг 2: Продвинутый сниффер для анализа

Простой перехватчик хорош для начала, но мне нужно было систематизировать данные. Я написал полноценный сниффер, который:

  • Собирает статистику по каждому коду события
  • Сохраняет примеры для анализа структуры
  • Позволяет искать и экспортировать данные
/**
 * VK Long Poll Event Sniffer
 * Продвинутый инструмент для анализа событий
 * 
 * После вставки введите __sniffer.help() для справки
 */
(function() {
  'use strict';

  if (window.__vkEventSniffer) {
    console.log('[Sniffer] Уже запущен. Команды: __sniffer.help()');
    return;
  }

  // Хранилище событий
  const eventCodes = new Map(); // code -> { count, samples, lastSeen }
  let totalEvents = 0;
  let isRunning = true;

  const originalFetch = window.fetch;

  window.fetch = async function(url, options) {
    const response = await originalFetch.apply(this, arguments);

    if (!isRunning) return response;

    const urlStr = url?.toString() || '';

    if (/api\.vk\.com\/ruim/.test(urlStr)) {
      try {
        const clone = response.clone();
        const data = await clone.json();

        if (data.updates?.length > 0) {
          console.groupCollapsed(
            `%c[Long Poll]%c ${data.updates.length} событий`,
            'background: #5181b8; color: white; padding: 2px 8px; border-radius: 4px;',
            'color: #666; margin-left: 8px;'
          );

          data.updates.forEach(update => {
            const code = update[0];

            // Обновляем статистику
            if (!eventCodes.has(code)) {
              eventCodes.set(code, { count: 0, samples: [], lastSeen: null });
              // Уведомляем о новом типе события
              console.log(
                `%c🆕 Обнаружен новый код события: ${code}%c`,
                'background: #4CAF50; color: white; padding: 2px 8px; border-radius: 4px; font-weight: bold;',
                ''
              );
            }
            
            const stats = eventCodes.get(code);
            stats.count++;
            stats.lastSeen = new Date().toLocaleTimeString();
            if (stats.samples.length < 5) {
              stats.samples.push([...update]);
            }
            totalEvents++;

            // Выводим в консоль
            console.log(`📨 Code ${code}:`, update.slice(1));
          });

          console.groupEnd();
        }
      } catch (e) {}
    }

    return response;
  };

  // API для управления
  window.__sniffer = {
    // Показать все найденные коды
    codes() {
      if (eventCodes.size === 0) {
        console.log('%c⚠️ Пока не обнаружено ни одного события.%c\n\nУбедитесь, что вы:\n1. Находитесь на странице VK\n2. Открыли диалог с кем-то\n3. Получаете/отправляете сообщения', 
          'color: #FF9800; font-weight: bold;', '');
        return;
      }
      
      console.log('%c\n📊 Статистика событий\n', 'font-size: 14px; font-weight: bold;');
      
      const sorted = [...eventCodes.entries()].sort((a, b) => b[1].count - a[1].count);
      
      console.table(sorted.map(([code, stats]) => ({
        'Код': code,
        'Количество': stats.count,
        'Последний раз': stats.lastSeen,
        'Пример (первые 4 элемента)': JSON.stringify(stats.samples[0]?.slice(0, 4) || [])
      })));
      
      console.log(`\nВсего событий: ${totalEvents}`);
      console.log(`Уникальных кодов: ${eventCodes.size}`);
      return sorted;
    },

    // Детальный анализ конкретного кода
    inspect(code) {
      const stats = eventCodes.get(code);
      if (!stats) {
        console.log(`❌ Код ${code} не найден.`);
        if (eventCodes.size > 0) {
          console.log(`Доступные коды:`, [...eventCodes.keys()].join(', '));
        } else {
          console.log('Пока не обнаружено ни одного события. Подождите или спровоцируйте события (напишите кому-нибудь).');
        }
        return;
      }
      
      console.log(`%c\n🔍 Анализ события ${code}\n`, 'font-size: 14px; font-weight: bold;');
      console.log(`Всего перехвачено: ${stats.count}`);
      console.log(`Последнее: ${stats.lastSeen}\n`);
      
      console.log('Примеры (до 5 штук):');
      stats.samples.forEach((sample, i) => {
        console.log(`\n#${i + 1}:`);
        sample.forEach((value, idx) => {
          const type = typeof value;
          const display = type === 'object' ? JSON.stringify(value) : value;
          console.log(`  [${idx}] (${type}): ${display}`);
        });
      });
      
      return stats;
    },

    // Поиск по содержимому
    search(keyword) {
      if (eventCodes.size === 0) {
        console.log('⚠️ Нет данных для поиска.');
        return [];
      }
      
      const found = [];
      eventCodes.forEach((stats, code) => {
        stats.samples.forEach(sample => {
          if (JSON.stringify(sample).toLowerCase().includes(keyword.toLowerCase())) {
            found.push({ code, sample });
          }
        });
      });
      
      if (found.length === 0) {
        console.log(`Ничего не найдено для "${keyword}"`);
      } else {
        console.log(`Найдено ${found.length} совпадений для "${keyword}":`, found);
      }
      return found;
    },

    // Управление
    stop() { isRunning = false; console.log('⏸️ Сниффер приостановлен'); },
    start() { isRunning = true; console.log('▶️ Сниффер запущен'); },
    clear() { eventCodes.clear(); totalEvents = 0; console.log('🗑️ Статистика очищена'); },
    
    // Экспорт
    export() {
      if (eventCodes.size === 0) {
        console.log('⚠️ Нет данных для экспорта.');
        return null;
      }
      
      const data = {
        exportedAt: new Date().toISOString(),
        totalEvents,
        events: Object.fromEntries(eventCodes)
      };
      console.log(JSON.stringify(data, null, 2));
      return data;
    },
    
    // Статус
    status() {
      console.log(`
%c📊 Статус сниффера%c
━━━━━━━━━━━━━━━━━━━━━
Активен: ${isRunning ? '✅ Да' : '❌ Нет'}
Всего событий: ${totalEvents}
Уникальных кодов: ${eventCodes.size}
${eventCodes.size > 0 ? `Коды: ${[...eventCodes.keys()].join(', ')}` : ''}
`,
        'font-weight: bold; font-size: 12px;', ''
      );
    },

    // Справка
    help() {
      console.log(`
%c╔═══════════════════════════════════════════════════════════╗
║           VK Long Poll Event Sniffer                       ║
╚═══════════════════════════════════════════════════════════╝%c

%cКак использовать:%c
1. Оставьте консоль открытой
2. Откройте диалог с кем-нибудь  
3. Попросите собеседника написать вам, начать печатать и т.д.
4. События будут появляться в консоли автоматически

%cКоманды:%c
  __sniffer.status()     — Текущий статус и найденные коды
  __sniffer.codes()      — Подробная статистика всех событий
  __sniffer.inspect(63)  — Детальный анализ события с кодом 63
  __sniffer.search('id') — Поиск по содержимому
  __sniffer.export()     — Экспорт всех данных в JSON
  
%cУправление:%c
  __sniffer.stop()       — Приостановить перехват
  __sniffer.start()      — Возобновить перехват
  __sniffer.clear()      — Очистить статистику
`,
        'color: #5181b8; font-weight: bold;', '',
        'color: #4CAF50; font-weight: bold;', '',
        'color: #2196F3; font-weight: bold;', '',
        'color: #FF9800; font-weight: bold;', ''
      );
    }
  };

  window.__vkEventSniffer = true;

  console.log(`
%c🔍 VK Event Sniffer активирован!%c

%cЧто дальше:%c
• Откройте любой диалог ВКонтакте
• Попросите собеседника написать вам сообщение
• События появятся здесь автоматически

%cКоманды:%c __sniffer.help() — справка, __sniffer.status() — статус
`,
    'color: #5181b8; font-weight: bold; font-size: 14px;', '',
    'color: #4CAF50; font-weight: bold;', '',
    'background: #151515; padding: 2px 6px; border-radius: 3px;', ''
  );

})();

Как пользоваться:

  1. Вставьте код в консоль на странице VK
  2. Откройте диалог с кем-нибудь
  3. Попросите собеседника начать печатать или отправить сообщение
  4. События появятся в консоли автоматически
  5. Введите __sniffer.codes() чтобы увидеть статистику

Сниффер в действии

После активации сниффер начинает перехватывать все Long Poll события. Каждый раз, когда приходит пакет данных, он группируется в консоли с указанием количества событий:

Интерфейс сниффера в консоли браузера

На скриншоте видно, как сниффер логирует входящие события. Каждая группа [Long Poll] содержит список событий с их кодами и данными. Обратите внимание на коды: 63 — это событие “печатает”, 10004 — новое сообщение, и так далее. Именно так я и собирал информацию о структуре каждого типа события.

После того как накопится достаточно данных, можно вызвать команду __sniffer.codes() для просмотра статистики:

Статистика событий команды __sniffer.codes()

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


Шаг 3: Что я обнаружил

После нескольких часов тестирования с помощью друга (спасибо, что терпел мои “напиши мне”, “удали сообщение”, “выйди из сети”) я составил карту событий.

Документированные события (работают)

Код 63
Событие Печатает сообщение
Структура [63, userId, [chatIds], flag, peerId]
Код 64
Событие Записывает голосовое
Структура [64, userId, [chatIds], flag, timestamp]
Код 52
Событие Заявка в друзья
Структура [52, type, userId, ?]
Код 90
Событие Действия друзей
Структура [90, actionType, userId]

События сообщений (10000+)

Код 10002
Событие Удалил для всех
Структура [10002, ?, flags, peerId, localId]
Код 10004
Событие Новое сообщение
Структура [10004, ?, flags, ?, fromId, peerId, text, {...}]
Код 10005
Событие Редактирование
Структура [10005, ?, flags, fromId, timestamp, text, {...}]
Код 10007
Событие Прочитал сообщение
Структура [10007, peerId, ?, ?, msgId]

Подкоды события 90 (друзья)

Подкод 1
Значение Отправил заявку в друзья
Подкод 2
Значение Принял вашу заявку
Подкод 3
Значение Удалил вас из друзей

Шаг 4: Загадка онлайн-статуса

По документации, события 8 (онлайн) и 9 (оффлайн) должны приходить через Long Poll. Но я их не видел!

Расширил поиск и начал перехватывать все сетевые запросы:

/**
 * Универсальный сетевой сниффер
 */
(function() {
  const originalFetch = window.fetch;
  const keywords = ['online', 'offline', 'last_seen', 'presence'];

  window.fetch = async function(url, options) {
    const response = await originalFetch.apply(this, arguments);
    
    try {
      const clone = response.clone();
      const text = await clone.text();
      
      // Ищем ключевые слова
      const found = keywords.filter(kw => text.toLowerCase().includes(kw));
      
      if (found.length > 0) {
        console.log(`%c[MATCH: ${found.join(', ')}]%c ${url}`,
          'background: #4CAF50; color: white; padding: 2px 6px;', '');
        console.log(text.substring(0, 500));
      }
    } catch (e) {}
    
    return response;
  };
  
  console.log('✅ Сетевой сниффер активирован. Ищу:', keywords.join(', '));
})();

И тут я заметил интересное в логах:

[WebSocket CREATED] wss://eh.vk.com/?v=1.000&format=json&app_id=6287487

Похоже, ВКонтакте перенёс онлайн-статусы на WebSocket!


Шаг 5: Перехват WebSocket

WebSocket — это другой протокол, и перехватывать его нужно иначе:

/**
 * WebSocket сниффер для VK
 */
(function() {
  const OriginalWebSocket = window.WebSocket;

  window.WebSocket = function(url, protocols) {
    console.log('%c[WS Created]%c ' + url, 
      'background: #9C27B0; color: white; padding: 2px 6px;', '');

    const ws = protocols 
      ? new OriginalWebSocket(url, protocols) 
      : new OriginalWebSocket(url);

    ws.addEventListener('message', async function(event) {
      let data = event.data;
      
      // Данные могут приходить как Blob
      if (data instanceof Blob) {
        const buffer = await data.arrayBuffer();
        const bytes = new Uint8Array(buffer);
        // Первый байт — тип сообщения, остальное — JSON
        const jsonStr = new TextDecoder().decode(bytes.slice(1));
        try {
          data = JSON.parse(jsonStr);
        } catch (e) {
          data = jsonStr;
        }
      }

      console.log('%c[WS Message]%c', 
        'background: #2196F3; color: white; padding: 2px 6px;', '', data);
    });

    return ws;
  };

  // Копируем константы
  window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
  window.WebSocket.OPEN = OriginalWebSocket.OPEN;
  window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;
  window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;
  
  console.log('✅ WebSocket сниффер активирован');
})();

Данные через eh.vk.com приходят в бинарном формате. Мне удалось частично их декодировать, но полностью разобрать протокол не получилось — данные приходили как Blob, и после декодирования первого байта (тип сообщения) оставался JSON, но структура была сложной.

Важно: Я не могу утверждать на 100%, что онлайн-статусы идут именно через этот WebSocket. Возможно, они передаются как-то иначе, или VK использует комбинацию методов. Если у вас получится разобраться глубже — напишите, будет интересно!

В рамках своего проекта я решил не тратить на это больше времени и убрал функцию отслеживания онлайн-статуса.


Итоговый модуль слежки

На основе своих исследований я написал production-ready модуль. Он выводит сообщение при активации и логирует все распознанные события:

/**
 * VKify Spy Module
 * Production-версия для браузерного расширения
 * 
 * Использование:
 * 1. Вставьте в консоль на странице VK
 * 2. Откройте диалог и ждите событий
 * 3. Или подпишитесь на события: window.addEventListener('vkify-spy-event', e => console.log(e.detail))
 */
(function() {
  'use strict';

  if (window.__vkifySpyModule) {
    console.log('[VKify Spy] Уже запущен');
    return;
  }
  window.__vkifySpyModule = true;

  // Конфигурация событий (собрана в ходе реверс-инжиниринга)
  const EVENT_CONFIG = {
    63: { action: 'печатает сообщение', icon: '⌨️', category: 'typing' },
    64: { action: 'записывает голосовое', icon: '🎤', category: 'voice' },
    52: { action: 'событие друзей', icon: '👥', category: 'friends' },
    90: { action: 'событие друзей', icon: '👥', category: 'friends' },
    115: { action: 'звонит вам', icon: '📞', category: 'calls' },
    10002: { action: 'удалил сообщение для всех', icon: '🗑️', category: 'delete' },
    10004: { action: 'отправил сообщение', icon: '💬', category: 'messages' },
    10005: { action: 'отредактировал сообщение', icon: '✏️', category: 'edit' },
    10007: { action: 'прочитал сообщение', icon: '👁️', category: 'read' }
  };

  let eventCount = 0;

  function parseEvent(update) {
    const code = update[0];
    const config = EVENT_CONFIG[code];
    if (!config) return null;

    let userId = null;
    let extra = {};

    switch (code) {
      case 63: case 64:
        userId = update[1];
        break;
      case 52:
        userId = update[2];
        extra.type = update[1];
        break;
      case 90:
        userId = update[2];
        extra.actionType = update[1];
        break;
      case 115:
        userId = update[1]?.user_id || update[1]?.peer_id;
        break;
      case 10002:
        userId = update[3];
        break;
      case 10004:
        if (update[2] & 2) return null; // Исходящее сообщение
        userId = update[4];
        extra.text = update[6]?.substring(0, 100);
        break;
      case 10005:
        if (update[2] & 2) return null;
        userId = update[3];
        extra.text = update[5]?.substring(0, 100);
        break;
      case 10007:
        userId = update[1];
        break;
    }

    if (!userId) return null;

    return { code, userId, ...config, extra, timestamp: new Date().toISOString() };
  }

  // Перехват Long Poll
  const originalFetch = window.fetch;

  window.fetch = async function(url, options) {
    const response = await originalFetch.apply(this, arguments);

    if (/api\.vk\.com\/ruim/.test(url?.toString() || '')) {
      try {
        const clone = response.clone();
        const data = await clone.json();

        data.updates?.forEach(update => {
          const event = parseEvent(update);
          if (event) {
            eventCount++;
            console.log(
              `%c[VKify Spy]%c ${event.icon} User ${event.userId}: ${event.action}`,
              'background: #5181b8; color: white; padding: 2px 6px; border-radius: 3px;',
              'color: inherit;',
              event.extra
            );
            
            // Отправляем кастомное событие для подписчиков
            window.dispatchEvent(new CustomEvent('vkify-spy-event', { 
              detail: event 
            }));
          }
        });
      } catch (e) {}
    }

    return response;
  };

  // Публичное API
  window.__vkifySpy = {
    getEventCount: () => eventCount,
    getConfig: () => EVENT_CONFIG,
    status() {
      console.log(`[VKify Spy] Активен. Перехвачено событий: ${eventCount}`);
    }
  };

  console.log(`
%c🕵️ VKify Spy Module активирован!%c

%cМодуль перехватывает:%c
• Печать сообщений (⌨️)
• Голосовые сообщения (🎤)
• Новые сообщения (💬)
• Прочтение сообщений (👁️)
• Удаление сообщений (🗑️)
• Редактирование (✏️)
• События друзей (👥)
• Звонки (📞)

%cКак проверить:%c
1. Откройте любой диалог
2. Попросите собеседника написать вам
3. События появятся в консоли

%cКоманды:%c __vkifySpy.status()
`,
    'color: #5181b8; font-weight: bold; font-size: 14px;', '',
    'color: #4CAF50; font-weight: bold;', '',
    'color: #2196F3; font-weight: bold;', '',
    'background: #151515; padding: 2px 6px; border-radius: 3px;', ''
  );

})();

Пример работы модуля слежки


Выводы и рекомендации

Что я узнал

  1. Long Poll всё ещё работает для большинства событий мессенджера
  2. Онлайн-статусы, возможно, вынесены на WebSocket (eh.vk.com) — данные приходят в бинарном формате, полностью разобрать не удалось
  3. Структура событий не всегда соответствует документации — нужно тестировать
  4. Коды 10000+ — это “новые” события для сообщений

Советы по реверс-инжинирингу

  1. Начинайте с простого — базовый перехват fetch даёт 80% информации
  2. Собирайте статистику — сохраняйте примеры каждого типа событий
  3. Тестируйте с помощником — некоторые события видны только со стороны
  4. Проверяйте WebSocket — современные приложения всё больше используют его
  5. Документируйте находки — через месяц вы забудете, что означает update[3]

Этичность

Помните, что перехват данных — это инструмент. Используйте его для:

  • ✅ Улучшения собственного опыта использования
  • ✅ Образовательных целей
  • ✅ Разработки полезных инструментов

И не используйте для:

  • ❌ Слежки за людьми без их ведома
  • ❌ Сбора чужих персональных данных
  • ❌ Нарушения ToS сервисов

Полезные ссылки

Статья написана в рамках разработки VKify

© 2025 - 2026 0x69.ru

Powered by ❤️

Readme.md

whoami 👨‍💻

$ cat /etc/profile

Username: Rianvy
Real name: Максим Александров
Age: 28 Location: Тула, RU
Role: Senior Developer & UI/UX Designer 😎

About

Этот блог — мой personal knowledge base, где я пушу заметки про tech stack, life experience и всё, что триггерит мой interest => 🚀

Contact

Open for collaboration 🤝 — готов к мёрджу идей и совместным проектам.

Stack

💻 ЯП

  • C#, PHP, JavaScript/TypeScript, Python

⚛️ Frontend

  • React / Next.js, Vue.js / Nuxt.js
  • Tailwind CSS / SCSS
  • Адаптивная вёрстка (HTML, CSS, JS) 📱

🔧 Backend

  • Laravel, Node.js / Express / NestJS
  • REST API

🗄️ Базы данных

  • MySQL, PostgreSQL, MongoDB, Redis

🛠️ DevOps

  • Docker, Git, CI/CD, Linux 🐧

🎨 Дизайн и графика

  • Figma — UI/UX дизайн и прототипирование
  • Photoshop — графика и обработка изображений
  • After Effects — моушн-дизайн и анимация 🎬
Мои Open Source проекты 🌟
Название VKify
Описание Мощное браузерное расширение для кастомизации ВКонтакте с блокировкой рекламы, темами оформления, режимом приватности и пользовательским CSS
Мои проекты 🚀
Название GreenShield VPN
Описание VPN-сервис в Telegram
Социальные сети