Add IPv6 and array() support for *proxy_ips* configuration
diff --git a/application/config/config.php b/application/config/config.php
index eaccbf7..ab1508e 100644
--- a/application/config/config.php
+++ b/application/config/config.php
@@ -406,15 +406,19 @@
 | Reverse Proxy IPs
 |--------------------------------------------------------------------------
 |
-| If your server is behind a reverse proxy, you must whitelist the proxy IP
-| addresses from which CodeIgniter should trust the HTTP_X_FORWARDED_FOR
-| header in order to properly identify the visitor's IP address.
-| Comma-delimited, e.g. '10.0.1.200,10.0.1.201'
+| If your server is behind a reverse proxy, you must whitelist the proxy
+| IP addresses from which CodeIgniter should trust headers such as
+| HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
+| the visitor's IP address.
 |
+| You can use both an array or a comma-separated list of proxy addresses,
+| as well as specifying whole subnets. Here are a few examples:
+|
+| Comma-separated:	'10.0.1.200,192.168.5.0/24'
+| Array:		array('10.0.1.200', '192.168.5.0/24')
 */
 $config['proxy_ips'] = '';
 
 
-
 /* End of file config.php */
-/* Location: ./application/config/config.php */
+/* Location: ./application/config/config.php */
\ No newline at end of file
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)
diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst
index 486a676..1eb8d10 100644
--- a/user_guide_src/source/changelog.rst
+++ b/user_guide_src/source/changelog.rst
@@ -212,18 +212,20 @@
 -  Core
 
    -  Changed private methods in the :doc:`URI Library <libraries/uri>` to protected so MY_URI can override them.
-   -  Removed ``CI_CORE`` boolean constant from CodeIgniter.php (no longer Reactor and Core versions).
+   -  Removed ``CI_CORE`` boolean constant from *CodeIgniter.php* (no longer Reactor and Core versions).
    -  :doc:`Loader Library <libraries/loader>` changes include:
-	 -  Added method get_vars() to the Loader to retrieve all variables loaded with $this->load->vars().
-	 -  CI_Loader::_ci_autoloader() is now a protected method.
-	 -  Added autoloading of drivers with $autoload['drivers'].
-	 -  CI_Loader::library() will now load drivers as well, for backward compatibility of converted libraries (like Session).
+	 -  Added method ``get_vars()`` to the Loader to retrieve all variables loaded with ``$this->load->vars()``.
+	 -  ``CI_Loader::_ci_autoloader()`` is now a protected method.
+	 -  Added autoloading of drivers with ``$autoload['drivers']``.
+	 -  ``CI_Loader::library()`` will now load drivers as well, for backward compatibility of converted libraries (like Session).
    -  ``is_loaded()`` function from *system/core/Commons.php* now returns a reference.
-   -  $config['rewrite_short_tags'] now has no effect when using PHP 5.4 as *<?=* will always be available.
-   -  Added ``method()`` to the :doc:`Input Library <libraries/input>` to retrieve ``$_SERVER['REQUEST_METHOD']``.
-   -  Modified valid_ip() to use PHP's filter_var() in the :doc:`Input Library <libraries/input>`.
+   -  ``$config['rewrite_short_tags']`` now has no effect when using PHP 5.4 as *<?=* will always be available.
+   -  :doc:`Input Library <libraries/input>` changes include:
+	 -  Added ``method()`` to retrieve ``$_SERVER['REQUEST_METHOD']``.
+	 -  Modified ``valid_ip()`` to use PHP's ``filter_var()``.
+	 -  Added support for arrays and network addresses (e.g. 192.168.1.1/24) for use with the *proxy_ips* setting.
    -  Added support for HTTP-Only cookies with new config option *cookie_httponly* (default FALSE).
-   -  Renamed method _call_hook() to call_hook() in the :doc:`Hooks Library <general/hooks>`.
+   -  Renamed method ``_call_hook()`` to ``call_hook()`` in the :doc:`Hooks Library <general/hooks>`.
    -  :doc:`Output Library <libraries/output>` changes include:
 	 -  Added method ``get_content_type()``.
 	 -  Added a second argument to method ``set_content_type()`` that allows setting the document charset as well.
@@ -235,9 +237,8 @@
 	 -  Added method ``strip_image_tags()``.
 	 -  Added ``$config['csrf_regeneration']``, which makes token regeneration optional.
 	 -  Added ``$config['csrf_exclude_uris']``, which allows you list URIs which will not have the CSRF validation methods run.
-   -  Changed ``_exception_handler()`` to respect php.ini 'display_errors' setting.
+   -  Changed ``_exception_handler()`` to respect php.ini *display_errors* setting.
    -  Removed redundant conditional to determine HTTP server protocol in ``set_status_header()``.
-   -  Added support for IPv4 range masks (e.g. 192.168.1.1/24) to specify ranges of IP addresses for use with the *proxy_ips* setting.
 
 Bug fixes for 3.0
 ------------------