//! \addtogroup HTTPServer //! \~\file pihttpserversessionauth.h //! \brief HTTP server with bearer session management //! \~english HTTP server with bearer session management //! \~russian HTTP-сервер с управлением bearer-сессиями /* PIP - Platform Independent Primitives HTTP server with bearer session management Andrey Bychkov andrey@signalmodelling.ru This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . */ #ifndef PIHTTPSERVERSESSIONAUTH_H #define PIHTTPSERVERSESSIONAUTH_H #include "pihttpserverbearerauth.h" #include "pimap.h" #include "pimutex.h" #include "pisystemtime.h" //! \~\ingroup HTTPServer //! \~\brief //! \~english Protected HTTP server that manages bearer sessions in memory. It does not store //! user accounts: credential verification is delegated to \a checkCredentials(), implemented //! by a client subclass; this class only stores issued tokens and resolves them to \c AuthInfo. //! The \c POST /api/login route authenticates a user and replies with a token, the //! \c POST /api/logout route revokes the token of the request (the route paths are configurable //! with \a setLoginPath() / \a setLogoutPath()). Application routes must be registered with //! \a registerProtectedPath(); their handlers receive the \c AuthInfo of the token owner. //! \~russian Защищенный HTTP-сервер, управляющий bearer-сессиями в памяти. Он не хранит учетные //! записи пользователей: проверка учетных данных делегируется \a checkCredentials(), реализуемому //! клиентским наследником; этот класс хранит только выданные токены и разрешает их в \c AuthInfo. //! Маршрут \c POST /api/login аутентифицирует пользователя и отвечает токеном, маршрут //! \c POST /api/logout отзывает токен запроса (пути маршрутов настраиваются через //! \a setLoginPath() / \a setLogoutPath()). Маршруты приложения регистрируются через //! \a registerProtectedPath(); их обработчики получают \c AuthInfo владельца токена. //! \note //! \~english \a checkCredentials() is called without the internal class lock held, so an //! override may take its own locks and use its own user storage. \c PIMutex is recursive. //! \~russian \a checkCredentials() вызывается без захваченного внутреннего замка класса, //! поэтому переопределение может брать собственные замки и использовать свое хранилище //! пользователей. \c PIMutex рекурсивен. //! \note //! \~english The class is designed for inheritance: subclasses must implement //! \a checkCredentials() and may override \a checkToken() to extend token validation, //! \a issueToken() to customize session issuing and \a handleLogin()/\a handleLogout() to //! customize the login/logout route replies. //! \~russian Класс рассчитан на наследование: подклассы должны реализовать //! \a checkCredentials() и могут переопределять \a checkToken() для расширения проверки токенов, //! \a issueToken() для кастомизации выдачи сессий и \a handleLogin()/\a handleLogout() для //! кастомизации ответов маршрутов login/logout. class PIP_HTTP_SERVER_EXPORT PIHTTPServerSessionAuth: public PIHTTPServerBearerAuth { PIOBJECT_SUBCLASS(PIHTTPServerSessionAuth, PIHTTPServerBearerAuth) public: //! \~english Token data generator: receives the requested length in bytes and returns //! that many random bytes; the token is passed to the client as a hex string. //! \~russian Генератор данных токена: получает запрашиваемую длину в байтах и возвращает //! столько случайных байтов; токен передается клиенту как hex-строка. using TokenGenerator = std::function; //! \~english Creates a session server and registers the \c POST /api/login and //! \c POST /api/logout routes. //! \~russian Создает сервер сессий и регистрирует маршруты \c POST /api/login и //! \c POST /api/logout. PIHTTPServerSessionAuth(); //! \~english Destroys the server. //! \~russian Удаляет сервер. virtual ~PIHTTPServerSessionAuth() = default; //! \~english Sets the token data generator. When no generator is set, a built-in //! generator based on \c PIDigest and the current time is used; for production use //! a cryptographically secure generator is recommended. The generator must be //! collision-resistant: \a issueToken() refuses to issue a token that is already used //! by an active session (a collision makes the login reply \c 500 instead of silently //! merging two sessions). //! \~russian Устанавливает генератор данных токена. Когда генератор не установлен, //! используется встроенный генератор на основе \c PIDigest и текущего времени; //! для production рекомендуется криптографически стойкий генератор. Генератор должен //! быть устойчив к коллизиям: \a issueToken() отказывается выдавать токен, уже занятый //! активной сессией (коллизия приводит к ответу логина \c 500 вместо молчаливого //! объединения двух сессий). void setTokenGenerator(TokenGenerator g); //! \~english Sets the session time-to-live: an expired token is denied and removed //! lazily on the next request. A null time means tokens live until logout. //! \~russian Устанавливает время жизни сессии: истекший токен отклоняется и удаляется //! лениво при следующем запросе. Нулевое время означает, что токены живут до logout. void setTokenTtl(PISystemTime ttl); //! \~english Returns the session time-to-live set by \a setTokenTtl(). //! \~russian Возвращает время жизни сессии, установленное \a setTokenTtl(). PISystemTime tokenTtl() const; //! \~english Returns whether a session time-to-live is set. //! \~russian Возвращает, установлено ли время жизни сессии. bool hasTokenTtl() const; //! \~english Enables the idle timeout: a session is denied and removed after "t" without a //! successful request, and every successful request extends it. A null time disables the //! idle timeout (the default), so sessions are not extended and live until \a setTokenTtl() //! expires them or they are revoked. When both are set, the session ends at whichever //! deadline comes first. //! \~russian Включает idle-таймаут: сессия отклоняется и удаляется после "t" без успешного //! запроса, а каждый успешный запрос продлевает её. Нулевое время отключает idle-таймаут //! (по умолчанию): сессии не продлеваются и живут, пока их не ограничит \a setTokenTtl() //! или не отзовут. Если заданы оба, сессия завершается по более раннему сроку. void setSessionIdleTimeout(PISystemTime t); //! \~english Returns the idle timeout set by \a setSessionIdleTimeout(). //! \~russian Возвращает idle-таймаут, установленный \a setSessionIdleTimeout(). PISystemTime sessionIdleTimeout() const; //! \~english Returns whether the idle timeout is enabled. //! \~russian Возвращает, включен ли idle-таймаут. bool hasSessionIdleTimeout() const; //! \~english Changes the route path used for login. The old route is unregistered and the //! new one registered immediately; call it before \a listen() to avoid racing with requests. //! \~russian Изменяет путь маршрута логина. Старый маршрут снимается, новый регистрируется //! сразу; вызывайте до \a listen(), чтобы избежать гонки с запросами. void setLoginPath(const PIString & p); //! \~english Changes the route path used for logout. The old route is unregistered and the //! new one registered immediately; call it before \a listen() to avoid racing with requests. //! \~russian Изменяет путь маршрута выхода. Старый маршрут снимается, новый регистрируется //! сразу; вызывайте до \a listen(), чтобы избежать гонки с запросами. void setLogoutPath(const PIString & p); //! \~english Returns the login route path (default \c "/api/login"). //! \~russian Возвращает путь маршрута логина (по умолчанию \c "/api/login"). const PIString & loginPath() const { return login_path; } //! \~english Returns the logout route path (default \c "/api/logout"). //! \~russian Возвращает путь маршрута выхода (по умолчанию \c "/api/logout"). const PIString & logoutPath() const { return logout_path; } protected: //! \~english Stored session data. //! \~russian Хранимые данные сессии. struct SessionRec { bool isInvalid() const { return created.isNull(); } //! \~english Id of the session owner. //! \~russian Идентификатор владельца сессии. int user_id = 0; //! \~english Session creation time. //! \~russian Время создания сессии. PISystemTime created; //! \~english Time of the last successful \a checkToken(). Updated only while the //! idle timeout is enabled; used as the base of the idle timeout. //! \~russian Время последнего успешного \a checkToken(). Обновляется только при //! включенном idle-таймауте; служит базой idle-таймаута. PISystemTime last_used; }; //! \~english Resolves a Bearer token to the \c AuthInfo of its owner, applying the //! session time-to-live (an expired session is removed). Returns an \c AuthInfo with //! \c authorized \c false for unknown or expired tokens. Called for every protected //! request; override to extend token validation (e.g. a revocation list). //! \~russian Разрешает Bearer-токен в \c AuthInfo его владельца, применяя время жизни //! сессии (истекшая сессия удаляется). Для неизвестного или истекшего токена возвращает //! \c AuthInfo с \c authorized \c false. Вызывается для каждого защищенного запроса; //! переопределяется для расширения проверки токенов (например, список отзыва). virtual PIHTTP::AuthInfo checkToken(const PIString & token); //! \~english Issues a new session token for the user with id "user_id" using the token //! generator and stores the session. Returns an empty string when the token generator //! produces no data. //! \~russian Выдает новый токен сессии для пользователя с идентификатором "user_id" //! генератором токенов и сохраняет сессию. Возвращает пустую строку, когда генератор //! токенов не выдал данных. virtual PIString issueToken(int user_id); //! \~english Verifies the credentials submitted at the login route and resolves them to an //! \c AuthInfo (with the application user id). Must be implemented by a client subclass that //! owns the user storage. The access is denied when \c AuthInfo::authorized is \c false. //! Called without the internal class lock held. //! \~russian Проверяет учетные данные, переданные в маршрут логина, и разрешает их в //! \c AuthInfo (с идентификатором пользователя приложения). Должен быть реализован //! клиентским наследником, владеющим хранилищем пользователей. Доступ запрещается, когда //! в \c AuthInfo::authorized \c false. Вызывается без захваченного внутреннего замка класса. virtual PIHTTP::AuthInfo checkCredentials(const PIString & login, const PIString & password) = 0; //! \~english \c POST /api/login route handler: verifies credentials and issues a token. //! Override to customize the login reply. //! \~russian Обработчик маршрута \c POST /api/login: проверяет учетные данные и //! выдает токен. Переопределяется для кастомизации ответа логина. virtual PIHTTP::MessageMutable handleLogin(const PIHTTP::MessageConst & request); //! \~english \c POST /api/logout route handler: revokes the token of the request. //! Override to customize the logout reply. //! \~russian Обработчик маршрута \c POST /api/logout: отзывает токен запроса. //! Переопределяется для кастомизации ответа выхода. virtual PIHTTP::MessageMutable handleLogout(const PIHTTP::MessageConst & request); //! \~english Revokes a single session token. Returns \c false when the token is unknown. //! \~russian Отзывает одну сессию по токену. Возвращает \c false, если токен неизвестен. bool revokeToken(const PIString & token); //! \~english Revokes all sessions of the user with id "user_id". Call it from a subclass //! when a user is removed. //! \~russian Отзывает все сессии пользователя с идентификатором "user_id". Вызывайте из //! наследника при удалении пользователя. void revokeUserTokens(int user_id); //! \~english Returns the internal class lock. Guard any direct access to the session table //! with it. //! \~russian Возвращает внутренний замок класса. Любое прямое обращение к таблице сессий //! защищайте им. PIMutex & lock() { return mutex; } //! \~english Returns the session table (token to session record). Guard with \a lock(). //! \~russian Возвращает таблицу сессий (токен на запись сессии). Защищайте \a lock(). PIMap & sessionTable() { return sessions; } //! \~english Removes sessions expired by \a setTokenTtl() or \a setSessionIdleTimeout(). //! Called opportunistically on token issuing to bound the session table; subclasses may //! also call it periodically (e.g. from a timer thread). Acquires the internal lock itself. //! \~russian Удаляет сессии, истекшие по \a setTokenTtl() или \a setSessionIdleTimeout(). //! Вызывается оппортунистически при выдаче токена, чтобы ограничить таблицу сессий; //! наследники также могут вызывать её периодически (например, из потока таймера). //! Захватывает внутренний замок самостоятельно. void cleanupExpired(); private: //! \~english Built-in token data generator based on \c PIDigest and the current time. //! \~russian Встроенный генератор данных токена на основе \c PIDigest и текущего времени. PIByteArray defaultTokenData(uint len); //! \~english Registers the login route at the current \a login_path. //! \~russian Регистрирует маршрут логина по текущему \a login_path. void registerLoginRoute(); //! \~english Registers the logout route at the current \a logout_path. //! \~russian Регистрирует маршрут выхода по текущему \a logout_path. void registerLogoutRoute(); //! \~english Same as \a cleanupExpired() but assumes \a mutex is already held. //! \~russian То же, что \a cleanupExpired(), но предполагает, что \a mutex уже захвачен. void cleanupExpiredLocked(); mutable PIMutex mutex; PIMap sessions; PISystemTime token_ttl; PISystemTime session_idle_timeout; uchar token_length; TokenGenerator token_generator; PIString login_path; PIString logout_path; }; #endif // PIHTTPSERVERSESSIONAUTH_H