Middleware::asDoublePass()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\Forwarded;
6
7
use Psr\Http\Message\ServerRequestInterface as ServerRequest;
8
use Psr\Http\Message\ResponseInterface as Response;
9
use Psr\Http\Server\MiddlewareInterface;
10
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
11
12
/**
13
 * Middleware to handle Forwarded header for PSR-7 server requests.
14
 * Can be used both as single pass (PSR-15) and double pass middleware.
15
 *
16
 * Set the `client_ip` and `original_uri` request attributes.
17
 */
18
class Middleware implements MiddlewareInterface
19
{
20
    /**
21
     * Logic to check if a proxy is trusted.
22
     * @var \Closure
23
     */
24
    protected $trust;
25
26
    /**
27
     * ServerMiddleware constructor.
28
     *
29
     * @param callable $trust Logic to check if a proxy is trusted.
30
     */
31 22
    public function __construct(callable $trust)
32
    {
33 22
        $this->trust = \Closure::fromCallable($trust);
34 22
    }
35
36
    /**
37
     * Process an incoming server request (PSR-15).
38
     *
39
     * @param ServerRequest  $request
40
     * @param RequestHandler $handler
41
     * @return Response
42
     */
43 16
    public function process(ServerRequest $request, RequestHandler $handler): Response
44
    {
45 16
        $updatedRequest = $this->apply($request);
46
47 16
        return $handler->handle($updatedRequest);
48
    }
49
50
    /**
51
     * Get a callback that can be used as double pass middleware.
52
     *
53
     * @return callable
54
     */
55 6
    public function asDoublePass(): callable
56
    {
57
        return function (ServerRequest $request, Response $response, callable $next): Response {
58 6
            $updatedRequest = $this->apply($request);
59 6
            return $next($updatedRequest, $response);
60 6
        };
61
    }
62
63
64
    /**
65
     * Apply `Forwarded` header to server request.
66
     *
67
     * @param ServerRequest $request
68
     * @return ServerRequest
69
     */
70 22
    protected function apply(ServerRequest $request): ServerRequest
71
    {
72 22
        $clientIp = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown';
73 22
        $forwards = $this->parseForwarded($request->getHeaderLine('Forwarded'));
74
75 22
        $trustedForward = $this->getTrustedForward($clientIp, $forwards);
76
77 22
        return $this->applyForward($request, $trustedForward);
78
    }
79
80
    /**
81
     * Parse the forwarded header
82
     *
83
     * @param string $header  Forwarded header
84
     * @return array
85
     */
86 22
    protected function parseForwarded(string $header): array
87
    {
88 22
        if ($header === '') {
89 1
            return [];
90
        }
91
92 21
        $forwards = [];
93 21
        preg_match_all('/(?:[^",]++|"[^"]++")+/', $header, $matches);
94
95 21
        foreach ($matches[0] as $part) {
96 21
            $regex = '/(?P<key>\w+)\s*=\s*(?|(?P<value>[^",;]*[^",;\s])|"(?P<value>[^"]+)")/';
97 21
            preg_match_all($regex, $part, $matches, PREG_SET_ORDER);
98
99
            $pairs = array_map(static function (array $match) {
100 21
                return [$match['key'] => $match['value']];
101 21
            }, $matches);
102
103 21
            $forwards[] = array_merge(...$pairs);
104
        }
105
106 21
        return $forwards;
107
    }
108
109
    /**
110
     * Iterate over forwards while trusted.
111
     *
112
     * @param string $clientIp
113
     * @param array  $forwards
114
     * @return array
115
     */
116 22
    protected function getTrustedForward(string $clientIp, array $forwards): array
117
    {
118 22
        $trusted = ['for' => $clientIp];
119
120 22
        foreach (array_reverse($forwards) as $forward) {
121 21
            if (!($this->trust)($trusted['for'] ?? 'unknown', $forward)) {
122 4
                break;
123
            }
124
125 20
            $trusted = $forward;
126
        }
127
128 22
        return $trusted;
129
    }
130
131
    /**
132
     * Apply trusted forward to server request.
133
     *
134
     * @param ServerRequest $request
135
     * @param string[]      $forward
136
     * @return ServerRequest
137
     */
138 22
    protected function applyForward(ServerRequest $request, array $forward): ServerRequest
139
    {
140 22
        $uri = $request->getUri();
141
142 22
        if (isset($forward['proto'])) {
143 20
            $uri = $uri->withScheme($forward['proto']);
144
        }
145 22
        if (isset($forward['host'])) {
146 20
            $uri = $uri->withHost($forward['host']);
147
        }
148 22
        if (isset($forward['port']) && ctype_digit($forward['port'])) {
149 20
            $port = (int)$forward['port'];
150 20
            $defaultPort = ['http' => 80, 'https' => 443][$uri->getScheme()] ?? null;
151
152 20
            $uri = $uri->withPort($port !== $defaultPort ? $port : null);
153
        }
154 22
        if (isset($forward['path'])) {
155 20
            $uri = $uri->withPath($forward['path']);
156
        }
157
158
        return $request
159 22
            ->withAttribute('client_ip', $forward['for'] ?? null)
160 22
            ->withAttribute('original_uri', $uri);
161
    }
162
}
163