В рамках работы по оценке различных способов реализации 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<void(Request *req, HandlerContext *context)> next) = 0;
};
Их загрузка в контейнер осуществляется динамически, аналогично другим компонентам приложения:
<modules>
<module name="example" path="./example.so"/>
...
</modules>
<components>
<component name="example_filter_1" type="example:filter1">
<logger>daemon-logger</logger>
</component>
<component name="example_filter_2" type="example:filter2">
<logger>daemon-logger</logger>
</component>
...
</components>
Фильтры могут быть либо глобальными для данного приложения:
<handlers urlPrefix="/myapp">
<filter>
<component name="example_filter_1"/>
</filter>
...
</handlers>
либо предназначены для обработки группы запросов с URL, соответствующим заданному регулярному выражению:
<handlers urlPrefix="/myapp">
<filter url="/.*">
<component name="example_filter_2"/>
</filter>
...
</handlers>
Если контейнер нашёл более одного фильтра для текущего запроса, они будут выполнены в той же очерёдности, в которой были добавлены в конфигурационный файл.
Для передачи управления следующему по очереди фильтру (или целевому обработчику/сервлету, если фильтр единственный или последний в очереди), текущий фильтр вызывает функцию next
, переданную ему через список параметров. Для прерывания цепочки фильтр может возвратить управление без вызова функции next
.
В общем, каждый фильтр получает управление два раза: до передачи управления следующему фильтру или целевому обработчику/сервлету, а также после окончания работы следующему фильтра или обработчика/сервлета. Фильтр может изменить тело ответа (response) и/или заголовки (HTTP headers) как до, так и после работы целевого обработчика/сервлета при условии, что тело и заголовки ещё на отправлены клиенту.
Аутентификация
Аутентификация осуществляется специальными фильтрами. В состав Fastcgi Container включены фильтры для следующих типов аутентификации: Basic access authentication
, Form authentication
, и Delegated authentication
.
Последний из названных типов делегирует процесс аутентификации HTTP серверу, ожидая от него идентификатор пользователя, переданный как стандартная CGI переменная REMOTE_USER
.
Два других типа осуществляют аутентификацию, используя предоставленный Security Realm
.
Как и в случае обычных фильтров, загрузка в контейнер осуществляется динамически:
<modules>
<module name="auth" path="/usr/local/lib64/fastcgi3/fastcgi3-authenticator.so"/>
...
</modules>
<components>
<component name="form_authenticator" type="auth:form-authenticator">
<form-page>/login</form-page>
<realm>example_realm</realm>
<logger>daemon-logger</logger>
<store-request>true</store-request>
</component>
<component name="basic_authenticator" type="auth:basic-authenticator">
<realm>example_realm</realm>
<logger>daemon-logger</logger>
</component>
<component name="delegated_authenticator" type="auth:delegated-authenticator">
<realm>example_realm</realm>
<logger>daemon-logger</logger>
</component>
...
</components>
Фильтр аутентификации, как правило, указывается первым в цепочке фильтров:
<handlers urlPrefix="/myapp">
<filter url="/.*">
<component name="form_authenticator"/>
</filter>
...
</handlers>
Для своей работы фильтры аутентификации требуют Security Realm
, который должен быть реализован в приложении как расширение класса fastcgi::security::Realm
:
class Realm : public fastcgi::Component {
public:
Realm(std::shared_ptr<fastcgi::ComponentContext> context);
virtual ~Realm();
virtual void onLoad() override;
virtual void onUnload() override;
virtual std::shared_ptr<Subject> authenticate(const std::string& username, const std::string& credentials);
virtual std::shared_ptr<Subject> getSubject(const std::string& username);
const std::string& getName() const;
protected:
std::string name_;
std::shared_ptr<fastcgi::Logger> logger_;
};
Пример простой реализации Security Realm
с заданием списка пользователей непосредственно в конфигурационном файле:
class ExampleRealm : virtual public fastcgi::security::Realm {
public:
ExampleRealm(std::shared_ptr<fastcgi::ComponentContext> context);
virtual ~ExampleRealm();
virtual void onLoad() override;
virtual void onUnload() override;
virtual std::shared_ptr<fastcgi::security::Subject> authenticate(const std::string& username, const std::string& credentials) override;
virtual std::shared_ptr<fastcgi::security::Subject> getSubject(const std::string& username) override;
private:
std::unordered_map<std::string, std::shared_ptr<UserData>> users_;
};
ExampleRealm::ExampleRealm(std::shared_ptr<fastcgi::ComponentContext> context)
: fastcgi::security::Realm(context) {
const fastcgi::Config *config = context->getConfig();
const std::string componentXPath = context->getComponentXPath();
std::vector<std::string> users;
config->subKeys(componentXPath+"/users/user[count(@name)=1]", users);
for (auto& u : users) {
std::string username = config->asString(u + "/@name", "");
std::shared_ptr<UserData> data = std::make_shared<UserData>();
data->password = config->asString(u + "/@password", "");
std::vector<std::string> 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<fastcgi::security::Subject> ExampleRealm::authenticate(const std::string& username, const std::string& credentials) {
std::shared_ptr<fastcgi::security::Subject> subject;
auto it = users_.find(username);
if (users_.end()!=it && it->second && credentials==it->second->password) {
subject = std::make_shared<fastcgi::security::Subject>();
for (auto &r : it->second->roles) {
subject->setPrincipal(std::make_shared<fastcgi::security::Principal>(r));
}
subject->setReadOnly();
}
return subject;
}
std::shared_ptr<fastcgi::security::Subject> ExampleRealm::getSubject(const std::string& username) {
std::shared_ptr<fastcgi::security::Subject> subject;
auto it = users_.find(username);
if (users_.end()!=it && it->second) {
subject = std::make_shared<fastcgi::security::Subject>();
for (auto &r : it->second->roles) {
subject->setPrincipal(std::make_shared<fastcgi::security::Principal>(r));
}
subject->setReadOnly();
}
return subject;
}
Его загрузка в контейнер аналогична загрузке других компонентам приложения:
<modules>
<module name="example" path="./example.so"/>
...
</modules>
<components>
<component name="example_realm" type="example:example-realm">
<name>Example Realm</name>
<logger>daemon-logger</logger>
<users>
<user name="test1" password="1234">
<role name="ROLE1"/>
<role name="ROLE2"/>
<role name="ROLE3"/>
</user>
<user name="test2" password="5678">
<role name="ROLE1"/>
<role name="ROLE4"/>
</user>
</users>
</component>
...
</components>
Для корректной работы фильтр аутентификации Form Authentication
требует активации поддержки сессий. При этом сессии используются для сохранения начального запроса от клиентов не прошедших аутентификацию.
Авторизация
Для декларативной авторизации используется элемент <security-constraints>
в конфигурационном файле:
<security-constraints>
<constraint url=".*" role="ROLE1"/>
<constraint url="/servlet" role="ROLE2"/>
</security-constraints>
Авторизация может осуществляется программно. Для этой цели классы fastcgi::Request
и fastcgi::HttpRequest
предоставляют методы:
std::shared_ptr<security::Subject> Request::getSubject() const;
bool Request::isUserInRole(const std::string& roleName) const;
template<class T> bool Request::isUserInRole(const std::string &roleName) {
return getSubject()->hasPrincipal<T>(roleName);
}
Если клиент не аутентифицирован, метод getSubject()
возвращает указатель на объект-«аноним» с пустым множеством ролей и возвращающим true
при вызове следующего метода:
bool security::Subject::isAnonymous() const;
Сессии
Контейнер предоставляет реализацию Simple Session Manager
. Для его активации в конфигурационный файл нужно добавить следующее:
<modules>
<module name="manager" path="/usr/local/lib64/fastcgi3/fastcgi3-session-manager.so"/>
...
</modules>
<components>
<component name="session-manager" type="manager:simple-session-manager">
<logger>daemon-logger</logger>
</component>
...
</components>
<session attach="true" component="session-manager">
<timeout>30</timeout>
</session>
Для доступа к текущей сессии класс fastcgi::Request
предоставляет метод:
std::shared_ptr<Session> 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 <chrono>
%>
<%
auto p = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(p);
%>
<html>
<head>
<title>Time Sample</title>
</head>
<body>
<h1>Time Sample</h1>
<p><%= std::ctime(&t) %></p>
</body>
</html>
Подробное описание директив доступно на GitHub проекта.
Автор: lpre