Refactor proxy handling for Composer2
parent
8564dd8dac
commit
d47261eb93
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)'),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue