//! \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