1
0
Fork 0

Refactor proxy handling for Composer2

pull/9324/head
johnstevenson 2020-09-24 16:48:22 +01:00
parent 8564dd8dac
commit d47261eb93
10 changed files with 950 additions and 85 deletions

View File

@ -41,6 +41,9 @@ class CurlDownloader
private $authHelper; private $authHelper;
private $selectTimeout = 5.0; private $selectTimeout = 5.0;
private $maxRedirects = 20; private $maxRedirects = 20;
/** @var ProxyManager */
private $proxyManager;
private $supportsSecureProxy;
protected $multiErrors = array( protected $multiErrors = array(
CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'), 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."), 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->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['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 (self::$options as $type => $curlOptions) {
foreach ($curlOptions as $name => $curlOption) { 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); $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo);
$this->jobs[(int) $curlHandle] = array( $this->jobs[(int) $curlHandle] = array(
@ -206,7 +234,7 @@ class CurlDownloader
'reject' => $reject, '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' : ''; $ifModified = false !== stripos(implode(',', $options['http']['header']), 'if-modified-since:') ? ' if modified' : '';
if ($attributes['redirects'] === 0) { if ($attributes['redirects'] === 0) {
$this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG);

View File

@ -0,0 +1,179 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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 <john-stevenson@blueyonder.co.uk>
*/
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;
}
}

View File

@ -0,0 +1,178 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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 <john-stevenson@blueyonder.co.uk>
*/
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;
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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 <john-stevenson@blueyonder.co.uk>
*/
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;
}
}

View File

@ -18,6 +18,7 @@ use Composer\IO\IOInterface;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\CaBundle\CaBundle; use Composer\CaBundle\CaBundle;
use Composer\Util\Http\Response; use Composer\Util\Http\Response;
use Composer\Util\Http\ProxyManager;
/** /**
* @internal * @internal
@ -46,6 +47,7 @@ class RemoteFilesystem
private $degradedMode = false; private $degradedMode = false;
private $redirects; private $redirects;
private $maxRedirects = 20; private $maxRedirects = 20;
private $proxyManager;
/** /**
* Constructor. * Constructor.
@ -72,6 +74,7 @@ class RemoteFilesystem
$this->options = array_replace_recursive($this->options, $options); $this->options = array_replace_recursive($this->options, $options);
$this->config = $config; $this->config = $config;
$this->authHelper = isset($authHelper) ? $authHelper : new AuthHelper($io, $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'))); $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet')));
$actualContextOptions = stream_context_get_options($ctx); $proxy = ProxyManager::getInstance()->getProxyForRequest($fileUrl);
$usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : ''; $usingProxy = $proxy->getLastProxy(' using proxy (%s)');
$this->io->writeError((strpos($origFileUrl, 'http') === 0 ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); $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 // 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) { if ((!preg_match('{^http://(repo\.)?packagist\.org/p/}', $fileUrl) || (false === strpos($fileUrl, '$') && false === strpos($fileUrl, '%24'))) && empty($degradedPackagist) && $this->config) {

View File

@ -15,6 +15,7 @@ namespace Composer\Util;
use Composer\Composer; use Composer\Composer;
use Composer\CaBundle\CaBundle; use Composer\CaBundle\CaBundle;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\Util\Http\ProxyManager;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
@ -57,10 +58,11 @@ final class StreamContextFactory
/** /**
* @param string $url * @param string $url
* @param array $options * @param array $options
* @param bool $forCurl
* @psalm-return array{http:{header: string[], proxy?: string, request_fulluri: bool}, ssl: array} * @psalm-return array{http:{header: string[], proxy?: string, request_fulluri: bool}, ssl: array}
* @return array formatted as a stream context 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 // Make sure the headers are in an array form
if (!isset($options['http']['header'])) { if (!isset($options['http']['header'])) {
@ -70,76 +72,26 @@ final class StreamContextFactory
$options['http']['header'] = explode("\r\n", $options['http']['header']); $options['http']['header'] = explode("\r\n", $options['http']['header']);
} }
// Handle HTTP_PROXY/http_proxy on CLI only for security reasons // Add stream proxy options if there is a proxy
if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { if (!$forCurl) {
$proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); $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://')) {
// Prefer CGI_HTTP_PROXY if available throw new TransportException('PHP does not support https requests to a secure proxy.');
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)) { $proxyOptions = $proxy->getContextOptions();
$proxyURL = isset($proxy['scheme']) ? $proxy['scheme'] . '://' : '';
$proxyURL .= isset($proxy['host']) ? $proxy['host'] : '';
if (isset($proxy['port'])) { // Header will be a Proxy-Authorization string or not set
$proxyURL .= ":" . $proxy['port']; if (isset($proxyOptions['http']['header'])) {
} elseif (strpos($proxyURL, 'http://') === 0) { $options['http']['header'][] = $proxyOptions['http']['header'];
$proxyURL .= ":80"; unset($proxyOptions['http']['header']);
} 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']);
}
$auth = base64_encode($auth);
$options['http']['header'][] = "Proxy-Authorization: Basic {$auth}";
} }
$options = array_replace_recursive($options, $proxyOptions);
} }
if (defined('HHVM_VERSION')) { if (defined('HHVM_VERSION')) {
@ -148,7 +100,7 @@ final class StreamContextFactory
$phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
} }
if (extension_loaded('curl')) { if ($forCurl) {
$curl = curl_version(); $curl = curl_version();
$httpVersion = 'curl '.$curl['version']; $httpVersion = 'curl '.$curl['version'];
} else { } else {

View File

@ -0,0 +1,190 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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()),
);
}
}

View File

@ -0,0 +1,167 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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'
),
);
}
}

View File

@ -0,0 +1,61 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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)'),
);
}
}

View File

@ -12,6 +12,7 @@
namespace Composer\Test\Util; namespace Composer\Test\Util;
use Composer\Util\Http\ProxyManager;
use Composer\Util\StreamContextFactory; use Composer\Util\StreamContextFactory;
use Composer\Test\TestCase; use Composer\Test\TestCase;
@ -20,11 +21,13 @@ class StreamContextFactoryTest extends TestCase
protected function setUp() protected function setUp()
{ {
unset($_SERVER['HTTP_PROXY'], $_SERVER['http_proxy'], $_SERVER['HTTPS_PROXY'], $_SERVER['https_proxy'], $_SERVER['NO_PROXY'], $_SERVER['no_proxy']); unset($_SERVER['HTTP_PROXY'], $_SERVER['http_proxy'], $_SERVER['HTTPS_PROXY'], $_SERVER['https_proxy'], $_SERVER['NO_PROXY'], $_SERVER['no_proxy']);
ProxyManager::reset();
} }
protected function tearDown() protected function tearDown()
{ {
unset($_SERVER['HTTP_PROXY'], $_SERVER['http_proxy'], $_SERVER['HTTPS_PROXY'], $_SERVER['https_proxy'], $_SERVER['NO_PROXY'], $_SERVER['no_proxy']); 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['http_proxy'] = 'http://username:password@proxyserver.net';
$_SERVER['https_proxy'] = 'https://woopproxy.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'))); $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'); StreamContextFactory::getContext('http://example.org');
$this->fail(); $this->fail();
} catch (\RuntimeException $e) { } 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); $ctxoptions = stream_context_get_options($context);
$this->assertEquals(end($expectedOptions['http']['header']), end($ctxoptions['http']['header'])); $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'));
}
} }