mirror of
https://github.com/php/web-wiki.git
synced 2025-08-06 10:33:23 +00:00
284 lines
9.3 KiB
PHP
284 lines
9.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* DokuWiki IP address functions.
|
|
*
|
|
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
|
* @author Zebra North <mrzebra@mrzebra.co.uk>
|
|
*/
|
|
|
|
namespace dokuwiki;
|
|
|
|
use dokuwiki\Input\Input;
|
|
use Exception;
|
|
|
|
class Ip
|
|
{
|
|
/**
|
|
* Determine whether an IP address is within a given CIDR range.
|
|
* The needle and haystack may be either IPv4 or IPv6.
|
|
*
|
|
* Example:
|
|
*
|
|
* ipInRange('192.168.11.123', '192.168.0.0/16') === true
|
|
* ipInRange('192.168.11.123', '::192.168.0.0/80') === true
|
|
* ipInRange('::192.168.11.123', '192.168.0.0/16') === true
|
|
* ipInRange('::192.168.11.123', '::192.168.0.0/80') === true
|
|
*
|
|
* @param string $needle The IP to test, either IPv4 in dotted decimal
|
|
* notation or IPv6 in colon notation.
|
|
* @param string $haystack The CIDR range as an IP followed by a forward
|
|
* slash and the number of significant bits.
|
|
*
|
|
* @return bool Returns true if $needle is within the range specified
|
|
* by $haystack, false if it is outside the range.
|
|
*
|
|
* @throws Exception Thrown if $needle is not a valid IP address.
|
|
* @throws Exception Thrown if $haystack is not a valid IP range.
|
|
*/
|
|
public static function ipInRange(string $needle, string $haystack): bool
|
|
{
|
|
$range = explode('/', $haystack);
|
|
$networkIp = Ip::ipToNumber($range[0]);
|
|
$maskLength = $range[1];
|
|
|
|
// For an IPv4 address the top 96 bits must be zero.
|
|
if ($networkIp['version'] === 4) {
|
|
$maskLength += 96;
|
|
}
|
|
|
|
if ($maskLength > 128) {
|
|
throw new Exception('Invalid IP range mask: ' . $haystack);
|
|
}
|
|
|
|
$maskLengthUpper = min($maskLength, 64);
|
|
$maskLengthLower = max(0, $maskLength - 64);
|
|
|
|
$maskUpper = ~0 << intval(64 - $maskLengthUpper);
|
|
$maskLower = ~0 << intval(64 - $maskLengthLower);
|
|
|
|
$needle = Ip::ipToNumber($needle);
|
|
|
|
return ($needle['upper'] & $maskUpper) === ($networkIp['upper'] & $maskUpper) &&
|
|
($needle['lower'] & $maskLower) === ($networkIp['lower'] & $maskLower);
|
|
}
|
|
|
|
/**
|
|
* Convert an IP address from a string to a number.
|
|
*
|
|
* This splits 128 bit IP addresses into the upper and lower 64 bits, and
|
|
* also returns whether the IP given was IPv4 or IPv6.
|
|
*
|
|
* The returned array contains:
|
|
*
|
|
* - version: Either '4' or '6'.
|
|
* - upper: The upper 64 bits of the IP.
|
|
* - lower: The lower 64 bits of the IP.
|
|
*
|
|
* For an IPv4 address, 'upper' will always be zero.
|
|
*
|
|
* @param string $ip The IPv4 or IPv6 address.
|
|
*
|
|
* @return int[] Returns an array of 'version', 'upper', 'lower'.
|
|
*
|
|
* @throws Exception Thrown if the IP is not valid.
|
|
*/
|
|
public static function ipToNumber(string $ip): array
|
|
{
|
|
$binary = inet_pton($ip);
|
|
|
|
if ($binary === false) {
|
|
throw new Exception('Invalid IP: ' . $ip);
|
|
}
|
|
|
|
if (strlen($binary) === 4) {
|
|
// IPv4.
|
|
return [
|
|
'version' => 4,
|
|
'upper' => 0,
|
|
'lower' => unpack('Nip', $binary)['ip'],
|
|
];
|
|
} else {
|
|
// IPv6.
|
|
$result = unpack('Jupper/Jlower', $binary);
|
|
$result['version'] = 6;
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if an IP address is equal to another IP or within an IP range.
|
|
* IPv4 and IPv6 are supported.
|
|
*
|
|
* @param string $ip The address to test.
|
|
* @param string $ipOrRange An IP address or CIDR range.
|
|
*
|
|
* @return bool Returns true if the IP matches, false if not.
|
|
*/
|
|
public static function ipMatches(string $ip, string $ipOrRange): bool
|
|
{
|
|
try {
|
|
// If it's not a range, compare the addresses directly.
|
|
// Addresses are converted to numbers because the same address may be
|
|
// represented by different strings, e.g. "::1" and "::0001".
|
|
if (strpos($ipOrRange, '/') === false) {
|
|
return Ip::ipToNumber($ip) === Ip::ipToNumber($ipOrRange);
|
|
}
|
|
|
|
return Ip::ipInRange($ip, $ipOrRange);
|
|
} catch (Exception $ex) {
|
|
// The IP address was invalid.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given the IP address of a proxy server, determine whether it is
|
|
* a known and trusted server.
|
|
*
|
|
* This test is performed using the config value `trustedproxies`.
|
|
*
|
|
* @param string $ip The IP address of the proxy.
|
|
*
|
|
* @return bool Returns true if the IP is trusted as a proxy.
|
|
*/
|
|
public static function proxyIsTrusted(string $ip): bool
|
|
{
|
|
global $conf;
|
|
|
|
// If the configuration is empty then no proxies are trusted.
|
|
if (empty($conf['trustedproxies'])) {
|
|
return false;
|
|
}
|
|
|
|
foreach ((array)$conf['trustedproxies'] as $trusted) {
|
|
if (Ip::ipMatches($ip, $trusted)) {
|
|
return true; // The given IP matches one of the trusted proxies.
|
|
}
|
|
}
|
|
|
|
return false; // none of the proxies matched
|
|
}
|
|
|
|
/**
|
|
* Get the originating IP address and the address of every proxy that the
|
|
* request has passed through, according to the X-Forwarded-For header.
|
|
*
|
|
* To prevent spoofing of the client IP, every proxy listed in the
|
|
* X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint
|
|
* from which the connection was received (i.e. the final proxy).
|
|
*
|
|
* If the header is not present or contains an untrusted proxy then
|
|
* an empty array is returned.
|
|
*
|
|
* The client IP is the first entry in the returned list, followed by the
|
|
* proxies.
|
|
*
|
|
* @return string[] Returns an array of IP addresses.
|
|
*/
|
|
public static function forwardedFor(): array
|
|
{
|
|
/* @var Input $INPUT */
|
|
global $INPUT, $conf;
|
|
|
|
$forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR');
|
|
|
|
if (empty($conf['trustedproxies']) || !$forwardedFor) {
|
|
return [];
|
|
}
|
|
|
|
// This is the address from which the header was received.
|
|
$remoteAddr = $INPUT->server->str('REMOTE_ADDR');
|
|
|
|
// Get the client address from the X-Forwarded-For header.
|
|
// X-Forwarded-For: <client> [, <proxy>]...
|
|
$forwardedFor = explode(',', str_replace(' ', '', $forwardedFor));
|
|
|
|
// The client address is the first item, remove it from the list.
|
|
$clientAddress = array_shift($forwardedFor);
|
|
|
|
// The remaining items are the proxies through which the X-Forwarded-For
|
|
// header has passed. The final proxy is the connection's remote address.
|
|
$proxies = $forwardedFor;
|
|
$proxies[] = $remoteAddr;
|
|
|
|
// Ensure that every proxy is trusted.
|
|
foreach ($proxies as $proxy) {
|
|
if (!Ip::proxyIsTrusted($proxy)) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Add the client address before the list of proxies.
|
|
return array_merge([$clientAddress], $proxies);
|
|
}
|
|
|
|
/**
|
|
* Return the IP of the client.
|
|
*
|
|
* The IP is sourced from, in order of preference:
|
|
*
|
|
* - The X-Real-IP header if $conf[realip] is true.
|
|
* - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy].
|
|
* - The TCP/IP connection remote address.
|
|
* - 0.0.0.0 if all else fails.
|
|
*
|
|
* The 'realip' config value should only be set to true if the X-Real-IP header
|
|
* is being added by the web server, otherwise it may be spoofed by the client.
|
|
*
|
|
* The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For
|
|
* may be spoofed by the client.
|
|
*
|
|
* @return string Returns an IPv4 or IPv6 address.
|
|
*/
|
|
public static function clientIp(): string
|
|
{
|
|
return Ip::clientIps()[0];
|
|
}
|
|
|
|
/**
|
|
* Return the IP of the client and the proxies through which the connection has passed.
|
|
*
|
|
* The IPs are sourced from, in order of preference:
|
|
*
|
|
* - The X-Real-IP header if $conf[realip] is true.
|
|
* - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies].
|
|
* - The TCP/IP connection remote address.
|
|
* - 0.0.0.0 if all else fails.
|
|
*
|
|
* @return string[] Returns an array of IPv4 or IPv6 addresses.
|
|
*/
|
|
public static function clientIps(): array
|
|
{
|
|
/* @var Input $INPUT */
|
|
global $INPUT, $conf;
|
|
|
|
// IPs in order of most to least preferred.
|
|
$ips = [];
|
|
|
|
// Use the X-Real-IP header if it is enabled by the configuration.
|
|
if (!empty($conf['realip']) && $INPUT->server->str('HTTP_X_REAL_IP')) {
|
|
$ips[] = $INPUT->server->str('HTTP_X_REAL_IP');
|
|
}
|
|
|
|
// Add the X-Forwarded-For addresses if all proxies are trusted.
|
|
$ips = array_merge($ips, Ip::forwardedFor());
|
|
|
|
// Add the TCP/IP connection endpoint.
|
|
$ips[] = $INPUT->server->str('REMOTE_ADDR');
|
|
|
|
// Remove invalid IPs.
|
|
$ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP));
|
|
|
|
// Remove duplicated IPs.
|
|
$ips = array_values(array_unique($ips));
|
|
|
|
// Add a fallback if for some reason there were no valid IPs.
|
|
if (!$ips) {
|
|
$ips[] = '0.0.0.0';
|
|
}
|
|
|
|
return $ips;
|
|
}
|
|
}
|