Files
pip/libs/main/http_server/pihttpserversessionauth.h
andrey 6bf50344fd fix: harden PIHTTPServerSessionAuth session table
- cleanupExpired() drops sessions expired by tokenTtl or sessionIdleTimeout and is
  called opportunistically on issuing a token, bounding the session table; it is
  protected and self-locking so subclasses can run it periodically
- issueToken() refuses a token already used by an active session instead of
  silently merging two sessions (login replies 500 on a generator collision)
- document that the token generator must be collision-resistant
- tests: SessionCleanupOnLogin, ManualCleanup, TokenCollisionRejected
2026-09-23 08:49:10 +03:00

266 lines
18 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! \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 <http://www.gnu.org/licenses/>.
*/
#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<PIByteArray(uint len)>;
//! \~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<PIString, SessionRec> & 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<PIString, SessionRec> sessions;
PISystemTime token_ttl;
PISystemTime session_idle_timeout;
uchar token_length;
TokenGenerator token_generator;
PIString login_path;
PIString logout_path;
};
#endif // PIHTTPSERVERSESSIONAUTH_H