StreamContextFactory.php 6.1 KB

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