Фреймворк 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<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 с заданием списка пользователей непосредственно в конфигурационном файле:

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 проекта.

ссылка на оригинал статьи https://habrahabr.ru/post/280814/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *