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