[Из песочницы] Фреймворк Fastcgi Container

В рамках работы по оценке различных способов реализации Web UI для существующего C++ приложения, на основе хорошо известного на Хабре фреймворка Fastcgi Daemon был создан фреймворк Fastcgi Container.

При сохранении всех возможностей прототипа, основные отличия нового фреймворка от него заключаются в следующем:

  • фреймворк переписан на C++11
  • добавлена поддержка фильтров
  • добавлена поддержка аутентификации и авторизации клиента
  • добавлена поддержка сессий
  • добавлена поддержка сервлетов (расширение обработчиков запросов из оригинального фреймворка)
  • добавлен Page Compiler для генерирования C++ сервлетов из JSP-подобных страниц


Особенности и детали реализации прототипа обсуждались на Хабре несколько раз (например, здесь). В данной статье приведены особенности нового фреймворка Fastcgi Container.

Использование C++11


Фреймворк-прототип Fastcgi Daemon широко использует библиотеки Boost. Производный фреймворк было решено переписать на C++11, заменив использование Boost на новые стандартные конструкции. Исключение составила библиотека Boost.Any, эквивалент которой отсутствует в C++11. Необходимый функционал был добавлен через использование библиотеки MNMLSTC Core.

Фильтры


Протокол FastCGI предусматривает роли Filter и Authorizer для организации соответствующего функционала, однако распространённые реализации (например, модули для Apache HTTPD и NGINX) поддерживают только роль Responder.

В результате поддержка фильтров была добавлена непосредственно в Fastcgi Container.

В приложении фильтры создаются как расширение класса fastcgi::Filter:

class Filter {
public:
        Filter();
        virtual ~Filter();
        Filter(const Filter&) = delete;
        Filter& operator=(const Filter&) = delete;
        virtual void onThreadStart();
        virtual void doFilter(Request *req, HandlerContext *context, std::function next) = 0;
};


Их загрузка в контейнер осуществляется динамически, аналогично другим компонентам приложения:


        
        ...


         
                daemon-logger
        
         
                daemon-logger
        
        ...



Фильтры могут быть либо глобальными для данного приложения:


        
                
        
        ...



либо предназначены для обработки группы запросов с URL, соответствующим заданному регулярному выражению:


        
                
        
        ...



Если контейнер нашёл более одного фильтра для текущего запроса, они будут выполнены в той же очерёдности, в которой были добавлены в конфигурационный файл.

Для передачи управления следующему по очереди фильтру (или целевому обработчику/сервлету, если фильтр единственный или последний в очереди), текущий фильтр вызывает функцию next, переданную ему через список параметров. Для прерывания цепочки фильтр может возвратить управление без вызова функции next.

В общем, каждый фильтр получает управление два раза: до передачи управления следующему фильтру или целевому обработчику/сервлету, а также после окончания работы следующему фильтра или обработчика/сервлета. Фильтр может изменить тело ответа (response) и/или заголовки (HTTP headers) как до, так и после работы целевого обработчика/сервлета при условии, что тело и заголовки ещё на отправлены клиенту.

Аутентификация


Аутентификация осуществляется специальными фильтрами. В состав Fastcgi Container включены фильтры для следующих типов аутентификации: Basic access authentication, Form authentication, и Delegated authentication.

Последний из названных типов делегирует процесс аутентификации HTTP серверу, ожидая от него идентификатор пользователя, переданный как стандартная CGI переменная REMOTE_USER.

Два других типа осуществляют аутентификацию, используя предоставленный Security Realm.

Как и в случае обычных фильтров, загрузка в контейнер осуществляется динамически:


         
        ...


         
                /login
                example_realm
                daemon-logger
                true
        
         
                example_realm
                daemon-logger
        
         
                example_realm
                daemon-logger
        
        ...



Фильтр аутентификации, как правило, указывается первым в цепочке фильтров:


        
                
        
        ...



Для своей работы фильтры аутентификации требуют Security Realm, который должен быть реализован в приложении как расширение класса fastcgi::security::Realm:

class Realm : public fastcgi::Component {
public:
        Realm(std::shared_ptr context);
        virtual ~Realm();
        virtual void onLoad() override;
        virtual void onUnload() override;
        virtual std::shared_ptr authenticate(const std::string& username, const std::string& credentials);
        virtual std::shared_ptr getSubject(const std::string& username);
        const std::string& getName() const;
protected:
        std::string name_;
        std::shared_ptr logger_;
};


Пример простой реализации Security Realm с заданием списка пользователей непосредственно в конфигурационном файле:

Security Realm
class ExampleRealm : virtual public fastcgi::security::Realm {
public:
        ExampleRealm(std::shared_ptr context);
        virtual ~ExampleRealm();
        virtual void onLoad() override;
        virtual void onUnload() override;
        virtual std::shared_ptr authenticate(const std::string& username, const std::string& credentials)  override;
        virtual std::shared_ptr getSubject(const std::string& username) override;
private:
    std::unordered_map> users_;
};

ExampleRealm::ExampleRealm(std::shared_ptr context)
: fastcgi::security::Realm(context) {
    const fastcgi::Config *config = context->getConfig();
    const std::string componentXPath = context->getComponentXPath();

    std::vector users;
    config->subKeys(componentXPath+"/users/user[count(@name)=1]", users);
    for (auto& u : users) {
        std::string username = config->asString(u + "/@name", "");

        std::shared_ptr data = std::make_shared();
        data->password = config->asString(u + "/@password", "");

        std::vector roles;
        config->subKeys(u+"/role[count(@name)=1]", roles);
        for (auto& r : roles) {
                data->roles.push_back(config->asString(r + "/@name", ""));
        }

        users_.insert({username, std::move(data)});
    }
}

ExampleRealm::~ExampleRealm() {
        ;
}

void ExampleRealm::onLoad() {
        fastcgi::security::Realm::onLoad();
}

void ExampleRealm::onUnload() {
        fastcgi::security::Realm::onUnload();
}

std::shared_ptr ExampleRealm::authenticate(const std::string& username, const std::string& credentials) {
        std::shared_ptr subject;
        auto it = users_.find(username);
        if (users_.end()!=it && it->second && credentials==it->second->password) {
                subject = std::make_shared();
                for (auto &r : it->second->roles) {
                        subject->setPrincipal(std::make_shared(r));
                }
                subject->setReadOnly();
        }
        return subject;
}

std::shared_ptr ExampleRealm::getSubject(const std::string& username)  {
        std::shared_ptr subject;
        auto it = users_.find(username);
        if (users_.end()!=it && it->second) {
                subject = std::make_shared();
                for (auto &r : it->second->roles) {
                        subject->setPrincipal(std::make_shared(r));
                }
                subject->setReadOnly();
        }
        return subject;
}


Его загрузка в контейнер аналогична загрузке других компонентам приложения:


        
        ...


         
                Example Realm
                daemon-logger
                
                        
                                
                                
                                
                        
                        
                                
                                
                        
                
        
        ...



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

Авторизация


Для декларативной авторизации используется элемент в конфигурационном файле:


         
        



Авторизация может осуществляется программно. Для этой цели классы fastcgi::Request и fastcgi::HttpRequest предоставляют методы:

std::shared_ptr Request::getSubject() const;
bool Request::isUserInRole(const std::string& roleName) const;
template bool Request::isUserInRole(const std::string &roleName) {
        return getSubject()->hasPrincipal(roleName);
}


Если клиент не аутентифицирован, метод getSubject() возвращает указатель на объект-«аноним» с пустым множеством ролей и возвращающим true при вызове следующего метода:

bool security::Subject::isAnonymous() const;


Сессии


Контейнер предоставляет реализацию Simple Session Manager. Для его активации в конфигурационный файл нужно добавить следующее:


         
        ...


                              
                daemon-logger
        
        ...


    30



Для доступа к текущей сессии класс fastcgi::Request предоставляет метод:

std::shared_ptr Request::getSession();


Среди прочего, класс fastcgi::Session предоставляет следующие методы:

virtual void setAttribute(const std::string &name, const core::any &value);
virtual core::any getAttribute(const std::string &name) const;
virtual bool hasAttribute(const std::string &name) const;
virtual void removeAttribute(const std::string& name);
virtual void removeAllAttributes();
std::type_info const& type(const std::string &name) const;
std::size_t addListener(ListenerType f);
void removeListener(std::size_t index);


Simple Session Manager не имеет поддержки кластера контейнеров, поэтому в случае использования более одного контейнера на балансировщике нагрузки следует настроить режим «sticky sessions».

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

Сервлеты


В дополнение к классам fastcgi::Request и fastcgi::Handler, контейнер предоставляет классы-оболочки fastcgi::HttpRequest, fastcgi::HttpResponse и fastcgi::Servlet.

В приложении можно использовать как «старые» так и «новые» классы.

C++ Server Pages и Page Compiler


Page Compiler является форком из проекта POCO, и предназначен для трансляции HTML страниц со специальными директивами (C++ server pages, CPSP) в сервлеты.

Простой пример C++ server page:

<%@ page class="TimeHandler" %>
<%@ component name="TestServlet" %>
<%!
    #include 
%>
<%
    auto p = std::chrono::system_clock::now();
    auto t = std::chrono::system_clock::to_time_t(p);
%>

    
        Time Sample
    
    
        

Time Sample

<%= std::ctime(&t) %>


Подробное описание директив доступно на GitHub проекта.

© Habrahabr.ru