1<?php
2declare(strict_types = 1);
3
4namespace Middlewares;
5
6use Psr\Http\Message\ResponseInterface;
7use Psr\Http\Message\ServerRequestInterface;
8use Psr\Http\Server\MiddlewareInterface;
9use Psr\Http\Server\RequestHandlerInterface;
10
11class ClientIp implements MiddlewareInterface
12{
13    /**
14     * @var bool
15     */
16    private $remote = false;
17
18    /**
19     * @var string The attribute name
20     */
21    private $attribute = 'client-ip';
22
23    /**
24     * @var array The trusted proxy headers
25     */
26    private $proxyHeaders = [];
27
28    /**
29     * @var array The trusted proxy ips
30     */
31    private $proxyIps = [];
32
33    /**
34     * Configure the proxy.
35     */
36    public function proxy(
37        array $ips = [],
38        array $headers = [
39            'Forwarded',
40            'Forwarded-For',
41            'X-Forwarded',
42            'X-Forwarded-For',
43            'X-Cluster-Client-Ip',
44            'Client-Ip',
45        ]
46    ): self {
47        $this->proxyIps = $ips;
48        $this->proxyHeaders = $headers;
49
50        return $this;
51    }
52
53    /**
54     * To get the ip from a remote service.
55     * Useful for testing purposes on localhost.
56     */
57    public function remote(bool $remote = true): self
58    {
59        $this->remote = $remote;
60
61        return $this;
62    }
63
64    /**
65     * Set the attribute name to store client's IP address.
66     */
67    public function attribute(string $attribute): self
68    {
69        $this->attribute = $attribute;
70
71        return $this;
72    }
73
74    /**
75     * Process a server request and return a response.
76     */
77    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
78    {
79        $ip = $this->getIp($request);
80
81        return $handler->handle($request->withAttribute($this->attribute, $ip));
82    }
83
84    /**
85     * Detect and return the ip.
86     *
87     * @return string|null
88     */
89    private function getIp(ServerRequestInterface $request)
90    {
91        $remoteIp = $this->getRemoteIp();
92
93        if (!empty($remoteIp)) {
94            // Found IP address via remote service.
95            return $remoteIp;
96        }
97
98        $localIp = $this->getLocalIp($request);
99
100        if ($this->proxyIps && !$this->isInProxiedIps($localIp)) {
101            // Local IP address does not point at a known proxy, do not attempt
102            // to read proxied IP address.
103            return $localIp;
104        }
105
106        $proxiedIp = $this->getProxiedIp($request);
107
108        if (!empty($proxiedIp)) {
109            // Found IP address via proxy-defined headers.
110            return $proxiedIp;
111        }
112
113        return $localIp;
114    }
115
116    /**
117     * checks if the given ip address is in the list of proxied ips provided
118     *
119     * @param  string $ip
120     * @return bool
121     */
122    private function isInProxiedIps(string $ip): bool
123    {
124        foreach ($this->proxyIps as $proxyIp) {
125            if ($ip === $proxyIp || self::isInCIDR($ip, $proxyIp)) {
126                return true;
127            }
128        }
129        return false;
130    }
131
132    private static function isInCIDR(string $ip, string $cidr): bool
133    {
134        $tokens = explode('/', $cidr);
135        if (count($tokens) !== 2 || !self::isValid($ip) || !self::isValid($tokens[0]) || !is_numeric($tokens[1])) {
136            return false;
137        }
138
139        $cidr_base = ip2long($tokens[0]);
140        $ip_long = ip2long($ip);
141        $mask = (0xffffffff << intval($tokens[1])) & 0xffffffff;
142
143        return ($cidr_base & $mask) === ($ip_long & $mask);
144    }
145
146    /**
147     * Returns the IP address from remote service.
148     *
149     * @return string|null
150     */
151    private function getRemoteIp()
152    {
153        if ($this->remote) {
154            $ip = file_get_contents('http://ipecho.net/plain');
155
156            if ($ip && self::isValid($ip)) {
157                return $ip;
158            }
159        }
160    }
161
162    /**
163     * Returns the first valid proxied IP found.
164     *
165     * @return string|null
166     */
167    private function getProxiedIp(ServerRequestInterface $request)
168    {
169        foreach ($this->proxyHeaders as $name) {
170            if ($request->hasHeader($name)) {
171                if (substr($name, -9) === 'Forwarded') {
172                    $ip = $this->getForwardedHeaderIp($request->getHeaderLine($name));
173                } else {
174                    $ip = $this->getHeaderIp($request->getHeaderLine($name));
175                }
176
177                if ($ip !== null) {
178                    return $ip;
179                }
180            }
181        }
182    }
183
184    /**
185     * Returns the remote address of the request, if valid.
186     *
187     * @return string|null
188     */
189    private function getLocalIp(ServerRequestInterface $request)
190    {
191        $server = $request->getServerParams();
192        $ip = trim($server['REMOTE_ADDR'] ?? '', '[]');
193
194        if (self::isValid($ip)) {
195            return $ip;
196        }
197    }
198
199    /**
200     * Returns the first valid ip found in the Forwarded or X-Forwarded header.
201     *
202     * @return string|null
203     */
204    private function getForwardedHeaderIp(string $header)
205    {
206        foreach (array_reverse(array_map('trim', explode(',', strtolower($header)))) as $values) {
207            foreach (array_reverse(array_map('trim', explode(';', $values))) as $directive) {
208                if (strpos($directive, 'for=') !== 0) {
209                    continue;
210                }
211
212                $ip = trim(substr($directive, 4));
213
214                if (self::isValid($ip) && !$this->isInProxiedIps($ip)) {
215                    return $ip;
216                }
217            }
218        }
219    }
220
221    /**
222     * Returns the first valid ip found in the header.
223     *
224     * @return string|null
225     */
226    private function getHeaderIp(string $header)
227    {
228        foreach (array_reverse(array_map('trim', explode(',', $header))) as $ip) {
229            if (self::isValid($ip) && !$this->isInProxiedIps($ip)) {
230                return $ip;
231            }
232        }
233    }
234
235    /**
236     * Check that a given string is a valid IP address.
237     */
238    private static function isValid(string $ip): bool
239    {
240        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) !== false;
241    }
242}
243