• Руководство по кросс-доменным запросам (CORS)

    На прошлой неделе я внедрял в проект CORS-запросы – современный способ кросс-доменного Аякса. По следам прочитанной документации и набитых шишек подготовил небольшой мануал. Это вольный пересказ англоязычных статей, вопросов со Стека и скромный личный опыт.

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

    Один из вопросов на тему фронтенда звучит банально: как на клиенте получить данные с другого домена?

    Некоторые кандидаты отвечают, что проблемы нет, достаточно выполнить Аякс-запрос. И с большим удивлением узнают, что, оказывается, нельзя: сработают какие-то там политики безопасности.

    Один собеседуемый заявил, что в Фаерфоксе это работает, достаточно зайти на страницу для разработчиков и что-то там переключить. Я не против такого ответа. Следующий вопрос, как вы заставите всех пользователей установить именно Фаерфокс и поменять системный флаг?

    С понятием JSONP вообще беда – никто не может объяснить, как это устроено. Разработчики думают, что это обычный Аякс, только с каким-то P на конце, то ли баг, то ли фича. А это вообще ни разу не Аякс.

    Аббревиатура CORS появилась недавно, и спрашивать о ней нет смысла. Этот пробел восполняет данный мануал.

    Разберем вопросы из предыдущих абзацев. Действительно, слать Аякс-запросы к серверам с другим доменом запрещено на уровне браузера. Однако, в интернете полно сайтов, где значимая часть контента подгружается со сторонних серверов. Например, этот блог работает на статичном генераторе Jekyll, в котором нет комментариев. Делиться мнениями помогает сервис Discuss: лента комментариев встраивается Джаваскриптом. Получая и отправляя комментарии, вы взаимодействуете с серверами Discuss, а мой блог вообще ни при чем. Значит, слать запросы Аяксом все же можно?

    Нет, здесь работает JSONP. Аббревиатура значит JSON with Padding (с подкладкой). Идея основана на лазейке в стандартах: загружать скрипты с других доменов не запрещено! Скажем, если в файле example.js на чужом сервере написано что-то вроде:

    alert("hello!");
    

    , то достаточно подгрузить его тегом <script> на страницу:

    <script src="http://example.com/static/example.js"></script>
    

    и браузер выполнит все, что внутри. В данном случае покажет окошко.

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

    <script
    src="http://example.com/api.js?method=get_user&user_id=42&callback=processUser"
    ></script>
    

    В переменой method указываем, какое действие требуем от сервера. В данном случае, получить пользователя по идентификатору. Айдишку передаем следующим параметром user_id. Пусть такой пользователь найден на сервере, теперь его нужно отдать клиенту. Если просто выплюнуть объект:

    {name: "Ivan", age: 30}
    

    , то на клиенте ничего не произойдет: объект просто считается в память. Именно поэтому передают третий параметр callback – имя функции, объявленной на клиенте, в которую нужно завернуть пользователя. С этим параметром ответ станет другим:

    processUser({
        name: "Ivan",
        age: 30
    });
    

    На клиенте отработает код, зашитый в функцию processUser: вывести данные в консоль, отрисовать виджет и т.д. Вот как работает JSONP.

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

    Далее, подгрузка скриптов ни разу не безопасней, чем Аякс. Целое семейство вирусов занимается тем, что добавляет на страницу браузера скрипты для отрисовки баннеров порно и казино. Когда вы подключаетесь к интернету через мобильных операторов, обсосы вставляют в HTML-трафик скрипты для отрисовки виджетов (если соединение не HTTPS).

    JSONP работает только методом GET, что сводит на нет возможности REST-интерфейса. Для REST-сервисов приходится писать прокладки-прокси, т.е. множить костыли.

    Добавив скрипт на страницу, в дальнейшем вы не можете отследить его судьбу. Если у Аякс-запроса есть специальные коллбеки для основных событий (начало, удачное завершение, таймаут, неудачное завершение), то у скрипта ничего такого нет. Загрузился ли он? Ответил ли сервер? Была ли ошибка? Никто не знает.

    Ясно, что в 2016 году приложениям на js нужен надежный способ забирать данные с серверов. Чтобы это была законно, а не по-воровски в обход протоколов и стандартов. Таким способом стал CORSCross-Origin Resource Sharing, кросс-доменные запросы.

    Идея проста – пусть клиент шлет Аякс-запрос к чужому серверу. Браузер добавит в запрос особые заголовки с информацией о том, что запрос с другого домена. На их основании сервер решит, как обрабатывать такой запрос, и добавит особые заголовки в ответ. Удобно, правда?

    Техническая реализация несколько сложнее. Стандарт CORS различает “простые” и “сложные” запросы. Простым считается запрос методами:

    • HEAD
    • GET
    • POST

    и заголовками:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type, но только со значениями:
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

    Если ваш запрос удовлетворяет этим критериям, можно слать Аякс к другому домену из любого современного браузера. При этом браузер добавит заголовок Origin с адресом страницы, откуда инициирован запрос. Подделать заголовок скриптом не удастся.

    Сервер, получив на обработку подобный запрос, должен прочесть Origin и решить, как его обрабатывать. Заголовок ответа Access-Control-Allow-Origin регулирует, с какого домена разрешено запрашивать данные. Это может быть как веб-адрес, так и знак астерикса (звездочки), если разрешено всем. Несколько разных адресов через запятую, к сожалению, не поддерживаются.

    Пример CORS-запроса:

    POST /foo/bar HTTP/1.1
    Origin: http://foreign.com
    Host: test.com
    

    и ответа с разрешением на получение данных:

    200 OK HTTP/1.1
    Access-Control-Allow-Origin: http://foreign.com
    Content-Type: text/html; charset=utf-8
    
    <h1>Welldone</h1>
    

    Обратите внимание на такую вещь: мы намерены использовать CORS, чтобы дергать чужие API. С вероятностью почти 100% они работают по протоколу JSON, то есть принимают и отдают заголовок Content-Type: application/json. Вроде бы мелочь, но такой запрос автоматом перестает быть простым и переходит в разряд “сложных”, где схема взаимодействия иная.

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

    При этом браузер не дурак и все запомнит: если разрешили только методы GET и POST, то PUT и DELETE не сработают. Аналогично с заголовками: если помимо стандартных разрешено использовать только Authorization, то нужно передать его и ничего другого.

    Пример сложного запроса:

    OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    

    Клиент хотел отправить Аяксом запрос методом PUT на урл http://api.alice.com/cors с сайта http://api.bob.com. Поскольку это сложный запрос, браузер запросил разрешение: типа, хочу сделать PUT на этот урл с особым заголовком X-Custom-Header. Сервер ему на это:

    200 OK HTTP/1.1
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Content-Type: text/html; charset=utf-8
    

    Или иными слвами: разрешено ходить методами GET, POST, PUT и с заголовком X-Custom-Header. Это подходит под критерии первоначального запроса. Браузер делает второй запрос куда мы намеревались вначале.

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

    Вот такие строгости. В нашем проекте API на стороне сервера требует заголовки Version (версия операции), Authorization (авторизация по токену) и Content-Type (JSON), поэтому в ответе указываем

    Access-Control-Allow-Headers: Version, Authorization, Content-Type
    

    , иначе запрос не пройдет.

    Теперь все это можно протестировать. Запускаем локальный сервер, открываем консоль Хрома и пишем:

    var xhr = new XMLHttpRequest();
    xhr.open("GET", "http://127.0.0.1:5000/api/users", true);
    xhr.setRequestHeader("authorization", "Token xxxxxx");
    xhr.setRequestHeader("Version", "1");
    xhr.send();
    xhr.responseText
    >> "{"users":[{name "Ivan"...
    

    Забавно, что заголовок Origin в этом случае будет равен https://google.com, потому что Хром считает пустой страницей главную Гугла.

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

    Несмотря на кажущуюся простоту, реализовать поддержку CORS на сервере требует времени. Первоначально я хотел использовать чужую библиотеку, но после 10 минут чтения исходного кода понял, что автор НЕПРАВИЛЬНО понял спецификацию и реализовал ее с ошибками. Лишний раз убедился, авторы сторонних библиотек – не боги, а такие же смертные. Они могут тупить, ошибаться. Лучше потратить день на чтение спеки и сделать все правильно, чем доверять первому встречному решению.

    Расскажу теперь о тонкостях, с которыми столкнулся при внедрении CORS в проекте. Прежде всего, чтобы сократить число preflight-запросов, стоит либо кешировать эндпоинт OPTIONS заголовками:

    Cache-Control: no-cache, must-revalidate
    

    , либо вообще объявить его на уровне Nginx:

    location / {
         if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Version, Authorization, Content-Type';
         }
    

    Еще одна тонкость: даже если возвращаете ответ с не-положительным статусом, например, не прошла валидация или нет прав, заголовок Access-Control-Allow-Origin обязан присутствовать. Если заголовка нет, браузер решит, что CORS-запрос запрещен и не прочитает ответ.

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

    С CORS разобрались, теперь не будет лишним почитать другое руководство про часовые пояса

    Ссылки:

    • Using CORS

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

    • Enable CORS

      Сайт, целиком посвященный CORS: описание, статьи, конфиги, примеры кода.

  • Что почитать на выходных №5

    Поехали, техническое:

    • MVC vs. OOP

      Замечания Егора Бугаенко об известных паттернах.

    • What’s Wrong With Object-Oriented Programming?

      У того же автора: подборка цитат известных программистов о том, что ООП отстой.

    • Using CORS

      Отличный мануал как прикрутить кросс-доменные запросы к серверу и клиенту. Намереваюсь написать свой туториал по следам недавних изысканий.

    На сладкое:

  • Мой вклад в clj-http

    В проект clj-http приняли мой скромный пулл-реквест. Теперь, если запрос был вызван с флагом :throw-exceptions true и вернулся плохой ответ (статус не 2хх, 3хх), то во вбрасываемый словарь добавляеся ключ :type :clj-http.client/unexceptional-status. Это на ура работает с библиотекой Slingshot, потому что отлов исключений по полю :type стал негласным стандартом.

    Если абзац выше звучит для вас бессмыслецей, расскажу подробней. Изначально в Кложе идут унылые Java-исключения, которые плохо ложаться на мир структур и коллекций. Но поскольку Кложа это Лисп, а в Лиспе любая проблема решается его же силами, умельцы написали библиотеку для альтернативных исключений slingshot.

    Библиотека упрощает работу с исключениями до мыслимого предела. Теперь можно вбрасывать не только объект, унаследованный от Throwable, а вообще что угодно. Лучше всего подходит словарь. Форма отлова исключения, выброшенного из slingshot, может иметь разную структуру. Например, предикат, но удобней и короче будет вектор двух элементов, где первый – ключ, а второй – значение.

    Если раньше приходилось передавать неочевидный предикат, который проверял, что в словаре есть поле status, и оно числовое:

    (catch
      (fn [response]
        (-> response :statsus integer?))
      ...)
    

    , то теперь достаточно сравнить так:

    (catch [:type :clj-http.client/unexceptional-status])
    

    Визуально разницы мало, но селектор по ключу :type открывает новые горизонты. Например, во врейморке Compojure API, на котором наш проект, обработка исключений целиком работает на этом принципе. В особом словаре лежат ключи – потенциальные варианты :type, а значения – функции-обработчики. Хорошие примеры в документации.

    Пока пулл-реквест не приняли, приходилось писать свой костыль для добавления ключа. Теперь с этим покончено.

  • Статья в Associated Press

    ivan

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

    Фотографии в хорошем качестве и видео на сайте AP доступны только по платной подписке.

  • Синтаксис

    Незрелый программист судит о языке по синтаксису. Эти скобочки люблю, а такие нет. Языки с отступами не рассматриваю. Требую коммерческую ИДЕ с дебагом мышкой.

    Это проблема.

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

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

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

    Обыватели полагают, что Кложа – это очередной “современный” Лисп. Это рассуждение дилетанта. Кложа – самостоятельный язык с принципиально новыми идеями, оформленный в виде Лиспа. Его создатель мог бы выбрать сишный или питонячий синтаксис и получить какой-нибудь Котлин или Скалу со своими особенностями.

    Синтаксис – это внешность языка. По налогии с людьми, внешность бывает обманчива. Человек заметил это еще в древние времена: не тот друг, кто красиво выглядит, и не тот враг, кто некрасив.

    seth-godin

    Посмотрите на этого человека. Выглядит он слегка комично. Волос нет, черты лица ассиметричны: нос скошен, левое ухо больше, глаза неровные. А это, на минутку, Сет Годин – мировой эксперт по маркетингу, автор научных работ и бестеллеров.

    Сделал он столько всего хорошего, что другим и за несколько жизней не сделать. С такой внешностью, да.

    Следующий тезис выделю особо.

    Я слышал много раз, что у Лиспа странный синтаксис, но ни разу не слышал, что он не решил какую-то задачу

    Когда вы жаловались, пытались сделать что-то полезное? Например, распарсить XML, JSON, дернуть урл или сходить в базу. Спорю, что даже не пытались, потому что все это Лисп делает на раз.

    Фразы о синтаксисе обычно исходят от самовлюбленных нарциссов, которым лень пошевелить головой. Малейшая неожиданность, мозг потревожен – включается агрессия. Плохой синтаксис!

    Человек привыкает ко всему. Садишься за Лисп, и непривычно. А недавно, после двух месяцев Кложи, сел за Питон и все вызовы функций напихал в скобки. Уже автомат.

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

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

  • Что почитать на выходных №4

    Поехали:

    • Starting a Node.js app with ClojureScript and Boot (английский)

      Небольшой туториал как начать проект на Ноде с Кложе-скриптом. Да, под Ноду можно спокойно писать на cljs.

    • Что значит «сделать»?

      Николай Товеровский (сотрудник Бюро Горбунова) объясняет разницу между “делать” и “сделать”. В рамочку и на стену.

  • Двенадцатая встреча

    Провели двенадцатую встречу. Двенадцатую, Карл! Целый год без пропусков, в жару и холод, в будни и на майских. 25 выступлений, 12 докладчиков. Отдельным постом я расскажу, с чего все начиналось и как вообще держится, я пока что свежие видосы.

    Антон Чикин рассказал про мердж в Гите:

    Слайды

    Артем Трубачев топил за веб-сокеты, послушал с удовольствием:

    Слайды

    Косяки: я вел трансляцию в Перископе, ушла только половина эфира. В следующий раз нам грозятся дать личный пароль от Дом.ру, так что есть надежда. Во время записи опять налажал со звуком: писал на встроенный микрофон мимо петличного, звук вышел не ахти. Зато слышен весь бред из зала :-)

    На следующую встречу уже запланированы выступления: про Докер и No-SQL базы данных. Следите за новостями. Хотите выступить – пишите в личку. Напомню, на тему Рефакторинга мы общаемся в чате Телеграма.

  • Что почитать на выходных №3

    В этом выпуске:

  • Емакс в терминале

    Долгое время я пользовался Емаксом с графическим интерфейсом. Для Мака такой Емакс качается с сайта Emacs For Mac OS X и ставится как обычное приложение в папке Applications.

    Два месяца назад перешел на версию для терминала. Ниже короткие заметки о том, как это сделать и что изменилось.

    Установка из пакетов. Для Мака ставится из brew: brew install emacs. На момент написания статьи скачивается версия 25.1. Важно: бинарник находится по пути /usr/local/Cellar/emacs/25.1/bin/emacs! Если вы введете в терминале просто emacs, то запустится /usr/bin/emacs, у меня это какое-то старье.

    Внешний вид. Я использую стандартную цветовую схему light-blue из коробки. Включается командой (load-theme 'light-blue t). В терминальной версии цвета немного другие. Сравните графическую версию:

    gui

    и терминал:

    terminal

    Размер текста. Для комфортной работы я нашел полезным ставить шрифт покрупнее. Глаза нам даны одни на всю жизнь, так что лучше их поберечь. В графической версии высота шрифта меняется командой (set-face-attribute 'default nil :height 140) (140 – моя метрика, подобранная эмпирически). В терминале, конечно, это не прокатит. Поэтому я просто жму три раза Cmd + =. Получается как на рисунке ниже (картинка специально обрезана, чтобы вошла в колонку без масштабирования):

    font

    Курсор. Больше недели не мог настроить цвет курсора в терминале. В графической версии был красный, а в терминале серый. Сливается в голубым фоном, глазам трудно найти. Команды вроде (set-cursor-color "#ffffff") не помогают. Оказывается, в маковском терминале Iterm2 цвет курсора и текста под ним регулируется силами самого терминала. Пришлось поправить настройки:

    iterm2

    Вместе с цветом курсора (Cursor) можно включить легкое цветовое выделение текущей строки (Cursor Guide).

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

    Напомню, мой конфиг Емакса с комментариями лежит в Гитхабе.

  • Что почитать на выходных №2

    В этом выпуске:

    • История одного тестового задания (русский) – забавный пост о том, как кандидаты справляются с практическим заданием при трудоустройстве. Каждый абзац прекрасен. С юмором, тонко и по делу.

    • Simple Made Easy (видео, слайды, английский) – ТОТ САМЫЙ доклад Рича Хики, что делит жизнь пополам. Серьезно: очень мощное выступление Рича. Без привязки к конкретному языку.

    • Культ женской глупости (русский) – Арина Холина о женщинах и интеллекте.

    Что, мало про технологии? Да забейте. Дайте мозгу отдохнуть.

Страница 23 из 49