chore: complete update
This commit is contained in:
25
src/Framework/Http/Middlewares/AuthMiddleware.php
Normal file
25
src/Framework/Http/Middlewares/AuthMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
53
src/Framework/Http/Middlewares/CORSMiddleware.php
Normal file
53
src/Framework/Http/Middlewares/CORSMiddleware.php
Normal 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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
47
src/Framework/Http/Middlewares/CsrfMiddleware.php
Normal file
47
src/Framework/Http/Middlewares/CsrfMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/Framework/Http/Middlewares/HoneypotMiddleware.php
Normal file
75
src/Framework/Http/Middlewares/HoneypotMiddleware.php
Normal 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
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Framework/Http/Middlewares/LoggingMiddleware.php
Normal file
56
src/Framework/Http/Middlewares/LoggingMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
172
src/Framework/Http/Middlewares/RateLimitingMiddleware.php
Normal file
172
src/Framework/Http/Middlewares/RateLimitingMiddleware.php
Normal 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';
|
||||
}
|
||||
}
|
||||
46
src/Framework/Http/Middlewares/RemovePoweredByMiddleware.php
Normal file
46
src/Framework/Http/Middlewares/RemovePoweredByMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
48
src/Framework/Http/Middlewares/RequestIdMiddleware.php
Normal file
48
src/Framework/Http/Middlewares/RequestIdMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
59
src/Framework/Http/Middlewares/RequestLoggingMiddleware.php
Normal file
59
src/Framework/Http/Middlewares/RequestLoggingMiddleware.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
99
src/Framework/Http/Middlewares/RoutingMiddleware.php
Normal file
99
src/Framework/Http/Middlewares/RoutingMiddleware.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
48
src/Framework/Http/Middlewares/SecurityHeaderConfig.php
Normal file
48
src/Framework/Http/Middlewares/SecurityHeaderConfig.php
Normal 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=()',
|
||||
);
|
||||
}
|
||||
}
|
||||
78
src/Framework/Http/Middlewares/SecurityHeaderMiddleware.php
Normal file
78
src/Framework/Http/Middlewares/SecurityHeaderMiddleware.php
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user