chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Request;
use App\Framework\Http\Status;
#[MiddlewarePriorityAttribute(MiddlewarePriority::AUTHENTICATION)]
final readonly class AuthMiddleware implements HttpMiddleware
{
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
/*if (str_starts_with($context->request->path, '/admin')) {
return new HttpResponse(Status::FORBIDDEN, body: 'Zugriff verweigert');
}*/
return $next($context);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Request;
use App\Framework\Http\Response;
use App\Framework\Http\ResponseManipulator;
#[MiddlewarePriorityAttribute(MiddlewarePriority::CORS)]
final readonly class CORSMiddleware implements HttpMiddleware
{
public function __construct(
private ResponseManipulator $manipulator
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
// Debug-Ausgabe vor der Verarbeitung
#var_dump('CORS Middleware Start - Kontext hat Response: ' . ($context->hasResponse() ? 'ja' : 'nein'));
// Nächste Middleware aufrufen UND den Kontext mit der Response behalten
$resultContext = $next($context);
// Debug-Ausgabe nach der Verarbeitung
#var_dump('CORS Middleware End - Kontext hat Response: ' . ($resultContext->hasResponse() ? 'ja' : 'nein'));
// Wenn eine Response vorhanden ist, CORS-Header hinzufügen
if ($resultContext->hasResponse()) {
$response = $resultContext->response;
// Aktualisierte Headers erstellen
$updatedHeaders = $response->headers->with('Access-Control-Allow-Origin', '*');
$updatedHeaders = $updatedHeaders->with('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$updatedHeaders = $updatedHeaders->with('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Neue Response mit aktualisierten Headers erstellen
$updatedResponse = $this->manipulator->withHeaders($response, $updatedHeaders);
// Kontext mit der aktualisierten Response zurückgeben
return $resultContext->withResponse($updatedResponse);
}
// Wenn keine Response vorhanden ist, Kontext unverändert zurückgeben
return $resultContext;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\DI\DefaultContainer;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Request;
use App\Framework\Http\Response;
final readonly class ControllerRequestMiddleware implements HttpMiddleware
{
public function __construct(
#private Container $container
) {}
/**
* @param RequestStateManager $stateManager
* @inheritDoc
*/
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
return $next($context);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\Method;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Session\Session;
#[MiddlewarePriorityAttribute(MiddlewarePriority::SECURITY, -150)] // Push after Session Creation
final readonly class CsrfMiddleware implements HttpMiddleware
{
public function __construct(
private Session $session,
){}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
if (!$this->session->isStarted()) {
throw new \RuntimeException('Session must be started before CSRF validation');
}
if($request->method === Method::POST) {
// FormId ist jetzt immer vorhanden durch automatische Generierung
$formId = $request->parsedBody->get('_form_id');
$token = $request->parsedBody->get('_token');
if (!$formId || !$token) {
throw new \Exception('CSRF-Daten fehlen');
}
$valid = $this->session->csrf->validateToken($formId, $token);
if(!$valid) {
throw new \Exception('CSRF-Token ungültig');
}
}
return $next($context);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Framework\Http\Middlewares;
use App\Framework\ErrorHandling\ErrorHandler;
use App\Framework\ErrorHandling\ExceptionConverter;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\RequestIdGenerator;
use App\Framework\Logging\DefaultLogger;
use App\Framework\Validation\Exceptions\ValidationException;
#[MiddlewarePriorityAttribute(MiddlewarePriority::ERROR_HANDLING)]
final readonly class ExceptionHandlingMiddleware implements HttpMiddleware
{
public function __construct(
private DefaultLogger $logger,
private RequestIdGenerator $requestIdGenerator,
private ErrorHandler $errorHandler,
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
try {
return $next($context);
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
$response = $this->errorHandler->createHttpResponse($e, $context);
return $context->withResponse($response);
// Detaillierte Fehlerinformationen loggen
$this->logger->error('Unbehandelte Exception in Middleware-Chain', [
'exception' => $e,
'path' => $context->request->path,
'method' => $context->request->method->value,
]);
// Request-ID für die Antwort holen
$requestId = $this->requestIdGenerator->generate();
// Passende HTTP-Response basierend auf der Exception
$status = ExceptionConverter::getStatusFromException($e);
// Debug-Modus prüfen
$isDebug = filter_var($_ENV['APP_DEBUG'] ?? false, FILTER_VALIDATE_BOOLEAN);
// Response-Body erstellen
$body = ExceptionConverter::getResponseBody(
$e,
$isDebug,
$requestId->getId()
);
$response = new HttpResponse(
status: $status,
body: json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
return $context->withResponse($response);
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\Method;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use Psr\Log\LoggerInterface;
#[MiddlewarePriorityAttribute(MiddlewarePriority::SECURITY, -140)] // Nach CSRF, vor anderen Validierungen
final readonly class HoneypotMiddleware implements HttpMiddleware
{
public function __construct(
private ?LoggerInterface $logger = null
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
if ($request->method === Method::POST) {
$this->validateHoneypot($request);
}
return $next($context);
}
private function validateHoneypot($request): void
{
$honeypotName = $request->parsedBody->get('_honeypot_name');
if (!$honeypotName) {
$this->logSuspiciousActivity('Missing honeypot name', $request);
throw new \Exception('Spam-Schutz ausgelöst');
}
$honeypotValue = $request->parsedBody->get($honeypotName);
// Honeypot wurde ausgefüllt = Bot erkannt
if (!empty($honeypotValue)) {
$this->logSuspiciousActivity("Honeypot filled: {$honeypotName} = {$honeypotValue}", $request);
throw new \Exception('Spam-Schutz ausgelöst');
}
// Zusätzliche Zeit-basierte Validierung (optional)
$this->validateSubmissionTime($request);
}
private function validateSubmissionTime($request): void
{
// Formulare, die zu schnell abgeschickt werden, sind verdächtig
$startTime = $request->parsedBody->get('_form_start_time');
if ($startTime && (time() - (int)$startTime) < 2) {
$this->logSuspiciousActivity('Form submitted too quickly', $request);
throw new \Exception('Spam-Schutz ausgelöst');
}
}
private function logSuspiciousActivity(string $reason, $request): void
{
if ($this->logger) {
$this->logger->warning('Honeypot triggered', [
'reason' => $reason,
'ip' => $request->getClientIp(),
'user_agent' => $request->headers->get('User-Agent') ?? 'unknown',
'url' => $request->uri
]);
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Logging\DefaultLogger;
#[MiddlewarePriorityAttribute(MiddlewarePriority::LOGGING)]
final readonly class LoggingMiddleware implements HttpMiddleware
{
public function __construct(
private DefaultLogger $logger
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$start = microtime(true);
// Request-Informationen loggen
$this->logger->info("HTTP Request", [
'path' => $context->request->path,
'method' => $context->request->method->value,
'query' => $context->request->queryParams,
]);
// Nachfolgende Middlewares aufrufen
$resultContext = $next($context);
// Dauer berechnen
$duration = number_format((microtime(true) - $start) * 1000, 2);
// Status bestimmen
$status = $resultContext->hasResponse()
? $resultContext->response->status->value
: 'keine Response';
// Response-Informationen loggen - Warnung bei Fehlercodes
if ($resultContext->hasResponse() && $resultContext->response->status->value >= 400) {
$this->logger->warning("HTTP Response", [
'status' => $status,
'duration_ms' => $duration,
]);
} else {
$this->logger->info("HTTP Response", [
'status' => $status,
'duration_ms' => $duration,
]);
}
return $resultContext;
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Request;
use App\Framework\Http\Status;
use Predis\Client as RedisClient;
/**
* Middleware zum Schutz vor zu vielen Anfragen von einer IP-Adresse.
* Verwendet Redis zum Zählen der Anfragen und implementiert ein Sliding Window Limiting.
*/
#[MiddlewarePriorityAttribute(MiddlewarePriority::SECURITY)]
final class RateLimitingMiddleware implements XHttpMiddleware
{
private const string REDIS_KEY_PREFIX = 'rate_limit:';
/**
* @param RedisClient $redis Redis-Client zum Speichern der Zähler
* @param int $defaultLimit Standardmäßige Anzahl an erlaubten Anfragen im Zeitfenster
* @param int $windowSeconds Länge des Zeitfensters in Sekunden
* @param array<string, array{limit: int, window: int}> $pathLimits Spezifische Limits für bestimmte Pfade
*/
public function __construct(
private readonly RedisClient $redis,
private readonly int $defaultLimit = 60,
private readonly int $windowSeconds = 60,
private readonly array $pathLimits = []
) {
}
public function __invoke(MiddlewareContext $context, callable $next): MiddlewareContext
{
$clientIp = $this->getClientIp($context->request);
$path = $context->request->path;
// Limits für den aktuellen Pfad ermitteln
[$limit, $window] = $this->getLimitsForPath($path);
// Redis-Schlüssel für diese IP und diesen Pfad
$redisKey = $this->getRedisKey($clientIp, $path);
// Anfrage zählen und prüfen
$requestCount = $this->countRequest($redisKey, $window);
// Rate-Limiting-Header zur Antwort hinzufügen
$remaining = max(0, $limit - $requestCount);
$context = $context->withResponseHeader('X-RateLimit-Limit', (string)$limit)
->withResponseHeader('X-RateLimit-Remaining', (string)$remaining)
->withResponseHeader('X-RateLimit-Reset', (string)$this->redis->ttl($redisKey));
// Wenn das Limit überschritten wurde, 429-Antwort zurückgeben
if ($requestCount > $limit) {
$response = new HttpResponse(
Status::TOO_MANY_REQUESTS,
new Headers(['Content-Type' => 'application/json'])
);
// Retry-After Header hinzufügen
$retryAfter = $this->redis->ttl($redisKey);
$response = $response->withHeader('Retry-After', (string)$retryAfter);
// JSON-Antwort mit Fehlermeldung
$errorResponse = [
'error' => 'Zu viele Anfragen',
'message' => 'Sie haben das Limit für Anfragen überschritten. Bitte versuchen Sie es später erneut.',
'retry_after' => $retryAfter
];
$response->getBody()->write(json_encode($errorResponse));
return $context->withResponse($response);
}
// Anfrage weiterleiten
return $next($context);
}
/**
* Zählt eine Anfrage und gibt die aktuelle Anzahl zurück.
*
* @param string $key Der Redis-Schlüssel für den Counter
* @param int $window Das Zeitfenster in Sekunden
* @return int Die aktuelle Anzahl an Anfragen im Zeitfenster
*/
private function countRequest(string $key, int $window): int
{
// Aktuelle Zeit als Score (für Sorted Set)
$now = time();
// Alten Einträge entfernen (älter als das Zeitfenster)
$this->redis->zremrangebyscore($key, 0, $now - $window);
// Neue Anfrage hinzufügen
$this->redis->zadd($key, [$now => $now]);
// Gesamtanzahl der Anfragen im Zeitfenster ermitteln
$count = $this->redis->zcard($key);
// TTL für den Key setzen (Aufräumen)
$this->redis->expire($key, $window);
return (int)$count;
}
/**
* Ermittelt die Limits für einen bestimmten Pfad.
*
* @param string $path Der Pfad
* @return array{int, int} [limit, window]
*/
private function getLimitsForPath(string $path): array
{
// Prüfen auf spezifische Pfad-Limits
foreach ($this->pathLimits as $pattern => $config) {
if (preg_match($pattern, $path)) {
return [$config['limit'], $config['window']];
}
}
// Standardwerte zurückgeben
return [$this->defaultLimit, $this->windowSeconds];
}
/**
* Generiert den Redis-Schlüssel für eine IP und einen Pfad.
*/
private function getRedisKey(string $ip, string $path): string
{
// Für allgemeines Rate-Limiting alle Pfade gleich behandeln
if (empty($this->pathLimits)) {
return self::REDIS_KEY_PREFIX . $ip;
}
// Für pfad-spezifisches Rate-Limiting einen pfad-spezifischen Schlüssel erstellen
$pathHash = md5($path);
return self::REDIS_KEY_PREFIX . "{$ip}:{$pathHash}";
}
/**
* Ermittelt die IP-Adresse des Clients unter Berücksichtigung von Proxies.
*/
private function getClientIp(Request $request): string
{
$headers = [
'X-Forwarded-For',
'X-Real-IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_CLIENT_IP',
'REMOTE_ADDR'
];
foreach ($headers as $header) {
$value = $request->headers->get($header)[0] ?? null;
if ($value) {
// Bei mehreren IPs (X-Forwarded-For kann mehrere enthalten) die erste nehmen
$ips = explode(',', $value);
return trim($ips[0]);
}
}
return '0.0.0.0';
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\ResponseManipulator;
#[MiddlewarePriorityAttribute(MiddlewarePriority::SECURITY)]
final readonly class RemovePoweredByMiddleware implements HttpMiddleware
{
public function __construct(
private ResponseManipulator $manipulator
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
// Nächste Middleware aufrufen
$resultContext = $next($context);
// Wenn eine Response vorhanden ist, X-Powered-By Header entfernen
if ($resultContext->hasResponse()) {
$response = $resultContext->response;
$updatedHeaders = $response->headers;
// X-Powered-By Header entfernen, falls vorhanden
if ($updatedHeaders->has('X-Powered-By')) {
$updatedHeaders = $updatedHeaders->without('X-Powered-By');
// Neue Response mit aktualisierten Headers erstellen
$updatedResponse = $this->manipulator->withHeaders($response, $updatedHeaders);
return $resultContext->withResponse($updatedResponse);
}
}
return $resultContext;
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\RequestIdGenerator;
use App\Framework\Http\ResponseManipulator;
/**
* Middleware zur Verarbeitung von Request-IDs.
* Generiert eine HMAC-signierte Request-ID und fügt sie als Header hinzu.
*/
#[MiddlewarePriorityAttribute(MiddlewarePriority::VERY_EARLY)]
final readonly class RequestIdMiddleware implements HttpMiddleware
{
public function __construct(
private RequestIdGenerator $requestIdGenerator,
private ResponseManipulator $manipulator
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
// Request-ID generieren/validieren
$requestId = $this->requestIdGenerator->generate();
// Request-ID an den weiteren Verarbeitungskontext weitergeben
$resultContext = $next($context);
// Request-ID in die Response-Header setzen, falls eine Response vorhanden ist
if ($resultContext->hasResponse()) {
$response = $resultContext->response;
$headers = $response->headers->with(
$this->requestIdGenerator::getHeaderName(),
$requestId->toString()
);
$updatedResponse = $this->manipulator->withHeaders($response, $headers);
return $resultContext->withResponse($updatedResponse);
}
return $resultContext;
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Request;
use App\Framework\Http\Response;
use App\Framework\Logging\Logger;
#[MiddlewarePriorityAttribute(MiddlewarePriority::LOGGING)]
final readonly class RequestLoggingMiddleware implements HttpMiddleware
{
public function __construct(
private Logger $logger
){}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$startTime = microtime(true);
$request = $context->request;
// Anfrage loggen
$path = $request->path;
$method = $request->method;
// Nächste Middleware aufrufen
$resultContext = $next($context);
// Antwort loggen
$endTime = microtime(true);
$duration = round(($endTime - $startTime) * 1000, 2);
// Statuscode nur abrufen, wenn eine Response vorhanden ist
$status = $resultContext->hasResponse()
? $resultContext->response->status->value
: 'keine Response';
// Log-Eintrag erstellen
$logEntry = sprintf(
'[%s] %s %s - %d (%s ms)',
date('Y-m-d H:i:s'),
$method->value,
$path,
$status,
$duration
);
$this->logger->info($logEntry);
// In Log-Datei schreiben...
return $resultContext;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Router\RouteResponder;
#[MiddlewarePriorityAttribute(MiddlewarePriority::ROUTING, -1)]
final readonly class ResponseGeneratorMiddleware implements HttpMiddleware
{
public function __construct(
private RouteResponder $responder,
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
if($stateManager->has('controllerResult')) {
$originalResponse = $this->responder->respond($stateManager->get('controllerResult'));
// Kontext mit der generierten Response aktualisieren
$updatedContext = $context->withResponse($originalResponse);
$resultContext = $next($updatedContext);
if(!$resultContext->hasResponse() && $originalResponse !== null) {
return $resultContext->withResponse($originalResponse);
}
return $resultContext;
}
/*// Prüfen, ob es sich um einen angereicherten Request handelt
if ($request instanceof EnrichedRequest && $request->result) {
// Response aus dem Controller-Ergebnis generieren
$originalResponse = $this->responder->respond($request->result);
// Kontext mit der generierten Response aktualisieren
$updatedContext = $context->withResponse($originalResponse);
$resultContext = $next($updatedContext);
if(!$resultContext->hasResponse() && $originalResponse !== null) {
return $resultContext->withResponse($originalResponse);
}
return $resultContext;
}*/
// Immer die nächste Middleware aufrufen, auch wenn bereits eine Response generiert wurde
return $next($context);
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Auth\Auth;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Meta\MetaData;
use App\Framework\Router\ActionResult;
use App\Framework\Router\Exception\RouteNotFound;
use App\Framework\Router\HttpRouter;
use App\Framework\Router\Result\ContentNegotiationResult;
use App\Framework\Router\Result\JsonResult;
use App\Framework\Router\Result\Redirect;
use App\Framework\Router\Result\ViewResult;
use App\Framework\Router\RouteDispatcher;
#[MiddlewarePriorityAttribute(MiddlewarePriority::ROUTING)]
final readonly class RoutingMiddleware implements HttpMiddleware
{
public function __construct(
private HttpRouter $router,
private RouteDispatcher $dispatcher,
) {}
/**
* Invokes the middleware logic to process a request and response through the provided context.
* It enriches the request based on the matched route and dispatches the corresponding controller logic.
* Then, it updates the context with the enriched request and passes it to the next middleware.
*
* @param MiddlewareContext $context The context containing the request and response.
* @param callable $next The next middleware to invoke in the chain.
* @param RequestStateManager $stateManager
* @return MiddlewareContext The updated middleware context after processing.
* @throws RouteNotFound If the route cannot be matched in the current context.
*/
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$request = $context->request;
$routeContext = $this->router->match($request);
if (!$routeContext->isSuccess()) {
throw new RouteNotFound($routeContext->path);
}
if (in_array(Auth::class, $routeContext->match->route->attributes)) {
#debug($request->server->getClientIp());
$wireguardIp = '172.20.0.1';
$ip = $request->server->getClientIp();
if ($ip->value !== $wireguardIp) {
throw new RouteNotFound($routeContext->path);
}
}
// Controller-Logik ausführen
$result = $this->dispatcher->dispatch($routeContext);
if ($result instanceof ContentNegotiationResult) {
$result = $this->contentNegotiation($result);
}
$stateManager->set('controllerResult', $result);
// Erstellen eines angereicherten Requests mit dem Controller-Ergebnis
#$enrichedRequest = new EnrichedRequest($request, $result);
// Kontext mit dem angereicherten Request aktualisieren
#$updatedContext = new MiddlewareContext($enrichedRequest, $context->response);
$updatedContext = $context;
// Nächste Middleware aufrufen
return $next($updatedContext);
}
private function contentNegotiation(ContentNegotiationResult $response): ActionResult
{
$isJson = true;
if ($isJson) {
return new JsonResult($response->jsonPayload);
}
if ($response->redirectTo !== null) {
return new Redirect($response->redirectTo);
}
if ($response->viewTemplate !== null) {
return new ViewResult(template: $response->viewTemplate,metaData: new MetaData(''), data: $response->viewData);
}
// Optional: Fallback, z.B. Fehler- oder Defaultseite
return new JsonResult(['message' => 'Not found']);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
final readonly class SecurityHeaderConfig
{
public function __construct(
public string $hstsHeader = 'max-age=63072000; includeSubDomains; preload',
public string $frameOptions = 'DENY',
public string $referrerPolicy = 'strict-origin-when-cross-origin',
public string $contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; child-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'",
public string $permissionsPolicy = 'geolocation=(), microphone=(), camera=()',
public string $crossOriginEmbedderPolicy = 'require-corp',
public string $crossOriginOpenerPolicy = 'same-origin',
public string $crossOriginResourcePolicy = 'same-origin',
public bool $enableInDevelopment = false
) {}
/**
* Erstellt eine Konfiguration für Entwicklungsumgebung mit weniger restriktiven Einstellungen
*/
public static function forDevelopment(): self
{
return new self(
hstsHeader: 'max-age=3600', // Kürzere HSTS-Zeit für Development
frameOptions: 'SAMEORIGIN', // Weniger restriktiv für Development-Tools
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: blob:; font-src 'self' https:; connect-src 'self' ws: wss:; media-src 'self'; object-src 'none'; child-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'",
crossOriginEmbedderPolicy: 'unsafe-none',
crossOriginOpenerPolicy: 'unsafe-none',
crossOriginResourcePolicy: 'cross-origin',
enableInDevelopment: true
);
}
/**
* Erstellt eine Konfiguration für Produktionsumgebung mit maximaler Sicherheit
*/
public static function forProduction(): self
{
return new self(
hstsHeader: 'max-age=63072000; includeSubDomains; preload',
frameOptions: 'DENY',
contentSecurityPolicy: "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; child-src 'none'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; upgrade-insecure-requests",
permissionsPolicy: 'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()',
);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Framework\Http\Middlewares;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\ResponseManipulator;
#[MiddlewarePriorityAttribute(MiddlewarePriority::SECURITY)]
final readonly class SecurityHeaderMiddleware implements HttpMiddleware
{
public function __construct(
private ResponseManipulator $manipulator,
private SecurityHeaderConfig $config = new SecurityHeaderConfig()
) {}
public function __invoke(MiddlewareContext $context, callable $next, RequestStateManager $stateManager): MiddlewareContext
{
$this->removePoweredByHeader();
// Nächste Middleware aufrufen
$resultContext = $next($context);
// Wenn eine Response vorhanden ist, Security-Header hinzufügen
if ($resultContext->hasResponse()) {
$response = $resultContext->response;
$updatedHeaders = $response->headers;
// Alle Security-Header hinzufügen
foreach ($this->getSecurityHeaders() as $name => $value) {
if ($this->shouldAddHeader($name, $updatedHeaders)) {
$updatedHeaders = $updatedHeaders->with($name, $value);
}
}
// Neue Response mit aktualisierten Headers erstellen
$updatedResponse = $this->manipulator->withHeaders($response, $updatedHeaders);
return $resultContext->withResponse($updatedResponse);
}
return $resultContext;
}
private function getSecurityHeaders(): array
{
return [
'Strict-Transport-Security' => $this->config->hstsHeader,
'X-Frame-Options' => $this->config->frameOptions,
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => $this->config->referrerPolicy,
'Content-Security-Policy' => $this->config->contentSecurityPolicy,
'Permissions-Policy' => $this->config->permissionsPolicy,
'X-Permitted-Cross-Domain-Policies' => 'none',
'Cross-Origin-Embedder-Policy' => $this->config->crossOriginEmbedderPolicy,
'Cross-Origin-Opener-Policy' => $this->config->crossOriginOpenerPolicy,
'Cross-Origin-Resource-Policy' => $this->config->crossOriginResourcePolicy,
];
}
private function shouldAddHeader(string $headerName, $currentHeaders): bool
{
// Header nur hinzufügen, wenn er noch nicht gesetzt ist
return !$currentHeaders->has($headerName);
}
private function removePoweredByHeader():void
{
if (!headers_sent()) {
header_remove('X-Powered-By');
header_remove('Server');
}
}
}