PageRenderTime 50ms CodeModel.GetById 4ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/Util/NoProxyPattern.php

http://github.com/composer/composer
PHP | 435 lines | 219 code | 67 blank | 149 comment | 38 complexity | d74fb863b52e5f4aafd0411d77fbf065 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Util;
  12. use stdClass;
  13. /**
  14. * Tests URLs against NO_PROXY patterns
  15. */
  16. class NoProxyPattern
  17. {
  18. /**
  19. * @var string[]
  20. */
  21. protected $hostNames = array();
  22. /**
  23. * @var object[]
  24. */
  25. protected $rules = array();
  26. /**
  27. * @var bool
  28. */
  29. protected $noproxy;
  30. /**
  31. * @param string $pattern NO_PROXY pattern
  32. */
  33. public function __construct($pattern)
  34. {
  35. $this->hostNames = preg_split('{[\s,]+}', $pattern, null, PREG_SPLIT_NO_EMPTY);
  36. $this->noproxy = empty($this->hostNames) || '*' === $this->hostNames[0];
  37. }
  38. /**
  39. * Returns true if a URL matches the NO_PROXY pattern
  40. *
  41. * @param string $url
  42. *
  43. * @return bool
  44. */
  45. public function test($url)
  46. {
  47. if ($this->noproxy) {
  48. return true;
  49. }
  50. if (!$urlData = $this->getUrlData($url)) {
  51. return false;
  52. }
  53. foreach ($this->hostNames as $index => $hostName) {
  54. if ($this->match($index, $hostName, $urlData)) {
  55. return true;
  56. }
  57. }
  58. return false;
  59. }
  60. /**
  61. * Returns false is the url cannot be parsed, otherwise a data object
  62. *
  63. * @param string $url
  64. *
  65. * @return bool|stdclass
  66. */
  67. protected function getUrlData($url)
  68. {
  69. if (!$host = parse_url($url, PHP_URL_HOST)) {
  70. return false;
  71. }
  72. $port = parse_url($url, PHP_URL_PORT);
  73. if (empty($port)) {
  74. switch (parse_url($url, PHP_URL_SCHEME)) {
  75. case 'http':
  76. $port = 80;
  77. break;
  78. case 'https':
  79. $port = 443;
  80. break;
  81. }
  82. }
  83. $hostName = $host . ($port ? ':' . $port : '');
  84. list($host, $port, $err) = $this->splitHostPort($hostName);
  85. if ($err || !$this->ipCheckData($host, $ipdata)) {
  86. return false;
  87. }
  88. return $this->makeData($host, $port, $ipdata);
  89. }
  90. /**
  91. * Returns true if the url is matched by a rule
  92. *
  93. * @param int $index
  94. * @param string $hostName
  95. * @param string $url
  96. *
  97. * @return bool
  98. */
  99. protected function match($index, $hostName, $url)
  100. {
  101. if (!$rule = $this->getRule($index, $hostName)) {
  102. // Data must have been misformatted
  103. return false;
  104. }
  105. if ($rule->ipdata) {
  106. // Match ipdata first
  107. if (!$url->ipdata) {
  108. return false;
  109. }
  110. if ($rule->ipdata->netmask) {
  111. return $this->matchRange($rule->ipdata, $url->ipdata);
  112. }
  113. $match = $rule->ipdata->ip === $url->ipdata->ip;
  114. } else {
  115. // Match host and port
  116. $haystack = substr($url->name, - strlen($rule->name));
  117. $match = stripos($haystack, $rule->name) === 0;
  118. }
  119. if ($match && $rule->port) {
  120. $match = $rule->port === $url->port;
  121. }
  122. return $match;
  123. }
  124. /**
  125. * Returns true if the target ip is in the network range
  126. *
  127. * @param stdClass $network
  128. * @param stdClass $target
  129. *
  130. * @return bool
  131. */
  132. protected function matchRange(stdClass $network, stdClass $target)
  133. {
  134. $net = unpack('C*', $network->ip);
  135. $mask = unpack('C*', $network->netmask);
  136. $ip = unpack('C*', $target->ip);
  137. for ($i = 1; $i < 17; ++$i) {
  138. if (($net[$i] & $mask[$i]) !== ($ip[$i] & $mask[$i])) {
  139. return false;
  140. }
  141. }
  142. return true;
  143. }
  144. /**
  145. * Finds or creates rule data for a hostname
  146. *
  147. * @param int $index
  148. * @param string $hostName
  149. *
  150. * @return {null|stdClass} Null if the hostname is invalid
  151. */
  152. private function getRule($index, $hostName)
  153. {
  154. if (array_key_exists($index, $this->rules)) {
  155. return $this->rules[$index];
  156. }
  157. $this->rules[$index] = null;
  158. list($host, $port, $err) = $this->splitHostPort($hostName);
  159. if ($err || !$this->ipCheckData($host, $ipdata, true)) {
  160. return null;
  161. }
  162. $this->rules[$index] = $this->makeData($host, $port, $ipdata);
  163. return $this->rules[$index];
  164. }
  165. /**
  166. * Creates an object containing IP data if the host is an IP address
  167. *
  168. * @param string $host
  169. * @param null|stdclass $ipdata Set by method if IP address found
  170. * @param bool $allowPrefix Whether a CIDR prefix-length is expected
  171. *
  172. * @return bool False if the host contains invalid data
  173. */
  174. private function ipCheckData($host, &$ipdata, $allowPrefix = false)
  175. {
  176. $ipdata = null;
  177. $netmask = null;
  178. $prefix = null;
  179. $modified = false;
  180. // Check for a CIDR prefix-length
  181. if (strpos($host, '/') !== false) {
  182. list($host, $prefix) = explode('/', $host);
  183. if (!$allowPrefix || !$this->validateInt($prefix, 0, 128)) {
  184. return false;
  185. }
  186. $prefix = (int) $prefix;
  187. $modified = true;
  188. }
  189. // See if this is an ip address
  190. if (!filter_var($host, FILTER_VALIDATE_IP)) {
  191. return !$modified;
  192. }
  193. list($ip, $size) = $this->ipGetAddr($host);
  194. if ($prefix !== null) {
  195. // Check for a valid prefix
  196. if ($prefix > $size * 8) {
  197. return false;
  198. }
  199. list($ip, $netmask) = $this->ipGetNetwork($ip, $size, $prefix);
  200. }
  201. $ipdata = $this->makeIpData($ip, $size, $netmask);
  202. return true;
  203. }
  204. /**
  205. * Returns an array of the IP in_addr and its byte size
  206. *
  207. * IPv4 addresses are always mapped to IPv6, which simplifies handling
  208. * and comparison.
  209. *
  210. * @param string $host
  211. *
  212. * @return mixed[] in_addr, size
  213. */
  214. private function ipGetAddr($host)
  215. {
  216. $ip = inet_pton($host);
  217. $size = strlen($ip);
  218. $mapped = $this->ipMapTo6($ip, $size);
  219. return array($mapped, $size);
  220. }
  221. /**
  222. * Returns the binary network mask mapped to IPv6
  223. *
  224. * @param string $prefix CIDR prefix-length
  225. * @param int $size Byte size of in_addr
  226. *
  227. * @return string
  228. */
  229. private function ipGetMask($prefix, $size)
  230. {
  231. $mask = '';
  232. if ($ones = floor($prefix / 8)) {
  233. $mask = str_repeat(chr(255), $ones);
  234. }
  235. if ($remainder = $prefix % 8) {
  236. $mask .= chr(0xff ^ (0xff >> $remainder));
  237. }
  238. $mask = str_pad($mask, $size, chr(0));
  239. return $this->ipMapTo6($mask, $size);
  240. }
  241. /**
  242. * Calculates and returns the network and mask
  243. *
  244. * @param string $rangeIp IP in_addr
  245. * @param int $size Byte size of in_addr
  246. * @param string $prefix CIDR prefix-length
  247. *
  248. * @return string[] network in_addr, binary mask
  249. */
  250. private function ipGetNetwork($rangeIp, $size, $prefix)
  251. {
  252. $netmask = $this->ipGetMask($prefix, $size);
  253. // Get the network from the address and mask
  254. $mask = unpack('C*', $netmask);
  255. $ip = unpack('C*', $rangeIp);
  256. $net = '';
  257. for ($i = 1; $i < 17; ++$i) {
  258. $net .= chr($ip[$i] & $mask[$i]);
  259. }
  260. return array($net, $netmask);
  261. }
  262. /**
  263. * Maps an IPv4 address to IPv6
  264. *
  265. * @param string $binary in_addr
  266. * @param int $size Byte size of in_addr
  267. *
  268. * @return string Mapped or existing in_addr
  269. */
  270. private function ipMapTo6($binary, $size)
  271. {
  272. if ($size === 4) {
  273. $prefix = str_repeat(chr(0), 10) . str_repeat(chr(255), 2);
  274. $binary = $prefix . $binary;
  275. }
  276. return $binary;
  277. }
  278. /**
  279. * Creates a rule data object
  280. *
  281. * @param string $host
  282. * @param int $port
  283. * @param null|stdclass $ipdata
  284. *
  285. * @return stdclass
  286. */
  287. private function makeData($host, $port, $ipdata)
  288. {
  289. return (object) array(
  290. 'host' => $host,
  291. 'name' => '.' . ltrim($host, '.'),
  292. 'port' => $port,
  293. 'ipdata' => $ipdata,
  294. );
  295. }
  296. /**
  297. * Creates an ip data object
  298. *
  299. * @param string $ip in_addr
  300. * @param int $size Byte size of in_addr
  301. * @param null|string $netmask Network mask
  302. *
  303. * @return stdclass
  304. */
  305. private function makeIpData($ip, $size, $netmask)
  306. {
  307. return (object) array(
  308. 'ip' => $ip,
  309. 'size' => $size,
  310. 'netmask' => $netmask,
  311. );
  312. }
  313. /**
  314. * Splits the hostname into host and port components
  315. *
  316. * @param string $hostName
  317. *
  318. * @return mixed[] host, port, if there was error
  319. */
  320. private function splitHostPort($hostName)
  321. {
  322. // host, port, err
  323. $error = array('', '', true);
  324. $port = 0;
  325. $ip6 = '';
  326. // Check for square-bracket notation
  327. if ($hostName[0] === '[') {
  328. $index = strpos($hostName, ']');
  329. // The smallest ip6 address is ::
  330. if (false === $index || $index < 3) {
  331. return $error;
  332. }
  333. $ip6 = substr($hostName, 1, $index - 1);
  334. $hostName = substr($hostName, $index + 1);
  335. if (strpbrk($hostName, '[]') !== false
  336. || substr_count($hostName, ':') > 1) {
  337. return $error;
  338. }
  339. }
  340. if (substr_count($hostName, ':') === 1) {
  341. $index = strpos($hostName, ':');
  342. $port = substr($hostName, $index + 1);
  343. $hostName = substr($hostName, 0, $index);
  344. if (!$this->validateInt($port, 1, 65535)) {
  345. return $error;
  346. }
  347. $port = (int) $port;
  348. }
  349. $host = $ip6 . $hostName;
  350. return array($host, $port, false);
  351. }
  352. /**
  353. * Wrapper around filter_var FILTER_VALIDATE_INT
  354. *
  355. * @param string $int
  356. * @param int $min
  357. * @param int $max
  358. */
  359. private function validateInt($int, $min, $max)
  360. {
  361. $options = array(
  362. 'options' => array(
  363. 'min_range' => $min,
  364. 'max_range' => $max)
  365. );
  366. return false !== filter_var($int, FILTER_VALIDATE_INT, $options);
  367. }
  368. }