Files
EMS-ESP32/src/ESP32React/SecurityManager.h
T
2026-10-15 21:21:50 +02:00

143 lines
5.0 KiB
C++

#ifndef SecurityManager_h
#define SecurityManager_h
#include "ArduinoJsonJWT.h"
#include <ESPAsyncWebServer.h>
#include <AsyncJson.h>
#include <memory>
#include <vector>
#define ACCESS_TOKEN_PARAMETER "access_token"
#define AUTHORIZATION_HEADER "Authorization"
#define AUTHORIZATION_HEADER_PREFIX "Bearer "
#define AUTHORIZATION_HEADER_PREFIX_LEN 7
class User {
public:
String username;
String password;
bool admin;
public:
User(String username, String password, bool admin)
: username(std::move(username))
, password(std::move(password))
, admin(admin) {
}
};
class Authentication {
public:
std::unique_ptr<User> user;
boolean authenticated = false;
public:
explicit Authentication(const User & u)
: user(std::make_unique<User>(u))
, authenticated(true) {
}
Authentication() = default;
};
typedef std::function<boolean(Authentication & authentication)> AuthenticationPredicate;
class AuthenticationPredicates {
public:
static bool NONE_REQUIRED(const Authentication & authentication) {
(void)authentication;
return true;
};
static bool IS_AUTHENTICATED(const Authentication & authentication) {
return authentication.authenticated;
};
static bool IS_ADMIN(const Authentication & authentication) {
return authentication.authenticated && authentication.user->admin;
};
};
class SecurityManager {
public:
virtual ~SecurityManager() = default;
// Authenticate, returning the user if found
virtual Authentication authenticate(const String & username, const String & password) = 0;
// Generate a JWT for the user provided
virtual String generateJWT(const User * user) = 0;
// Check the request header for the Authorization token
virtual Authentication authenticateRequest(AsyncWebServerRequest * request) = 0;
// Json endpoints - default POST. Registered with the shared dispatcher.
void addEndpoint(AsyncWebServer * server,
const String & path,
AuthenticationPredicate predicate,
ArJsonRequestHandlerFunction function,
WebRequestMethodComposite method = HTTP_POST);
// non-Json endpoints - default GET. Registered with the shared dispatcher.
void addEndpoint(AsyncWebServer * server,
const String & path,
AuthenticationPredicate predicate,
ArRequestHandlerFunction function,
WebRequestMethodComposite method = HTTP_GET);
private:
// Single internal route record. Each route holds either a plain or JSON handler.
// The URI matcher uses backward-compatible mode by default (constructed from a
// plain String), which preserves the original library handler's matching semantics
// (e.g. /api also matches /api/boiler/heating).
struct RestRoute {
AsyncURIMatcher uri;
WebRequestMethodComposite method;
AuthenticationPredicate predicate;
ArRequestHandlerFunction plainHandler;
ArJsonRequestHandlerFunction jsonHandler;
bool isJson;
bool requiresAuth; // false when predicate is NONE_REQUIRED, lets dispatchRest skip the JWT parse
};
// Single catch-all handler. canHandle() claims a request only if some registered
// route matches its URI + method, so non-matching URLs (static files, websockets,
// etc.) still fall through to other handlers. handleBody buffers JSON bodies, then
// handleRequest hands off to SecurityManager::dispatchRest for routing + auth.
//
// We can't reuse AsyncCallbackJsonWebHandler here because its canHandle() rejects
// POST/PUT/PATCH without an application/json content-type (so a bodyless POST like
// /rest/resetCustomizations would fall through to a 404), and both canHandle and
// handleRequest are marked final on that class.
class RestCatchAllHandler : public AsyncWebHandler {
public:
explicit RestCatchAllHandler(SecurityManager * owner)
: _owner(owner) {
}
bool canHandle(AsyncWebServerRequest * request) const override;
bool isRequestHandlerTrivial() const override;
void handleBody(AsyncWebServerRequest * request, uint8_t * data, size_t len, size_t index, size_t total) override;
void handleRequest(AsyncWebServerRequest * request) override;
private:
static constexpr size_t kMaxBodySize = 16384;
static bool isJsonContent(AsyncWebServerRequest * request);
SecurityManager * _owner;
};
static bool isPublicPredicate(const AuthenticationPredicate & predicate);
void ensureRestDispatcher(AsyncWebServer * server);
void dispatchRest(AsyncWebServerRequest * request, JsonVariant json);
std::vector<RestRoute> _restRoutes;
bool _restDispatcherInstalled = false;
};
#endif