Фреймворк Fastcgi Container

в 7:08, , рубрики: c++, fastcgi, Разработка веб-сайтов

В рамках работы по оценке различных способов реализации 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 проекта.

Автор: lpre

Источник

Поделиться новостью

* - обязательные к заполнению поля