CompatMiddleware::apply()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 17
ccs 10
cts 10
cp 1
crap 5
rs 9.6111
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 convert X-Forwarded-* headers to Forwarded header.
14
 * Can be used both as single pass (PSR-15) and double pass middleware.
15
 */
16
class CompatMiddleware implements MiddlewareInterface
17
{
18
    protected const DEFAULT_MAP = [
19
        'X-Forwarded-For' => 'for',
20
        'X-Forwarded-Proto' => 'proto',
21
        'X-Forwarded-Host' => 'host',
22
        'X-Forwarded-Port' => 'port',
23
    ];
24
25
    /**
26
     * Map header to directive.
27
     * @var array
28
     */
29
    protected $map;
30
31
    /**
32
     * CompatMiddleware constructor.
33
     *
34
     * @param array|null $map  Map header to directive.
35
     */
36 8
    public function __construct(?array $map = null)
37
    {
38 8
        $this->map = $map ?? self::DEFAULT_MAP;
39 8
    }
40
41
    /**
42
     * Process an incoming server request (PSR-15).
43
     *
44
     * @param ServerRequest $request
45
     * @param RequestHandler $handler
46
     * @return Response
47
     */
48 7
    public function process(ServerRequest $request, RequestHandler $handler): Response
49
    {
50 7
        $updatedRequest = $this->apply($request);
51
52 7
        return $handler->handle($updatedRequest);
53
    }
54
55
    /**
56
     * Get a callback that can be used as double pass middleware.
57
     *
58
     * @return callable
59
     */
60 1
    public function asDoublePass(): callable
61
    {
62
        return function (ServerRequest $request, Response $response, callable $next): Response {
63 1
            $updatedRequest = $this->apply($request);
64 1
            return $next($updatedRequest, $response);
65 1
        };
66
    }
67
68
69
    /**
70
     * Apply `Forwarded` header to server request.
71
     *
72
     * @param ServerRequest $request
73
     * @return ServerRequest
74
     */
75 8
    protected function apply(ServerRequest $request): ServerRequest
76
    {
77 8
        $directives = [];
78
79 8
        foreach ($this->map as $header => $directive) {
80 8
            if (isset($directives[$directive]) || !$request->hasHeader($header)) {
81 5
                continue;
82
            }
83
84 7
            $directives[$directive] = $request->getHeaderLine($header);
85
        }
86
87 8
        $forwarded = $this->createForwardedHeader($directives);
88
89 8
        return $forwarded !== ''
90 7
            ? $request->withHeader('Forwarded', $forwarded)
91 8
            : $request->withoutHeader('Forwarded');
92
    }
93
94
    /**
95
     * Create a Forwarded header from directives.
96
     *
97
     * @param array $directives
98
     * @return string
99
     */
100 8
    protected function createForwardedHeader(array $directives): string
101
    {
102
        // Multiple 'for' ips. Other directives apply to first proxy.
103 8
        if (isset($directives['for']) && strpos($directives['for'], ',') !== false) {
104 2
            $for = array_map('trim', explode(',', $directives['for']));
105 2
            $directives['for'] = array_shift($for);
106
        } else {
107 6
            $for = [];
108
        }
109
110
        $pair = static function (string $key, string $value): string {
111 7
            if ((bool)preg_match('/^([A-f0-9:]+:+)+[A-f0-9]+$/i', $value)) { // IPv6
112 1
                $value = sprintf('"[%s]"', $value);
113 7
            } elseif ((bool)preg_match('/[,;:=]/', $value)) {
114 1
                $value = sprintf('"%s"', $value);
115
            }
116
117 7
            return "$key=$value";
118 8
        };
119
120 8
        $header = join(';', array_map($pair, array_keys($directives), array_values($directives)));
121
122 8
        foreach ($for as $forIp) {
123 2
            $header .= ", " . $pair('for', $forIp);
124
        }
125
126 8
        return $header;
127
    }
128
}
129