diff --git a/1-js/08-prototypes/04-prototype-methods/article.md b/1-js/08-prototypes/04-prototype-methods/article.md index 82a751f9..3314645e 100644 --- a/1-js/08-prototypes/04-prototype-methods/article.md +++ b/1-js/08-prototypes/04-prototype-methods/article.md @@ -1,5 +1,5 @@ -# Very plain objects, no __proto__ +# Plain objects: no __proto__ In the first chapter of this section, we mentioned that there are modern methods to setup a prototype. diff --git a/7-network/08-websocket/article.md b/7-network/08-websocket/article.md index 892be8d1..30bfbd77 100644 --- a/7-network/08-websocket/article.md +++ b/7-network/08-websocket/article.md @@ -16,7 +16,15 @@ let socket = new WebSocket("*!*ws*/!*://javascript.info"); There's also encrypted `wss://` protocol. It's like HTTPS for websockets. -Once the socket is created, we should listen for events on it. There are totally 4 events: +```smart header="Always prefer `wss://`" +The `wss://` protocol not only encrypted, but also more reliable. + +That's because `ws://` data is not encrypted, visible for any intermediary. Old proxy servers do not know about WebSocket, they may see "strange" headers and abort the connection. + +On the other hand, `wss://` is WebSocket over TLS, (same as HTTPS is HTTP over TLS), the transport security layer encrypts the data at sender and decrypts at the receiver, so it passes encrypted through proxies. They can't see what's inside and let it through. +``` + +Once the socket is created, we should listen to events on it. There are totally 4 events: - **`open`** -- connection established, - **`message`** -- data received, - **`error`** -- websocket error, @@ -27,18 +35,18 @@ Once the socket is created, we should listen for events on it. There are totally Here's an example: ```js run -let socket = new WebSocket("ws://javascript.local/article/websocket/demo/hello"); +let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello"); -*!*socket.onopen*/!* = function(e) { +socket.onopen = function(e) { alert("[open] Connection established, send -> server"); - *!*socket.send*/!*("My name is John"); + socket.send("My name is John"); }; -*!*socket.onmessage*/!* = function(event) { +socket.onmessage = function(event) { alert(`[message] Data received: ${event.data} <- server`); }; -*!*socket.onclose*/!* = function(event) { +socket.onclose = function(event) { if (event.wasClean) { alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { @@ -48,106 +56,90 @@ let socket = new WebSocket("ws://javascript.local/article/websocket/demo/hello") } }; -*!*socket.onerror*/!* = function(error) { +socket.onerror = function(error) { alert(`[error] ${error.message}`); }; ``` -For demo purposes, there's a small backend [server.js](demo/server.js) written in Node.js (could be even simpler). It responds with "hello", then waits 5 seconds and closes the connection. +For demo purposes, there's a small server [server.js](demo/server.js) written in Node.js, for the example above, running. It responds with "hello", then waits 5 seconds and closes the connection. So you'll see events `open` -> `message` -> `close`. -Quite simple, isn't it? +That's actually it, we can talk WebSocket already. Quite simple, isn't it? -There are few peculiarities though, so let's talk more in-depth, step by step. +Now let's talk more in-depth. +## Opening a websocket -## Установление WebSocket-соединения +When `new WebSocket(url)` is created, it starts an HTTP handshake (HTTPS for `wss://`). -Протокол `WebSocket` работает *над* TCP. +The browser asks the server: "Do you support Websocket?" And if the server says "yes", then the talk continues in WebSocket protocol, which is not HTTP at all. -Это означает, что при соединении браузер отправляет по HTTP специальные заголовки, спрашивая: "поддерживает ли сервер WebSocket?". + -Если сервер в ответных заголовках отвечает "да, поддерживаю", то дальше HTTP прекращается и общение идёт на специальном протоколе WebSocket, который уже не имеет с HTTP ничего общего. - -### Установление соединения - -Пример запроса от браузера при создании нового объекта `new WebSocket("ws://server.example.com/chat")`: +Here's an example of browser request for `new WebSocket("wss://javascript.info/chat")`. ``` -GET /chat HTTP/1.1 -Host: server.example.com -Upgrade: websocket +GET /chat +Host: javascript.info +Origin: https://javascript.info Connection: Upgrade -Origin: http://javascript.ru +Upgrade: websocket Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 ``` -Описания заголовков: +- `Origin` -- the origin of the client page. WebSocket is cross-origin by nature. There are no special headers or other limitations. Old servers are unable to handle WebSocket anyway, so there are no compabitility issues. But `Origin` header is important, as it allows the server to decide whether or not to talk WebSocket with this website. +- `Connection: Upgrade` -- signals that the client would like to change the protocol. +- `Upgrade: websocket` -- the requested protocol is "websocket". +- `Sec-WebSocket-Key` -- a random browser-generated key for security. +- `Sec-WebSocket-Version` -- WebSocket protocol version, 13 is the current one. -GET, Host -: Стандартные HTTP-заголовки из URL запроса - -Upgrade, Connection -: Указывают, что браузер хочет перейти на websocket. - -Origin -: Протокол, домен и порт, откуда отправлен запрос. - -Sec-WebSocket-Key -: Случайный ключ, который генерируется браузером: 16 байт в кодировке [Base64](http://ru.wikipedia.org/wiki/Base64). - -Sec-WebSocket-Version -: Версия протокола. Текущая версия: 13. - -Все заголовки, кроме `GET` и `Host`, браузер генерирует сам, без возможности вмешательства JavaScript. - -```smart header="Такой XMLHttpRequest создать нельзя" -Создать подобный XMLHttpRequest-запрос (подделать `WebSocket`) невозможно, по одной простой причине: указанные выше заголовки запрещены к установке методом `setRequestHeader`. +```smart header="WebSocket handshake can't be emulated" +We can't use `XMLHttpRequest` or `fetch` to make this kind of HTTP-request, because Javascript is not allowed to set these headers. ``` -**Сервер может проанализировать эти заголовки и решить, разрешает ли он `WebSocket` с данного домена `Origin`.** - -Ответ сервера, если он понимает и разрешает `WebSocket`-подключение: +If the server agrees to switch to WebSocket, it should send code 101 response: ``` -HTTP/1.1 101 Switching Protocols +101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= ``` -Здесь строка `Sec-WebSocket-Accept` представляет собой перекодированный по специальному алгоритму ключ `Sec-WebSocket-Key`. Браузер использует её для проверки, что ответ предназначается именно ему. +Here `Sec-WebSocket-Accept` is `Sec-WebSocket-Key`, recoded using a special algorithm. The browser uses it to make sure that the response corresponds to the request. -Затем данные передаются по специальному протоколу, структура которого ("фреймы") изложена далее. И это уже совсем не HTTP. +Afterwards, the data is transfered using WebSocket protocol, we'll see its structure ("frames") soon. And that's not HTTP at all. -### Расширения и подпротоколы -Также возможны дополнительные заголовки `Sec-WebSocket-Extensions` и `Sec-WebSocket-Protocol`, описывающие расширения и подпротоколы (subprotocol), которые поддерживает данный клиент. +### Extensions and subprotocols -Посмотрим разницу между ними на двух примерах: +There may be additional headers `Sec-WebSocket-Extensions` and `Sec-WebSocket-Protocol` that describe extensions and subprotocols. -- Заголовок `Sec-WebSocket-Extensions: deflate-frame` означает, что браузер поддерживает модификацию протокола, обеспечивающую сжатие данных. +For instance: - Это говорит не о самих данных, а об улучшении способа их передачи. Браузер сам формирует этот заголовок. -- Заголовок `Sec-WebSocket-Protocol: soap, wamp` говорит о том, что по WebSocket браузер собирается передавать не просто какие-то данные, а данные в протоколах [SOAP](http://ru.wikipedia.org/wiki/SOAP) или WAMP ("The WebSocket Application Messaging Protocol"). Стандартные подпротоколы регистрируются в специальном каталоге [IANA](http://www.iana.org/assignments/websocket/websocket.xml). +- `Sec-WebSocket-Extensions: deflate-frame` means that the browser supports data compression. An extension is something related to transferring the data, not data itself. - Этот заголовок браузер поставит, если указать второй необязательный параметр `WebSocket`: +- `Sec-WebSocket-Protocol: soap, wamp` means that we're going to transfer not just any data, but the data in [SOAP](http://en.wikipedia.org/wiki/SOAP) or WAMP ("The WebSocket Application Messaging Protocol") protocols. WebSocket subprotocols are registered in the [IANA catalogue](http://www.iana.org/assignments/websocket/websocket.xml). - ```js - var socket = new WebSocket("*!*ws*/!*://javascript.ru/ws", ["soap", "wamp"]); - ``` +`Sec-WebSocket-Extensions` is sent by the browser automatically, with a list of possible extensions it supports. -При наличии таких заголовков сервер может выбрать расширения и подпротоколы, которые он поддерживает, и ответить с ними. +`Sec-WebSocket-Protocol` is depends on us: we decide what kind of data we send. The second optional parameter of `new WebSocket` lists subprotocols: -Например, запрос: +```js +let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]); +``` + +The server should respond with a list of protocls and extensions that it agrees to use. + +For example, the request: ``` -GET /chat HTTP/1.1 -Host: server.example.com +GET /chat +Host: javascript.info Upgrade: websocket Connection: Upgrade -Origin: http://javascript.ru +Origin: https://javascript.info Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 *!* @@ -156,10 +148,10 @@ Sec-WebSocket-Protocol: soap, wamp */!* ``` -Ответ: +Response: ``` -HTTP/1.1 101 Switching Protocols +101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= @@ -169,337 +161,225 @@ Sec-WebSocket-Protocol: soap */!* ``` -В ответе выше сервер указывает, что поддерживает расширение `deflate-frame`, а из запрошенных подпротоколов -- только SOAP. +Here the server responds that it supports the extension `deflate-frame`, and only SOAP of the requested subprotocols. -### WSS +## WebSocket data -Соединение `WebSocket` можно открывать как `WS://` или как `WSS://`. Протокол `WSS` представляет собой WebSocket над HTTPS. +WebSocket communication consists of "frames" that can be sent from either side: -**Кроме большей безопасности, у `WSS` есть важное преимущество перед обычным `WS` -- большая вероятность соединения.** +- "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. +- "connection close frame" and a few other service frames. -Дело в том, что HTTPS шифрует трафик от клиента к серверу, а HTTP -- нет. +In the browser, we only care about text or binary frames. -Если между клиентом и сервером есть прокси, то в случае с HTTP все WebSocket-заголовки и данные передаются через него. Прокси имеет к ним доступ, ведь они никак не шифруются, и может расценить происходящее как нарушение протокола HTTP, обрезать заголовки или оборвать передачу. +**WebSocket `.send()` can send either text or binary data, doesn't matter.** -А в случае с `WSS` весь трафик сразу кодируется и через прокси проходит уже в закодированном виде. Поэтому заголовки гарантированно пройдут, и общая вероятность соединения через `WSS` выше, чем через `WS`. +For sending, `socket.send(body)` allows strings or any binary format, including `Blob`, `ArrayBuffer`, etc. No settings required: just send it out. -## Формат данных +**Textual data always comes as string. For receiving binary data, we can choose between `Blob` and `ArrayBuffer` formats.** -Полное описание протокола содержится в [RFC 6455](http://tools.ietf.org/html/rfc6455). +The `socket.bufferType` is `"blob"` by default, so binary data comes in Blobs. -Здесь представлено частичное описание с комментариями самых важных его частей. Если вы хотите понять стандарт, то рекомендуется сначала прочитать это описание. - -### Описание фрейма - -В протоколе WebSocket предусмотрены несколько видов пакетов ("фреймов"). - -Они делятся на два больших типа: фреймы с данными ("data frames") и управляющие ("control frames"), предназначенные для проверки связи (PING) и закрытия соединения. - -Фрейм, согласно стандарту, выглядит так: - -
- 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-------+-+-------------+-------------------------------+ - |F|R|R|R| опкод |М| Длина тела | Расширенная длина тела | - |I|S|S|S|(4бита)|А| (7бит) | (1 байт) | - |N|V|V|V| |С| |(если длина тела==126 или 127) | - | |1|2|3| |К| | | - | | | | | |А| | | - +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + - | Продолжение расширенной длины тела, если длина тела = 127 | - + - - - - - - - - - - - - - - - +-------------------------------+ - | | Ключ маски, если МАСКА = 1 | - +-------------------------------+-------------------------------+ - | Ключ маски (продолжение) | Данные фрейма ("тело") | - +-------------------------------- - - - - - - - - - - - - - - - + - : Данные продолжаются ... : - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - | Данные продолжаются ... | - +---------------------------------------------------------------+ -- -С виду -- не очень понятно, во всяком случае, для большинства людей. - -**Позвольте пояснить: читать следует слева-направо, сверху-вниз, каждая горизонтальная полоска это 32 бита.** - -То есть, вот первые 32 бита: - -
- 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-------+-+-------------+-------------------------------+ - |F|R|R|R| опкод |М| Длина тела | Расширенная длина тела | - |I|S|S|S|(4бита)|А| (7бит) | (1 байт) | - |N|V|V|V| |С| |(если длина тела==126 или 127) | - | |1|2|3| |К| | | - | | | | | |А| | | - +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + -- -Сначала идёт бит FIN (вертикальная надпись на рисунке), затем биты RSV1, RSV2, RSV3 (их смысл раскрыт ниже), затем "опкод", "МАСКА" и, наконец, "Длина тела", которая занимает 7 бит. Затем, если "Длина тела" равна 126 или 127, идёт "Расширенная длина тела", потом (на следующей строке, то есть после первых 32 бит) будет её продолжение, ключ маски, и потом данные. - -А теперь -- подробное описание частей фрейма, то есть как именно передаются сообщения: - -FIN: 1 бит -: Одно сообщение, если оно очень длинное (вызовом `send` можно передать хоть целый файл), может состоять из множества фреймов ("быть фрагментированным"). - - У всех фреймов, кроме последнего, этот фрагмент установлен в `0`, у последнего -- в `1`. - - Если сообщение состоит из одного-единственного фрейма, то `FIN` в нём равен `1`. - -RSV1, RSV2, RSV3: 1 бит каждый -: В обычном WebSocket равны `0`, предназначены для расширений протокола. Расширение может записать в эти биты свои значения. - -Опкод: 4 бита -: Задаёт тип фрейма, который позволяет интерпретировать находящиеся в нём данные. Возможные значения: - - - `0x1` обозначает текстовый фрейм. - - `0x2` обозначает двоичный фрейм. - - `0x3-7` зарезервированы для будущих фреймов с данными. - - `0x8` обозначает закрытие соединения этим фреймом. - - `0x9` обозначает PING. - - `0xA` обозначает PONG. - - `0xB-F` зарезервированы для будущих управляющих фреймов. - - `0x0` обозначает фрейм-продолжение для фрагментированного сообщения. Он интерпретируется, исходя из ближайшего предыдущего ненулевого типа. - -Маска: 1 бит -: Если этот бит установлен, то данные фрейма маскированы. Более подробно маску и маскирование мы рассмотрим далее. - -Длина тела: 7 битов, 7+16 битов, или 7+64 битов -: Если значение поле "Длина тела" лежит в интервале `0-125`, то оно обозначает длину тела (используется далее). - Если `126`, то следующие 2 байта интерпретируются как 16-битное беззнаковое целое число, содержащее длину тела. - Если `127`, то следующие 8 байт интерпретируются как 64-битное беззнаковое целое, содержащее длину. - - Такая хитрая схема нужна, чтобы минимизировать накладные расходы. Для сообщений длиной `125` байт и меньше хранение длины потребует всего 7 битов, для бóльших (до 65536) -- 7 битов + 2 байта, ну а для ещё бóльших -- 7 битов и 8 байт. Этого хватит для хранения длины сообщения размером в гигабайт и более. - -Ключ маски: 4 байта. -: Если бит `Маска` установлен в 0, то этого поля нет. Если в `1` то эти байты содержат маску, которая налагается на тело (см. далее). - -Данные фрейма (тело) -: Состоит из "данных расширений" и "данных приложения", которые идут за ними. Данные расширений определяются конкретными расширениями протокола и по умолчанию отсутствуют. Длина тела должна быть равна указанной в заголовке. - -### Примеры - -Некоторые примеры сообщений: - -- Нефрагментированное текстовое сообщение `Hello` без маски: - - ``` - 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (содержит "Hello") - ``` - - В заголовке первый байт содержит `FIN=1` и `опкод=0x1` (получается `10000001` в двоичной системе, то есть `0x81` -- в 16-ричной), далее идёт длина `0x5`, далее текст. - -- Фрагментированное текстовое сообщение `Hello World` из трёх частей, без маски, может выглядеть так: - - ``` - 0x01 0x05 0x48 0x65 0x6c 0x6c 0x6f (содержит "Hello") - 0x00 0x01 0x20 (содержит " ") - 0x80 0x05 0x57 0x6f 0x72 0x6c 0x64 (содержит "World") - ``` - - - У первого фрейма `FIN=0` и текстовый опкод `0x1`. - - У второго `FIN=0` и опкод `0x0`. При фрагментации сообщения, у всех фреймов, кроме первого, опкод пустой (он один на всё сообщение). - - У третьего, последнего фрейма `FIN=1`. - - -А теперь посмотрим на все те замечательные возможности, которые даёт этот формат фрейма. - -### Фрагментация - -Позволяет отправлять сообщения в тех случаях, когда на момент начала посылки полный размер ещё неизвестен. - -Например, идёт поиск в базе данных и что-то уже найдено, а что-то ещё может быть позже. - -- У всех сообщений, кроме последнего, бит `FIN=0`. -- Опкод указывается только у первого, у остальных он должен быть равен `0x0`. - -### PING / PONG - -В протокол встроена проверка связи при помощи управляющих фреймов типа PING и PONG. - -Тот, кто хочет проверить соединение, отправляет фрейм PING с произвольным телом. Его получатель должен в разумное время ответить фреймом PONG с тем же телом. - -Этот функционал встроен в браузерную реализацию, так что браузер ответит на PING сервера, но управлять им из JavaScript нельзя. - -**Иначе говоря, сервер всегда знает, жив ли посетитель или у него проблема с сетью.** - -### Чистое закрытие - -При закрытии соединения сторона, желающая это сделать (обе стороны в WebSocket равноправны) отправляет закрывающий фрейм (опкод `0x8`), в теле которого указывает причину закрытия. - -В браузерной реализации эта причина будет содержаться в свойстве `reason` события `onclose`. - -**Наличие такого фрейма позволяет отличить "чистое закрытие" от обрыва связи.** - -В браузерной реализации событие `onclose` при чистом закрытии имеет `event.wasClean = true`. - -### Коды закрытия - -Коды закрытия вебсокета `event.code`, чтобы не путать их с HTTP-кодами, состоят из 4 цифр: - -`1000` -: Нормальное закрытие. - -`1001` -: Удалённая сторона "исчезла". Например, процесс сервера убит или браузер перешёл на другую страницу. - -`1002` -: Удалённая сторона завершила соединение в связи с ошибкой протокола. - -`1003` -: Удалённая сторона завершила соединение в связи с тем, что она получила данные, которые не может принять. Например, сторона, которая понимает только текстовые данные, может закрыть соединение с таким кодом, если приняла бинарное сообщение. - -### Атака "отравленный кэш" - -В ранних реализациях WebSocket существовала уязвимость, называемая "отравленный кэш" (cache poisoning). - -**Она позволяла атаковать кэширующие прокси-сервера, в частности, корпоративные.** - -Атака осуществлялась так: - -1. Хакер заманивает доверчивого посетителя (далее Жертва) на свою страницу. -2. Страница открывает `WebSocket`-соединение на сайт хакера. Предполагается, что Жертва сидит через прокси. Собственно, на прокси и направлена эта атака. -3. Страница формирует специального вида WebSocket-запрос, который (и здесь самое главное!) ряд прокси серверов не понимают. - - Они пропускают начальный запрос через себя (который содержит `Connection: upgrade`) и думают, что далее идёт уже следующий HTTP-запрос. - - ...Но на самом деле там данные, идущие через вебсокет! И обе стороны вебсокета (страница и сервер) контролируются Хакером. Так что хакер может передать в них нечто похожее на GET-запрос к известному ресурсу, например `http://code.jquery.com/jquery.js`, а сервер ответит "якобы кодом jQuery" с кэширующими заголовками. - - Прокси послушно проглотит этот ответ и закэширует "якобы jQuery". -4. В результате при загрузке последующих страниц любой пользователь, использующий тот же прокси, что и Жертва, получит вместо `http://code.jquery.com/jquery.js` хакерский код. - -Поэтому эта атака и называется "отравленный кэш". - -**Такая атака возможна не для любых прокси, но при анализе уязвимости было показано, что она не теоретическая, и уязвимые прокси действительно есть.** - -Поэтому придумали способ защиты -- "маску". - -### Маска для защиты от атаки - -Для того, чтобы защититься от атаки, и придумана маска. - -*Ключ маски* -- это случайное 32-битное значение, которое варьируется от пакета к пакету. Тело сообщения проходит через XOR `^` с маской, а получатель восстанавливает его повторным XOR с ней (можно легко доказать, что `(x ^ a) ^ a == x`). - -Маска служит двум целям: - -1. Она генерируется браузером. Поэтому теперь хакер не сможет управлять реальным содержанием тела сообщения. После накладывания маски оно превратится в бинарную мешанину. -2. Получившийся пакет данных уже точно не может быть воспринят промежуточным прокси как HTTP-запрос. - -**Наложение маски требует дополнительных ресурсов, поэтому протокол WebSocket не требует её.** - -Если по этому протоколу связываются два клиента (не обязательно браузеры), доверяющие друг другу и посредникам, то можно поставить бит `Маска` в `0`, и тогда ключ маски не указывается. - -## Пример - -Рассмотрим прототип чата на WebSocket и Node.JS. - -HTML: посетитель отсылает сообщения из формы и принимает в `div` - -```html - - - - - -``` - -Код на клиенте: +[Blob](info:blob) is a high-level binary object, it directly integrates with ``, `