StreamContextFactory.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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 HTTP_PROXY/http_proxy on CLI only for security reasons
  38. if (PHP_SAPI === 'cli' && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) {
  39. $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']);
  40. }
  41. // Prefer CGI_HTTP_PROXY if available
  42. if (!empty($_SERVER['CGI_HTTP_PROXY'])) {
  43. $proxy = parse_url($_SERVER['CGI_HTTP_PROXY']);
  44. }
  45. // Override with HTTPS proxy if present and URL is https
  46. if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) {
  47. $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']);
  48. }
  49. // Remove proxy if URL matches no_proxy directive
  50. if (!empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) {
  51. $pattern = new NoProxyPattern($_SERVER['no_proxy']);
  52. if ($pattern->test($url)) {
  53. unset($proxy);
  54. }
  55. }
  56. if (!empty($proxy)) {
  57. $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : '';
  58. $proxyURL .= isset($proxy['host']) ? $proxy['host'] : '';
  59. if (isset($proxy['port'])) {
  60. $proxyURL .= ":" . $proxy['port'];
  61. } elseif ('http://' == substr($proxyURL, 0, 7)) {
  62. $proxyURL .= ":80";
  63. } elseif ('https://' == substr($proxyURL, 0, 8)) {
  64. $proxyURL .= ":443";
  65. }
  66. // http(s):// is not supported in proxy
  67. $proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL);
  68. if (0 === strpos($proxyURL, 'ssl:') && !extension_loaded('openssl')) {
  69. throw new \RuntimeException('You must enable the openssl extension to use a proxy over https');
  70. }
  71. $options['http']['proxy'] = $proxyURL;
  72. // enabled request_fulluri unless it is explicitly disabled
  73. switch (parse_url($url, PHP_URL_SCHEME)) {
  74. case 'http': // default request_fulluri to true
  75. $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI');
  76. if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
  77. $options['http']['request_fulluri'] = true;
  78. }
  79. break;
  80. case 'https': // default request_fulluri to true
  81. $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI');
  82. if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) {
  83. $options['http']['request_fulluri'] = true;
  84. }
  85. break;
  86. }
  87. // add SNI opts for https URLs
  88. if ('https' === parse_url($url, PHP_URL_SCHEME)) {
  89. $options['ssl']['SNI_enabled'] = true;
  90. if (PHP_VERSION_ID < 50600) {
  91. $options['ssl']['SNI_server_name'] = parse_url($url, PHP_URL_HOST);
  92. }
  93. }
  94. // handle proxy auth if present
  95. if (isset($proxy['user'])) {
  96. $auth = urldecode($proxy['user']);
  97. if (isset($proxy['pass'])) {
  98. $auth .= ':' . urldecode($proxy['pass']);
  99. }
  100. $auth = base64_encode($auth);
  101. // Preserve headers if already set in default options
  102. if (isset($defaultOptions['http']['header'])) {
  103. if (is_string($defaultOptions['http']['header'])) {
  104. $defaultOptions['http']['header'] = array($defaultOptions['http']['header']);
  105. }
  106. $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}";
  107. } else {
  108. $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}");
  109. }
  110. }
  111. }
  112. $options = array_replace_recursive($options, $defaultOptions);
  113. if (isset($options['http']['header'])) {
  114. $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']);
  115. }
  116. if (defined('HHVM_VERSION')) {
  117. $phpVersion = 'HHVM ' . HHVM_VERSION;
  118. } else {
  119. $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
  120. }
  121. if (!isset($options['http']['header']) || false === strpos(strtolower(implode('', $options['http']['header'])), 'user-agent')) {
  122. $options['http']['header'][] = sprintf(
  123. 'User-Agent: Composer/%s (%s; %s; %s)',
  124. Composer::VERSION === '@package_version@' ? 'source' : Composer::VERSION,
  125. php_uname('s'),
  126. php_uname('r'),
  127. $phpVersion
  128. );
  129. }
  130. return stream_context_create($options, $defaultParams);
  131. }
  132. /**
  133. * A bug in PHP prevents the headers from correctly being sent when a content-type header is present and
  134. * NOT at the end of the array
  135. *
  136. * This method fixes the array by moving the content-type header to the end
  137. *
  138. * @link https://bugs.php.net/bug.php?id=61548
  139. * @param $header
  140. * @return array
  141. */
  142. private static function fixHttpHeaderField($header)
  143. {
  144. if (!is_array($header)) {
  145. $header = explode("\r\n", $header);
  146. }
  147. uasort($header, function ($el) {
  148. return preg_match('{^content-type}i', $el) ? 1 : -1;
  149. });
  150. return $header;
  151. }
  152. }