StreamContextFactory.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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 Composer\Composer;
  13. /**
  14. * Allows the creation of a basic context supporting http proxy
  15. *
  16. * @author Jordan Alliot <jordan.alliot@gmail.com>
  17. * @author Markus Tacker <m@coderbyheart.de>
  18. */
  19. final class StreamContextFactory
  20. {
  21. /**
  22. * Creates a context supporting HTTP proxies
  23. *
  24. * @param string $url URL the context is to be used for
  25. * @param array $defaultOptions Options to merge with the default
  26. * @param array $defaultParams Parameters to specify on the context
  27. * @throws \RuntimeException if https proxy required and OpenSSL uninstalled
  28. * @return resource Default context
  29. */
  30. public static function getContext($url, array $defaultOptions = array(), array $defaultParams = array())
  31. {
  32. $options = array('http' => array(
  33. // specify defaults again to try and work better with curlwrappers enabled
  34. 'follow_location' => 1,
  35. 'max_redirects' => 20,
  36. ));
  37. // Handle system proxy
  38. if (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy'])) {
  39. // Some systems seem to rely on a lowercased version instead...
  40. $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']);
  41. }
  42. // Override with HTTPS proxy if present and URL is https
  43. if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) {
  44. $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']);
  45. }
  46. // Remove proxy if URL matches no_proxy directive
  47. if (!empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) {
  48. $pattern = new NoProxyPattern($_SERVER['no_proxy']);
  49. if ($pattern->test($url)) {
  50. unset($proxy);
  51. }
  52. }
  53. if (!empty($proxy)) {
  54. $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : '';
  55. $proxyURL .= isset($proxy['host']) ? $proxy['host'] : '';
  56. if (isset($proxy['port'])) {
  57. $proxyURL .= ":" . $proxy['port'];
  58. } elseif ('http://' == substr($proxyURL, 0, 7)) {
  59. $proxyURL .= ":80";
  60. } elseif ('https://' == substr($proxyURL, 0, 8)) {
  61. $proxyURL .= ":443";
  62. }
  63. // http(s):// is not supported in proxy
  64. $proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL);
  65. if (0 === strpos($proxyURL, 'ssl:') && !extension_loaded('openssl')) {
  66. throw new \RuntimeException('You must enable the openssl extension to use a proxy over https');
  67. }
  68. $options['http']['proxy'] = $proxyURL;
  69. // enabled request_fulluri unless it is explicitly disabled
  70. switch (parse_url($url, PHP_URL_SCHEME)) {
  71. case 'http': // default request_fulluri to true
  72. $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI');
  73. if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
  74. $options['http']['request_fulluri'] = true;
  75. }
  76. break;
  77. case 'https': // default request_fulluri to true
  78. $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI');
  79. if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
  80. $options['http']['request_fulluri'] = true;
  81. }
  82. break;
  83. }
  84. // add SNI opts for https URLs
  85. if ('https' === parse_url($url, PHP_URL_SCHEME)) {
  86. $options['ssl']['SNI_enabled'] = true;
  87. if (PHP_VERSION_ID < 50600) {
  88. $options['ssl']['SNI_server_name'] = parse_url($url, PHP_URL_HOST);
  89. }
  90. }
  91. // handle proxy auth if present
  92. if (isset($proxy['user'])) {
  93. $auth = urldecode($proxy['user']);
  94. if (isset($proxy['pass'])) {
  95. $auth .= ':' . urldecode($proxy['pass']);
  96. }
  97. $auth = base64_encode($auth);
  98. // Preserve headers if already set in default options
  99. if (isset($defaultOptions['http']['header'])) {
  100. if (is_string($defaultOptions['http']['header'])) {
  101. $defaultOptions['http']['header'] = array($defaultOptions['http']['header']);
  102. }
  103. $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}";
  104. } else {
  105. $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}");
  106. }
  107. }
  108. }
  109. $options = array_replace_recursive($options, $defaultOptions);
  110. if (isset($options['http']['header'])) {
  111. $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']);
  112. }
  113. if (defined('HHVM_VERSION')) {
  114. $phpVersion = 'HHVM ' . HHVM_VERSION;
  115. } else {
  116. $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
  117. }
  118. if (!isset($options['http']['header']) || false === strpos(strtolower(implode('', $options['http']['header'])), 'user-agent')) {
  119. $options['http']['header'][] = sprintf(
  120. 'User-Agent: Composer/%s (%s; %s; %s)',
  121. Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION,
  122. php_uname('s'),
  123. php_uname('r'),
  124. $phpVersion
  125. );
  126. }
  127. return stream_context_create($options, $defaultParams);
  128. }
  129. /**
  130. * A bug in PHP prevents the headers from correctly being sent when a content-type header is present and
  131. * NOT at the end of the array
  132. *
  133. * This method fixes the array by moving the content-type header to the end
  134. *
  135. * @link https://bugs.php.net/bug.php?id=61548
  136. * @param $header
  137. * @return array
  138. */
  139. private static function fixHttpHeaderField($header)
  140. {
  141. if (!is_array($header)) {
  142. $header = explode("\r\n", $header);
  143. }
  144. uasort($header, function ($el) {
  145. return preg_match('{^content-type}i', $el) ? 1 : -1;
  146. });
  147. return $header;
  148. }
  149. }