📜 ⬆️ ⬇️

Framework fastcgi container

As part of work on evaluating various ways of implementing Web UI for an existing C ++ application, the Fastcgi Container framework was created based on the Fastcgi Daemon framework, which is well-known on Habré.

While maintaining all the capabilities of the prototype, the main differences of the new framework from it are as follows:


Features and details of the implementation of the prototype were discussed on Habré several times (for example, here ). This article presents the features of the new Fastcgi Container framework.
')

Using C ++ 11


The prototype framework Fastcgi Daemon makes extensive use of the Boost libraries. The derivative framework was decided to be rewritten in C ++ 11, replacing the use of Boost with new standard constructs. An exception was the Boost.Any library, the equivalent of which is not in C ++ 11. The required functionality was added through the use of the MNMLSTC Core library.

Filters


The FastCGI protocol provides for the Filter and Authorizer roles to organize the corresponding functionality, however, common implementations (for example, modules for Apache HTTPD and NGINX) only support the Responder role.

As a result, filter support was added directly to the Fastcgi Container.

In the application, filters are created as an extension of the fastcgi::Filter class:

 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; }; 

Their loading into the container is carried out dynamically, like other components of the application:

 <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> 

Filters can be either global for this application:

 <handlers urlPrefix="/myapp"> <filter> <component name="example_filter_1"/> </filter> ... </handlers> 

or they are intended for processing a group of requests with URLs that match the specified regular expression:

 <handlers urlPrefix="/myapp"> <filter url="/.*"> <component name="example_filter_2"/> </filter> ... </handlers> 

If the container has found more than one filter for the current request, they will be executed in the same order in which they were added to the configuration file.

To transfer control to the next filter in turn (or to the target handler / servlet, if the filter is single or last in the queue), the current filter calls the next function passed to it via the parameter list. To break a chain, a filter can return control without calling the next function.

In general, each filter gets control twice: before transferring control to the next filter or target handler / servlet, and also after the end of the next filter or handler / servlet. The filter can change the response body and / or headers (HTTP headers) both before and after the operation of the target handler / servlet, provided that the body and headers are not yet sent to the client.

Authentication


Authentication is carried out by special filters. The Fastcgi Container includes filters for the following types of authentication: Basic access authentication , Form authentication , and Delegated authentication .

The last of these types delegates the authentication process to the HTTP server, expecting the user ID from it, which is passed as the standard CGI variable REMOTE_USER .

The other two types authenticate using the provided Security Realm .

As in the case of conventional filters, loading into a container is carried out dynamically:

 <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> 

The authentication filter is usually listed first in the filter chain:

 <handlers urlPrefix="/myapp"> <filter url="/.*"> <component name="form_authenticator"/> </filter> ... </handlers> 

For their work, authentication filters require Security Realm , which should be implemented in the application as an extension of the fastcgi::security::Realm class:

 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_; }; 

An example of a simple implementation of Security Realm with specifying a list of users directly in the configuration file:

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; } 

Loading it into a container is similar to loading other components of the application:

 <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> 

For correct operation, the Form Authentication authentication filter requires activation of session support. At the same time, sessions are used to save the initial request from unauthenticated clients.

Authorization


For declarative authorization, the <security-constraints> element in the configuration file is used:

 <security-constraints> <constraint url=".*" role="ROLE1"/> <constraint url="/servlet" role="ROLE2"/> </security-constraints> 

Authorization can be done in software. For this purpose, the fastcgi::Request and fastcgi::HttpRequest classes provide methods:

 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); } 

If the client is not authenticated, the getSubject() method returns a pointer to an “anonymous” object with an empty set of roles and returning true when calling the following method:

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

Sessions


The container provides the implementation of Simple Session Manager . To activate it, add the following to the configuration file:

 <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> 

To access the current session, the fastcgi::Request class provides a method:

 std::shared_ptr<Session> Request::getSession(); 

Among other things, the fastcgi::Session class provides the following methods:

 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 does not have container cluster support, so if you use more than one container on the load balancer, you should configure “sticky sessions”.

In general, the use of sessions should be avoided in systems that are expected to achieve high performance with a large load, since solutions with sessions do not scale well.

Servlets


In addition to the fastcgi::Request and fastcgi::Handler classes, the container provides the wrapper classes fastcgi::HttpRequest , fastcgi::HttpResponse and fastcgi::Servlet .

In the application, you can use both the "old" and "new" classes.

C ++ Server Pages and Page Compiler


Page Compiler is a fork from the POCO project, and is designed to translate HTML pages with special directives (C ++ server pages, CPSP) into servlets.

A simple example 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> 

A detailed description of the directives is available on the GitHub project .

Source: https://habr.com/ru/post/280814/


All Articles