четверг, 24 февраля 2011 г.

RESTAS middleware

Я сегодня рассматривал Clack и должен сказать, что мне это определённо не нравится. Впрочем WSGI у меня тоже никогда больших восторгов не вызывал. Слишком универсальный протокол и слишком слабый API. Но, сама концепция middleware (в том виде, как она есть, например, в Pylons) не так уж и плоха. Просто, на мой вкус, её надо сделать более конкретной. Кое какие мысли у меня на эту тему уже крутились и я даже показывал недавно, как можно тонко влиять на процесс обработки запроса. И вот сегодня идея начала приобретать конкретную форму. В частности, изменился макрос restas:mount-submodule, теперь можно писать примерно так:
(restas:mount-submodule -mysubmodule- (#:mymodule middleware1 middleawre2)
..)
Здесь middleware1 и middleware2 это функции, которые должны принимать маршрут и возвращать другой.

В момент построения дерева маршрутов (а это происходит каждый раз при вызове функции restas:recconect-all-routes) для конкретного субмодуля строиться список обрабатываемых им маршрутов и пропускается через цепочку middleware-вызовов. middleware-функция может как угодно изменить маршрут или вообще вернуть другой. Специально для поддержки этого я добавил в cl-routes новый класс routes:proxy-route (который реализует известный паттерн proxy).

Скажем, с помощью модуля restas-directory-publisher можно публиковать директории со статикой:
(restas:mount-submodule -tmp- (#:restas.directory-publisher)
(restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/)
(restas.directory-publisher:*autoindex* t))
Такой код позволит любому просматривать содержимое директории /tmp на сервере, но кто знает что там может быть. Теперь с помощью middleware можно защитить содержимое этого каталога, требуя от посетителей пройти HTTP-авторизацию:
(defclass http-auth-route (routes:proxy-route) () )

(defmethod routes:route-check-conditions ((route http-auth-route) bindings)
(if (call-next-method)
(multiple-value-bind (user password) (hunchentoot:authorization)
(or (and (string= user "hello")
(string= password "world"))
(hunchentoot:require-authorization)))))

(defun http-auth-middleware (route)
(make-instance 'http-auth-route :target route))

(restas:mount-submodule -tmp- (#:restas.directory-publisher http-auth-middleware)
(restas.directory-publisher:*baseurl* '("tmp"))
(restas.directory-publisher:*directory* #P"/tmp/)
(restas.directory-publisher:*autoindex* t))
Здесь определяется новый класс http-auth-route, наследующий от routes:proxy-route, и для него специализируется метод routes:route-check-conditions, который вызывается для проверки соответствия маршрута условиям запроса. Если маршрут проходит все проверки, то проверяется прошёл ли пользователь HTTP-авторизацию. Функция http-auth-middleware используется для создания таких маршрутов и указывается в списке middlewares макроса restas:mount-submodule.

Для маршрутов, наследующих от routes:proxy-route, имеет смысл определять специализации методов routes:route-check-conditions и/или restas:process-route.

Чуть более сложный пример использования описанных возможностей можно найти здесь.

10 комментариев:

  1. Интересная ссылка на Clack. Неожиданно узнаешь, что и такое уже сделано.

    Сама модель что "application is just a function" не очень убедительна для меня, потому что фактически аргументов этой ф-ции является весь здоровый environment и возвращает эта функция весь environment с возможными модификациями. Но окончательных выводов я не делаю, потому что недостаточно близко знаком с этой моделью - не пробовал ей пользоваться или понять, что именно она упрощает по сравнению с другими моделями.

    Вопрос про RESTAS middleware - мне слегка подозрительно, что вещь, которая проверяет авторизацию называется "маршрут" (здесь тоже говорю с позиции поверхностного знакомства - только прочитал этот пост).

    ОтветитьУдалить
  2. > Неожиданно узнаешь, что и такое уже сделано.

    Ну оно, мягко говоря, ещё очень сырое )

    > вещь, которая проверяет авторизацию называется
    > "маршрут"

    В RESTAS обработка запросов производится в маршрутах. Сами маршруты собираются в одно дерево. Диспетчер ищёт наиболее подходящий запросы маршрут. В-первую очередь по шаблону URL. Но маршрут может накладывать также дополнительные условия, более конкретные (например, тип запроса).

    Авторизация обычно должна быть привязана к конкретному маршруту, поскольку не все запросы надо проверять (например, какой-нибудь style.css можно отдавать всем желающим), а бывает это операцией затратной. Это как наиболее удобный способ, мне так кажется. Вообще, более подробно про маршруты здесь: http://restas.lisper.ru/ru/manual/routes.html

    ОтветитьУдалить
  3. Меняем килобайты кода на включение мозга :)

    ОтветитьУдалить
  4. > http://restas.lisper.ru/ru/manual/routes.html
    Эту страницу я раньше читал, но сам RESTAS я не исипользовал, поэтому и говорю, что знаю поверхностно (хотя прочнение этой сраницы оказалось мне полезным в моем коде для одной задачи на джаве).

    > В RESTAS обработка запросов производится в маршрутах.

    Вот это мне и подозрительно или, другими словами, смущает. В ментальной модели, которая у меня сложилась, я представлял, что маршрут определяет соответствие УРЛа и обработчкика. Т.е. обработка делается в не в маршруте а в обработчике.

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

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

    Возможно, что мне нужно сделать усилие и уточнить мое представление. Поэтому мой коментарий на не претендует истинность.

    ОтветитьУдалить
  5. Маршрут - это сложившаяся терминология, кстати вполне удобная. И нет такого что обработка "делается в маршруте". Маршрут определяет некоторые условия, при соблюдении которых вызывается обработчик этого маршрута. И можно специфицировать одну из обобщеных ф-ий для дополнительной обработки условия. Так что никакого противоречия.
    Но возможно, автор меня в чём то поправит.

    З.Ы. А вообще это демагогия, надо обязательно всё пробовать. Я предлагаю вам весьма наглядные примеры идущие в комплекте.

    ОтветитьУдалить
  6. Демагогия или нет, но слово маршрут мне понятее если оно отнсится к УРЛам (потому что УРЛы похожи на пути в файловой системе, т.е. путь или способ достижения узла в дереве). Если это обобщается/расширяется до "некоторых условий", то уже слово маршрут не так хорошо подходит. Я не предлагагаю вводить дополнительные сущности или переименовывать что-то, просто замечание. Делюсь своими мыслями.

    ОтветитьУдалить
  7. @Anton Vodonosov

    С самого начал использования термина routes (в RoR) это был не просто конкретные маршруты, а шаблоны, т.е. он содержали в себе переменные параметры. Плюс обычно к ним добавляют тип запроса, а также есть какие-то возможности для ограничения значений параметров (например, только цифры).

    Естественным образом концепция routes обощается до "шаблон URL + набор дополнительных условий". А библиотека cl-routes вообще ничего не знает об обработчиках, маршрут там это отдельный объект, находящийся в общем дереве, плюс инструменты для поиска подходящего маршрута в это дереве. Что делать с найденным маршрутом решает конкретное приложение.

    ОтветитьУдалить
  8. Понятно.

    Т.е. можно сказать, что Middleware применяется именно для композиции условий выбора обработчика для запроса.

    ОтветитьУдалить
  9. @Anton Vodonosov

    Не только для композиции. Также можно использовать для изменения результатов обработки запросов, например для добавления заголовков, для пост-обработки, да как угодно. middleware даёт возможность "обернуть" оригинальный маршрут и изменить его поведение.

    ОтветитьУдалить
  10. @Anton Vodonosov

    Вообще решил вместо middleware использовать более адекватный символ "декоратор".

    ОтветитьУдалить