diff --git a/1-js/4-data-structures/11-datetime/article.md b/1-js/4-data-structures/11-datetime/article.md index bfd6539c..148c00a8 100644 --- a/1-js/4-data-structures/11-datetime/article.md +++ b/1-js/4-data-structures/11-datetime/article.md @@ -414,7 +414,7 @@ alert( d.toString() ); // вывод, похожий на 'Wed Jan 26 2011 16:40
То же самое, что `toString()`, но дата в зоне UTC.
`toISOString()`
-
Возвращает дату в формате ISO Детали формата будут далее. Поддерживается современными браузерами, не поддерживается IE<9. +
Возвращает дату в формате ISO Детали формата будут далее. Поддерживается современными браузерами, не поддерживается IE8-. ```js //+ run diff --git a/1-js/5-functions-closures/2-closures/article.md b/1-js/5-functions-closures/2-closures/article.md index a87125fb..07d6781a 100644 --- a/1-js/5-functions-closures/2-closures/article.md +++ b/1-js/5-functions-closures/2-closures/article.md @@ -102,7 +102,7 @@ sayHi.[[Scope]] = window При запуске функции её объект переменных `LexicalEnvironment` получает ссылку на "внешнее лексическое окружение" со значением из `[[Scope]]`. -Если переменная не найдена в функции -- она будет искаться в снаружи. +Если переменная не найдена в функции -- она будет искаться снаружи. Именно благодаря этой механике в примере выше `alert(userName)` выводит внешнюю переменную. На уровне кода это выглядит как поиск во внешней области видимости, вне функции. diff --git a/3-more/2-ajax/10-ajax-jsonp/article.md b/3-more/2-ajax/10-ajax-jsonp/article.md index fde6abb0..9fdf1880 100644 --- a/3-more/2-ajax/10-ajax-jsonp/article.md +++ b/3-more/2-ajax/10-ajax-jsonp/article.md @@ -1,97 +1,100 @@ -# JSONP, получение данных через SCRIPT +# Протокол JSONP -Если создать узел `SCRIPT` со ссылкой на внешний исходник, то при добавлении в документ запустится процесс загрузки. В ответ сервер может прислать скрипт, содержащий нужные данные. +Если создать тег ` + ``` -Браузер тут же обработает его: запросит `/user.php?id=123` с заданного URL и выполнит. +Браузер тут же обработает его: запросит `/user?id=123` с заданного URL и выполнит. ## Обработка ответа, JSONP -В примере выше рассмотрено создание запроса, но как получить ответ? Самый простой способ -- это присвоить его в переменную, т.е. сервер ответит как-то так: +В примере выше рассмотрено создание запроса, но как получить ответ? Допустим, сервер хочет прислать объект с данными. + +Конечно, он может присвоить её в переменную, например так: + ```js -var response = "...."; +// ответ сервера +var user = {name: "Вася", age: 25 }; ``` -...А браузер по `script.onload` отловит окончание загрузки и получит значение `response`. +...А браузер по `script.onload` отловит окончание загрузки и прочитает значение `user`. -Но что, если одновременно делается несколько запросов? Получается, нужно присваивать в разные переменные. +Но что, если одновременно делается несколько запросов? Получается, нужно присваивать в разные переменные. -Для большей гибкости при ответе используется протокол `JSONP`. +Протокол JSONP как раз и призван облегчить эту задачу. -**При этом название функции браузер может передать вместе с запросом.** +Он очень простой: -Запрос: +
    +
  1. Вместе с запросом клиент в специальном, заранее оговорённом, параметре передаёт название функции. + +Обычно такой параметр называется `callback`. Например : ```js -attachScript('/user.php?id=123&*!*callback=onUserData*/!*'); +addScript('user?id=123&*!*callback=onUserData*/!*'); ``` - -Cервер кодирует данные как JavaScript-объект и оборачивает их в функцию, название которой получает из параметра `callback`: +
  2. +
  3. Cервер кодирует данные в JSON и оборачивает их в функцию, название которой получает из параметра `callback`: ```js +// ответ сервера onUserData({ name: "Вася", - age: 25, - isAdmin: false + age: 25 }); ``` - -**Формат запроса, когда JSON заворачивается в функцию, называется JSONP ("JSON with Padding").** - -[warn header="Аспекты безопасности"] -
      -
    1. При кросс-доменном получении скрипта JavaScript не имеет доступа к его тексту.
    2. -
    3. Клиентский код должен доверять серверу при таком запросе. Ведь серверу ничего не стоит добавить в скрипт любые команды.
    4. +
    + +Это и называется JSONP ("JSON with Padding"). + + +[warn header="Аспект безопасности"] +Клиентский код должен доверять серверу при таком запросе. Ведь серверу ничего не стоит добавить в скрипт любые команды. [/warn] ## Реестр CallbackRegistry -В примере выше функция `onUserData` должна быть глобальной, иначе скрипт её не увидит. +В примере выше функция `onUserData` должна быть глобальной, ведь ` + ``` Сервер обернёт ответ в функцию `CallbackRegistry.func12345`, она вызывает нужный обработчик и очищает память, удаляя себя. -Далее мы посмотрим код всего этого, но перед этим -- важный момент! Нужно предусмотреть обработку ошибок. +Далее мы посмотрим более полный код всего этого, но перед этим -- важный момент! Нужно предусмотреть обработку ошибок. ## Обнаружение ошибок @@ -106,39 +109,23 @@ scriptRequest("/user.php?id=123", function(data) { Чтобы отловить их все "одним махом", используем следующий алгоритм:
      -
    1. Создаётся `SCRIPT`.
    2. -
    3. На `SCRIPT` ставятся обработчики `onreadystatechange` (для старых IE) и `onload/onerror` (для остальных браузеров).
    4. +
    5. Создаётся ` -[/head] \ No newline at end of file diff --git a/3-more/2-ajax/10-ajax-jsonp/jsonp.view/index.html b/3-more/2-ajax/10-ajax-jsonp/jsonp.view/index.html new file mode 100644 index 00000000..d4cbb4c2 --- /dev/null +++ b/3-more/2-ajax/10-ajax-jsonp/jsonp.view/index.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/3-more/2-ajax/10-ajax-jsonp/scriptRequest.js b/3-more/2-ajax/10-ajax-jsonp/jsonp.view/scriptRequest.js similarity index 97% rename from 3-more/2-ajax/10-ajax-jsonp/scriptRequest.js rename to 3-more/2-ajax/10-ajax-jsonp/jsonp.view/scriptRequest.js index 601bc41d..acca02eb 100644 --- a/3-more/2-ajax/10-ajax-jsonp/scriptRequest.js +++ b/3-more/2-ajax/10-ajax-jsonp/jsonp.view/scriptRequest.js @@ -6,7 +6,7 @@ function scriptRequest(url, onSuccess, onError) { var scriptOk = false; // флаг, что вызов прошел успешно // сгенерировать имя JSONP-функции для запроса - var callbackName = 'f'+String(Math.random()).slice(2); + var callbackName = 'cb' + String(Math.random()).slice(-6); // укажем это имя в URL запроса url += ~url.indexOf('?') ? '&' : '?'; diff --git a/3-more/2-ajax/10-ajax-jsonp/jsonp.view/server.js b/3-more/2-ajax/10-ajax-jsonp/jsonp.view/server.js new file mode 100644 index 00000000..1f2103df --- /dev/null +++ b/3-more/2-ajax/10-ajax-jsonp/jsonp.view/server.js @@ -0,0 +1,38 @@ +var http = require('http'); +var url = require('url'); +var static = require('node-static'); +var file = new static.Server('.', { cache: 0 }); + + +function accept(req, res) { + + var urlParsed = url.parse(req.url, true); + + if (urlParsed.pathname == '/user') { + var id = urlParsed.query.id; + var callback = urlParsed.query.callback; + + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + + var user = { + name: "Вася", + id: id + }; + + res.end(callback + '(' + JSON.stringify(user) + ')'); + + } else { + file.serve(req, res); + } + +} + + +// ------ запустить сервер ------- + +if (!module.parent) { + http.createServer(accept).listen(8080); +} else { + exports.accept = accept; +} + diff --git a/3-more/2-ajax/11-server-sent-events/article.md b/3-more/2-ajax/11-server-sent-events/article.md index 0c3a050d..232e5ea1 100644 --- a/3-more/2-ajax/11-server-sent-events/article.md +++ b/3-more/2-ajax/11-server-sent-events/article.md @@ -1,6 +1,15 @@ -# EventSource - события с сервера +# Server Side Events -- события с сервера + +Сразу заметим, что на текущий момент этот способ поддерживают все современные браузеры, кроме IE. + +Современный стандарт [Server-Sent Events](https://html.spec.whatwg.org/multipage/comms.html#the-eventsource-interface) позволяет браузеру создавать специальный объект `EventSource`, который сам обеспечивает соединение с сервером, делает пересоединение в случае обрыва и генерирует события при поступлении данных. + +Он, по дизайну, может меньше, чем WebSocket'ы. + +С другой стороны, Server Side Events проще в реализации, работают по обычному протоколу HTTP и сразу поддерживают ряд возможностей, которые для WebSocket ещё надо реализовать. + +Поэтому в тех случаях, когда нужна преимущественно односторонняя передача данных от сервера к браузеру, они могут быть удачным выбором. -Современный стандарт [Server-Sent Events](http://dev.w3.org/html5/eventsource/) позволяет браузеру создавать специальный объект `EventSource`, который сам обеспечивает соединение с сервером, делает пересоединение в случае обрыва и генерирует события при поступлении данных. [cut] ## Получение сообщений @@ -12,7 +21,7 @@ var eventSource = new EventSource("/events/subscribe"); eventSource.onmessage = function(e) { console.log("Пришло сообщение: " + e.data); -} +}; ``` Чтобы соединение успешно открылось, сервер должен ответить с заголовком `Content-Type: text/event-stream`, а затем оставить соединение висящим и писать в него сообщения в специальном формате: @@ -35,13 +44,15 @@ data: из двух строк
    6. -Здесь все очень просто и удобно, кроме разделения сообщения при переводе строки. Но, если подумать -- это не так уж страшно: на практике сложные сообщения обычно передаются в формате JSON. А перевод строки в нём кодируется как `\n`. Соответственно, что данные будут пересылаться так: +Здесь все очень просто и удобно, кроме разделения сообщения при переводе строки. Но, если подумать -- это не так уж страшно: на практике сложные сообщения обычно передаются в формате JSON. А перевод строки в нём кодируется как `\n`. + +Соответственно, многострочные данные будут пересылаться так: ``` data: {"user":"Вася","message":"Сообщение 3\n из двух строк"} ``` -..То есть, строка `data:` будет одна. +...То есть, строка `data:` будет одна, и накаких проблем с разделением сообщения нет. ## Восстановление соединения @@ -49,7 +60,7 @@ data: {"user":"Вася","message":"Сообщение 3\n из двух стр Это очень удобно, никакой другой транспорт не обладает такой встроенной способностью. -[smart header="А что, если сервер "чисто" закрыл соединение?"] +[smart header="Как серверу полностью закрыть соединение?"] При любом закрытии соединения, в том числе если сервер ответит на запрос и закроет соединение сам -- браузер через короткое время повторит свой запрос. Есть лишь два способа, которыми сервер может "отшить" надоедливый `EventSource`: @@ -145,7 +156,7 @@ eventSource.onmessage = function(e) { }; ``` -### Своё имя события: event +## Своё имя события: event По умолчанию на события срабатывает обработчик `onmessage`, но можно сделать и свои события. Для этого сервер должен указать перед событием его имя после `event:`. @@ -170,34 +181,22 @@ data: Вася ```js eventSource.addEventListener('join', function(e) { alert('Пришёл ' + e.data); -} +}); eventSource.addEventListener('message', function(e) { alert('Сообщение ' + e.data); -} +}); eventSource.addEventListener('leave', function(e) { alert('Ушёл ' + e.data); -} +}); ``` -На момент написания этой статьи (конец 2012) браузер Chrome не поддерживал свои события. - ## Демо -В примере ниже сервер посылает в соединение числа от 1 до 5, а затем -- событие `bye` и закрывает соединение. Браузер автоматически откроет его заново. +В примере ниже сервер посылает в соединение числа от 1 до 3, а затем -- событие `bye` и закрывает соединение. Браузер автоматически откроет его заново. -```js -//+ src="browser.js" -``` - - - -```js -//+ src="index.js" hide="Открыть код для сервера" -``` - -[iframe src="eventsource" border="1" zip link height=200] +[codetabs src="eventsource"] ## Кросс-доменность @@ -209,21 +208,46 @@ var source = new EventSource("http://pupkin.ru/stream", { withCredentials: true Второй аргумент сделан объектом с расчётом на будущее. Пока что никаких других свойств там не поддерживается, только `withCredentials`. -При обработки события у `event` также появится свойство `origin`, содержащее адрес источника, откуда пришёл ответ. +Сервер при этом получит заголовок `Origin` с доменом запроса и должен ответить с заголовком `Access-Control-Allow-Origin` (и `Access-Control-Allow-Credentials`, если стоит `withCredentials`), в точности как в главе [](/xhr-crossdomain). + +При кросс-доменных запросах у событий `event` также появится дополнительное свойство `origin`, содержащее адрес источника, откуда пришли данные. Его можно использовать для дополнительной проверки со стороны браузера: + +```js +eventSource.addEventListener('message', function(e) { + if (e.origin != 'http://javascript.ru') return; + alert('Сообщение ' + e.data); +}); +``` ## Итого -Объект `EventSource` создаётся так: +Объект `EventSource` предназначен для передачи текстовых сообщений с сервера, используя обычный протокол HTTP. +Он предлагает не только передачу сообщений, но и встроенную поддержку важных вспомогательных функций: + +
        +
      • События `event`.
      • +
      • Автоматическое пересоединение, с настраиваемой задержкой `retry`.
      • +
      • Проверка текущего состояния подключения по `readyState`.
      • +
      • Идентификаторы сообщений `id` для точного возобновления потока данных, последний полученный идентификатор передаётся в заголовке `Last-Event-ID`.
      • +
      • Кросс-доменность CORS.
      • +
      + +Этот набор функций делает EventSource достойной альтернативой WebSocket, которые хоть и потенциально мощнее, но требуют реализации всех этих функций на клиенте и сервере, поверх протокола. + +Поддержка -- все браузеры, кроме IE. + +
        +
      • Синтаксис: ```js -var source = new EventSource(src); // src - адрес с любого домена +var source = new EventSource(src[, credentials]); // src - адрес с любого домена ``` Второй необязательный аргумент, если указан в виде `{ withCredentials: true }`, инициирует отправку Cookie и данных авторизации при кросс-доменных запросах. Безопасность при кросс-доменных запросах обеспечивается аналогично `XMLHttpRequest`. - -**Свойства объекта:** +
      • +
      • Свойства объекта:
        `readyState`
        Текущее состояние соединения, одно из `EventSource.CLOSING (=0)`, `EventSource.OPEN (=1)` или `EventSource.CLOSED (=2)`.
        @@ -233,15 +257,15 @@ var source = new EventSource(src); // src - адрес с любого доме
        Параметры, переданные при создании объекта. Менять их нельзя.
        - -**Методы:** +
      • +
      • Методы:
        `close()`
        Закрывает соединение.
        - -**События:** +
      • +
      • События:
        `onmessage`
        @@ -255,33 +279,17 @@ var source = new EventSource(src); // src - адрес с любого доме Эти события можно ставить напрямую через свойство: `source.onmessage = ...`. Если сервер присылает имя события в `event:`, то такие события нужно обрабатывать через `addEventListener`. - -**Формат ответа сервера:** +
      • +
      • Формат ответа сервера: Сервер присылает пустые строки, либо строки, начинающиеся с:
          -
        • `date:` -- сообщение.
        • +
        • `date:` -- сообщение, несколько таких строк подряд склеиваются и образуют одно сообщение.
        • `id:` -- обновляет `lastEventId`.
        • `retry:` -- указывает паузу между пересоединениями, в миллисекундах. JavaScript не может указать это значение, только сервер.
        • `event:` -- имя события, должен быть перед `date:`.
        - -Сообщения разделяются двойным переводом строки. - -Основным преимуществом `EventSource` является поддержка пересоединения и событий "из коробки". - -Поддержка -- все браузеры, кроме IE. +
      • +
      - - - - - - - - - -[head] - -[/head] \ No newline at end of file diff --git a/3-more/2-ajax/11-server-sent-events/browser.js b/3-more/2-ajax/11-server-sent-events/browser.js deleted file mode 100644 index b64a2d39..00000000 --- a/3-more/2-ajax/11-server-sent-events/browser.js +++ /dev/null @@ -1,36 +0,0 @@ - -if (!window.EventSource) { - document.write('В этом браузере нет поддержки EventSource.'); -} - -var eventSource; - -function start() { // при нажатии на Старт - eventSource = new EventSource('digits'); - - var log = document.getElementById('log'); - - eventSource.onopen = function(e) { - log.innerHTML += "Соединение открыто
      "; - }; - - eventSource.onerror = function(e) { - if (this.readyState == EventSource.CONNECTING) { - log.innerHTML += "Соединение порвалось, пересоединяемся...
      "; - } else { - log.innerHTML += "Ошибка, состояние: " + this.readyState; - } - }; - - eventSource.addEventListener('bye', function(e) { - log.innerHTML += "Bye
      "; - }, false); - - eventSource.onmessage = function(e) { - log.innerHTML += e.data + '
      '; - }; -} - -function stop() { // при нажатии на Стоп - eventSource.close(); -} \ No newline at end of file diff --git a/3-more/2-ajax/11-server-sent-events/eventsource.view/index.html b/3-more/2-ajax/11-server-sent-events/eventsource.view/index.html new file mode 100644 index 00000000..fb82827f --- /dev/null +++ b/3-more/2-ajax/11-server-sent-events/eventsource.view/index.html @@ -0,0 +1,61 @@ + + + + + + + + + + + +Нажмите "Старт" для начала. +
      + + + + diff --git a/3-more/2-ajax/11-server-sent-events/index.js b/3-more/2-ajax/11-server-sent-events/eventsource.view/server.js similarity index 78% rename from 3-more/2-ajax/11-server-sent-events/index.js rename to 3-more/2-ajax/11-server-sent-events/eventsource.view/server.js index 760cca7b..d078282a 100644 --- a/3-more/2-ajax/11-server-sent-events/index.js +++ b/3-more/2-ajax/11-server-sent-events/eventsource.view/server.js @@ -6,7 +6,7 @@ var fileServer = new (require('node-static')).Server('.'); function onDigits(req, res) { res.writeHead(200, { - 'Content-Type': 'text/event-stream', + 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache' }); @@ -16,12 +16,16 @@ function onDigits(req, res) { write(); function write() { - res.write('data: ' + ++i + '\n\n'); - if (i == 5) { - clearInterval(timer); + i++; - res.end('event: bye\ndata: до свидания'); + if (i == 4) { + res.write('event: bye\ndata: до свидания\n\n'); + clearInterval(timer); + res.end(); + return; } + + res.write('data: ' + i + '\n\n'); } } diff --git a/3-more/2-ajax/11-server-sent-events/eventsource/browser.js b/3-more/2-ajax/11-server-sent-events/eventsource/browser.js deleted file mode 100644 index b64a2d39..00000000 --- a/3-more/2-ajax/11-server-sent-events/eventsource/browser.js +++ /dev/null @@ -1,36 +0,0 @@ - -if (!window.EventSource) { - document.write('В этом браузере нет поддержки EventSource.'); -} - -var eventSource; - -function start() { // при нажатии на Старт - eventSource = new EventSource('digits'); - - var log = document.getElementById('log'); - - eventSource.onopen = function(e) { - log.innerHTML += "Соединение открыто
      "; - }; - - eventSource.onerror = function(e) { - if (this.readyState == EventSource.CONNECTING) { - log.innerHTML += "Соединение порвалось, пересоединяемся...
      "; - } else { - log.innerHTML += "Ошибка, состояние: " + this.readyState; - } - }; - - eventSource.addEventListener('bye', function(e) { - log.innerHTML += "Bye
      "; - }, false); - - eventSource.onmessage = function(e) { - log.innerHTML += e.data + '
      '; - }; -} - -function stop() { // при нажатии на Стоп - eventSource.close(); -} \ No newline at end of file diff --git a/3-more/2-ajax/11-server-sent-events/eventsource/index.html b/3-more/2-ajax/11-server-sent-events/eventsource/index.html deleted file mode 100644 index bf8d4634..00000000 --- a/3-more/2-ajax/11-server-sent-events/eventsource/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - -Нажмите "Старт" для начала. -
      - - - - diff --git a/3-more/2-ajax/11-server-sent-events/eventsource/index.js b/3-more/2-ajax/11-server-sent-events/eventsource/index.js deleted file mode 100644 index 760cca7b..00000000 --- a/3-more/2-ajax/11-server-sent-events/eventsource/index.js +++ /dev/null @@ -1,50 +0,0 @@ -var http = require('http'); -var url = require('url'); -var querystring = require('querystring'); - -var fileServer = new (require('node-static')).Server('.'); - -function onDigits(req, res) { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache' - }); - - var i=0; - - var timer = setInterval(write, 1000); - write(); - - function write() { - res.write('data: ' + ++i + '\n\n'); - if (i == 5) { - clearInterval(timer); - - res.end('event: bye\ndata: до свидания'); - } - - } -} - -function accept(req, res) { - - if (req.url == '/digits') { - onDigits(req, res); - return; - } - - // всё остальное -- статика - fileServer.serve(req, res); - - -} - - - -// ----- запуск accept как сервера из консоли или как модуля ------ - -if (!module.parent) { - http.createServer(accept).listen(8080); -} else { - exports.accept = accept; -} diff --git a/3-more/2-ajax/12-ajax-iframe/article.md b/3-more/2-ajax/12-ajax-iframe/article.md index 169156ce..77be4652 100644 --- a/3-more/2-ajax/12-ajax-iframe/article.md +++ b/3-more/2-ajax/12-ajax-iframe/article.md @@ -1,6 +1,8 @@ -# IFRAME для AJAX-запросов +# IFRAME для AJAX и COMET -Транспорт `iframe` -- самый кросс-браузерный, это его основное преимущество. Он работает везде: от IE6 до Opera Mobile. +Эта глава посвящена `IFRAME` -- самому древнему и кросс-браузерному способу AJAX-запросов. + +Сейчас он используется, разве что, для поддержки кросс-доменных запросов в IE7- и для COMET в IE9-. Для общения с сервером создается невидимый `IFRAME`. В него отправляются данные, и в него же сервер пишет ответ. @@ -12,18 +14,18 @@ ### Двуличность IFRAME: окно+документ -Что такое `IFRAME`? На этот вопрос у браузера два ответа +Что такое IFRAME? На этот вопрос у браузера два ответа
        -
      1. `IFRAME` -- это HTML-тег: <iframe> со стандартным набором свойств. +
      2. IFRAME -- это HTML-тег: <iframe> со стандартным набором свойств.
        • Тег можно создавать в JavaScript
        • У тега есть стили, можно менять.
        • К тегу можно обратиться через `document.getElementById` и другие методы.
      3. -
      4. `IFRAME` -- это окно браузера, вложенное в основное +
      5. IFRAME -- это окно браузера, вложенное в основное
          -
        • `IFRAME` -- такое же по функционалу окно браузера, как и основное, с адресом и т.п.
        • +
        • IFRAME -- такое же по функционалу окно браузера, как и основное, с адресом и т.п.
        • Если документ в `IFRAME` и внешнее окно находятся на разных доменах, то прямой вызов методов друг друга невозможен.
        • Ссылку на это окно можно получить через `window.frames['имя фрейма']`.
        @@ -32,7 +34,7 @@ Для достижения цели мы будем работать как с тегом, так и с окном. Они, конечно же, взаимосвязаны. -**В теге `IFRAME` свойство `contentWindow` хранит ссылку на окно.** +**В теге `"); + return htmlfile.body.lastChild; +} +``` + +
          +
        1. Вспомогательный объект `htmlfile` будет один и он будет глобальным. Можно и спрятать переменную в замыкании. Смысл в том, что в один `htmlfile` можно записать много ифреймов, так что не будем множить сущности и занимать ими лишнюю память.
        2. +
        3. В `htmlfile` можно записать любой текст и, при необходимости, через `document.write(' +``` + +Здесь `parent'ом` для `iframe'а` будет `htmlfile`, т.е. `CallbackRegistry` будет искаться среди переменных соответствующего ему окна, а вовсе не верхнего `window`. + +Окно для `htmlfile` доступно как `htmlfile.parentWindow`, копируем в него ссылку на реестр коллбэков `CallbackRegistry`. Теперь ифрейм его найдёт.
        4. +
        5. Далее вставляем ифрейм в документ. В старых `IE` нельзя поменять `name` ифрейму через DOM, поэтому вставляем строкой через `insertAdjacentHTML`.
        6. +
        + +Пример в действии (только IE): + +[codetabs src="date-activex"] + +Запрос, который происходит, полностью незаметен. + +Метод POST делается аналогично, только форму ужно добавлять не в основное окно, а в `htmlfile`, через вызов `htmlfile.appendChild`. В остальном -- всё так же, как и при обычной отправке через ифрейм. + +Впрочем, для COMET нужен именно GET. + +Можно и сочетать эти способы: если есть ActiveX: `if ("ActiveXObject" in window)` -- используем методы для IE, описанные выше, а иначе -- обычные методы. + +Вот мини-приложение с сервером на Node.JS, непрерывно получающее текущее время с сервера через `"); - return htmlfile.body.lastChild; -} -``` - -
          -
        1. Вспомогательный объект `htmlfile` будет один и он будет глобальным. Можно и спрятать переменную в замыкании. Смысл в том, что в один `htmlfile` можно записать много ифреймов, так что не будем множить сущности и занимать ими лишнюю память.
        2. -
        3. В `htmlfile` можно записать любой текст и, при необходимости, через `document.write(' -``` - -Здесь `parent'ом` для `iframe'а` будет `htmlfile`, т.е. `CallbackRegistry` будет искаться среди переменных соответствующего ему окна, а вовсе не верхнего `window`. - -Окно для `htmlfile` доступно как `htmlfile.parentWindow`, копируем в него ссылку на реестр коллбэков `CallbackRegistry`. Теперь ифрейм его найдёт.
        4. -
        5. Далее вставляем ифрейм в документ. В старых `IE` нельзя поменять `name` ифрейму через DOM, поэтому вставляем строкой через `insertAdjacentHTML`.
        6. -
        - -Пример в действии (только IE): - -[iframe src="activex" border=1 link zip] - -Запрос, который происходит, полностью незаметен. Мы ещё воспользуемся описанным способом для реализации COMET. - -## POST через ActiveX - -Ранее мы рассмотрели метод GET. Метод POST подразумевает отправку формы в ифрейм. - -Для этого форму нужно добавить не во внешнее окно, а в `htmlfile`, через `htmlfile.appendChild`. - -В остальном -- всё также, как и при обычной отправке через ифрейм. - - - - - - - - -[head] - -[/head] \ No newline at end of file diff --git a/3-more/2-ajax/13-csrf/article.md b/3-more/2-ajax/13-csrf/article.md new file mode 100644 index 00000000..da54aff6 --- /dev/null +++ b/3-more/2-ajax/13-csrf/article.md @@ -0,0 +1,140 @@ +# Атака CSRF + +Нельзя говорить про AJAX и не упомянуть про важнейшую деталь его реализации -- анти-CSRF. + +[CSRF](http://ru.wikipedia.org/wiki/%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D0%BB%D0%BA%D0%B0_%D0%BC%D0%B5%D0%B6%D1%81%D0%B0%D0%B9%D1%82%D0%BE%D0%B2%D1%8B%D1%85_%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2) (Cross-Site Request Forgery, также XSRF) -- опаснейшая атака, которая приводит к тому, что хакер может выполнить на неподготовленном сайте массу различных действий от имени других, зарегистрированных посетителей. + +Какие это действия -- отправка ли сообщений, перевод денег со счёта на счёт или смена паролей -- зависят от сайта, но в любом случае эта атака входит в образовательный минимум веб-разработчика. + +[cut] + +## Злая форма + +"Классический" сценарий атаки таков: + +
          +
        • Вася является залогиненным на сайт, допустим, `mail.com`. У него есть сессия в куках.
        • +
        • Вася попал на "злую страницу", например хакер пригласил его сделать это письмом или как-то иначе.
        • +
        • На злой странице находится форма такого вида: + +```html +
          + + ... +
          +``` +
        • +
        • При заходе на злую страницу JavaScript вызывает `form.submit`, отправляя таким образом форму на `mail.com`.
        • +
        • Сайт `mail.com` проверяет куки, видит, что посетитель авторизован и обрабатывает форму. В данном форма предполагает посылку сообщения.
        • +
        + +Итог атаки -- Вася, зайдя на злую страницу, ненароком отправил письмо от своего имени. Содержимое письма сформировано хакером. + +## Защита + +В примере выше атака использовала слабое звено авторизации. + +**Куки позволяют сайту `mail.com` проверить, что пришёл именно Вася, но ничего не говорят про данные, которые он отправляет.** + +Иначе говоря, куки не гарантируют, что форму создал именно Вася. Они только удостоверяют личность, но не данные. + +Типичный способ защиты сайтов -- это "секретный ключ" (`secret`), специальное значение, которое генерируется случайным образом и сохраняется в сессии посетителя. Его знает только сервер, посетителю мы его даже не будем показывать. + +Затем на основе ключа генерируется "токен" (`token`). Токен делается так, чтобы с одной стороны он был отличен от ключа, в частности, может быть много токенов для одного ключа, с другой -- чтобы было легко проверить по токену, сгенерирован ли он на основе данного ключа или нет. + +Для каждого токена нужно дополнительное случайное значение, которое называют "соль" `salt`. + +Формула вычисления токена: +``` +token = salt + ":" + MD5(salt + ":" + secret) +``` + +Например: +
          +
        1. В сессии хранится `secret="abcdef"`, это значение создаётся один раз.
        2. +
        3. Для нового токена сгенерируем `salt`, например пусть `salt="1234"`.
        4. +
        5. `token = "1234" + ":" + MD5("1234" + ":" + "abcdef") = "1234:5ad02792a3285252e524ccadeeda3401"`.
        6. +
        + +Это значение -- с одной стороны, случайное, с другой -- имея такой `token`, мы можем взять его первую часть `1234` в качестве `salt` и, зная `secret`, проверить по формуле, верно ли он вычислен. + +Не зная `secret`, невозможно сгенерировать token, который сервер воспримет как правильный. + +Далее, токен добавляется в качестве скрытого поля к каждой форме, генерируемой на сервере. + +То есть, "честная" форма для отсылки сообщений, созданная на `http://mail.com`, будет выглядеть так: + +```html +
        +*!* + +*/!* + +
        +``` + +При её отправке сервер проверит поле `csrf`, удостоверится в правильности токена, и лишь после этого отошлёт сообщение. + +"Злая страница" при всём желании не сможет сгенерировать подобную форму, так как не владеет `secret`, и токен будет неверным. + +Такой токен также называют "подписью" формы, которая удостоверяет, что форма сгенерирована именно на сервере. + +[smart header="Подпись с полями формы"] +Эта подпись говорит о том, что автор формы -- сервер, но ничего не гарантирует относительно её содержания. + +Есть ситуации, когда мы хотим быть уверены, что некоторые из полей формы посетитель не изменил самовольно. Тогда мы можем включить в MD5 для формулы токена эти поля, например: +``` +token = salt + ":" + MD5(salt + ":" + secret + ":" + fields.money) +``` + +При отправке формы сервер проверит подпись, подставив в неё известный ему `secret` и присланное значение `fields.money`. При несовпадении либо `secret` не тот (хакер), либо `fields.money` изменено. +[/smart] + +## Токен и AJAX + +Теперь перейдём к AJAX-запросам. + +Что если посылка сообщений в нашем интерфейсе реализуется через XMLHttpRequest? + +Как и в случае с формой, мы должны "подписать" запрос токеном, чтобы гарантировать, что его содержимое прислано на сервер именно интерфейсом сайта, а не "злой страницей". + +Здесь возможны варианты, самый простой -- это дополнительная кука. + +
          +
        1. При авторизации сервер устанавливает куку с именем `CSRF-TOKEN`, и пишет в неё токен.
        2. +
        3. Код, осуществляющий XMLHttpRequest, получает куку и ставит заголовок `X-CSRF-TOKEN` с ней: + +```js +var request = new XMLHttpRequest(); + +var csrfCookie = document.cookie.match(/CSRF-TOKEN=([\w-]+)/); +if (csrfCookie) { + request.setRequestHeader("X-CSRF-TOKEN", csrfCookie[1]); +} +``` +
        4. +
        5. Сервер проверяет, есть ли заголовок и содержит ли он правильный токен.
        6. +
        + +Защита действует потому, что прочитать куку может только JavaScript с того же домена. "Злая страница" не сможет "переложить" куку в заголовок. + +Если нужно сделать не XMLHttpRequest, а, к примеру, динамически сгенерировать форму из JavaScript -- она также подписывается аналогичным образом, скрытое поле или дополнительный URL-параметр генерируется по куке. + + +## Итого + +
          +
        • CSRF-атака -- это когда "злая страница" отправляет форму или запрос на сайт, где посетитель, предположительно, залогинен. + +Если сайт проверяет только куки, то он такую форму принимает. А делать это не следует, так как её сгенерировал злой хакер.
        • +
        • Для защиты от атаки формы, которые генерирует `mail.com`, подписываются специальным токеном. Можно не все формы, а только те, которые осуществляют действия от имени посетителя, то есть могут служить объектом атаки.
        • +
        • Для подписи XMLHttpRequest токен дополнительно записывается в куку. Тогда JavaScript с домена `mail.com` сможет прочитать её и добавить в заголовок, а сервер -- проверить, что заголовок есть и содержит корректный токен.
        • +
        • Динамически сгенерированные формы подписываются аналогично: токен из куки добавляется как URL-параметр или дополнительное поле.
        • +
        + + +Чтобы + + diff --git a/3-more/2-ajax/14-ajax-iframe-xdomain/article.md b/3-more/2-ajax/14-ajax-iframe-xdomain/article.md deleted file mode 100644 index eee67293..00000000 --- a/3-more/2-ajax/14-ajax-iframe-xdomain/article.md +++ /dev/null @@ -1,199 +0,0 @@ -# IFRAME для кросс-доменных запросов - -Современные браузеры поддерживают множество альтернатив ифреймам. В первую очередь, это современный [XMLHttpRequest](/ajax-xmlhttprequest), который может делать любые запросы, в том числе кросс-доменные и с пересылкой файлов. - -А в IE6-9 ифреймы в некоторых ситуациях незаменимы, поэтому способ, описанный в этой статье, в первую очередь используется для поддержки этих браузеров. -Существует несколько вариантов реализации кросс-доменных запросов. Конкретный выбор зависит от версии IE, которую мы собираемся поддерживать. - -## IE8+: postMessage-ответ - -IE8 и выше поддерживают интерфейс [postMessage](https://developer.mozilla.org/en-US/docs/DOM/window.postMessage) для общения между окнами с разных доменов. - -Для его использования серверный код должен ответить на запрос примерно так: - -```js - -``` - -Браузер проверит, действительно ли родительское окно `parent` с домена `parentDomain`, и если это так, то на нём будет вызвано событие `onmessage`, которое получит доступ к тексту `message`. - -В `message` должна содержаться информация о том, на какой запрос пришёл ответ, и данные. Так как кросс-браузерно поддерживаются только сообщения-строки, то передать эту информацию можно через JSON. - -Серверный код, который это делает: - -```php - - -``` - -Код на клиенте, который будет реагировать на `postMessage`: - -```js -window.onmessage = function(event) { - // распаковать сообщение - var message = JSON.parse(event.data); - // вызвать обработку результата запроса - CallbackRegistry[message.name](message.body); -} -``` - -Ещё одна небольшая тонкость -- время вызова `iframe.onload`. - -Задача этого обработчика -- проверить, всё ли вызвалось. Для этого он должен отработать обязательно *после* `onmessage`. А вызов `postMessage` -- асинхронный, поэтому нужно отложить проверку на `setTimeout(..,0)`. - -При этом получится следующий поток выполнения: - -
          -
        1. Пришёл ответ с сервера, вызвал `postMessage`.
        2. -
        3. Событие `onmessage` встало в очередь выполнения.
        4. -
        5. Обработано окончание загрузки, вызвался `iframe.onload`, поставил в очередь проверку.
        6. -
        7. Сработает `onmessage`, а затем проверка.
        8. -
        - -Полный код кросс-доменного `iframeGet` (`iframePost` -- аналогично): - -```js -//+ src="postmessage.js" -``` - -Пример в действии: - -[iframe src="postmessage" border="1" link zip] - -### postMessage-прокси - -Если нужно часто обмениваться данными с одним доменом, то можно загрузить `iframe` с этого домена, содержащий код `postMessage` и `onmessage`: - -```html - -``` - -Время от времени этому `iframe` пересылаются сообщения, он обменивается информацией со своей страницей (можно через `XMLHttpRequest`) и отвечает основной странице. - -При этом ифрейм будет только один, пересоздавать ничего не нужно. - - -## IE6: передача через window.name - -Этот способ активно используется, так как с одной стороны покрывает ряд важных сценариев использования, а с другой -- поддерживается всеми браузерами, включая IE6+. - -У `IFRAME` есть атрибут `name`, который задаёт "имя окна". Обычно он используется для отправки форм в данный ифрейм или для доступа к внутреннему окну через `window.frames[name]`. - -Но можно и использовать его для другой, непредусмотренной цели -- передачи данных. - -Дело в том, что имя окна не меняется при навигации в `IFRAME`, и при этом доступно из JavaScript. Поэтому если одна страница поставила `window.name = "Вася"`, а потом посетитель нажал на ссылку в `IFRAME` и перешёл на другую страницу (домен не важен), то она сможет прочитать "Вася" из `window.name`. - -Это делает возможным следующий алгоритм: - -
          -
        1. Страница на основном домене создаёт `IFRAME` и отправляет в него запрос *на другой домен*.
        2. -
        3. Сервер отвечает кодом, который выглядит примерно так: - -```php - - -``` - -
        4. -
        5. Страница по `iframe.onload` читает `iframe.contentWindow.name` и получает данные.
        6. -
        - -Ниже реализован метод POST, т.к. кросс-доменный GET можно сделать и при помощи [Транспорта SCRIPT](/ajax-jsonp): - -```js -//+ src="name.js" -``` - -Обратите внимание на комментарии, они более подробно описывают важные детали коммуникации. - -Пример в действии: - -[iframe src="name" border="1" zip link] - -### Сфера использования - -У метода `window.name` почти нет недостатков. - -
          -
        • Он работает во всех браузерах.
        • -
        • Он прост в реализации.
        • -
        • В `window.name` кросс-браузерно влезает до 2МБ.
        • -
        • Пустой документ `blank.html` будет сразу же закэширован браузером, и в дальнейшем лишнего запроса не будет.
        • -
        - -Единственное ограничение -- ответ до 2МБ. Многие сценарии использования, например кросс-доменная авторизация, легко вписываются в них. - -## IE6+: hash-прокси - -Этот способ представляет собой отвратительнейший хак. Он заключается в том, что при общении между основной страницей и ифреймом с другого домена -- они могут легко менять `hash` друг другу (часть пути после `#`). - -Считается, что это безопасно, т.к. изменение хэша -- всего лишь прыжок по странице, поэтому браузеры это разрешают. - -При этом читать хэши друг друга нельзя, можно только писать в них. - -Соответственно, логика взаимодействия такая: - -
          -
        1. Главная страница подключает ифрейм с другого домена.
        2. -
        3. Ифрейму меняется `hash`, он видит это и понимает, что ему "хотят что-то этим сказать", обрабатывает полученное сообщение.
        4. -
        5. Когда ифрейм готов, он, в свою очередь, меняет хэш родителю. Тот принимает ответ.
        6. -
        - -При этом у родителя и ифрейма существует протокол общения через хэш. Ограничение -- не более 4kb (минус длина базовой части URL) на сообщение. - -Обход проблем: -
          -
        • **При такой реализации на п.3 окно-родитель будет "прыгать" -- ему же меняют хэш!** - -Поэтому в реальности используют три ифрейма: родительское окно `main` включает ифрейм с того же домена `localProxy.html`, а уже он включает в себя ифрейм с другого домена `remoteProxy.html`. - -Общение осуществляется по цепочке `main -> localProxy -> remoteProxy -> localProxy -> main`. Хэш меняется у промежуточного прокси, который сообщает об этом родительскому окну (он на одном домене с ним). - -Можно сделать фреймы `localProxy` и `remoteProxy` на одном уровне. Тогда не во всех браузерах будет работать (в Opera не будет?), но в IE всё (вроде!) будет ок.
        • **Как окну(или ифрейму) обнаружить изменение хэща?** - -В старых браузерах отсутствует событие `onhashchange`, поэтому используются два подхода: -
            -
          1. Окно раз в 50мс проверяет, поменялся ли хэш.
          2. -
          3. Используется `window.onresize` как сигнал: когда ифрейму меняем хэш -- тут же немного изменим и его размер, на нём сработает событие `onresize`.
          4. -
          - -
        - -Автор этих строк реализовывал такой протокол в очень бородатые времена, несколько раз участвовал в обсуждениях проектов, использующих такое решение -- и всегда был удивлён, насколько стабильно оно работает. Несмотря на хакерскую природу самого решения. - -В этом случае ифрейм перегружать не надо, поэтому большей частью такой подход используется даже не для AJAX, а для общения между постоянно встроенным виджетом-ифреймом и основной страницей. - -Область применения -- когда обязательна поддержка IE6,7 и никакие другие способы, почему-то, не подходят. - - -[head] - -[/head] \ No newline at end of file diff --git a/3-more/2-ajax/14-ajax-iframe-xdomain/name.js b/3-more/2-ajax/14-ajax-iframe-xdomain/name.js deleted file mode 100644 index 15a49d63..00000000 --- a/3-more/2-ajax/14-ajax-iframe-xdomain/name.js +++ /dev/null @@ -1,36 +0,0 @@ - -function iframePost(url, data, onSuccess, onError) { - - var iframeName = Math.random(); - var iframe = createIframe(iframeName); - - // onload после append, чтобы не срабатывал лишний раз - iframe.onload = function() { - - var newName = iframe.contentWindow.name; - - if (newName == iframeName) { - // имя получили, но оно осталось тем же - значит сервер его не заменил - // значит что-то не так в ответе (ошибка сервера или неверный формат) - onError(); - return; - } - - // имя получено, коммуникация завершена - iframe.parentNode.removeChild(iframe); // очистка - - // такой eval можно использовать вместо JSON.parse для старых IE, - // при условии доверия содержимому к newName - // если доверия нет -- используйте библиотеку json2.js - onSuccess( eval('(' + newName +')') ); - }; - - // для IE8- нужно использовать attachEvent вместо iframe.onload - if (iframe.attachEvent && !iframe.addEventListener) { - iframe.attachEvent("onload", iframe.onload); - iframe.onload = null; - } - - postToIframe(url, data, iframeName); -} - diff --git a/3-more/2-ajax/14-ajax-iframe-xdomain/name/.zip b/3-more/2-ajax/14-ajax-iframe-xdomain/name/.zip deleted file mode 100644 index d3f5a12f..00000000 --- a/3-more/2-ajax/14-ajax-iframe-xdomain/name/.zip +++ /dev/null @@ -1 +0,0 @@ - diff --git a/3-more/2-ajax/14-ajax-iframe-xdomain/name/anydomain.php b/3-more/2-ajax/14-ajax-iframe-xdomain/name/anydomain.php deleted file mode 100644 index c2bcc4ef..00000000 --- a/3-more/2-ajax/14-ajax-iframe-xdomain/name/anydomain.php +++ /dev/null @@ -1,13 +0,0 @@ - - diff --git a/3-more/2-ajax/14-ajax-iframe-xdomain/name/blank.html b/3-more/2-ajax/14-ajax-iframe-xdomain/name/blank.html deleted file mode 100644 index 95222a60..00000000 --- a/3-more/2-ajax/14-ajax-iframe-xdomain/name/blank.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/3-more/2-ajax/14-ajax-iframe-xdomain/name/iframe.js b/3-more/2-ajax/14-ajax-iframe-xdomain/name/iframe.js deleted file mode 100644 index f7cf9169..00000000 --- a/3-more/2-ajax/14-ajax-iframe-xdomain/name/iframe.js +++ /dev/null @@ -1,49 +0,0 @@ - - -function createIframe(name, src, debug) { - src = src || 'javascript:false'; // пустой src - - var tmpElem = document.createElement('div'); - - // в старых IE нельзя присвоить name после создания iframe, поэтому создаём через innerHTML - tmpElem.innerHTML = '