Как построить свое приложение для Tarantool и при этом не городить огород каждый раз, когда требуется сделать, казалось бы, элементарную вещь? Это продолжение цикла статей о том, как создавать свои приложения для Tarantool.
Сегодня мы рассмотрим вопросы сетевого взаимодействия, установки и использования сторонних модулей.

Содержание цикла «Приложения для Tarantool»
— Часть 1. Хранимые процедуры
— Часть 2. OAuth2-авторизация
— Часть 3. Тестирование и запуск
Взаимодействие с внешними сервисами
В качестве примера рассмотрим реализацию OAuth2-авторизации через Facebook в приложении tarantool-authman. При OAuth2-авторизации пользователь переходит по ссылке, которая ведет на окно логина в социальной сети. После ввода авторизационных данных и подтверждения разрешений (permisssions) соцсеть переадресует пользователя обратно на сайт с авторизационным кодом в GET-параметре запроса. Сервер должен обменять этот код на токен (либо на пару токенов — access и refresh). С токеном можно получить информацию о пользователе из социальной сети. Подробнее о OAuth2-авторизации написано в этой статье.

Tarantool-приложение возьмет на себя обмен авторизационного кода (code) на токен доступа (token) к информации о пользователе из социальной сети, а также получит по этому токену пользовательские данные. В нашем случае это email, имя и фамилия. Для обмена авторизационного кода на токен доступа нужно отправить запрос к Facebook с кодом, а также параметрами приложения Facebook — client_id и client_secret.
В Tarantool с версии 1.7.4-151 встроен модуль http.client, работающий на базе libcurl. Модуль позволяет принимать и отправлять HTTP-запросы. Воспользуемся этим модулем для реализации OAuth2-авторизации. Для начала создадим вспомогательную функцию для отправки HTTP-запросов в модуле authman/utils/http.lua:
local http = {} local utils = require('authman.utils.utils') local curl_http = require('http.client') -- config — общая конфигурация приложения authman function http.api(config) local api = {} -- Конфигурация сетевых запросов local timeout = config.request_timeout function api.request(method, url, params, param_values) local response, body, ok, msg if method == 'POST' then -- utils.format — функция для подстановки значений в placeholder’ы body = utils.format(params, param_values) -- Безопасный вызов pcall не прервет исполнение программы при ошибке сети ok, msg = pcall(function() response = curl_http.post(url, body, { headers = {['Content-Type'] = 'application/x-www-form-urlencoded'}, timeout = timeout }) end) end return response end return api end return http
Стоит обратить внимание на функцию pcall. Она обрабатывает исключения, возникшие при выполнении анонимной функции. В нашем случае необходимо обработать сетевые ошибки, которые генерирует HTTP-клиент. Результат вызова pcall записывается в переменные ok (true/false) и msg (сообщение об ошибке, nil при успехе).
OAuth2-авторизации в приложении
Создадим модель social и напишем метод для получения токена по авторизационному коду get_token(provider, code), а также метод для получения или обновления данных профиля get_profile_info(provider, token, user_tuple). Рассмотрим эти методы:
-- Метод получения токена function model.get_token(provider, code) local response, data, token if provider == 'facebook' then -- Здесь http — модуль authman/utils/http.lua response = http.request( 'GET', 'https://graph.facebook.com/v2.8/oauth/access_token', '?client_id=${client_id}&redirect_uri=${redirect_uri}&client_secret=${client_secret}&code=${code}', { -- config — конфигурации проекта, которые передаются в модель при инициализации -- Это параметры приложения в социальной сети client_id = config[provider].client_id, redirect_uri = config[provider].redirect_uri, client_secret = config[provider].client_secret, code = code, } ) if response == nil or response.code ~= 200 then return nil else data = json.decode(response.body) return data.access_token end end end -- Метод получения профиля пользователя function model.get_profile_info(provider, token, user_tuple) local response, data user_tuple[user.PROFILE] = {} if provider == 'facebook' then response = http.request( 'GET', 'https://graph.facebook.com/me', '?access_token=${token}&fields=email,first_name,last_name', { token = token } ) if response == nil or response.code ~= 200 then return nil else data = json.decode(response.body) user_tuple[user.EMAIL] = data.email user_tuple[user.PROFILE][user.PROFILE_FIRST_NAME] = data.first_name user_tuple[user.PROFILE][user.PROFILE_LAST_NAME] = data.last_name return data.id end end end
Теперь добавим метод API приложения, позволяющий создать пользователя или войти под уже существующим через Facebook. Метод будет возвращать пользователя вместе с сессионными данными. Подробнее о том, как формируется и валидируется сессия, можно посмотреть в исходном коде.
-- Метод api в authman/init.lua function api.social_auth(provider, code) local token, social_id, social_tuple local user_tuple = {} if not (validator.provider(provider) and validator.not_empty_string(code)) then return response.error(error.WRONG_PROVIDER) end -- Получим OAuth2-токен token = social.get_token(provider, code, user_tuple) if not validator.not_empty_string(token) then return response.error(error.WRONG_AUTH_CODE) end -- Получим информацию о пользователе social_id = social.get_profile_info(provider, token, user_tuple) if not validator.not_empty_string(social_id) then return response.error(error.SOCIAL_AUTH_ERROR) end user_tuple[user.EMAIL] = utils.lower(user_tuple[user.EMAIL]) user_tuple[user.IS_ACTIVE] = true user_tuple[user.TYPE] = user.SOCIAL_TYPE -- Проверим, есть ли в space пользователь с таким же social_id social_tuple = social.get_by_social_id(social_id, provider) if social_tuple == nil then -- Если нет — создадим его user_tuple = user.create(user_tuple) social_tuple = social.create({ [social.USER_ID] = user_tuple[user.ID], [social.PROVIDER] = provider, [social.SOCIAL_ID] = social_id, [social.TOKEN] = token }) else -- А если есть — обновим информацию профиля user_tuple[user.ID] = social_tuple[social.USER_ID] user_tuple = user.create_or_update(user_tuple) social_tuple = social.update({ [social.ID] = social_tuple[social.ID], [social.USER_ID] = user_tuple[user.ID], [social.TOKEN] = token }) end -- Создание пользовательской сессии local new_session = session.create( user_tuple[user.ID], session.SOCIAL_SESSION_TYPE, social_tuple[social.ID] ) return response.ok(user.serialize(user_tuple, { session = new_session, social = social.serialize(social_tuple), })) end
Как проверить, что метод работает? Для начала необходимо зарегистрировать приложение в Facebook. Сделаем это на странице разработчика Facebook. В созданном приложении нужно добавить продукт «Вход через Facebook» и указать redirect_uri — «Действительные URL-адреса для перенаправления OAuth». Параметр redirect_uri — это урл вашего сайта, куда социальная сеть перенаправит пользователя с параметром code после успешной авторизации в социальной сети. Далее откройте в браузере урл https://www.facebook.com/v2.8/dialog/oauth?client_id=${client_id}&redirect_uri=${redirect_uri}&scope=email, где
• client_id — id вашего приложения в Facebook;
• redirect_uri — урл для редиректа, который вы указали ранее;
• scope — список разрешений (в данном случае только email).
Facebook запросит подтверждение разрешений, после подтверждения переадресует вас с GET-параметром code. Это и есть тот самый авторизационный код, которой принимает метод api.social_auth(). Прежде чем проверять работоспособность кода, создадим конфигурационный файл authman/config/config.lua, в котором укажем настройки приложения Facebook.
return { facebook = { client_id = 'id from fb application', client_secret = 'secret from fb application'', redirect_uri='http://redirect_to_your_service', } }
Теперь проверим, что код работает и приложение получает информацию о пользователе из социальной сети:
$ tarantool version 1.7.4-384-g70898fd type 'help' for interactive help tarantool> config = require('config.config') tarantool> box.cfg({listen = 3331}) tarantool> auth = require('authman').api(config) tarantool> code = 'auth_code_from_get_param' tarantool> ok, user = auth.social_auth('facebook', code) tarantool> user --- - is_active: true social: provider: facebook social_id: '000000000000001' profile: {'first_name': 'Иван', 'last_name': 'Иванов'} id: b1e1fe02-47a2-41c6-ac8e-44dae71cde5e email: ivanov@mail.ru session: ... ...
Установка сторонних модулей
Для решения многих задач хорошо иметь под рукой готовые решения. Например, в версиях Tarantool ниже 1.7.4-151 отправить HTTP-запрос «из коробки» было нельзя. Требовался модуль tarantool-curl. Сейчас этот модуль больше не поддерживается, не рекомендуется его использовать. Однако есть много других полезных модулей, один из них — tarantool-queue — реализация FIFO-очереди.
Есть несколько способов установить tarantool-queue. Первый, самый простой и удобный, появился относительно недавно, в версии Tarantool 1.7.4-294.
$ tarantoolctl rocks install queue
Другие пакеты Tarantool также доступны для установки с использованием менеджера пакетов. Полный список модулей для Tarantool можно найти на странице Rocks.
Второй способ — с помощью менеджера пакетов вашей ОС. Здесь необходимо подключить репозиторий Tarantool, если вы еще не подключили его на этапе установки, а также убедиться, что соответствующий пакет есть в репозитории. Например, для Ubuntu:
$ sudo apt-get install tarantool-queue
Третий способ сложнее, однако позволяет использовать не только приложения для Tarantool, но и готовые модули Lua. Устанавливать модули для Tarantool и Lua удобно с помощью менеджера пакетов LuaRocks. Подробно о нем и доступных модулях можно прочитать в документации. Установим LuaRocks и настроим его для работы с репозиторием Tarantool:
$ sudo apt-get install luarocks
Теперь настроим LuaRocks, чтобы устанавливать не только Lua-пакеты, но и пакеты для Tarantool. Для этого требуется создать файл ~/.luarocks/config.lua со следующим содержимым:
rocks_servers = { [[http://luarocks.org/repositories/rocks]], [[http://rocks.tarantool.org/]] }
Установим сам модуль и проверим его работу:
# Установка tarantool-queue $ sudo luarocks install queue # Запустим интерактивную консоль и проверим работоспособность модуля: $ tarantool version 1.7.3-433-gef900f2 type 'help' for interactive help tarantool> box.cfg({listen = 3331}) tarantool> queue = require('queue') tarantool> test_queue = queue.create_tube('test_queue', 'fifo') tarantool> test_queue:put({'task_1'}) tarantool> test_queue:put({'task_2'}) tarantool> test_queue:take() --- - [0, 't', ['task_1']] ...
Итак, теперь мы можем создавать приложения со сложной архитектурой и внешними взаимодействиями. В следующей части рассмотрим тестирование приложений, а также конфигурацию и запуск в бою. До новых встреч!

