Как работает js: websocket и http/2+sse. что выбрать?

Специальные требования к серверу

В нашем случае лучше всего использовать сервер на основе цикла событий. Например, NodeJS, Kestrel или Twisted. Идея состоит в том, что при использовании потокового решения будет один поток на соединение. То есть, 1000 соединений = 1000 потоков. В решении на основе цикла событий у нас будет один поток для 1000 соединений.

  1. Вы можете принимать запросы EventSource только в том случае, если HTTP-запрос говорит, что он может принимать MIME-тип event-stream;
  2. Необходимо вести список всех подключенных пользователей, чтобы запускать новые события;
  3. Вы должны прослушивать сброшенные соединения и удалять их из списка подключенных пользователей;
  4. Вы должны поддерживать историю сообщений, чтобы при повторном подключении клиентов можно было отправить им пропущенные сообщения.

Мы получили все, чтобы приложение работало эффективно. Но столкнулись с некоторыми проблемами:

  • Устаревшие прокси-серверы в некоторых случаях удаляют HTTP-соединения после короткого таймаута. Чтобы защитить соединения, авторы могут включать строку комментариев (начинающуюся с символа «:») каждые 15 секунд или около того.
  • Авторы, желающие связать соединения источника событий друг с другом или с определенными ранее документами, могут обнаружить, что использование IP-адресов не работает. Отдельные клиенты могут иметь несколько IP-адресов (из-за наличия нескольких прокси-серверов) и отдельные IP-адреса могут иметь несколько клиентов (из-за совместного использования прокси-сервера). Лучше включать в документ уникальный идентификатор и передавать его как часть URL-адреса при установлении соединения.
  • Использование chunked transfer encoding может уменьшить надежность HTTP протокола, если блокирование выполняется другим слоем, не подозревающим о требованиях к синхронизации. Если эта проблема возникнет, блокирование может быть отключено для обслуживания потоков событий.
  • Клиенты, которые поддерживают ограничение на подключение к серверу через протокол HTTP, могут столкнуться с трудностями при открытии нескольких страниц сайта, если на каждой из этих страниц есть источник событий, расположенный в том же домене. Можно избежать этого, применяя механизм уникальных доменных имен для каждого соединения и разрешая пользователям включать функции EventSource для каждой страницы.
  • Поддержка браузера и полифиллы: Microsoft Edge не поддерживает эту реализацию. Но существует полифиллы, которые позволяют решить данную проблему. Тем не менее, самый важный сегмент для SSE — это мобильные устройства, где браузеры IE / Edge распространены незначительно.

Некоторые из доступных полифиллов:

· Yaffle.

· amvtek.

· remy.

Бесплатное подключение и другие функции

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

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

  1. Браузер подключается к удаленному HTTP-серверу и запрашивает ресурс, указанный автором в конструкторе EventSource.
  2. Сервер отправляет случайные сообщения.
  3. В промежутке между двумя сообщениями браузер обнаруживает, что он неактивен, за исключением активности сети, связанной с поддержанием TCP- соединения, и решает переключиться в спящий режим для экономии энергии.
  4. Браузер отключается от сервера.
  5. Браузер связывается с сервисом в сети и просит, чтобы служба «push proxy» поддерживала соединение.
  6. Служба «push proxy» связывается с удаленным HTTP-сервером и запрашивает ресурс, указанный в конструкторе EventSource (возможно, включая HTTP-заголовок последнего события и т. д.).
  7. Браузер позволяет мобильному устройству перейти в спящий режим.
  8. Сервер отправляет другое сообщение.
  9. Служба «push proxy» использует технологию OMA push для передачи события на мобильное устройство, которое выходит из спящего режима на время, достаточное для обработки события. Затем возвращается в спящий режим.

Подобный подход может снизить объем передаваемых данных и привести к значительной экономии энергии.

Помимо реализации существующего API и формата передаваемых данных ext/event-stream также могут поддерживаться форматы фреймворка событий, определенные другими спецификациями.

Обработка разрывов соединения

Если соединение разрывается, оно автоматически восстанавливается браузером. Сервер может отправить таймаут для повторного завершения или закрытия соединения. В таком случае браузер попытается подключиться после завершения таймаута или не будет ничего делать, если соединение завершено.

Реализация образца сервера

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

function handler(response)
{
// настраиваем заголовки для ответа с целью получить постоянное HTTP-соединение
response.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});

// составляем сообщение
response.write('id: UniqueIDn');
response.write("data: " + data + 'nn'); // каждый раз, когда мы вводим два символа новой строки, сообщение отправляется автоматически
}

Определяем функцию, которая будет обрабатывать ответ:

  1. Устанавливать заголовки;
  2. Создавать сообщение;
  3. Отправлять.

Обратите внимание, что здесь нет вызов метода send() или метода push(). Стандарт определяет: сообщение будет отправлено, как только в него будет добавлено два символа n n, как например: response.write(«data: » + data + ‘nn’);

В результате сообщение будет немедленно отправлено клиенту.

Составление сообщений

Сообщение может содержать несколько свойств:

1. ID

Если значение этого поля не содержит U + 0000 NULL, устанавливаем для буфера последнего идентификатора события значение поля. Иначе игнорируем поле.

2. Data

Добавляем значение поля в буфер, затем добавляем в буфер один символ U + 000A LINE FEED (LF).

3. Event

Устанавливаем для буфера тип события и значение поля. Это приводит к тому, что для event.type задается пользовательское имя события.

4. Retry

Если значение поля состоит только из цифр ASCII, тогда интерпретируем значение поля как целое число в десятичной системе исчисления. А также устанавливаем для времени повторного соединения потока событий это целое число. В противном случае игнорируем поле.

Все остальное будет проигнорировано. Мы не можем вводить собственные поля.

Пример с добавленным event:

response.write('id: UniqueIDn');    response.write('event: addn');    response.write('retry: 10000n');    response.write("data: " + data + 'nn');

В клиенте это обрабатывается с помощью addEventListener следующим образом:

source.addEventListener("add", function(event) {        // выполняем действия с данными        event.data;    });

Вы можете отправлять несколько сообщений, разделенных символом новой строки, а также использовав для них разные идентификаторы.

...    id: 54    event: add    data: ""     id: 55    event: remove    data: JSON.stringify(some_data)     id: 56    event: remove    data: {    data: "msg" : "JSON data"n    data: "field": "value"n    data: "field2": "value2"n    data: }nn

Это значительно упрощает то, что мы можем сделать с нашими данными.

Оптимизации потребления RAM

Формула Камардина

Миллион WebSocket и pub/sub

  1. Две горутины (одна читает, другая пишет);
  2. HTTP-буферы на чтение и на запись от стандартной библиотеки Go, которая аллоцируется, потому что WebSocket-соединение начинается с HTTP;
  3. Буферы, которые мы в приложении используем для ввода-вывода.

Going Infinite, handling 1 millions websockets connections in Go

Gobwas/ws: beyond std lib

Библиотека

  1. . Мы убираем использование стандартного HTTP-сервера Go и сами парсим HTTP, при этом не аллоцируя дополнительной памяти.
  2. Возможность использовать epoll/kqueue (см. netpoll, gnet, evio, gaio), используя syscalls и минуя рантайм и Go netpoller. Тем самым можно даже отойти от двух горутин, одна из которых пишет, а другая читает. Также можно переиспользовать все буферы, о которых шла речь.

#15735 net: add mechanism to wait for readability on a TCPConn

  • JSON
  • Protobuf
  • Msgpack, CBOR
  • Свой протокол

gogo/protobufне рекомендовал

Инвалидация соединения

  • Push. Вы можете подписаться на события, если у вас есть какая-то шина данных. Например, у нас в Авито есть Kafka как шина данных. Мы подписались на такие события и отключаем WebSocket.
  • Pull. Но если такой шины данных нет, можно просто периодически проверять, что WebSocket-соединение активно (периодическая валидация). Это дороже, но тут нужно найти trade-off между производительностью и тем интервалом, с которым вы проверяете каждое висящее соединение.

Summary

WebSocket is a modern way to have persistent browser-server connections.

  • WebSockets don’t have cross-origin limitations.
  • They are well-supported in browsers.
  • Can send/receive strings and binary data.

The API is simple.

Methods:

  • ,
  • .

Events:

  • ,
  • ,
  • ,
  • .

WebSocket by itself does not include reconnection, authentication and many other high-level mechanisms. So there are client/server libraries for that, and it’s also possible to implement these capabilities manually.

Sometimes, to integrate WebSocket into existing project, people run WebSocket server in parallel with the main HTTP-server, and they share a single database. Requests to WebSocket use , a subdomain that leads to WebSocket server, while goes to the main HTTP-server.

Surely, other ways of integration are also possible.

Инициализация WebSocket-сервера

ChatWorker.php
<?php 

// Подключаем библиотеку Workerman
require_once __DIR__ . '/vendor/autoload.php';

use Workerman\Lib\Timer;
use Workerman\Worker;

$connections = []; // сюда будем складывать все подключения

// Стартуем WebSocket-сервер на порту 27800
$worker = new Worker("websocket://0.0.0.0:27800");

Worker::runAll();

И это всё. При запуске этого php-скрипта WebSocket-сервер будет запущен на порту 27800 и к нему уже можно будет подключиться.

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

iptables -I INPUT -p tcp —dport {PORT} —syn -j ACCEPT

где {PORT} – выбранный вами порт для чата.

Запускаем WebSocket-сервер командой:

php ChatWorker.php start

Запущенный Workerman

Для проверки соединения и дальнейшей отладки можно воспользоваться плагином Simple WebSocket Client для браузера Google Chrome.

Окно плагина Simple WebSocket Client

В поле Server Location -> URL: вводим адрес сервера, начиная с названия протокола: ws:// и нажимаем кнопку Open. 

При успешном подключении метка Status: CLOSED будет заменена на OPENED и разблокируется поле Request, которое в дальнейшем можно будет использовать для отправки тестовых запросов как от клиента. По сути, наш браузер уже является клиентом для сервера, просто не имеет визуального оформления и обработчиков сообщений.

Инициализировать сервер было легко, но надо ведь ещё обработать события!

Based on the CPU? Memory?

Most of the time, a WS connection will stay idle after the connection is established. Except when you use it for business logic, and for the keep-alive protocol (ping/pong requests), a Websocket doesn’t use a lot of resources.

Setting up an autoscaling system based on memory and/or CPU is not always the best idea and this is why automatically scaling a WebSocket application is not really easy.

Even the number of requests per server is not a good indicator because your connections are stateful, and every user will not reconnect very often. The best way to do it is to scale on open connections per server. You can have access to this value with CloudWatch if you use AWS for example.

Another thing to keep in mind is the tuning of your instances. The best way to handle a lot of persistent connections is to increase some values of your operating system and/or your application. For Node.js under a Linux based OS, you can refer to this great article: https://blog.jayway.com/2015/04/13/600k-concurrent-websocket-connections-on-aws-using-node-js/

Или getting started with WebSocket PHP без phpDaemon

Здравствуйте! Простите за столь длинный заголовок, но, надеюсь, что новичкам вроде меня будет легче найти эту статью, ведь мне ничего подобного найти не удалось. Несколько недель назад я принял решение переработать игровой клиент и сервер своей игры Growing Crystals с AJAX, на WebSocket, но всё оказалось не просто непросто, а очень сложно. Поэтому я и решил написать статью, которая бы помогла самым что ни на есть начинающим разработчикам на WebSocket + PHP сэкономить несколько дней времени, максимально подробно объясняя каждый свой шаг по настройке и запуску первого WebSocket скрипта на PHP.

Что у меня есть: Денвер на локальной машине, на нём я веду разработку проекта и дешевый PHP хостинг, на котором я публикую свой проект для того, чтобы получить обратную связь от Интернет-пользователей.

Что я хочу: Без установки phpDaemon (phpd), NodeJS и прочих вещей на локальную машину и хостинг, продолжить разработку своего проекта, но теперь с WebSocket, в этой статье разберем простой WebSocket эхо сервер.

Чего я не хочу: Говоря о NodeJS, не хочется переписывать серерную логику с PHP на другой язык, тем более устанавливать NodeJS, хотя и люблю JavaScript больше чем PHP.

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

Но для начала работы с WebSocket на дешевом хостинге можно обойтись и без него.

HTTP Streaming

HTTP Streaming — provides a long-lived connection for instant and continuous data push (Image from realtimeapi.io)

The client makes an HTTP request, and the server trickles out a response of indefinite length (it’s like polling infinitely).HTTP streaming is performant, easy to consume and can be an alternative to WebSockets.

Issue: Intermediaries can interrupt the connection (e.g. timeout, intermediaries serving other requests in a round-robin manner). In such cases, it cannot guarantee the complete realtimeness.

00:00:00 CLIENT-> I need cakes 00:00:01 SERVER-> Wait for a moment.00:00:01 SERVER-> Cake-1 is in process.00:00:02 SERVER-> Have cake-1.00:00:02 SERVER-> Wait for cake-2.00:00:03 SERVER-> Cake-2 is in process.00:00:03 SERVER-> You must be enjoying cake-1.00:00:04 SERVER-> Have cake-2.00:00:04 SERVER-> Wait for cake-3.00:00:05 CLIENT-> Enough, I'm full.

REST

The architectural style, REST (REpresentational State Transfer) is by far the most standardized way of structuring the web APIs for requests. REST is purely an architectural style based on several principles. The APIs adhering to REST principles are called RESTful APIs. REST APIs use a request/response model where every message from the server is the response to a message from the client. In general, RESTful APIs uses HTTP as its transport protocol. For such cases, lookups should use requests. , , and requests should be used for mutation, creation, and deletion respectively (avoid using requests for updating information).

High-level API¶

Server

The module defines a simple WebSocket server API.

(ws_handler, host=None, port=None, *, klass=WebSocketServerProtocol, origins=None, **kwds)

This coroutine creates a WebSocket server.

It’s a thin wrapper around the event loop’s create_server method.
host, port as well as extra keyword arguments are passed to
create_server.

ws_handler is the WebSocket handler. It must be a coroutine accepting
two arguments: a and
the request URI. If provided, origin is a list of acceptable Origin HTTP
headers. Include if the lack of an origin is acceptable.

It returns a Server object with a close method to stop the server.

Whenever a client connects, the server accepts the connection, creates a
, performs the opening
handshake, and delegates to the WebSocket handler. Once the handler
completes, the server performs the closing handshake and closes the
connection.

Since there’s no useful way to propagate exceptions triggered in handlers,
they’re sent to the websockets.server logger instead. Debugging is much
easier if you configure logging to print them:

import logging
logger = logging.getLogger('websockets.server')
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())
class (ws_handler, *, origins=None, host=None, port=None, secure=None, timeout=10, max_size=2 ** 20, loop=None)

Complete WebSocket server implementation as an asyncio protocol.

This class inherits most of its methods from
.

For the sake of simplicity, this protocol doesn’t inherit a proper HTTP
implementation. Its support for HTTP responses is very limited.

(origins=None)

Perform the server side of the opening handshake.

If provided, is a list of acceptable HTTP Origin values.
Include if the lack of an origin is acceptable.

Return the URI of the request.

Client

The module defines a simple WebSocket client API.

(uri, *, klass=WebSocketClientProtocol, origin=None, **kwds)

This coroutine connects to a WebSocket server.

It accepts an keyword argument to set the Origin HTTP header.

It’s a thin wrapper around the event loop’s create_connection method.
Extra keyword arguments are passed to create_server.

It returns a which can
then be used to send and receive messages.

It raises if uri is invalid and
if the handshake fails.

Clients shouldn’t close the WebSocket connection. Instead, they should
wait until the server performs the closing handshake by yielding from the
protocol’s attribute.

implements the sequence called “Establish a WebSocket
Connection” in RFC 6455, except for the requirement that “there MUST be no
more than one connection in a CONNECTING state.”

class (*, host=None, port=None, secure=None, timeout=10, max_size=2 ** 20, loop=None)

Complete WebSocket client implementation as an asyncio protocol.

This class inherits most of its methods from
.

(wsuri, origin=None)

Perform the client side of the opening handshake.

If provided, sets the HTTP Origin header.

Установка Workerman

Чтобы скачать Workerman, сначала устанавливаем composer:

# apt update
# apt install composer

Теперь скачиваем Workerman в папку /usr/local/workerman:

# mkdir /usr/local/workerman
# cd /usr/local/workerman
# composer require workerman/workerman

И создаём php-файл, в котором будем писать код сервера чата:

touch ChatWorker.php

Далее открываем файл ChatWorker.php для редактирования. Это можно сделать разными способами. Самый хардкорный и олдскульный вариант — редактировать прямо в терминале, воспользовавшись консольными редакторами nano, mcedit, vim и др.

Если работаете в Linux, то из рабочего окружения KDE можно подключиться через файловый менеджер Dolphin по протоколу SFTP и открыть файл в любом редакторе или даже в IDE (например, в KDevelop).

Если работаете в Windows, то можете скачать Notepad++ с плагином NppFTP, либо что-то более продвинутое, вроде Sublime / Atom / Visual Studio Code, и так же подключиться по протоколу SFTP.

Реализация клиента на Javascript

Протокол веб-сокет создан уже давно (приобрёл статус RFC в 11.12.2011) и поддерживается большинством браузеров.
Чтобы узнать поддерживает ли ваш браузер веб-сокеты перейдите по .

Работа в браузерах с вебсокетам проходит в несколько этапов:

  • Установка соединения или рукопожатие (handshake).
  • Создание обработчиков событий: onopen (соединение создано), onclose(соединение закрыто), onmessage (пришло сообщение от сервера), onerror (ошибка при работе веб-сокетов).
  • Отправка сообщений (фреймов) на сервер.

Тестировать веб-сокеты мы будем на сервере websocket.org «ws://echo.websocket.org», который будет принимать от нас сообщения и отвечать на них повторением сообщением.
Этот сайт как раз существует, что лучше понять веб-сокеты, он понимает кросс-доменные запросы, поэтому страницу с JavaScript будем размещать у себя на локальном компьютере.

Этап. Рукопожатие

Чтобы создать соединение по веб-сокету достаточно создать объект WebSocket, в котором указывается урл для подключения.

Используйте протокол «ws://», если нужно не шифрованное соединение или протокол «wss://» для шифрованного соединения.

Этап. Создание обработчиков событий.

После того как мы создали объект WebSocket необходимо повесить функции-обработчики на события.

Если нужно повесить несколько функций на событие используем методы «addEventListener» и «removeEventListener». Пример:

Этап. Отправка сообщений на сервер

По веб-сокету сообщения отправляются в виде строки. Пример отправки простого текстового сообщения.

Обработка приходящих данных лежит уже на стороне сервера. Чаще для удобства работы по вебсокету отправляют JSON данные серилизованные в строку и обрабатывают приходящие данные как строка в JSON-e. Пример использования:

Удобный способ отправки сообщений по веб-сокету служит протокол «JSON-RPC» (ссылка). Это очень простой протокол, который облегчит взаимодействие браузера и сервера. Пример использования JSON RPC:

Параметры json-rpc объекта:

  • jsonrpc — версия протокола, может быть «2.0» или «1.0»
  • id — идентификатор запроса. Используется для идентификации ответа от сервера по своем запросу. Т.е. если отправить два запроса, то ответ от сервера по каждому запросу прийдёт в разное время, для этого и нужен id. На сервере необходимо учитывать этот параметр и в ответ прислать именно нужный id.
  • method — наименование метода, любая строка, к примеру «get», «hello», «set» и др.
  • params — параметры связанные с этим методом, тип переменной может быть любой, всё зависит от сервера.

Чтобы закрыть соединение используем метод close().

От сокета к веб-сокету

Теперь, когда модуль сокетов активирован в PHP, нам предстоит путь от написания простого сокет скрипта, до скрипта способного корректно общаться по протоколу веб-сокет. Дело в том, что PHP умеет только технически работать с сокетами: принимать соедние, заводить очередь, отправлять/принимать данные, находиться в режиме ожидания и другие технические вещи. Но в нём совершенно отсутствуют готовые заголовки и другие метаданные необходимые для веб-сокетов. Обычное сокет-соединение отличается от веб-сокетов тем, что мы можем отправлять любые сообщения в произвольном формате, в то время, как веб-сокет требует обязательной отправки заголовков вида “GET / HTTP/1.1\r\nUpgrade: websocket\r\n” и соблюдение достаточного количества других правил.

Есть еще один важный нюанс, теперь касающийся PHP — скрипт обрабатывающий сокет-соединения отличается от обычного скрипта PHP, который многократно выполняется от начала до конца при разгрузке страницы обычно менее чем за 0,1 секунды. Отличаются скрипты работы с WebSocket-ами тем, что длительность их выполнения должна быть бесконечной. Т.е. мы должны инициировать выполнение PHP-скрипта содержащего бесконечный цикл, в котором происходит получение/отправка сообщений по протоколу веб-сокет. На самом деле с этим не должно быть никаких проблем, т.к. существует большое количество способов снять ограничение по времени выполнения PHP скрипта, помимо этого можно запустить скрипт в бэкграунде пользуясь консолью сервера. На PHP вполне возможно даже создание полноценного демона. Существуют готовые решения, о которых я упоминал выше, например PhpDaemon, но сегодня речь не о них.

Для начала, чтобы убедиться что мне не мешают файрволлы, всё настроено правильно и связь между клиентом и сервером может быть установлена, я решил написать и протестировать небольшой PHP скрипт выполняющий роль сокет-сервера (именно сокет, а не веб-сокет!), устанавливающий соединение и отвечающий всем фразой “Hello, Client!”. Но для его тестирования нужно несколько клиентов (веб-сокет клиент, чтобы понять базовое отличие от простого сокета и обычный telnet).

Установление WebSocket-соединения

Протокол работает над TCP.

Это означает, что при соединении браузер отправляет по HTTP специальные заголовки, спрашивая: «поддерживает ли сервер WebSocket?».

Если сервер в ответных заголовках отвечает «да, поддерживаю», то дальше HTTP прекращается и общение идёт на специальном протоколе WebSocket, который уже не имеет с HTTP ничего общего.

Пример запроса от браузера при создании нового объекта :

Описания заголовков:

GET, Host
Стандартные HTTP-заголовки из URL запроса
Upgrade, Connection
Указывают, что браузер хочет перейти на websocket.
Origin
Протокол, домен и порт, откуда отправлен запрос.
Sec-WebSocket-Key
Случайный ключ, который генерируется браузером: 16 байт в кодировке Base64.
Sec-WebSocket-Version
Версия протокола. Текущая версия: 13.

Все заголовки, кроме и , браузер генерирует сам, без возможности вмешательства JavaScript.

Такой XMLHttpRequest создать нельзя

Создать подобный XMLHttpRequest-запрос (подделать ) невозможно, по одной простой причине: указанные выше заголовки запрещены к установке методом .

Сервер может проанализировать эти заголовки и решить, разрешает ли он с данного домена .

Ответ сервера, если он понимает и разрешает -подключение:

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

Затем данные передаются по специальному протоколу, структура которого («фреймы») изложена далее. И это уже совсем не HTTP.

Также возможны дополнительные заголовки и , описывающие расширения и подпротоколы (subprotocol), которые поддерживает данный клиент.

Посмотрим разницу между ними на двух примерах:

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

    Это говорит не о самих данных, а об улучшении способа их передачи. Браузер сам формирует этот заголовок.

  • Заголовок говорит о том, что по WebSocket браузер собирается передавать не просто какие-то данные, а данные в протоколах SOAP или WAMP («The WebSocket Application Messaging Protocol»). Стандартные подпротоколы регистрируются в специальном каталоге IANA.

    Этот заголовок браузер поставит, если указать второй необязательный параметр :

При наличии таких заголовков сервер может выбрать расширения и подпротоколы, которые он поддерживает, и ответить с ними.

Например, запрос:

Ответ:

В ответе выше сервер указывает, что поддерживает расширение , а из запрошенных подпротоколов – только SOAP.

Соединение можно открывать как или как . Протокол представляет собой WebSocket над HTTPS.

Кроме большей безопасности, у есть важное преимущество перед обычным – большая вероятность соединения. Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP – нет

Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP – нет.

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

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

Оценка угрозы

Ограничивающие факторы

Если говорить серьёзно, то этот вектор атаки достаточно слаб. Вам нужно заманить ничего не подозревающих пользователей на свой сайт и убедить их там оставаться во время разработки JS кода. 

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

Обобщая соображения

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

Представьте себе внутреннего работника Twitbook, который просто нажимает save в редакторе, обуславливая тем самым слив токена доступа или внутреннего адреса сервера “не той” аудитории. 

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

Устранение проблемы

Я следовал идее попыток прервать механизмы горячей перезагрузки JavaScript, потому что это фактически единственное общее использование websocket, с которым я знаком. Discord тоже использует websocket, но беглое его рассмотрение не принесло никаких очевидных результатов, поскольку этот канал спроектирован с учётом публичного интернета. 

Беспокоит то, что всего один этот простой случай использования канала односторонней связи для перезагрузки раскрыл так много потенциальной информации для вредоносных сайтов. 

Учитывая это, скорее всего, и другие случаи использования websocket (для данных, спроектированных не для публичного интернета) могут быть также скомпрометированы.

Вероятно, webpack-dev-server должен выполнять некоторую аутентификацию или для горячей перезагрузки должны использоваться альтернативные каналы связи браузера (думаю, это уже планируется и по другим причинам).

Определённо удивляет этот способ, которым браузеры и веб-стандарты реализует политика безопасности источников для websocket, и который приводит к тому, что ПО, предназначенное только для локальной разработки, предоставляется для публичного интернета не лучшим способом. 

Хочется надеяться на какое-то исправление, которое бы привнесло в браузеры дополнительные элементы управления.

  • Как сделать приложение-чат с Redis, WebSocket и Go
  • Как создавать веб-сокеты в Python
  • Веб-скрапинг для веб-разработчиков: краткие сведения

Перевод статьи Steve Stagg: Stealing Secrets from Developers using Websockets.

Data transfer

WebSocket communication consists of “frames” – data fragments, that can be sent from either side, and can be of several kinds:

  • “text frames” – contain text data that parties send to each other.
  • “binary data frames” – contain binary data that parties send to each other.
  • “ping/pong frames” are used to check the connection, sent from the server, the browser responds to these automatically.
  • there’s also “connection close frame” and a few other service frames.

In the browser, we directly work only with text or binary frames.

WebSocket method can send either text or binary data.

A call allows in string or a binary format, including , , etc. No settings required: just send it out in any format.

When we receive the data, text always comes as string. And for binary data, we can choose between and formats.

That’s set by property, it’s by default, so binary data comes as objects.

Blob is a high-level binary object, it directly integrates with , and other tags, so that’s a sane default. But for binary processing, to access individual data bytes, we can change it to :

Клиент

Примитивное приложение-клиент будет реализовано с использованием модуля QtWidgets и написано на C++. Сообщения будут отображаться в обычном текстовом поле в режиме readonly (привет любителям лампового IRC :-)).

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

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

Сообщения отправляются нажатием на кнопку Return (Enter) на клавиатуре.

Прототип окна чата

Диалог авторизации будет вызываться сразу после запуска приложения, а так же при разрыве соединений.

Весь проект доступен на GitHub: https://github.com/wxmaper/SimpleChat-client

В рамках этой статьи рассмотрим лишь основные моменты.

Пространства и «комнаты»¶

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

Пространства позволяют изолировать одни сокеты от других.

app.js

В приведенном примере с помощью метода на сервере определяются два пространства: и . На клиентской стороне подключение к тому или иному пространству происходит в зависимости от текущего маршрута. Таким образом, при отправке данных, например, из пространства , об этом будут оповещены только сокеты этого пространства. По умолчанию все сокеты находятся в пространстве .

Также и в пределах пространства можно распределять сокеты по так называемым «комнатам».

Чтобы отнести сокет к определенной «комнате» используется метод пространства , который принимает имя «комнаты» (задается пользователем модуля ). Для вынесения сокета из комнаты используйте метод .

Отправка данных в «комнату» осуществляется с помощью метода .

Обработка инициируемых в пределах «комнаты» событий осуществляется с использованием метода .

Установка соединения с сервером

В проекте реализован метод Widget::connectToServer, он открывает диалог авторизации. Если диалог будет закрыт (result != AuthDialog::Accepted), то приложение закроется вместе с ним.

Если же ввести все авторизационные данные, то будет осуществлена попытка подключения к серверу.

void Widget::connectToServer()
{
    AuthDialog authDialog(this);
    authDialog.setConnectionData(m_connectionData);

    int result = authDialog.exec();

    if (result == AuthDialog::Accepted) {
        m_connectionData = authDialog.connectionData();

        QString html = QString("%1 "
                               "Установка соединения с %2:%3...")
                .arg(datetime())
                .arg(m_connectionData.server)
                .arg(m_connectionData.port);
        ui->textBrowser->append(html);

        m_webSocket->open(QUrl(QString("ws://%1:%2?userName=%3&userColor=%4&gender=%5")
                               .arg(m_connectionData.server)
                               .arg(m_connectionData.port)
                               .arg(m_connectionData.userName)
                               .arg(QString(m_connectionData.userColor).replace("#","%23"))
                               .arg(m_connectionData.gender)));
    }
    else {
        qApp->quit();
    }
}
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector