diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 7e12870c6..9ab716e58 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -41,6 +41,9 @@ class CurlDownloader private $authHelper; private $selectTimeout = 5.0; private $maxRedirects = 20; + /** @var ProxyManager */ + private $proxyManager; + private $supportsSecureProxy; protected $multiErrors = array( CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'), CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."), @@ -92,6 +95,11 @@ class CurlDownloader } $this->authHelper = new AuthHelper($io, $config); + $this->proxyManager = ProxyManager::getInstance(); + + $version = curl_version(); + $features = $version['features']; + $this->supportsSecureProxy = defined('CURL_VERSION_HTTPS_PROXY') && ($features & CURL_VERSION_HTTPS_PROXY); } /** @@ -176,7 +184,8 @@ class CurlDownloader } $options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url); - $options = StreamContextFactory::initOptions($url, $options); + // Merge in headers - we don't get any proxy values + $options = StreamContextFactory::initOptions($url, $options, true); foreach (self::$options as $type => $curlOptions) { foreach ($curlOptions as $name => $curlOption) { @@ -190,6 +199,25 @@ class CurlDownloader } } + // Always set CURLOPT_PROXY to enable/disable proxy handling + // Any proxy authorization is included in the proxy url + $proxy = $this->proxyManager->getProxyForRequest($url); + curl_setopt($curlHandle, CURLOPT_PROXY, $proxy->getUrl()); + + // Curl needs certificate locations for secure proxies. + // CURLOPT_PROXY_SSL_VERIFY_PEER/HOST are enabled by default + if ($proxy->isSecure()) { + if (!$this->supportsSecureProxy) { + throw new TransportException('Connecting to a secure proxy using curl is not supported on PHP versions below 7.3.0.'); + } + if (!empty($options['ssl']['cafile'])) { + curl_setopt($curlHandle, CURLOPT_PROXY_CAINFO, $options['ssl']['cafile']); + } + if (!empty($options['ssl']['capath'])) { + curl_setopt($curlHandle, CURLOPT_PROXY_CAPATH, $options['ssl']['capath']); + } + } + $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); $this->jobs[(int) $curlHandle] = array( @@ -206,7 +234,7 @@ class CurlDownloader 'reject' => $reject, ); - $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : ''; + $usingProxy = $proxy->getLastProxy(' using proxy (%s)'); $ifModified = false !== stripos(implode(',', $options['http']['header']), 'if-modified-since:') ? ' if modified' : ''; if ($attributes['redirects'] === 0) { $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); diff --git a/src/Composer/Util/Http/ProxyHelper.php b/src/Composer/Util/Http/ProxyHelper.php new file mode 100644 index 000000000..2f03de735 --- /dev/null +++ b/src/Composer/Util/Http/ProxyHelper.php @@ -0,0 +1,179 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +/** + * Proxy discovery and helper class + * + * @internal + * @author John Stevenson + */ +class ProxyHelper +{ + /** + * Returns proxy environment values + * + * @throws \RuntimeException on malformed url + * @return array httpProxy, httpsProxy, noProxy values + */ + public static function getProxyData() + { + $httpProxy = null; + $httpsProxy = null; + + // Handle http_proxy/HTTP_PROXY on CLI only for security reasons + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + if ($env = self::getProxyEnv(array('http_proxy', 'HTTP_PROXY'), $name)) { + $httpProxy = self::checkProxy($env, $name); + } + } + + // Prefer CGI_HTTP_PROXY if available + if ($env = self::getProxyEnv(array('CGI_HTTP_PROXY'), $name)) { + $httpProxy = self::checkProxy($env, $name); + } + + // Handle https_proxy/HTTPS_PROXY + if ($env = self::getProxyEnv(array('https_proxy', 'HTTPS_PROXY'), $name)) { + $httpsProxy = self::checkProxy($env, $name); + } else { + $httpsProxy = $httpProxy; + } + + // Handle no_proxy + $noProxy = self::getProxyEnv(array('no_proxy', 'NO_PROXY'), $name); + + return array($httpProxy, $httpsProxy, $noProxy); + } + + /** + * Returns http context options for the proxy url + * + * @param string $proxyUrl + * @return array + */ + public static function getContextOptions($proxyUrl) + { + $proxy = parse_url($proxyUrl); + + // Remove any authorization + $proxyUrl = self::formatParsedUrl($proxy, false); + $proxyUrl = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyUrl); + + $options['http']['proxy'] = $proxyUrl; + + // Handle any authorization + if (isset($proxy['user'])) { + $auth = rawurldecode($proxy['user']); + + if (isset($proxy['pass'])) { + $auth .= ':' . rawurldecode($proxy['pass']); + } + $auth = base64_encode($auth); + // Set header as a string + $options['http']['header'] = "Proxy-Authorization: Basic {$auth}"; + } + + return $options; + } + + /** + * Sets/unsets request_fulluri value in http context options array + * + * @param string $requestUrl + * @param array $options Set by method + */ + public static function setRequestFullUri($requestUrl, array &$options) + { + if ('http' === parse_url($requestUrl, PHP_URL_SCHEME)) { + $options['http']['request_fulluri'] = true; + } else { + unset($options['http']['request_fulluri']); + } + } + + /** + * Searches $_SERVER for case-sensitive values + * + * @param array $names Names to search for + * @param mixed $name Name of any found value + * @return string|null The found value + */ + private static function getProxyEnv(array $names, &$name) + { + foreach ($names as $name) { + if (!empty($_SERVER[$name])) { + return $_SERVER[$name]; + } + } + } + + /** + * Checks and formats a proxy url from the environment + * + * @param string $proxyUrl + * @param string $envName + * @throws \RuntimeException on malformed url + * @return string The formatted proxy url + */ + private static function checkProxy($proxyUrl, $envName) + { + $error = sprintf('malformed %s url', $envName); + $proxy = parse_url($proxyUrl); + + if (!isset($proxy['host'])) { + throw new \RuntimeException($error); + } + + $proxyUrl = self::formatParsedUrl($proxy, true); + + if (!parse_url($proxyUrl, PHP_URL_PORT)) { + throw new \RuntimeException($error); + } + + return $proxyUrl; + } + + /** + * Formats a url from its component parts + * + * @param array $proxy Values from parse_url + * @param bool $includeAuth Whether to include authorization values + * @return string The formatted value + */ + private static function formatParsedUrl(array $proxy, $includeAuth) + { + $proxyUrl = isset($proxy['scheme']) ? strtolower($proxy['scheme']) . '://' : ''; + + if ($includeAuth && isset($proxy['user'])) { + $proxyUrl .= $proxy['user']; + + if (isset($proxy['pass'])) { + $proxyUrl .= ':' . $proxy['pass']; + } + $proxyUrl .= '@'; + } + + $proxyUrl .= $proxy['host']; + + if (isset($proxy['port'])) { + $proxyUrl .= ':' . $proxy['port']; + } elseif (strpos($proxyUrl, 'http://') === 0) { + $proxyUrl .= ':80'; + } elseif (strpos($proxyUrl, 'https://') === 0) { + $proxyUrl .= ':443'; + } + + return $proxyUrl; + } +} diff --git a/src/Composer/Util/Http/ProxyManager.php b/src/Composer/Util/Http/ProxyManager.php new file mode 100644 index 000000000..be8983771 --- /dev/null +++ b/src/Composer/Util/Http/ProxyManager.php @@ -0,0 +1,178 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +use Composer\Downloader\TransportException; +use Composer\Util\NoProxyPattern; +use Composer\Util\Url; + +/** + * @internal + * @author John Stevenson + */ +class ProxyManager +{ + private $error; + private $fullProxy; + private $safeProxy; + private $streams; + private $hasProxy; + private $info; + private $lastProxy; + /** @var NoProxyPattern */ + private $noProxyHandler; + + /** @var ProxyManager */ + private static $instance; + + private function __construct() + { + $this->fullProxy = $this->safeProxy = array( + 'http' => null, + 'https' => null, + ); + + $this->streams['http'] = $this->streams['https'] = array( + 'options' => null, + ); + + $this->hasProxy = false; + $this->initProxyData(); + } + + /** + * @return ProxyManager * + */ + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Clears the persistent instance + */ + public static function reset() + { + self::$instance = null; + } + + /** + * Returns a RequestProxy instance for the request url + * + * @param string $requestUrl + * @return RequestProxy + */ + public function getProxyForRequest($requestUrl) + { + if ($this->error) { + throw new TransportException('Unable to use a proxy: '.$this->error); + } + + $scheme = parse_url($requestUrl, PHP_URL_SCHEME) ?: 'http'; + $proxyUrl = ''; + $options = array(); + $lastProxy = ''; + + if ($this->hasProxy && $this->fullProxy[$scheme]) { + if ($this->noProxy($requestUrl)) { + $lastProxy = 'excluded by no_proxy'; + } else { + $proxyUrl = $this->fullProxy[$scheme]; + $options = $this->streams[$scheme]['options']; + ProxyHelper::setRequestFullUri($requestUrl, $options); + $lastProxy = $this->safeProxy[$scheme]; + } + } + + return new RequestProxy($proxyUrl, $options, $lastProxy); + } + + /** + * Returns true if a proxy is being used + * + * @param string|null $message Set to safe proxy values + * @return bool If false any error will be in $message + */ + public function getStatus(&$message) + { + $message = $this->hasProxy ? $this->info : $this->error; + return $this->hasProxy; + } + + /** + * Initializes proxy values from the environment + */ + private function initProxyData() + { + try { + list($httpProxy, $httpsProxy, $noProxy) = ProxyHelper::getProxyData(); + } catch (\RuntimeException $e) { + $this->error = $e->getMessage(); + return; + } + + $info = array(); + + if ($httpProxy) { + $info[] = $this->setData($httpProxy, 'http'); + } + if ($httpsProxy) { + $info[] = $this->setData($httpsProxy, 'https'); + } + if ($this->hasProxy) { + $this->info = implode(', ', $info); + if ($noProxy) { + $this->noProxyHandler = array(new NoProxyPattern($noProxy), 'test'); + } + } + } + + /** + * Sets initial data + * + * @param string $proxyUrl Proxy url + * @param string $scheme Environment variable scheme + */ + private function setData($url, $scheme) + { + $safeProxy = Url::sanitize($url); + $this->fullProxy[$scheme] = $url; + $this->safeProxy[$scheme] = $safeProxy; + $this->streams[$scheme]['options'] = ProxyHelper::getContextOptions($url); + $this->hasProxy = true; + + return sprintf('%s=%s', $scheme, $safeProxy); + } + + /** + * Returns true if a url matches no_proxy value + * + * @param string $requestUrl + * @return bool + */ + private function noProxy($requestUrl) + { + if ($this->noProxyHandler) { + if (call_user_func($this->noProxyHandler, $requestUrl)) { + $this->lastProxy = 'excluded by no_proxy'; + return true; + } + } + + return false; + } +} diff --git a/src/Composer/Util/Http/RequestProxy.php b/src/Composer/Util/Http/RequestProxy.php new file mode 100644 index 000000000..44f0c27cc --- /dev/null +++ b/src/Composer/Util/Http/RequestProxy.php @@ -0,0 +1,89 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +use Composer\Util\Url; + +/** + * @internal + * @author John Stevenson + */ +class RequestProxy +{ + private $contextOptions; + private $isSecure; + private $lastProxy; + private $safeUrl; + private $url; + + /** + * @param string $url + * @param array $contextOptions + * @param string $lastProxy + */ + public function __construct($url, array $contextOptions, $lastProxy) + { + $this->url = $url; + $this->contextOptions = $contextOptions; + $this->lastProxy = $lastProxy; + $this->safeUrl = Url::sanitize($url); + $this->isSecure = 0 === strpos($url, 'https://'); + } + + /** + * Returns an array of context options + * + * @return array + */ + public function getContextOptions() + { + return $this->contextOptions; + } + + /** + * Returns the safe proxy url from the last request + * + * @param string|null $format Output format specifier + * @return string Safe proxy, no proxy or empty + */ + public function getLastProxy($format = '') + { + $result = ''; + if ($this->lastProxy) { + $format = $format ?: '%s'; + $result = sprintf($format, $this->lastProxy); + } + + return $result; + } + + /** + * Returns the proxy url + * + * @return string Proxy url or empty + */ + public function getUrl() + { + return $this->url; + } + + /** + * Returns true if this is a secure-proxy + * + * @return bool False if not secure or there is no proxy + */ + public function isSecure() + { + return $this->isSecure; + } +} diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 03cd0cce8..c5e5eba53 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -18,6 +18,7 @@ use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; use Composer\Util\Http\Response; +use Composer\Util\Http\ProxyManager; /** * @internal @@ -46,6 +47,7 @@ class RemoteFilesystem private $degradedMode = false; private $redirects; private $maxRedirects = 20; + private $proxyManager; /** * Constructor. @@ -72,6 +74,7 @@ class RemoteFilesystem $this->options = array_replace_recursive($this->options, $options); $this->config = $config; $this->authHelper = isset($authHelper) ? $authHelper : new AuthHelper($io, $config); + $this->proxyManager = ProxyManager::getInstance(); } /** @@ -251,10 +254,10 @@ class RemoteFilesystem $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet'))); - $actualContextOptions = stream_context_get_options($ctx); - $usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : ''; + $proxy = ProxyManager::getInstance()->getProxyForRequest($fileUrl); + $usingProxy = $proxy->getLastProxy(' using proxy (%s)'); $this->io->writeError((strpos($origFileUrl, 'http') === 0 ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); - unset($origFileUrl, $actualContextOptions); + unset($origFileUrl, $proxy, $usingProxy); // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 if ((!preg_match('{^http://(repo\.)?packagist\.org/p/}', $fileUrl) || (false === strpos($fileUrl, '$') && false === strpos($fileUrl, '%24'))) && empty($degradedPackagist) && $this->config) { diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index d81119aff..e62bb7e52 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -15,6 +15,7 @@ namespace Composer\Util; use Composer\Composer; use Composer\CaBundle\CaBundle; use Composer\Downloader\TransportException; +use Composer\Util\Http\ProxyManager; use Psr\Log\LoggerInterface; /** @@ -57,10 +58,11 @@ final class StreamContextFactory /** * @param string $url * @param array $options + * @param bool $forCurl * @psalm-return array{http:{header: string[], proxy?: string, request_fulluri: bool}, ssl: array} * @return array formatted as a stream context array */ - public static function initOptions($url, array $options) + public static function initOptions($url, array $options, $forCurl = false) { // Make sure the headers are in an array form if (!isset($options['http']['header'])) { @@ -70,76 +72,26 @@ final class StreamContextFactory $options['http']['header'] = explode("\r\n", $options['http']['header']); } - // Handle HTTP_PROXY/http_proxy on CLI only for security reasons - if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { - $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); - } - - // Prefer CGI_HTTP_PROXY if available - if (!empty($_SERVER['CGI_HTTP_PROXY'])) { - $proxy = parse_url($_SERVER['CGI_HTTP_PROXY']); - } - - // Override with HTTPS proxy if present and URL is https - if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) { - $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']); - } - - // Remove proxy if URL matches no_proxy directive - if (!empty($_SERVER['NO_PROXY']) || !empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) { - $pattern = new NoProxyPattern(!empty($_SERVER['no_proxy']) ? $_SERVER['no_proxy'] : $_SERVER['NO_PROXY']); - if ($pattern->test($url)) { - unset($proxy); - } - } - - if (!empty($proxy)) { - $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : ''; - $proxyURL .= isset($proxy['host']) ? $proxy['host'] : ''; - - if (isset($proxy['port'])) { - $proxyURL .= ":" . $proxy['port']; - } elseif (strpos($proxyURL, 'http://') === 0) { - $proxyURL .= ":80"; - } elseif (strpos($proxyURL, 'https://') === 0) { - $proxyURL .= ":443"; - } - - // http(s):// is not supported in proxy - $proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL); - - if (0 === strpos($proxyURL, 'ssl:') && !extension_loaded('openssl')) { - throw new \RuntimeException('You must enable the openssl extension to use a proxy over https'); - } - - $options['http']['proxy'] = $proxyURL; - - // enabled request_fulluri unless it is explicitly disabled - switch (parse_url($url, PHP_URL_SCHEME)) { - case 'http': // default request_fulluri to true for HTTP - $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI'); - if ($reqFullUriEnv === false || $reqFullUriEnv === '' || (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv)) { - $options['http']['request_fulluri'] = true; - } - break; - case 'https': // default request_fulluri to false for HTTPS - $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI'); - if (strtolower($reqFullUriEnv) !== 'false' && (bool) $reqFullUriEnv) { - $options['http']['request_fulluri'] = true; - } - break; - } - - // handle proxy auth if present - if (isset($proxy['user'])) { - $auth = rawurldecode($proxy['user']); - if (isset($proxy['pass'])) { - $auth .= ':' . rawurldecode($proxy['pass']); + // Add stream proxy options if there is a proxy + if (!$forCurl) { + $proxy = ProxyManager::getInstance()->getProxyForRequest($url); + if ($proxy->isSecure()) { + if (!extension_loaded('openssl')) { + throw new TransportException('You must enable the openssl extension to use a proxy over https.'); + } + if (0 === strpos($url, 'https://')) { + throw new TransportException('PHP does not support https requests to a secure proxy.'); } - $auth = base64_encode($auth); - - $options['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; } + + $proxyOptions = $proxy->getContextOptions(); + + // Header will be a Proxy-Authorization string or not set + if (isset($proxyOptions['http']['header'])) { + $options['http']['header'][] = $proxyOptions['http']['header']; + unset($proxyOptions['http']['header']); + } + $options = array_replace_recursive($options, $proxyOptions); } if (defined('HHVM_VERSION')) { @@ -148,7 +100,7 @@ final class StreamContextFactory $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; } - if (extension_loaded('curl')) { + if ($forCurl) { $curl = curl_version(); $httpVersion = 'curl '.$curl['version']; } else { diff --git a/tests/Composer/Test/Util/Http/ProxyHelperTest.php b/tests/Composer/Test/Util/Http/ProxyHelperTest.php new file mode 100644 index 000000000..2b31fb470 --- /dev/null +++ b/tests/Composer/Test/Util/Http/ProxyHelperTest.php @@ -0,0 +1,190 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util\Http; + +use Composer\Util\Http\ProxyHelper; +use Composer\Test\TestCase; + +class ProxyHelperTest extends TestCase +{ + protected function setUp() + { + unset( + $_SERVER['HTTP_PROXY'], + $_SERVER['http_proxy'], + $_SERVER['HTTPS_PROXY'], + $_SERVER['https_proxy'], + $_SERVER['NO_PROXY'], + $_SERVER['no_proxy'], + $_SERVER['CGI_HTTP_PROXY'] + ); + } + + protected function tearDown() + { + unset( + $_SERVER['HTTP_PROXY'], + $_SERVER['http_proxy'], + $_SERVER['HTTPS_PROXY'], + $_SERVER['https_proxy'], + $_SERVER['NO_PROXY'], + $_SERVER['no_proxy'], + $_SERVER['CGI_HTTP_PROXY'] + ); + } + + /** + * @dataProvider dataMalformed + */ + public function testThrowsOnMalformedUrl($url) + { + $_SERVER['http_proxy'] = $url; + + $this->setExpectedException('RuntimeException'); + ProxyHelper::getProxyData(); + } + + public function dataMalformed() + { + return array( + 'no-host' => array('localhost'), + 'no-port' => array('scheme://localhost'), + ); + } + + /** + * @dataProvider dataFormatting + */ + public function testUrlFormatting($url, $expected) + { + $_SERVER['http_proxy'] = $url; + + list($httpProxy, $httpsProxy, $noProxy) = ProxyHelper::getProxyData(); + $this->assertSame($expected, $httpProxy); + } + + public function dataFormatting() + { + // url, expected + return array( + 'lowercases-scheme' => array('HTTP://proxy.com:8888', 'http://proxy.com:8888'), + 'adds-http-port' => array('http://proxy.com', 'http://proxy.com:80'), + 'adds-https-port' => array('https://proxy.com', 'https://proxy.com:443'), + ); + } + + /** + * @dataProvider dataCaseOverrides + */ + public function testLowercaseOverridesUppercase(array $server, $expected, $index) + { + $_SERVER = array_merge($_SERVER, $server); + + $list = ProxyHelper::getProxyData(); + $this->assertSame($expected, $list[$index]); + } + + public function dataCaseOverrides() + { + // server, expected, list index + return array( + array(array('HTTP_PROXY' => 'http://upper.com', 'http_proxy' => 'http://lower.com'), 'http://lower.com:80', 0), + array(array('HTTPS_PROXY' => 'http://upper.com', 'https_proxy' => 'http://lower.com'), 'http://lower.com:80', 1), + array(array('NO_PROXY' => 'upper.com', 'no_proxy' => 'lower.com'), 'lower.com', 2), + ); + } + + /** + * @dataProvider dataCGIOverrides + */ + public function testCGIUpperCaseOverridesHttp(array $server, $expected, $index) + { + $_SERVER = array_merge($_SERVER, $server); + + $list = ProxyHelper::getProxyData(); + $this->assertSame($expected, $list[$index]); + } + + public function dataCGIOverrides() + { + // server, expected, list index + return array( + array(array('http_proxy' => 'http://http.com', 'CGI_HTTP_PROXY' => 'http://cgi.com'), 'http://cgi.com:80', 0), + array(array('http_proxy' => 'http://http.com', 'cgi_http_proxy' => 'http://cgi.com'), 'http://http.com:80', 0), + ); + } + + public function testNoHttpsProxyUsesHttpProxy() + { + $_SERVER['http_proxy'] = 'http://http.com'; + + list($httpProxy, $httpsProxy, $noProxy) = ProxyHelper::getProxyData(); + $this->assertSame('http://http.com:80', $httpsProxy); + } + + public function testNoHttpProxyDoesNotUseHttpsProxy() + { + $_SERVER['https_proxy'] = 'http://https.com'; + + list($httpProxy, $httpsProxy, $noProxy) = ProxyHelper::getProxyData(); + $this->assertSame(null, $httpProxy); + } + + /** + * @dataProvider dataContextOptions + */ + public function testGetContextOptions($url, $expected) + { + $this->assertEquals($expected, ProxyHelper::getContextOptions($url)); + } + + public function dataContextOptions() + { + // url, expected + return array( + array('http://proxy.com', array('http' => array( + 'proxy' => 'tcp://proxy.com:80', + ))), + array('https://proxy.com', array('http' => array( + 'proxy' => 'ssl://proxy.com:443', + ))), + array('http://user:p%40ss@proxy.com', array('http' => array( + 'proxy' => 'tcp://proxy.com:80', + 'header' => 'Proxy-Authorization: Basic dXNlcjpwQHNz', + ))), + ); + } + + /** + * @dataProvider dataRequestFullUri + */ + public function testSetRequestFullUri($requestUrl, $expected) + { + $options = array(); + ProxyHelper::setRequestFullUri($requestUrl, $options); + + $this->assertEquals($expected, $options); + } + + public function dataRequestFullUri() + { + $options = array('http' => array('request_fulluri' => true)); + + // $requestUrl, expected + return array( + 'http' => array('http://repo.org', $options), + 'https' => array('https://repo.org', array()), + 'no-scheme' => array('repo.org', array()), + ); + } +} diff --git a/tests/Composer/Test/Util/Http/ProxyManagerTest.php b/tests/Composer/Test/Util/Http/ProxyManagerTest.php new file mode 100644 index 000000000..e4eb54a46 --- /dev/null +++ b/tests/Composer/Test/Util/Http/ProxyManagerTest.php @@ -0,0 +1,167 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util\Http; + +use Composer\Util\Http\ProxyHelper; +use Composer\Util\Http\ProxyManager; +use Composer\Test\TestCase; + +class ProxyManagerTest extends TestCase +{ + protected function setUp() + { + unset( + $_SERVER['HTTP_PROXY'], + $_SERVER['http_proxy'], + $_SERVER['HTTPS_PROXY'], + $_SERVER['https_proxy'], + $_SERVER['NO_PROXY'], + $_SERVER['no_proxy'], + $_SERVER['CGI_HTTP_PROXY'] + ); + ProxyManager::reset(); + } + + protected function tearDown() + { + unset( + $_SERVER['HTTP_PROXY'], + $_SERVER['http_proxy'], + $_SERVER['HTTPS_PROXY'], + $_SERVER['https_proxy'], + $_SERVER['NO_PROXY'], + $_SERVER['no_proxy'], + $_SERVER['CGI_HTTP_PROXY'] + ); + ProxyManager::reset(); + } + + public function testInstantiation() + { + $originalInstance = ProxyManager::getInstance(); + $this->assertInstanceOf('Composer\Util\Http\ProxyManager', $originalInstance); + + $sameInstance = ProxyManager::getInstance(); + $this->assertTrue($originalInstance === $sameInstance); + + ProxyManager::reset(); + $newInstance = ProxyManager::getInstance(); + $this->assertFalse($sameInstance === $newInstance); + } + + public function testGetProxyForRequestThrowsOnBadProxyUrl() + { + $_SERVER['http_proxy'] = 'localhost'; + $proxyManager = ProxyManager::getInstance(); + $this->setExpectedException('Composer\Downloader\TransportException'); + $proxyManager->getProxyForRequest('http://example.com'); + } + + /** + * @dataProvider dataRequest + */ + public function testGetProxyForRequest($server, $url, $expectedUrl, $expectedOptions, $expectedSecure, $expectedMessage) + { + $_SERVER = array_merge($_SERVER, $server); + $proxyManager = ProxyManager::getInstance(); + + $proxy = $proxyManager->getProxyForRequest($url); + $this->assertInstanceOf('Composer\Util\Http\RequestProxy', $proxy); + + $this->assertSame($expectedUrl, $proxy->getUrl()); + $this->assertSame($expectedOptions, $proxy->getContextOptions()); + $this->assertSame($expectedSecure, $proxy->isSecure()); + + $message = $proxy->getLastProxy(); + + if ($expectedMessage) { + $condition = stripos($message, $expectedMessage) !== false; + } else { + $condition = $expectedMessage === $message; + } + + $this->assertTrue($condition, 'lastProxy check'); + } + + public function dataRequest() + { + $server = array( + 'http_proxy' => 'http://user:p%40ss@proxy.com', + 'https_proxy' => 'https://proxy.com:443', + 'no_proxy' => 'other.repo.org', + ); + + // server, url, expectedUrl, expectedOptions, expectedSecure, expectedMessage + return array( + array(array(), 'http://repo.org', '', array(), false, ''), + array($server, 'http://repo.org', 'http://user:p%40ss@proxy.com:80', + array('http' => array( + 'proxy' => 'tcp://proxy.com:80', + 'header' => 'Proxy-Authorization: Basic dXNlcjpwQHNz', + 'request_fulluri' => true, + ) + ), + false, + 'http://user:***@proxy.com:80', + ), + array( + $server, 'https://repo.org', 'https://proxy.com:443', + array('http' => array( + 'proxy' => 'ssl://proxy.com:443', + ) + ), + true, + 'https://proxy.com:443', + ), + array($server, 'https://other.repo.org', '', array(), false, 'no_proxy'), + ); + } + + /** + * @dataProvider dataStatus + */ + public function testGetStatus($server, $expectedStatus, $expectedMessage) + { + $_SERVER = array_merge($_SERVER, $server); + $proxyManager = ProxyManager::getInstance(); + $status = $proxyManager->getStatus($message); + + $this->assertSame($expectedStatus, $status); + + if ($expectedMessage) { + $condition = stripos($message, $expectedMessage) !== false; + } else { + $condition = $expectedMessage === $message; + } + $this->assertTrue($condition, 'message check'); + } + + public function dataStatus() + { + // server, expectedStatus, expectedMessage + return array( + array(array(), false, null), + array(array('http_proxy' => 'localhost'), false, 'malformed'), + array( + array('http_proxy' => 'http://user:p%40ss@proxy.com:80'), + true, + 'http=http://user:***@proxy.com:80' + ), + array( + array('http_proxy' => 'proxy.com:80', 'https_proxy' => 'proxy.com:80'), + true, + 'http=proxy.com:80, https=proxy.com:80' + ), + ); + } +} diff --git a/tests/Composer/Test/Util/Http/RequestProxyTest.php b/tests/Composer/Test/Util/Http/RequestProxyTest.php new file mode 100644 index 000000000..f87002515 --- /dev/null +++ b/tests/Composer/Test/Util/Http/RequestProxyTest.php @@ -0,0 +1,61 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util\Http; + +use Composer\Util\Http\RequestProxy; +use Composer\Test\TestCase; + +class RequestProxyTest extends TestCase +{ + /** + * @dataProvider dataSecure + */ + public function testIsSecure($url, $expectedSecure) + { + $proxy = new RequestProxy($url, array(), ''); + + $this->assertSame($expectedSecure, $proxy->isSecure()); + } + + public function dataSecure() + { + // url, secure + return array( + 'basic' => array('http://proxy.com:80', false), + 'secure' => array('https://proxy.com:443', true), + 'none' => array('', false), + ); + } + + /** + * @dataProvider dataLastProxy + */ + public function testGetLastProxyFormat($url, $format, $expected) + { + $proxy = new RequestProxy($url, array(), $url); + + $message = $proxy->getLastProxy($format); + $this->assertSame($expected, $message); + } + + public function dataLastProxy() + { + $format = 'proxy (%s)'; + + // url, format, expected + return array( + array('', $format, ''), + array('http://proxy.com:80', $format, 'proxy (http://proxy.com:80)'), + ); + } +} diff --git a/tests/Composer/Test/Util/StreamContextFactoryTest.php b/tests/Composer/Test/Util/StreamContextFactoryTest.php index 973ff3254..80423a825 100644 --- a/tests/Composer/Test/Util/StreamContextFactoryTest.php +++ b/tests/Composer/Test/Util/StreamContextFactoryTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Util; +use Composer\Util\Http\ProxyManager; use Composer\Util\StreamContextFactory; use Composer\Test\TestCase; @@ -20,11 +21,13 @@ class StreamContextFactoryTest extends TestCase protected function setUp() { unset($_SERVER['HTTP_PROXY'], $_SERVER['http_proxy'], $_SERVER['HTTPS_PROXY'], $_SERVER['https_proxy'], $_SERVER['NO_PROXY'], $_SERVER['no_proxy']); + ProxyManager::reset(); } protected function tearDown() { unset($_SERVER['HTTP_PROXY'], $_SERVER['http_proxy'], $_SERVER['HTTPS_PROXY'], $_SERVER['https_proxy'], $_SERVER['NO_PROXY'], $_SERVER['no_proxy']); + ProxyManager::reset(); } /** @@ -147,16 +150,9 @@ class StreamContextFactoryTest extends TestCase $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net'; $_SERVER['https_proxy'] = 'https://woopproxy.net'; + // Pointless test replaced by ProxyHelperTest.php + $this->setExpectedException('Composer\Downloader\TransportException'); $context = StreamContextFactory::getContext('https://example.org', array('http' => array('method' => 'GET', 'header' => 'User-Agent: foo'))); - $options = stream_context_get_options($context); - - $this->assertEquals(array('http' => array( - 'proxy' => 'ssl://woopproxy.net:443', - 'method' => 'GET', - 'max_redirects' => 20, - 'follow_location' => 1, - 'header' => array('User-Agent: foo'), - )), $options); } /** @@ -182,7 +178,7 @@ class StreamContextFactoryTest extends TestCase StreamContextFactory::getContext('http://example.org'); $this->fail(); } catch (\RuntimeException $e) { - $this->assertInstanceOf('RuntimeException', $e); + $this->assertInstanceOf('Composer\Downloader\TransportException', $e); } } } @@ -216,4 +212,26 @@ class StreamContextFactoryTest extends TestCase $ctxoptions = stream_context_get_options($context); $this->assertEquals(end($expectedOptions['http']['header']), end($ctxoptions['http']['header'])); } + + public function testInitOptionsDoesIncludeProxyAuthHeaders() + { + $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:3128/'; + + $options = array(); + $options = StreamContextFactory::initOptions('https://example.org', $options); + $headers = implode(' ', $options['http']['header']); + + $this->assertTrue(false !== stripos($headers, 'Proxy-Authorization')); + } + + public function testInitOptionsForCurlDoesNotIncludeProxyAuthHeaders() + { + $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:3128/'; + + $options = array(); + $options = StreamContextFactory::initOptions('https://example.org', $options, true); + $headers = implode(' ', $options['http']['header']); + + $this->assertFalse(stripos($headers, 'Proxy-Authorization')); + } }