Add IPv6 and array() support for *proxy_ips* configuration
diff --git a/system/core/Input.php b/system/core/Input.php
index 657fce6..4a0caa5 100644
--- a/system/core/Input.php
+++ b/system/core/Input.php
@@ -328,60 +328,117 @@
 			return $this->ip_address;
 		}
 
-		if (config_item('proxy_ips') != '' && $this->server('HTTP_X_FORWARDED_FOR') && $this->server('REMOTE_ADDR'))
+		$proxy_ips = config_item('proxy_ips');
+		if (empty($proxy_ips))
 		{
-			$has_ranges = strpos($proxies, '/') !== FALSE;
-			$proxies = preg_split('/[\s,]/', config_item('proxy_ips'), -1, PREG_SPLIT_NO_EMPTY);
-			$proxies = is_array($proxies) ? $proxies : array($proxies);
+			$proxy_ips = FALSE;
+		}
+		elseif ( ! is_array($proxy_ips))
+		{
+			$proxy_ips = explode(',', str_replace(' ', '', $proxy_ips));
+		}
 
-			if ($has_ranges)
+		$this->ip_address = $this->server('REMOTE_ADDR');
+
+		if ($proxy_ips)
+		{
+			foreach (array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP') as $header)
 			{
-				$long_ip = ip2long($_SERVER['REMOTE_ADDR']);
-				$bit_32 = 1 << 32;
-
-				// Go through each of the IP Addresses to check for and
-				// test against range notation
-				foreach ($proxies as $ip)
+				if (($spoof = $this->server($header)) !== NULL)
 				{
-					list($address, $mask_length) = explode('/', $ip, 2);
-
-					// Generate the bitmask for a 32 bit IP Address
-					$bitmask = $bit_32 - (1 << (32 - (int) $mask_length));
-					if (($long_ip & $bitmask) === $address)
+					// Some proxies typically list the whole chain of IP
+					// addresses through which the client has reached us.
+					// e.g. client_ip, proxy_ip1, proxy_ip2, etc.
+					if (strpos($spoof, ',') !== FALSE)
 					{
-						$this->ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
+						$spoof = explode(',', $spoof, 2);
+						$spoof = $spoof[0];
+					}
+
+					if ( ! $this->valid_ip($spoof))
+					{
+						$spoof = NULL;
+					}
+					else
+					{
 						break;
 					}
 				}
-
 			}
-			else
+
+			if ($spoof !== NULL)
 			{
-				$this->ip_address = in_array($_SERVER['REMOTE_ADDR'], $proxies)
-					? $_SERVER['HTTP_X_FORWARDED_FOR']
-					: $_SERVER['REMOTE_ADDR'];
-			}
-		}
-		elseif ( ! $this->server('HTTP_CLIENT_IP') && $this->server('REMOTE_ADDR'))
-		{
-			$this->ip_address = $_SERVER['REMOTE_ADDR'];
-		}
-		elseif ($this->server('REMOTE_ADDR') && $this->server('HTTP_CLIENT_IP'))
-		{
-			$this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
-		}
-		elseif ($this->server('HTTP_CLIENT_IP'))
-		{
-			$this->ip_address = $_SERVER['HTTP_CLIENT_IP'];
-		}
-		elseif ($this->server('HTTP_X_FORWARDED_FOR'))
-		{
-			$this->ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
-		}
+				for ($i = 0, $c = count($proxy_ips), $separator = (strlen($ip) === 32 ? '.' : ':'); $i < $c; $i++)
+				{
+					// Check if we have an IP address or a subnet
+					if (strpos($proxy_ips[$i], '/') === FALSE)
+					{
+						// An IP address (and not a subnet) is specified.
+						// We can compare right away.
+						if ($proxy_ips[$i] === $this->ip_address)
+						{
+							$this->ip_address = $spoof;
+							break;
+						}
 
-		if ($this->ip_address === FALSE)
-		{
-			return $this->ip_address = '0.0.0.0';
+						continue;
+					}
+
+					// We have a subnet ... now the heavy lifting begins
+					isset($separator) OR $separator = $this->valid_ip($this->ip_address, 'ipv6') ? ':' : '.';
+
+					// If the proxy entry doesn't match the IP protocol - skip it
+					if (strpos($proxy_ips[$i], $separator) === FALSE)
+					{
+						continue;
+					}
+
+					// Convert the REMOTE_ADDR IP address to binary, if needed
+					if ( ! isset($ip, $convert_func))
+					{
+						if ($separator === ':')
+						{
+							// Make sure we're have the "full" IPv6 format
+							$ip = str_replace('::', str_repeat(':', 9 - substr_count($this->ip_address, ':')), $this->ip_address);
+							$convert_func = is_php('5.3')
+								? function ($value)
+									{
+										return str_pad(base_convert($value, 16, 2), 16, '0', STR_PAD_LEFT);
+									}
+								: create_function('$value', 'return str_pad(base_convert($value, 16, 2), 16, "0", STR_PAD_LEFT);');
+						}
+						else
+						{
+							$ip = $this->ip_address;
+							$convert_func = is_php('5.3')
+								? function ($value)
+									{
+										return str_pad(decbin($value), 8, '0', STR_PAD_LEFT);
+									}
+								: create_function('$value', 'return str_pad(decbin($value), 8, "0", STR_PAD_LEFT);');
+						}
+
+						$ip = implode(array_map($convert_func, explode($separator, $ip)));
+					}
+
+					// Split the netmask length off the network address
+					list($netaddr, $masklen) = explode('/', $proxy_ips[$i], 2);
+
+					// Again, an IPv6 address is most likely in a compressed form
+					if ($separator === ':')
+					{
+						$netaddr = str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr);
+					}
+
+					// Convert to a binary form and finally compare
+					$netaddr = implode(array_map($convert_func, explode($separator, $netaddr)));
+					if (strncmp($ip, $netaddr, $masklen) === 0)
+					{
+						$this->ip_address = $spoof;
+						break;
+					}
+				}
+			}
 		}
 
 		if (strpos($this->ip_address, ',') !== FALSE)