среда, 16 августа 2017 г.

Как работают веб-приложения


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

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

Поиск сервера

Процесс начинается, когда пользователь запускает любимый веб-браузер и вбивает в строку адреса текст вида https://www.vk.com/codeatcpp, а затем нажимает Enter. После этого веб-браузер посылает запрос DNS-серверу, чтобы узнать какой IP-адрес соответствует серверу www.vk.com. Тут нужно отметить, что большие сервисы используют не один сервер, чтобы справляться с нагрузкой. В таких случаях одному доменному имени будет соответствовать множество IP-адресов. DNS-клиенты обычно используют первый IP-адрес из ответа DNS-сервера, поэтому простейший способ балансировки нагрузки — выдавать разным клиентам различные ответы. Стандартно это делается по алгоритму Round-robin, что не всегда эффективно, так как не учитывает состояние отдельных серверов (решение этой проблемы — тема отдельной статьи). Вы можете легко проверить данный факт, запустив в командной строке следующую команду:
$ nslookup vk.com

Name: vk.com
Address: 87.240.165.85
Name: vk.com
Address: 87.240.165.86
Name: vk.com
Address: 87.240.165.83
Name: vk.com
Address: 87.240.165.84
Если у вас в системе разрешен протокол IPv6, то в ответе вы увидите еще и IPv6 адреса. Можно попробовать запустить команду несколько раз подряд и самостоятельно убедиться, что порядок адресов в ответе меняется.

Подключение к серверу

Определив IP-адрес сервера, можно попытаться к нему подключиться. Для этого веб-браузер открывает сокет с адресом 87.240.165.85 (первый из списка выше) и портом 443 для HTTPS или 80 для HTTP.

Если используется протокол HTTP, то веб-браузер просто посылает примерно такой текст в канал:
GET  HTTP/1.1
Host: vk.com
Если вы не используете прокси сервер, то можете попробовать самостоятельно послать такой текст, используя утилиту telnet и указав порт 80. Если используется HTTPS, то веб-браузер сначала договаривается с сервером о шифровании канала по протоколу TLS, а дальше все запросы и ответы передаются по зашифрованному каналу. Формат запросов и ответов остается таким же, как при обычном HTTP.

В ответ на ваш запрос сервер вышлет HTML код, который обычно лежит в файле index.html. Примитивная на первый взгляд страница www.google.ru присылает текст размером почти 70 килобайт и уже содержит в себе сжатые скрипты. Мы рассмотрим более простой пример, чтобы лучше разобраться что к чему. Наш сервер (пусть www.example.com) прислал веб-браузеру такой текст:
<html>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <body>
        <div id="content">Пожалуйста, разрешите выполнение JavaScript, чтобы увидеть сайт.</div>
    </body>
    <script type="text/javascript" src="scripts/app.js"></script>
</html>
Здесь видно, что в теле странице есть единственный тег с именем content. Также подключается некий внешний скрипт app.js — это и есть наше веб-приложение. Если выполнение скриптов не разрешено в веб-браузере, то ничего не произойдет и будет выведен текст из тега.

А вот если выполнение JavaScript разрешено, то веб-браузер получит с сервера файл app.js и будет выполнять код из него.

Веб-приложение

Код, который получается с сервера и представляет собой веб-приложение. Этот код на лету сгенерирует необходимое содержимое и вставит его внутрь тега content. Простейший вариант app.js приведен ниже:
function onLogin(form) {
    var xhttp = new XMLHttpRequest();
    xhttp.open("GET", "/rest_api/v1/me_page", false);
    xhttp.setRequestHeader("Content-type", "application/json");
    xhttp.setRequestHeader("Authorization", "Basic " + btoa(form.username.value + ":" + form.username.password));
    xhttp.send();
    var response = JSON.parse(xhttp.responseText);
}

(function(){
    document.getElementById("content").innerHTML = '\
        <form name="login">\
            Username: <input type="text" name="username"/></p>\
            Password: <input type="password" name="passwd"/></p>\
            <input type="button" onclick="onLogin(this.form)" value="Login"/>\
        </form>';
})()
Данный код найдет на странице элемент с именем content и заменит его содержимое на форму логина, которая состоит из двух полей ввода и кнопки Login. По кнопке будет запускаться функция onLogin, которая определена тут же.

Тут начинается самое интересное. Функция onLogin посылает серверу запрос GET. В виде текста это может выглядеть следующим образом:
GET /rest_api/v1/me_page HTTP/1.1
Host: www.example.com
Authorization:Basic YWRtaW46dW5kZWZpbmVk
Легко заметить, что это практически тоже самое, если бы вы адресной строке браузера написали текст вида http://www.example.com/rest_api/v1/me_page. Тут и начинается взаимодействие с приложением-сервером (см. схему в начале статьи).

Приложение-сервер

На схеме видно, что приложение-сервер не взаимодействует с Интернетом напрямую. Ему не нужно заботиться о передачи статического контента (в нашем случае, файлов index.html и app.js), который не меняется от запроса к запросу. И не нужно думать про организацию защищенного HTTPS канала до веб-браузера. Для этих целей лучше использовать проверенные средства, например, проксирующий веб-сервер nginx, который умеет эффективно отдавать статический контент и проксировать запросы к нашему приложению-серверу. В реальном применении на nginx также удобно возложить вопросы отказоустойчивости и распределения нагрузки, а также многие другие.

Какие запросы обрабатывает nginx, а какие передаются приложению-серверу, определяется настройками в файле nginx.conf.

Продолжение следует...

пятница, 21 апреля 2017 г.

Небольшие заметки про отладку Bash скриптов


Если вам когда-либо приходилось писать сложные скрипты на Bash, то вы наверняка думали о двух вещах:
  1. Никогда больше не писать сложные скрипты на Bash.
  2. Как отлаживать то, что уже написано?
Если с первым пунктом все понятно, то ответ на второй вопрос не так просто найти. Интегрированный среды для пошаговой отладки я не нашел (если не брать в расчет странный BASH Debugger). Но способ получить трейс есть, также как и выполнить скрипт по шагам.

Получить трейс выполнения проще простого. Надо всего лишь запустить bash с параметром -x. Либо в самом коде выполнить команду set +x. Первый вариант выглядит предпочтительней, потому что вы извне контролируете — нужен вам подробный вывод или нет. Конечному пользователю такой вывод обычно не только не нужен, но и откровенно мешает.

Посмотрим, как это работает на практике. Например, следующий простейший скрипт имеет ветвление:
#!/bin/bash

if [[ "$1" == "hello" ]]; then
 echo "hello world!!!"
fi
Запускаем его с трассировкой выполнения и сразу видим, что происходит внутри:
$ bash -x ./my_mega_script.sh hello

+ [[ hello == \h\e\l\l\o ]]
+ echo 'hello world!!!'
hello world!!!
Для запуска своего скрипта с сохранением трассировки удобно использовать вспомогательный скрипт примерно такого содержания:
#/bin/bash

/bin/bash -x ./my_mega_script.sh 2>my_mega_script.log
В таком режиме отладочный вывод не будет мешать смотреть результаты выполнения скрипта. А если выполняется много действий в течении длительного времени, то для просмотра трассировки удобно в отдельном окне запустить следующую команду:
$ tail -f ./my_mega_script.log
Утилита tail с параметром -f (follow) выводит текст на экран по мере обновления файла.

Ну и напоследок, режим пошагового выполнения:
#!/bin/bash
echo "Press CTRL+C to proceed."
trap "pkill -f 'sleep 1h'" INT
trap "set +x ; sleep 1h ; set -x" DEBUG

###
# ДАЛЬШЕ ИДЕТ ВАШ КОД
После каждого нажатия Ctrl+C выполняется один шаг скрипта и потом "отладчик" засыпает на 1 час либо до следующего нажатия Ctrl+C.

Книги по теме:

четверг, 2 марта 2017 г.

Google: кто-то завладел вашим паролем



Ночью мне пришло письмо на резервную почту о том, что кто-то использовал мой пароль для доступа к почте GMail, и что Google отважно заблокировал негодяев. Ну и мне, конечно, необходимо срочно принять меры. Я немного удивился, что почта, которая заведена специально для PayPal оказалась кем-то взломана. Учитывая, что она почти нигде не засвечена и имеет сгенерированный высокоэнтропийный пароль, это казалось невозможным. Однако, я сменил пароль, внутренне уже приготовившись к СМС с информацией о снятии денег с карты. А после смены пароля я начал разбираться в произошедшем и вот что выяснилось...

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

Но ссылки в письме вели на https://accounts.google.com и браузер говорил, что сайт не поддельный. Вот что я там увидел в списке устройств:


Ага, американские хакеры пытаются взломать мою почту. Немного необычно, что указан только IPv6 адрес. В голову начали приходить мысли об огромном ботнете из IoT устройств, который забрутфорсил мой пароль.

Из любопытства я решил проверить — не к Пентагону ли ведет трейс до этого адреса. Результат все расставил по местам:
traceroute to 2a00:1450:400c:c0c:0:0:0:211 (2a00:1450:400c:c0c::211), 30 hops max, 80 byte packets 
1 2600:3c01::8678:acff:fe0d:79c1 (2600:3c01::8678:acff:fe0d:79c1) 3.296 ms 3.057 ms 3.024 ms (United States) 
2 2600:3c01:3333:1::1 (2600:3c01:3333:1::1) 2.886 ms 2.970 ms 2.939 ms                     (United States) 
3 eqixsjc-v6.google.com (2001:504:0:1:0:1:5169:1) 2.866 ms as15169.sfmix.org (2001:504:30::ba01:5169:1) 3.986 ms 4.011 ms (United States) 
4 2001:4860:0:1004::2 (2001:4860:0:1004::2) 3.178 ms 2001:4860:0:1005::2 (2001:4860:0:1005::2) 2.576 ms 2.540 ms (United States) 
5 2001:4860::8:0:6117 (2001:4860::8:0:6117) 145.173 ms 141.854 ms 142.622 ms               (United States) 
6 2001:4860::c:4000:d20a (2001:4860::c:4000:d20a) 46.065 ms 40.774 ms 41.735 ms            (United States) 
7 2001:4860::8:4000:cbc2 (2001:4860::8:4000:cbc2) 49.985 ms 2001:4860::8:0:b0e2 (2001:4860::8:0:b0e2) 154.440 ms 154.322 ms (United States) 
8 2001:4860::c:4000:d2a0 (2001:4860::c:4000:d2a0) 59.087 ms 2001:4860::c:4000:d64b (2001:4860::c:4000:d64b) 154.324 ms 2001:4860::c:4000:d29f (2001:4860::c:4000:d29f) 90.399 ms (United States) 
9 2001:4860::8:0:bafa (2001:4860::8:0:bafa) 153.360 ms 2001:4860::8:4000:cd7f (2001:4860::8:4000:cd7f) 63.042 ms 2001:4860::8:0:bafa (2001:4860::8:0:bafa) 143.081 ms (United States) 
10 2001:4860::c:4000:d9af (2001:4860::c:4000:d9af) 170.679 ms 2001:4860::c:4000:d9ab (2001:4860::c:4000:d9ab) 139.273 ms 140.791 ms (United States) 
11 2001:4860::8:0:cc3f (2001:4860::8:0:cc3f) 145.014 ms 145.056 ms 2001:4860::8:4000:d324 (2001:4860::8:4000:d324) 144.381 ms (United States) 
12 2001:4860::2:0:76e7 (2001:4860::2:0:76e7) 144.918 ms 2001:4860::2:0:76e8 (2001:4860::2:0:76e8) 144.521 ms 2001:4860::2:0:76e7 (2001:4860::2:0:76e7) 145.444 ms (United States) 
13 * * *                                                                                   (?) 
14 * * *                                                                                   (?) 
15 * * *                                                                                   (?) 
16 * * *                                                                                   (?) 
17 * * *                                                                                   (?) 
18 * * *                                                                                   (?) 
19 * * *                                                                                   (?) 
20 * * *                                                                                   (?) 
21 mail-wr0-x211.google.com (2a00:1450:400c:c0c::211) 142.782 ms 143.318 ms 144.535 ms     (Ireland)
В конце указан mail-wr0-x211.google.com. Вот тут я и вспомнил, что пароль к этой почте знает сам же Google в другом аккаунте GMail, который аккумулирует почту с различных моих адресов. И в этот раз Google, видимо, стал проверять почту из другого датацентра, а не как обычно. И, подозреваю, что в этот момент не я один получил подобные письма о взломе. Тут я делаю вывод, что чехарда датацентров и оказалась причиной детектирования подозрительной активности. А может Google просто так форсирует людей включать дополнительные шаги аутентификации. А вы что скажете?

пятница, 25 ноября 2016 г.

Бесплатные технические книги в электронном виде

Коллекция ссылок на бесплатные (не ворованные) технические книги собрана на ресурсе devfreebooks.github.io. Там есть книги по различным языкам разработки, фреймворкам, протоколам. Некоторые книги доступны для скачивания, а некоторые только для чтения онлайн.

понедельник, 8 августа 2016 г.

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



Завтра (9 августа 2016) выступаю в Летней школе CTF с лекцией о пользе архитектурных артефактов для создания безопасных решений. Пока выяснял как туда лучше доехать выяснил интересный факт из жизни Подмосковья:
  • Маршрут Зеленоград — Университет Дубна: около 140 км на автомобиле и примерно 5 часов на общественном транспорте.
  • Маршрут Зеленоград — Мариинский театр: около 680 км на автомобиле и примерно 5 часов на общественном транспорте.
Отсюда выводы: во-первых, Питер стал невероятно близок к Москве после появления скоростных поездов, и это невероятно круто. Во-вторых, подмосковные центры научного притяжения до сих пор остаются сложнодоступными и это очень печально.

Видео доклада:

понедельник, 1 августа 2016 г.

Создаем и публикуем модуль Python



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

Разницы с C++ тут большой нет — программой может быть как один файл, так и группа файлов. Упрощенно можно сказать, что один файл — это модуль (module), а набор модулей в пределах одной директории — пакет (package) или библиотека. Пакет также может содержать в себе дочерние пакеты. Поскольку язык интерпретируемый, то запускать на выполнение отдельные файлы можно следующим образом:
$ python foo.py
Если у вас в системе установлен какой-либо пакет, либо вы просто находитесь в директории, где есть пакет, то запустить его можно так:
$ python -m bar.foo
Где foo — это файл foo.py в пакете bar. У пакетов есть также скрипты с зарезервированными названиями, которые запускаются автоматически. Так, если в пакете bar есть скрипт __main__.py, то он будет запущен автоматически при выполнении python -m bar.

Также стоит обратить внимание на файл __init__.py. Именно его наличие говорит Питону, что данная директория является пакетом. Часто бывает, что этот файл оставляют пустым, но можно там разместить какие-то общие для всех модулей вещи.

Теперь посмотрим на небольшой пакет из реальной жизни и его структуру. Для пояснений я буду использовать свой проект ZXTools, который предназначен для работы со старыми дисками от ZX-Spectrum. Как-нибудь я подробнее расскажу о процессе разбора дискет от Спектрума. Пока же посмотрим на структуру проекта. Он состоит из двух директорий (test и zxtools) и из нескольких файлов в корне. Далее рассмотрим зачем там каждый из них.

LICENSE

Файл LICENSE очень важен для публикации вашей работы и это касается не только Python. Текст лицензии позволяет другим понять можно ли использовать результаты вашего труда и если да, то как именно. GitHub при создании нового проекта предлагает создать этот файл включив туда одну из стандартных лицензий.

README.rst

Файл README также должен быть хорошо знаком тем, кто уже публиковал свои работы или использовал чужие. Тут указывается полезная информация о проекте, для чего он предназначен и как им пользоваться. Обратите внимание на формат файла. Чаще встречаются файлы README.md, что означает файл в формате Markdown, но для проектов на Python лучше использовать reStructuredText, так как только его корректно поддерживает Python Package Index. А ведь именно на PyPI публикуются пакеты, которые потом легко устанавливаются с помощью пакета pip. GitHub прекрасно поддерживает RST формат, поэтому вы ничего не потеряете.

Makefile

«Что в проекте на Python делает Makefile?» — спросите вы. А делает он то же, что и в C++ — автоматизирует некоторые шаги. В моем проекте он используется для удобной очистки директории (знакомая цель clean), для запуска юнит-тестов (цель test), для анализа тестового покрытия (цель coverage) и для статического анализа (цель lint). Если вы ведете разработку под Windows, то Makefile возможно окажется не самым привычным средством. Тогда используйте привычный и удобный именно вам механизм, но не слишком усложняйте.

setup.py

Файл setup.py нужен для автоматизации публикации и установки вашего пакета. По сути, это очередной файл на Питоне, в котором вы вызываете функцию setup пакета setuptools. В функцию setup вы передаете параметры, которые описывают ваш пакет: имя пакета, номер версии, тип лицензии, описание, имя автора, ссылки на документацию и исходные файлы, список дочерних пакетов, список зависимостей, классификаторы. В примере есть все, что вы захотите использовать вначале. Но подробнее про все параметры можно посмотреть в документации.

Полезно тут же указать какие командные скрипты необходимо создать при установке. Такие скрипты сильно упрощают использование вашего пакета, так как не нужно будет помнить в каком пакете находятся нужные команды и не нужно писать длинную строку их вызова. В примере можно видеть раздел entry_points, в котором определены две команды:
entry_points={
        'console_scripts': [
            'zeus2txt = zxtools.zeus2txt:main',
            'hobeta = zxtools.hobeta:main',
        ],
    },
Команды имеют следующий формат: <имя пакета>.<имя модуля>:<имя функции>.

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

Публикация, запуск тестов и continuous integration

Я уже упоминал, что самое популярное место для публикации пакетов — это индекс PyPI. Помимо этого, для открытых проектов TravisCI предоставляет бесплатную возможность запуска тестов на каждый коммит, а codecov.io позволяет отслеживать изменение покрытия и сигнализирует о проблемах. Именно эти средства я использовал в ZXTools. В следующий раз подробнее расскажу как опубликовать пакет, автоматически запускать проверку тестов, считать покрытие и как добавить красивые бейджики с информацией об этих процессах.

Книги по теме:

  1. Марк Лутц. Программирование на Python. Том 1
  2. Марк Лутц. Программирование на Python. Том 2

понедельник, 6 июня 2016 г.

 Мой путь из C++ в Python

Этот текст кратко поясняет как мне удалось быстро начать писать программы на Python. Все, что описывается, основывается на личном опыте и ощущениях, и поэтому может не описывать тот самый единственно верный путь. Тем не менее, изложенная информация агрегирует знания, которые пришлось собрать, чтобы создавать собственные проекты на Python. Для начала не огромные высокопроизводительные системы, но и на C++ вы тоже не сразу стали выдавать шедевры. Если у кто-то посчитает, что тут не хватает важных вещей, о который обязательно нужно сказать, то в комментариях можно это отметить.

Начинать разработку конечно нужно с установки интерпретатора. Повезло тем, у кого установлена операционная система семейства Linux — тогда вопрос установки скорее всего не стоит и интерпретатор Python уже установлен. Если Python не установлен, то инсталлятор для всех популярных ОС совершенно бесплатно можно скачать с официального сайта.

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

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

Хотя этот вопрос и не такой популярный как про выход из Vim, но сразу подскажу, что выйти из интерактивного режима можно написав quit().

Если очень хочется какую-нибудь среду разработки, то PyCharm мне показалось наиболее удобной (помимо платной версии есть и Community Edition). Под Windows конечно еще есть привычная Visual Studio с Python Tools.

С инструментами покончено, время понять какие важные отличия от C++ нужно сразу усвоить. Во-первых, Python использует duck typing. Это означает, что если в какой-то момент выполнения программы объект имеет все свойства класса, то он в этот момент считается объектом этого класса. Если сравнивать с C++, то ближе всего к этому концепты (Concepts), которые кстати не попадут в C++17.

If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck.

Во-вторых, многопоточности в Python нет. Если вы будете мучить Google на эту тему, то скорее всего наткнетесь на GIL (Global Interpreter Lock), но это совсем не та многопоточность, к которой привыкли C++ разработчики. Схематично принцип такой многопоточности показан на картинке — тут хорошо видно, что параллельно никакие потоки не выполняются, даже если у вас многоядерный процессор:

GIL description

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

Остальные вопросы, которые часто возникают у новичков, и ответы на них можно найти на StackOverflow.com в секции с часто задаваемыми вопросами. Стоит просмотреть первых штук 50.

Из этих вопросов вы узнаете, что файлы на Python имеют расширение py и их стоит начинать со следующей строки:

#! /usr/bin/env python
Это позволяет запускать скрипт в виртуальных окружениях с разными версиями Python. Сам же Python выполняет файл последовательно с первой строки и далее. При этом нет никакой функции main, как в C или C++. Хотя часто в скриптах встречается строка if __name__ == "__main__", которую ошибочно можно принять за точку входа. Но нет, если за пределами этого выражения будет другой код, то он также выполнится. А само выражение удобно использовать для создания и запуска тестов в подключаемых модулях.

В следующий раз расскажу как создать модуль и тесты для него, и как это потом опубликовать и «непрерывно интегрировать». А ниже моя первая программа на Python:

#! /usr/bin/env python
# vim: set fileencoding=utf-8 :
""" Hello world in Python """

def main():
    print("Hello world!")

if __name__ == "__main__":
    main()

Книги по теме:

  1. Марк Лутц. Программирование на Python. Том 1
  2. Марк Лутц. Программирование на Python. Том 2