|
@@ -12,34 +12,76 @@
|
|
|
|
|
|
namespace Composer\Util;
|
|
|
|
|
|
+use stdClass;
|
|
|
+
|
|
|
/**
|
|
|
- * Tests URLs against no_proxy patterns.
|
|
|
+ * Tests URLs against NO_PROXY patterns
|
|
|
*/
|
|
|
class NoProxyPattern
|
|
|
{
|
|
|
/**
|
|
|
* @var string[]
|
|
|
*/
|
|
|
+ protected $hostNames = array();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var object[]
|
|
|
+ */
|
|
|
protected $rules = array();
|
|
|
|
|
|
/**
|
|
|
- * @param string $pattern no_proxy pattern
|
|
|
+ * @var bool
|
|
|
+ */
|
|
|
+ protected $noproxy;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param string $pattern NO_PROXY pattern
|
|
|
*/
|
|
|
public function __construct($pattern)
|
|
|
{
|
|
|
- $this->rules = preg_split("/[\s,]+/", $pattern);
|
|
|
+ $this->hostNames = preg_split('{[\s,]+}', $pattern, null, PREG_SPLIT_NO_EMPTY);
|
|
|
+ $this->noproxy = empty($this->hostNames) || '*' === $this->hostNames[0];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Test a URL against the stored pattern.
|
|
|
+ * Returns true if a URL matches the NO_PROXY pattern
|
|
|
*
|
|
|
* @param string $url
|
|
|
*
|
|
|
- * @return bool true if the URL matches one of the rules.
|
|
|
+ * @return bool
|
|
|
*/
|
|
|
public function test($url)
|
|
|
{
|
|
|
- $host = parse_url($url, PHP_URL_HOST);
|
|
|
+ if ($this->noproxy) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!$urlData = $this->getUrlData($url)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($this->hostNames as $index => $hostName) {
|
|
|
+ if ($this->match($index, $hostName, $urlData)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns false is the url cannot be parsed, otherwise a data object
|
|
|
+ *
|
|
|
+ * @param string $url
|
|
|
+ *
|
|
|
+ * @return bool|stdclass
|
|
|
+ */
|
|
|
+ protected function getUrlData($url)
|
|
|
+ {
|
|
|
+ if (!$host = parse_url($url, PHP_URL_HOST)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
$port = parse_url($url, PHP_URL_PORT);
|
|
|
|
|
|
if (empty($port)) {
|
|
@@ -53,95 +95,341 @@ class NoProxyPattern
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- foreach ($this->rules as $rule) {
|
|
|
- if ($rule == '*') {
|
|
|
- return true;
|
|
|
+ $hostName = $host . ($port ? ':' . $port : '');
|
|
|
+ list($host, $port, $err) = $this->splitHostPort($hostName);
|
|
|
+
|
|
|
+ if ($err || !$this->ipCheckData($host, $ipdata)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->makeData($host, $port, $ipdata);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns true if the url is matched by a rule
|
|
|
+ *
|
|
|
+ * @param int $index
|
|
|
+ * @param string $hostName
|
|
|
+ * @param string $url
|
|
|
+ *
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function match($index, $hostName, $url)
|
|
|
+ {
|
|
|
+ if (!$rule = $this->getRule($index, $hostName)) {
|
|
|
+ // Data must have been misformatted
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($rule->ipdata) {
|
|
|
+ // Match ipdata first
|
|
|
+ if (!$url->ipdata) {
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
- $match = false;
|
|
|
-
|
|
|
- list($ruleHost) = explode(':', $rule);
|
|
|
- list($base) = explode('/', $ruleHost);
|
|
|
-
|
|
|
- if (filter_var($base, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
|
- // ip or cidr match
|
|
|
-
|
|
|
- if (!isset($ip)) {
|
|
|
- $ip = gethostbyname($host);
|
|
|
- }
|
|
|
-
|
|
|
- if (strpos($ruleHost, '/') === false) {
|
|
|
- $match = $ip === $ruleHost;
|
|
|
- } else {
|
|
|
- // gethostbyname() failed to resolve $host to an ip, so we assume
|
|
|
- // it must be proxied to let the proxy's DNS resolve it
|
|
|
- if ($ip === $host) {
|
|
|
- $match = false;
|
|
|
- } else {
|
|
|
- // match resolved IP against the rule
|
|
|
- $match = self::inCIDRBlock($ruleHost, $ip);
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- // match end of domain
|
|
|
-
|
|
|
- $haystack = '.' . trim($host, '.') . '.';
|
|
|
- $needle = '.'. trim($ruleHost, '.') .'.';
|
|
|
- $match = stripos(strrev($haystack), strrev($needle)) === 0;
|
|
|
+ if ($rule->ipdata->netmask) {
|
|
|
+ return $this->matchRange($rule->ipdata, $url->ipdata);
|
|
|
}
|
|
|
|
|
|
- // final port check
|
|
|
- if ($match && strpos($rule, ':') !== false) {
|
|
|
- list(, $rulePort) = explode(':', $rule);
|
|
|
- if (!empty($rulePort) && $port != $rulePort) {
|
|
|
- $match = false;
|
|
|
- }
|
|
|
+ $match = $rule->ipdata->ip === $url->ipdata->ip;
|
|
|
+ } else {
|
|
|
+ // Match host and port
|
|
|
+ $haystack = substr($url->name, - strlen($rule->name));
|
|
|
+ $match = stripos($haystack, $rule->name) === 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($match && $rule->port) {
|
|
|
+ $match = $rule->port === $url->port;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $match;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns true if the target ip is in the network range
|
|
|
+ *
|
|
|
+ * @param stdClass $network
|
|
|
+ * @param stdClass $target
|
|
|
+ *
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ protected function matchRange(stdClass $network, stdClass $target)
|
|
|
+ {
|
|
|
+ $net = unpack('C*', $network->ip);
|
|
|
+ $mask = unpack('C*', $network->netmask);
|
|
|
+ $ip = unpack('C*', $target->ip);
|
|
|
+
|
|
|
+ for ($i = 1; $i < 17; ++$i) {
|
|
|
+ if (($net[$i] & $mask[$i]) !== ($ip[$i] & $mask[$i])) {
|
|
|
+ return false;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if ($match) {
|
|
|
- return true;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Finds or creates rule data for a hostname
|
|
|
+ *
|
|
|
+ * @param int $index
|
|
|
+ * @param string $hostName
|
|
|
+ *
|
|
|
+ * @return {null|stdClass} Null if the hostname is invalid
|
|
|
+ */
|
|
|
+ private function getRule($index, $hostName)
|
|
|
+ {
|
|
|
+ if (array_key_exists($index, $this->rules)) {
|
|
|
+ return $this->rules[$index];
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->rules[$index] = null;
|
|
|
+ list($host, $port, $err) = $this->splitHostPort($hostName);
|
|
|
+
|
|
|
+ if ($err || !$this->ipCheckData($host, $ipdata, true)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->rules[$index] = $this->makeData($host, $port, $ipdata);
|
|
|
+
|
|
|
+ return $this->rules[$index];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates an object containing IP data if the host is an IP address
|
|
|
+ *
|
|
|
+ * @param string $host
|
|
|
+ * @param null|stdclass $ipdata Set by method if IP address found
|
|
|
+ * @param bool $allowPrefix Whether a CIDR prefix-length is expected
|
|
|
+ *
|
|
|
+ * @return bool False if the host contains invalid data
|
|
|
+ */
|
|
|
+ private function ipCheckData($host, &$ipdata, $allowPrefix = false)
|
|
|
+ {
|
|
|
+ $ipdata = null;
|
|
|
+ $netmask = null;
|
|
|
+ $prefix = null;
|
|
|
+ $modified = false;
|
|
|
+
|
|
|
+ // Check for a CIDR prefix-length
|
|
|
+ if (strpos($host, '/') !== false) {
|
|
|
+ list($host, $prefix) = explode('/', $host);
|
|
|
+
|
|
|
+ if (!$allowPrefix || !$this->validateInt($prefix, 0, 128)) {
|
|
|
+ return false;
|
|
|
}
|
|
|
+ $prefix = (int) $prefix;
|
|
|
+ $modified = true;
|
|
|
}
|
|
|
|
|
|
- return false;
|
|
|
+ // See if this is an ip address
|
|
|
+ if (!filter_var($host, FILTER_VALIDATE_IP)) {
|
|
|
+ return !$modified;
|
|
|
+ }
|
|
|
+
|
|
|
+ list($ip, $size) = $this->ipGetAddr($host);
|
|
|
+
|
|
|
+ if ($prefix !== null) {
|
|
|
+ // Check for a valid prefix
|
|
|
+ if ($prefix > $size * 8) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ list($ip, $netmask) = $this->ipGetNetwork($ip, $size, $prefix);
|
|
|
+ }
|
|
|
+
|
|
|
+ $ipdata = $this->makeIpData($ip, $size, $netmask);
|
|
|
+
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Check an IP address against a CIDR
|
|
|
+ * Returns an array of the IP in_addr and its byte size
|
|
|
*
|
|
|
- * http://framework.zend.com/svn/framework/extras/incubator/library/ZendX/Whois/Adapter/Cidr.php
|
|
|
+ * IPv4 addresses are always mapped to IPv6, which simplifies handling
|
|
|
+ * and comparison.
|
|
|
*
|
|
|
- * @param string $cidr IPv4 block in CIDR notation
|
|
|
- * @param string $ip IPv4 address
|
|
|
+ * @param string $host
|
|
|
*
|
|
|
- * @return bool
|
|
|
+ * @return mixed[] in_addr, size
|
|
|
+ */
|
|
|
+ private function ipGetAddr($host)
|
|
|
+ {
|
|
|
+ $ip = inet_pton($host);
|
|
|
+ $size = strlen($ip);
|
|
|
+ $mapped = $this->ipMapTo6($ip, $size);
|
|
|
+
|
|
|
+ return array($mapped, $size);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the binary network mask mapped to IPv6
|
|
|
+ *
|
|
|
+ * @param string $prefix CIDR prefix-length
|
|
|
+ * @param int $size Byte size of in_addr
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ private function ipGetMask($prefix, $size)
|
|
|
+ {
|
|
|
+ $mask = '';
|
|
|
+
|
|
|
+ if ($ones = floor($prefix / 8)) {
|
|
|
+ $mask = str_repeat(chr(255), $ones);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($remainder = $prefix % 8) {
|
|
|
+ $mask .= chr(0xff ^ (0xff >> $remainder));
|
|
|
+ }
|
|
|
+
|
|
|
+ $mask = str_pad($mask, $size, chr(0));
|
|
|
+
|
|
|
+ return $this->ipMapTo6($mask, $size);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Calculates and returns the network and mask
|
|
|
+ *
|
|
|
+ * @param string $rangeIp IP in_addr
|
|
|
+ * @param int $size Byte size of in_addr
|
|
|
+ * @param string $prefix CIDR prefix-length
|
|
|
+ *
|
|
|
+ * @return string[] network in_addr, binary mask
|
|
|
*/
|
|
|
- private static function inCIDRBlock($cidr, $ip)
|
|
|
+ private function ipGetNetwork($rangeIp, $size, $prefix)
|
|
|
{
|
|
|
- // Get the base and the bits from the CIDR
|
|
|
- list($base, $bits) = explode('/', $cidr);
|
|
|
+ $netmask = $this->ipGetMask($prefix, $size);
|
|
|
|
|
|
- // Now split it up into it's classes
|
|
|
- list($a, $b, $c, $d) = explode('.', $base);
|
|
|
+ // Get the network from the address and mask
|
|
|
+ $mask = unpack('C*', $netmask);
|
|
|
+ $ip = unpack('C*', $rangeIp);
|
|
|
+ $net = '';
|
|
|
|
|
|
- // Now do some bit shifting/switching to convert to ints
|
|
|
- $i = ($a << 24) + ($b << 16) + ($c << 8) + $d;
|
|
|
- $mask = $bits == 0 ? 0 : (~0 << (32 - $bits));
|
|
|
+ for ($i = 1; $i < 17; ++$i) {
|
|
|
+ $net .= chr($ip[$i] & $mask[$i]);
|
|
|
+ }
|
|
|
|
|
|
- // Here's our lowest int
|
|
|
- $low = $i & $mask;
|
|
|
+ return array($net, $netmask);
|
|
|
+ }
|
|
|
|
|
|
- // Here's our highest int
|
|
|
- $high = $i | (~$mask & 0xFFFFFFFF);
|
|
|
+ /**
|
|
|
+ * Maps an IPv4 address to IPv6
|
|
|
+ *
|
|
|
+ * @param string $binary in_addr
|
|
|
+ * @param int $size Byte size of in_addr
|
|
|
+ *
|
|
|
+ * @return string Mapped or existing in_addr
|
|
|
+ */
|
|
|
+ private function ipMapTo6($binary, $size)
|
|
|
+ {
|
|
|
+ if ($size === 4) {
|
|
|
+ $prefix = str_repeat(chr(0), 10) . str_repeat(chr(255), 2);
|
|
|
+ $binary = $prefix . $binary;
|
|
|
+ }
|
|
|
|
|
|
- // Now split the ip we're checking against up into classes
|
|
|
- list($a, $b, $c, $d) = explode('.', $ip);
|
|
|
+ return $binary;
|
|
|
+ }
|
|
|
|
|
|
- // Now convert the ip we're checking against to an int
|
|
|
- $check = ($a << 24) + ($b << 16) + ($c << 8) + $d;
|
|
|
+ /**
|
|
|
+ * Creates a rule data object
|
|
|
+ *
|
|
|
+ * @param string $host
|
|
|
+ * @param int $port
|
|
|
+ * @param null|stdclass $ipdata
|
|
|
+ *
|
|
|
+ * @return stdclass
|
|
|
+ */
|
|
|
+ private function makeData($host, $port, $ipdata)
|
|
|
+ {
|
|
|
+ return (object) array(
|
|
|
+ 'host' => $host,
|
|
|
+ 'name' => '.' . ltrim($host, '.'),
|
|
|
+ 'port' => $port,
|
|
|
+ 'ipdata' => $ipdata,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates an ip data object
|
|
|
+ *
|
|
|
+ * @param string $ip in_addr
|
|
|
+ * @param int $size Byte size of in_addr
|
|
|
+ * @param null|string $netmask Network mask
|
|
|
+ *
|
|
|
+ * @return stdclass
|
|
|
+ */
|
|
|
+ private function makeIpData($ip, $size, $netmask)
|
|
|
+ {
|
|
|
+ return (object) array(
|
|
|
+ 'ip' => $ip,
|
|
|
+ 'size' => $size,
|
|
|
+ 'netmask' => $netmask,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Splits the hostname into host and port components
|
|
|
+ *
|
|
|
+ * @param string $hostName
|
|
|
+ *
|
|
|
+ * @return mixed[] host, port, if there was error
|
|
|
+ */
|
|
|
+ private function splitHostPort($hostName)
|
|
|
+ {
|
|
|
+ // host, port, err
|
|
|
+ $error = array('', '', true);
|
|
|
+ $port = 0;
|
|
|
+ $ip6 = '';
|
|
|
+
|
|
|
+ // Check for square-bracket notation
|
|
|
+ if ($hostName[0] === '[') {
|
|
|
+ $index = strpos($hostName, ']');
|
|
|
+
|
|
|
+ // The smallest ip6 address is ::
|
|
|
+ if (false === $index || $index < 3) {
|
|
|
+ return $error;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ip6 = substr($hostName, 1, $index - 1);
|
|
|
+ $hostName = substr($hostName, $index + 1);
|
|
|
+
|
|
|
+ if (strpbrk($hostName, '[]') !== false
|
|
|
+ || substr_count($hostName, ':') > 1) {
|
|
|
+ return $error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (substr_count($hostName, ':') === 1) {
|
|
|
+ $index = strpos($hostName, ':');
|
|
|
+ $port = substr($hostName, $index + 1);
|
|
|
+ $hostName = substr($hostName, 0, $index);
|
|
|
+
|
|
|
+ if (!$this->validateInt($port, 1, 65535)) {
|
|
|
+ return $error;
|
|
|
+ }
|
|
|
+
|
|
|
+ $port = (int) $port;
|
|
|
+ }
|
|
|
+
|
|
|
+ $host = $ip6 . $hostName;
|
|
|
+
|
|
|
+ return array($host, $port, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Wrapper around filter_var FILTER_VALIDATE_INT
|
|
|
+ *
|
|
|
+ * @param string $int
|
|
|
+ * @param int $min
|
|
|
+ * @param int $max
|
|
|
+ */
|
|
|
+ private function validateInt($int, $min, $max)
|
|
|
+ {
|
|
|
+ $options = array(
|
|
|
+ 'options' => array(
|
|
|
+ 'min_range' => $min,
|
|
|
+ 'max_range' => $max)
|
|
|
+ );
|
|
|
|
|
|
- // If the ip is within the range, including highest/lowest values,
|
|
|
- // then it's within the CIDR range
|
|
|
- return $check >= $low && $check <= $high;
|
|
|
+ return false !== filter_var($int, FILTER_VALIDATE_INT, $options);
|
|
|
}
|
|
|
}
|