Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
RequestIdMiddleware
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
2 / 2
4
100.00% covered (success)
100.00%
1 / 1
 process
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 resolveRequestId
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Api\Middleware;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use Psr\Http\Message\ResponseInterface;
12use Psr\Http\Message\ServerRequestInterface;
13use Psr\Http\Server\MiddlewareInterface;
14use Psr\Http\Server\RequestHandlerInterface;
15
16/**
17 * Request Correlation ID Middleware (NIST AU-3 / OWASP ASVS V7.1).
18 *
19 * Extracts incoming X-Request-ID header or generates a cryptographically secure UUIDv4.
20 * Injects the request_id attribute into PSR-7 request and sets X-Request-ID response header.
21 *
22 * @package App\Core\Api\Middleware
23 */
24final readonly class RequestIdMiddleware implements MiddlewareInterface
25{
26    /** @var string Request ID header name. */
27    public const string HEADER_NAME = 'X-Request-ID';
28
29    /** @var string Request attribute key name. */
30    public const string ATTRIBUTE_NAME = 'request_id';
31
32    /**
33     * Processes request, ensuring a valid X-Request-ID header on request and response.
34     *
35     * @param ServerRequestInterface  $request Server request.
36     * @param RequestHandlerInterface $handler Request handler.
37     * @return ResponseInterface Response with X-Request-ID header attached.
38     */
39    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
40    {
41        $requestId = $this->resolveRequestId($request);
42        $requestWithId = $request->withAttribute(self::ATTRIBUTE_NAME, $requestId);
43
44        $response = $handler->handle($requestWithId);
45
46        return $response->withHeader(self::HEADER_NAME, $requestId);
47    }
48
49    /**
50     * Resolves incoming request ID or generates a new UUIDv4 string.
51     *
52     * @param ServerRequestInterface $request Incoming HTTP request.
53     * @return string Validated or generated UUID string.
54     */
55    private function resolveRequestId(ServerRequestInterface $request): string
56    {
57        $header = $request->getHeaderLine(self::HEADER_NAME);
58        if ($header !== '' && preg_match('/^[a-zA-Z0-9_\-]{8,64}$/', $header) === 1) {
59            return $header;
60        }
61
62        $bytes = random_bytes(16);
63        $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
64        $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
65
66        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
67    }
68}