Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.29% covered (warning)
85.29%
29 / 34
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
IpAddress
85.29% covered (warning)
85.29%
29 / 34
20.00% covered (danger)
20.00%
1 / 5
23.54
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 __invoke
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 determineClientIpAddress
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 isCheckProxyHeaders
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
6.17
 isValidIpAddress
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
1<?php
2
3/**
4 * @copyright MIT
5 * derived from akrabat/rka-ip-address-middleware via composer.phar
6 **/
7
8namespace BO\Slim\Middleware;
9
10use Psr\Http\Message\ServerRequestInterface;
11use Psr\Http\Message\ResponseInterface;
12use Psr\Http\Server\RequestHandlerInterface;
13use BO\Slim\Factory\ResponseFactory;
14
15class IpAddress
16{
17    /**
18     * Enable checking of proxy headers (X-Forwarded-For to determined client IP.
19     *
20     * Defaults to false as only $_SERVER['REMOTE_ADDR'] is a trustworthy source
21     * of IP address.
22     *
23     * @var bool
24     */
25    protected bool $checkProxyHeaders;
26
27    /**
28     * List of trusted proxy IP addresses
29     *
30     * If not empty, then one of these IP addresses must be in $_SERVER['REMOTE_ADDR']
31     * in order for the proxy headers to be looked at.
32     * If TRUE then trust every proxy
33     *
34     * @var array|true
35     */
36    protected mixed $trustedProxies;
37
38    /**
39     * Name of the attribute added to the ServerRequest object
40     *
41     * @var string
42     */
43    protected string $attributeName = 'ip_address';
44
45    /**
46     * List of proxy headers inspected for the client IP address
47     *
48     * @var array
49     */
50    protected array $headersToInspect = [
51        'X-Remote-Ip',
52        'X-Forwarded-For',
53        'X-Forwarded',
54        'X-Cluster-Client-Ip',
55        'Client-Ip',
56    ];
57
58    /**
59     * Constructor
60     *
61     * @param bool $checkProxyHeaders Whether to use proxy headers to determine client IP
62     * @param mixed $trustedProxies   List of IP addresses of trusted proxies or TRUE if all proxies should be trusted
63     * @param string|null $attributeName   Name of attribute added to ServerRequest object
64     * @param array $headersToInspect List of headers to inspect
65     */
66    public function __construct(
67        bool $checkProxyHeaders = false,
68        mixed $trustedProxies = [],
69        ?string $attributeName = null,
70        array $headersToInspect = []
71    ) {
72        $this->checkProxyHeaders = $checkProxyHeaders;
73        $this->trustedProxies = $trustedProxies;
74
75        if ($attributeName !== null && $attributeName !== '') {
76            $this->attributeName = $attributeName;
77        }
78        if ($headersToInspect !== []) {
79            $this->headersToInspect = $headersToInspect;
80        }
81    }
82
83    /**
84     * Set the "$attributeName" attribute to the client's IP address as determined from
85     * the proxy header (X-Forwarded-For or from $_SERVER['REMOTE_ADDR']
86     *
87     * @param ServerRequestInterface $request PSR7 request
88     * @param RequestHandlerInterface|null $next Next middleware
89     *
90     * @return ResponseInterface
91     * @psalm-api Called by Slim as middleware.
92     */
93    public function __invoke(ServerRequestInterface $request, ?RequestHandlerInterface $next): ResponseInterface
94    {
95        if (!$next) {
96            return (new ResponseFactory())->createResponse();
97        }
98
99        $ipAddress = $this->determineClientIpAddress($request);
100        $request = $request->withAttribute($this->attributeName, $ipAddress);
101
102        return $next->handle($request);
103    }
104
105    /**
106     * Find out the client's IP address from the headers available to us
107     *
108     * @param  ServerRequestInterface $request PSR-7 Request
109     * @return string|null
110     */
111    protected function determineClientIpAddress(ServerRequestInterface $request): ?string
112    {
113        $ipAddress = null;
114
115        $serverParams = $request->getServerParams();
116        if (isset($serverParams['REMOTE_ADDR']) && $this->isValidIpAddress((string) $serverParams['REMOTE_ADDR'])) {
117            $ipAddress = $serverParams['REMOTE_ADDR'];
118        }
119
120        if ($this->isCheckProxyHeaders($ipAddress)) {
121            foreach ($this->headersToInspect as $header) {
122                if ($request->hasHeader($header)) {
123                    $headerIp = current(explode(',', $request->getHeaderLine($header)));
124                    $ipString = is_string($headerIp) ? trim($headerIp) : '';
125                    if ($this->isValidIpAddress($ipString)) {
126                        $ipAddress = $ipString;
127                        break;
128                    }
129                }
130            }
131        }
132
133        return $ipAddress;
134    }
135
136    protected function isCheckProxyHeaders(?string $ipAddress): bool
137    {
138        $checkProxyHeaders = $this->checkProxyHeaders;
139        if ($checkProxyHeaders && ($this->trustedProxies === true || $this->trustedProxies !== [])) {
140            if (
141                $this->trustedProxies !== true
142                && !in_array($ipAddress, $this->trustedProxies, true)
143            ) {
144                $checkProxyHeaders = false;
145            }
146        }
147        return $checkProxyHeaders;
148    }
149
150    /**
151     * Check that a given string is a valid IP address
152     *
153     * @param  string  $ipString
154     * @return boolean
155     */
156    protected function isValidIpAddress(string $ipString): bool
157    {
158        $flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
159        if (filter_var($ipString, FILTER_VALIDATE_IP, $flags) === false) {
160            return false;
161        }
162        return true;
163    }
164}