Refactor proxy handling to require https_proxy (#11915)
Composer has always allowed a single http_proxy (or CGI_HTTP_PROXY) environment variable to be used for both HTTP and HTTPS requests. But many other tools and libraries require scheme-specific values. The landscape is already complicated by the use of and need for upper and lower case values, so to bring matters inline with current practice https_proxy is now required for HTTPS requests. The new proxy handler incorporates a transition mechanism, which allows http_proxy to be used for all requests when https_proxy is not set and provides a `needsTransitionWarning` method for the main application. Moving to scheme-specific environment variables means that a user may set a single proxy for either HTTP or HTTPS requests. To accomodate this situation during the transition period, an https_proxy value can be set to an empty string which will prevent http_proxy being used for HTTPS requests.pull/11932/head
parent
92f641ac3d
commit
3cc490d4c4
|
@ -4008,16 +4008,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Util/Http/CurlDownloader.php
|
path: ../src/Composer/Util/Http/CurlDownloader.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Constant CURLOPT_PROXY_CAINFO not found\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/CurlDownloader.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Constant CURLOPT_PROXY_CAPATH not found\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/CurlDownloader.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
||||||
count: 3
|
count: 3
|
||||||
|
@ -4199,72 +4189,17 @@ parameters:
|
||||||
path: ../src/Composer/Util/Http/CurlDownloader.php
|
path: ../src/Composer/Util/Http/CurlDownloader.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
|
message: "#^Method Composer\\\\Util\\\\Http\\\\RequestProxy::getCurlOptions\\(\\) should return array\\<int, int\\|string\\> but returns array\\<int\\|string, int\\|string\\>.$#"
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyHelper.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Foreach overwrites \\$name with its value variable\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyHelper.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Implicit array creation is not allowed \\- variable \\$options does not exist\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyHelper.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in a negated boolean, int\\<0, 65535\\>\\|false\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyHelper.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#"
|
|
||||||
count: 3
|
|
||||||
path: ../src/Composer/Util/Http/ProxyHelper.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$proxy of static method Composer\\\\Util\\\\Http\\\\ProxyHelper\\:\\:formatParsedUrl\\(\\) expects array\\{scheme\\?\\: string, host\\: string, port\\?\\: int, user\\?\\: string, pass\\?\\: string\\}, array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyHelper.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in &&, Composer\\\\Util\\\\NoProxyPattern\\|null given on the left side\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyManager.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in &&, string\\|null given on the right side\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyManager.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Util\\\\Http\\\\ProxyManager\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyManager.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#"
|
|
||||||
count: 4
|
|
||||||
path: ../src/Composer/Util/Http/ProxyManager.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#3 \\$formattedUrl of class Composer\\\\Util\\\\Http\\\\RequestProxy constructor expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyManager.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../src/Composer/Util/Http/ProxyManager.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in an if condition, string given\\.$#"
|
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Util/Http/RequestProxy.php
|
path: ../src/Composer/Util/Http/RequestProxy.php
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#"
|
message: "#^Constant CURLOPT_PROXY_CAINFO not found\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: ../src/Composer/Util/Http/RequestProxy.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Constant CURLOPT_PROXY_CAPATH not found\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
path: ../src/Composer/Util/Http/RequestProxy.php
|
path: ../src/Composer/Util/Http/RequestProxy.php
|
||||||
|
|
||||||
|
@ -5281,26 +5216,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: ../tests/Composer/Test/Util/GitTest.php
|
path: ../tests/Composer/Test/Util/GitTest.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with 'Composer\\\\\\\\Util\\\\\\\\Http…' and Composer\\\\Util\\\\Http\\\\ProxyManager will always evaluate to true\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with 'Composer\\\\\\\\Util\\\\\\\\Http…' and Composer\\\\Util\\\\Http\\\\RequestProxy will always evaluate to true\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Only booleans are allowed in an if condition, string given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Only booleans are allowed in an if condition, string\\|false\\|null given\\.$#"
|
message: "#^Only booleans are allowed in an if condition, string\\|false\\|null given\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|
|
@ -41,6 +41,7 @@ use Composer\EventDispatcher\ScriptExecutionException;
|
||||||
use Composer\Exception\NoSslException;
|
use Composer\Exception\NoSslException;
|
||||||
use Composer\XdebugHandler\XdebugHandler;
|
use Composer\XdebugHandler\XdebugHandler;
|
||||||
use Symfony\Component\Process\Exception\ProcessTimedOutException;
|
use Symfony\Component\Process\Exception\ProcessTimedOutException;
|
||||||
|
use Composer\Util\Http\ProxyManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console application that handles the commands
|
* The console application that handles the commands
|
||||||
|
@ -398,6 +399,13 @@ class Application extends BaseApplication
|
||||||
$io->writeError('<info>Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MiB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MiB), time: '.round(microtime(true) - $startTime, 2).'s');
|
$io->writeError('<info>Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MiB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MiB), time: '.round(microtime(true) - $startTime, 2).'s');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ProxyManager::getInstance()->needsTransitionWarning()) {
|
||||||
|
$io->writeError('');
|
||||||
|
$io->writeError('<warning>Composer now requires separate proxy environment variables for HTTP and HTTPS requests.</warning>');
|
||||||
|
$io->writeError('<warning>Please set `https_proxy` in addition to your existing proxy environment variables.</warning>');
|
||||||
|
$io->writeError('<warning>This fallback (and warning) will be removed in Composer 2.8.0.</warning>');
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
} catch (ScriptExecutionException $e) {
|
} catch (ScriptExecutionException $e) {
|
||||||
if ($this->getDisablePluginsByDefault() && $this->isRunningAsRoot() && !$this->io->isInteractive()) {
|
if ($this->getDisablePluginsByDefault() && $this->isRunningAsRoot() && !$this->io->isInteractive()) {
|
||||||
|
|
|
@ -52,10 +52,6 @@ class CurlDownloader
|
||||||
private $maxRedirects = 20;
|
private $maxRedirects = 20;
|
||||||
/** @var int */
|
/** @var int */
|
||||||
private $maxRetries = 3;
|
private $maxRetries = 3;
|
||||||
/** @var ProxyManager */
|
|
||||||
private $proxyManager;
|
|
||||||
/** @var bool */
|
|
||||||
private $supportsSecureProxy;
|
|
||||||
/** @var array<int, string[]> */
|
/** @var array<int, string[]> */
|
||||||
protected $multiErrors = [
|
protected $multiErrors = [
|
||||||
CURLM_BAD_HANDLE => ['CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'],
|
CURLM_BAD_HANDLE => ['CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'],
|
||||||
|
@ -117,11 +113,6 @@ 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -245,26 +236,8 @@ class CurlDownloader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always set CURLOPT_PROXY to enable/disable proxy handling
|
$proxy = ProxyManager::getInstance()->getProxyForRequest($url);
|
||||||
// Any proxy authorization is included in the proxy url
|
curl_setopt_array($curlHandle, $proxy->getCurlOptions($options['ssl'] ?? []));
|
||||||
$proxy = $this->proxyManager->getProxyForRequest($url);
|
|
||||||
if ($proxy->getUrl() !== '') {
|
|
||||||
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);
|
||||||
|
|
||||||
|
@ -283,7 +256,7 @@ class CurlDownloader
|
||||||
'primaryIp' => '',
|
'primaryIp' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
$usingProxy = $proxy->getFormattedUrl(' using proxy (%s)');
|
$usingProxy = $proxy->getStatus(' 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 && $attributes['retries'] === 0) {
|
if ($attributes['redirects'] === 0 && $attributes['retries'] === 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);
|
||||||
|
|
|
@ -1,183 +0,0 @@
|
||||||
<?php declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* @return array{string|null, string|null, string|null} httpProxy, httpsProxy, noProxy values
|
|
||||||
*
|
|
||||||
* @throws \RuntimeException on malformed url
|
|
||||||
*/
|
|
||||||
public static function getProxyData(): array
|
|
||||||
{
|
|
||||||
$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(['http_proxy', 'HTTP_PROXY'], $name)) {
|
|
||||||
$httpProxy = self::checkProxy($env, $name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer CGI_HTTP_PROXY if available
|
|
||||||
if ($env = self::getProxyEnv(['CGI_HTTP_PROXY'], $name)) {
|
|
||||||
$httpProxy = self::checkProxy($env, $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle https_proxy/HTTPS_PROXY
|
|
||||||
if ($env = self::getProxyEnv(['https_proxy', 'HTTPS_PROXY'], $name)) {
|
|
||||||
$httpsProxy = self::checkProxy($env, $name);
|
|
||||||
} else {
|
|
||||||
$httpsProxy = $httpProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle no_proxy
|
|
||||||
$noProxy = self::getProxyEnv(['no_proxy', 'NO_PROXY'], $name);
|
|
||||||
|
|
||||||
return [$httpProxy, $httpsProxy, $noProxy];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns http context options for the proxy url
|
|
||||||
*
|
|
||||||
* @return array{http: array{proxy: string, header?: string}}
|
|
||||||
*/
|
|
||||||
public static function getContextOptions(string $proxyUrl): array
|
|
||||||
{
|
|
||||||
$proxy = parse_url($proxyUrl);
|
|
||||||
|
|
||||||
// Remove any authorization
|
|
||||||
$proxyUrl = self::formatParsedUrl($proxy, false);
|
|
||||||
$proxyUrl = str_replace(['http://', 'https://'], ['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 mixed[] $options Set by method
|
|
||||||
*/
|
|
||||||
public static function setRequestFullUri(string $requestUrl, array &$options): void
|
|
||||||
{
|
|
||||||
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 non-empty-list<string> $names Names to search for
|
|
||||||
* @param string|null $name Name of any found value, you should only rely on it if the function returned a non-null value
|
|
||||||
*
|
|
||||||
* @return string|null The found value
|
|
||||||
*
|
|
||||||
* @param-out string $name
|
|
||||||
*/
|
|
||||||
private static function getProxyEnv(array $names, ?string &$name): ?string
|
|
||||||
{
|
|
||||||
foreach ($names as $name) {
|
|
||||||
if (!empty($_SERVER[$name])) {
|
|
||||||
return $_SERVER[$name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks and formats a proxy url from the environment
|
|
||||||
*
|
|
||||||
* @throws \RuntimeException on malformed url
|
|
||||||
* @return string The formatted proxy url
|
|
||||||
*/
|
|
||||||
private static function checkProxy(string $proxyUrl, string $envName): string
|
|
||||||
{
|
|
||||||
$error = sprintf('malformed %s url', $envName);
|
|
||||||
$proxy = parse_url($proxyUrl);
|
|
||||||
|
|
||||||
// We need parse_url to have identified a host
|
|
||||||
if (!isset($proxy['host'])) {
|
|
||||||
throw new \RuntimeException($error);
|
|
||||||
}
|
|
||||||
|
|
||||||
$proxyUrl = self::formatParsedUrl($proxy, true);
|
|
||||||
|
|
||||||
// We need a port because streams and curl use different defaults
|
|
||||||
if (!parse_url($proxyUrl, PHP_URL_PORT)) {
|
|
||||||
throw new \RuntimeException($error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $proxyUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a url from its component parts
|
|
||||||
*
|
|
||||||
* @param array{scheme?: string, host: string, port?: int, user?: string, pass?: string} $proxy
|
|
||||||
*
|
|
||||||
* @return string The formatted value
|
|
||||||
*/
|
|
||||||
private static function formatParsedUrl(array $proxy, bool $includeAuth): string
|
|
||||||
{
|
|
||||||
$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,119 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @author John Stevenson <john-stevenson@blueyonder.co.uk>
|
||||||
|
*/
|
||||||
|
class ProxyItem
|
||||||
|
{
|
||||||
|
/** @var non-empty-string */
|
||||||
|
private $url;
|
||||||
|
/** @var non-empty-string */
|
||||||
|
private $safeUrl;
|
||||||
|
/** @var ?non-empty-string */
|
||||||
|
private $curlAuth;
|
||||||
|
/** @var string */
|
||||||
|
private $optionsProxy;
|
||||||
|
/** @var ?non-empty-string */
|
||||||
|
private $optionsAuth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $proxyUrl The value from the environment
|
||||||
|
* @param string $envName The name of the environment variable
|
||||||
|
* @throws \RuntimeException If the proxy url is invalid
|
||||||
|
*/
|
||||||
|
public function __construct(string $proxyUrl, string $envName)
|
||||||
|
{
|
||||||
|
$syntaxError = sprintf('unsupported `%s` syntax', $envName);
|
||||||
|
|
||||||
|
if (strpbrk($proxyUrl, "\r\n\t") !== false) {
|
||||||
|
throw new \RuntimeException($syntaxError);
|
||||||
|
}
|
||||||
|
if (false === ($proxy = parse_url($proxyUrl))) {
|
||||||
|
throw new \RuntimeException($syntaxError);
|
||||||
|
}
|
||||||
|
if (!isset($proxy['host'])) {
|
||||||
|
throw new \RuntimeException('unable to find proxy host in ' . $envName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = isset($proxy['scheme']) ? strtolower($proxy['scheme']) . '://' : 'http://';
|
||||||
|
$safe = '';
|
||||||
|
|
||||||
|
if (isset($proxy['user'])) {
|
||||||
|
$safe = '***';
|
||||||
|
$user = $proxy['user'];
|
||||||
|
$auth = rawurldecode($proxy['user']);
|
||||||
|
|
||||||
|
if (isset($proxy['pass'])) {
|
||||||
|
$safe .= ':***';
|
||||||
|
$user .= ':' . $proxy['pass'];
|
||||||
|
$auth .= ':' . rawurldecode($proxy['pass']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$safe .= '@';
|
||||||
|
|
||||||
|
if (strlen($user) > 0) {
|
||||||
|
$this->curlAuth = $user;
|
||||||
|
$this->optionsAuth = 'Proxy-Authorization: Basic ' . base64_encode($auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $proxy['host'];
|
||||||
|
$port = null;
|
||||||
|
|
||||||
|
if (isset($proxy['port'])) {
|
||||||
|
$port = $proxy['port'];
|
||||||
|
} elseif ($scheme === 'http://') {
|
||||||
|
$port = 80;
|
||||||
|
} elseif ($scheme === 'https://') {
|
||||||
|
$port = 443;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need a port because curl uses 1080 for http. Port 0 is reserved,
|
||||||
|
// but is considered valid depending on the PHP or Curl version.
|
||||||
|
if ($port === null) {
|
||||||
|
throw new \RuntimeException('unable to find proxy port in ' . $envName);
|
||||||
|
}
|
||||||
|
if ($port === 0) {
|
||||||
|
throw new \RuntimeException('port 0 is reserved in ' . $envName);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->url = sprintf('%s%s:%d', $scheme, $host, $port);
|
||||||
|
$this->safeUrl = sprintf('%s%s%s:%d', $scheme, $safe, $host, $port);
|
||||||
|
|
||||||
|
$scheme = str_replace(['http://', 'https://'], ['tcp://', 'ssl://'], $scheme);
|
||||||
|
$this->optionsProxy = sprintf('%s%s:%d', $scheme, $host, $port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a RequestProxy instance for the scheme of the request url
|
||||||
|
*
|
||||||
|
* @param string $scheme The scheme of the request url
|
||||||
|
*/
|
||||||
|
public function toRequestProxy(string $scheme): RequestProxy
|
||||||
|
{
|
||||||
|
$options = ['http' => ['proxy' => $this->optionsProxy]];
|
||||||
|
|
||||||
|
if ($this->optionsAuth !== null) {
|
||||||
|
$options['http']['header'] = $this->optionsAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scheme === 'http') {
|
||||||
|
$options['http']['request_fulluri'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RequestProxy($this->url, $this->curlAuth, $options, $this->safeUrl);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,6 @@ namespace Composer\Util\Http;
|
||||||
|
|
||||||
use Composer\Downloader\TransportException;
|
use Composer\Downloader\TransportException;
|
||||||
use Composer\Util\NoProxyPattern;
|
use Composer\Util\NoProxyPattern;
|
||||||
use Composer\Util\Url;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
@ -24,40 +23,40 @@ class ProxyManager
|
||||||
{
|
{
|
||||||
/** @var ?string */
|
/** @var ?string */
|
||||||
private $error = null;
|
private $error = null;
|
||||||
/** @var array{http: ?string, https: ?string} */
|
/** @var ?ProxyItem */
|
||||||
private $fullProxy;
|
private $httpProxy = null;
|
||||||
/** @var array{http: ?string, https: ?string} */
|
/** @var ?ProxyItem */
|
||||||
private $safeProxy;
|
private $httpsProxy = null;
|
||||||
/** @var array{http: array{options: mixed[]|null}, https: array{options: mixed[]|null}} */
|
|
||||||
private $streams;
|
|
||||||
/** @var bool */
|
|
||||||
private $hasProxy;
|
|
||||||
/** @var ?string */
|
|
||||||
private $info = null;
|
|
||||||
/** @var ?NoProxyPattern */
|
/** @var ?NoProxyPattern */
|
||||||
private $noProxyHandler = null;
|
private $noProxyHandler = null;
|
||||||
|
|
||||||
/** @var ?ProxyManager */
|
/** @var ?self */
|
||||||
private static $instance = null;
|
private static $instance = null;
|
||||||
|
|
||||||
|
/** The following 3 properties can be removed after the transition period */
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $ignoreHttpsProxy = false;
|
||||||
|
/** @var bool */
|
||||||
|
private $isTransitional = false;
|
||||||
|
/** @var bool */
|
||||||
|
private $needsTransitionWarning = false;
|
||||||
|
|
||||||
private function __construct()
|
private function __construct()
|
||||||
{
|
{
|
||||||
$this->fullProxy = $this->safeProxy = [
|
// this can be removed after the transition period
|
||||||
'http' => null,
|
$this->isTransitional = true;
|
||||||
'https' => null,
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->streams['http'] = $this->streams['https'] = [
|
try {
|
||||||
'options' => null,
|
$this->getProxyData();
|
||||||
];
|
} catch (\RuntimeException $e) {
|
||||||
|
$this->error = $e->getMessage();
|
||||||
$this->hasProxy = false;
|
}
|
||||||
$this->initProxyData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getInstance(): ProxyManager
|
public static function getInstance(): ProxyManager
|
||||||
{
|
{
|
||||||
if (!self::$instance) {
|
if (self::$instance === null) {
|
||||||
self::$instance = new self();
|
self::$instance = new self();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,96 +78,116 @@ class ProxyManager
|
||||||
*/
|
*/
|
||||||
public function getProxyForRequest(string $requestUrl): RequestProxy
|
public function getProxyForRequest(string $requestUrl): RequestProxy
|
||||||
{
|
{
|
||||||
if ($this->error) {
|
if ($this->error !== null) {
|
||||||
throw new TransportException('Unable to use a proxy: '.$this->error);
|
throw new TransportException('Unable to use a proxy: '.$this->error);
|
||||||
}
|
}
|
||||||
|
|
||||||
$scheme = parse_url($requestUrl, PHP_URL_SCHEME) ?: 'http';
|
$scheme = (string) parse_url($requestUrl, PHP_URL_SCHEME);
|
||||||
$proxyUrl = '';
|
$proxy = $this->getProxyForScheme($scheme);
|
||||||
$options = [];
|
|
||||||
$formattedProxyUrl = '';
|
|
||||||
|
|
||||||
if ($this->hasProxy && in_array($scheme, ['http', 'https'], true) && $this->fullProxy[$scheme]) {
|
if ($proxy === null) {
|
||||||
if ($this->noProxy($requestUrl)) {
|
return RequestProxy::none();
|
||||||
$formattedProxyUrl = 'excluded by no_proxy';
|
}
|
||||||
} else {
|
|
||||||
$proxyUrl = $this->fullProxy[$scheme];
|
if ($this->noProxy($requestUrl)) {
|
||||||
$options = $this->streams[$scheme]['options'];
|
return RequestProxy::noProxy();
|
||||||
assert(is_array($options));
|
}
|
||||||
ProxyHelper::setRequestFullUri($requestUrl, $options);
|
|
||||||
$formattedProxyUrl = $this->safeProxy[$scheme];
|
return $proxy->toRequestProxy($scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user needs to set an https_proxy environment variable
|
||||||
|
*
|
||||||
|
* This method can be removed after the transition period
|
||||||
|
*/
|
||||||
|
public function needsTransitionWarning(): bool
|
||||||
|
{
|
||||||
|
return $this->needsTransitionWarning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a ProxyItem if one is set for the scheme, otherwise null
|
||||||
|
*/
|
||||||
|
private function getProxyForScheme(string $scheme): ?ProxyItem
|
||||||
|
{
|
||||||
|
if ($scheme === 'http') {
|
||||||
|
return $this->httpProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scheme === 'https') {
|
||||||
|
// this can be removed after the transition period
|
||||||
|
if ($this->isTransitional && $this->httpsProxy === null) {
|
||||||
|
if ($this->httpProxy !== null && !$this->ignoreHttpsProxy) {
|
||||||
|
$this->needsTransitionWarning = true;
|
||||||
|
|
||||||
|
return $this->httpProxy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->httpsProxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds proxy values from the environment and sets class properties
|
||||||
|
*/
|
||||||
|
private function getProxyData(): void
|
||||||
|
{
|
||||||
|
// Handle http_proxy/HTTP_PROXY on CLI only for security reasons
|
||||||
|
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||||
|
[$env, $name] = $this->getProxyEnv('http_proxy');
|
||||||
|
if ($env !== null) {
|
||||||
|
$this->httpProxy = new ProxyItem($env, $name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RequestProxy($proxyUrl, $options, $formattedProxyUrl);
|
// Handle cgi_http_proxy/CGI_HTTP_PROXY if needed
|
||||||
}
|
if ($this->httpProxy === null) {
|
||||||
|
[$env, $name] = $this->getProxyEnv('cgi_http_proxy');
|
||||||
/**
|
if ($env !== null) {
|
||||||
* Returns true if a proxy is being used
|
$this->httpProxy = new ProxyItem($env, $name);
|
||||||
*
|
|
||||||
* @return bool If false any error will be in $message
|
|
||||||
*/
|
|
||||||
public function isProxying(): bool
|
|
||||||
{
|
|
||||||
return $this->hasProxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns proxy configuration info which can be shown to the user
|
|
||||||
*
|
|
||||||
* @return string|null Safe proxy URL or an error message if setting up proxy failed or null if no proxy was configured
|
|
||||||
*/
|
|
||||||
public function getFormattedProxy(): ?string
|
|
||||||
{
|
|
||||||
return $this->hasProxy ? $this->info : $this->error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes proxy values from the environment
|
|
||||||
*/
|
|
||||||
private function initProxyData(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
[$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData();
|
|
||||||
} catch (\RuntimeException $e) {
|
|
||||||
$this->error = $e->getMessage();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$info = [];
|
|
||||||
|
|
||||||
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 = new NoProxyPattern($noProxy);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle https_proxy/HTTPS_PROXY
|
||||||
|
[$env, $name] = $this->getProxyEnv('https_proxy');
|
||||||
|
if ($env !== null) {
|
||||||
|
$this->httpsProxy = new ProxyItem($env, $name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle no_proxy/NO_PROXY
|
||||||
|
[$env, $name] = $this->getProxyEnv('no_proxy');
|
||||||
|
if ($env !== null) {
|
||||||
|
$this->noProxyHandler = new NoProxyPattern($env);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets initial data
|
* Searches $_SERVER for case-sensitive values
|
||||||
*
|
*
|
||||||
* @param non-empty-string $url Proxy url
|
* @return array{0: string|null, 1: string} value, name
|
||||||
* @param 'http'|'https' $scheme Environment variable scheme
|
|
||||||
*
|
|
||||||
* @return non-empty-string
|
|
||||||
*/
|
*/
|
||||||
private function setData($url, $scheme): string
|
private function getProxyEnv(string $envName): array
|
||||||
{
|
{
|
||||||
$safeProxy = Url::sanitize($url);
|
$names = [strtolower($envName), strtoupper($envName)];
|
||||||
$this->fullProxy[$scheme] = $url;
|
|
||||||
$this->safeProxy[$scheme] = $safeProxy;
|
|
||||||
$this->streams[$scheme]['options'] = ProxyHelper::getContextOptions($url);
|
|
||||||
$this->hasProxy = true;
|
|
||||||
|
|
||||||
return sprintf('%s=%s', $scheme, $safeProxy);
|
foreach ($names as $name) {
|
||||||
|
if (is_string($_SERVER[$name] ?? null)) {
|
||||||
|
if ($_SERVER[$name] !== '') {
|
||||||
|
return [$_SERVER[$name], $name];
|
||||||
|
}
|
||||||
|
// this can be removed after the transition period
|
||||||
|
if ($this->isTransitional && strtolower($name) === 'https_proxy') {
|
||||||
|
$this->ignoreHttpsProxy = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [null, ''];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -176,6 +195,10 @@ class ProxyManager
|
||||||
*/
|
*/
|
||||||
private function noProxy(string $requestUrl): bool
|
private function noProxy(string $requestUrl): bool
|
||||||
{
|
{
|
||||||
return $this->noProxyHandler && $this->noProxyHandler->test($requestUrl);
|
if ($this->noProxyHandler === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->noProxyHandler->test($requestUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,78 +12,157 @@
|
||||||
|
|
||||||
namespace Composer\Util\Http;
|
namespace Composer\Util\Http;
|
||||||
|
|
||||||
use Composer\Util\Url;
|
use Composer\Downloader\TransportException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
* @author John Stevenson <john-stevenson@blueyonder.co.uk>
|
* @author John Stevenson <john-stevenson@blueyonder.co.uk>
|
||||||
|
*
|
||||||
|
* @phpstan-type contextOptions array{http: array{proxy: string, header?: string, request_fulluri?: bool}}
|
||||||
*/
|
*/
|
||||||
class RequestProxy
|
class RequestProxy
|
||||||
{
|
{
|
||||||
/** @var mixed[] */
|
/** @var ?contextOptions */
|
||||||
private $contextOptions;
|
private $contextOptions;
|
||||||
/** @var bool */
|
/** @var ?non-empty-string */
|
||||||
private $isSecure;
|
private $status;
|
||||||
/** @var string */
|
/** @var ?non-empty-string */
|
||||||
private $formattedUrl;
|
|
||||||
/** @var string */
|
|
||||||
private $url;
|
private $url;
|
||||||
|
/** @var ?non-empty-string */
|
||||||
|
private $auth;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed[] $contextOptions
|
* @param ?non-empty-string $url The proxy url, without authorization
|
||||||
|
* @param ?non-empty-string $auth Authorization for curl
|
||||||
|
* @param ?contextOptions $contextOptions
|
||||||
|
* @param ?non-empty-string $status
|
||||||
*/
|
*/
|
||||||
public function __construct(string $url, array $contextOptions, string $formattedUrl)
|
public function __construct(?string $url, ?string $auth, ?array $contextOptions, ?string $status)
|
||||||
{
|
{
|
||||||
$this->url = $url;
|
$this->url = $url;
|
||||||
|
$this->auth = $auth;
|
||||||
$this->contextOptions = $contextOptions;
|
$this->contextOptions = $contextOptions;
|
||||||
$this->formattedUrl = $formattedUrl;
|
$this->status = $status;
|
||||||
$this->isSecure = 0 === strpos($url, 'https://');
|
}
|
||||||
|
|
||||||
|
public static function none(): RequestProxy
|
||||||
|
{
|
||||||
|
return new self(null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function noProxy(): RequestProxy
|
||||||
|
{
|
||||||
|
return new self(null, null, null, 'excluded by no_proxy');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of context options
|
* Returns the context options to use for this request, otherwise null
|
||||||
*
|
*
|
||||||
* @return mixed[]
|
* @return ?contextOptions
|
||||||
*/
|
*/
|
||||||
public function getContextOptions(): array
|
public function getContextOptions(): ?array
|
||||||
{
|
{
|
||||||
return $this->contextOptions;
|
return $this->contextOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the safe proxy url from the last request
|
* Returns an array of curl proxy options
|
||||||
*
|
*
|
||||||
* @param string|null $format Output format specifier
|
* @param array<string, string|int> $sslOptions
|
||||||
* @return string Safe proxy, no proxy or empty
|
* @return array<int, string|int>
|
||||||
*/
|
*/
|
||||||
public function getFormattedUrl(?string $format = ''): string
|
public function getCurlOptions(array $sslOptions): array
|
||||||
{
|
{
|
||||||
$result = '';
|
if ($this->isSecure() && !$this->supportsSecureProxy()) {
|
||||||
if ($this->formattedUrl) {
|
throw new TransportException('Cannot use an HTTPS proxy. PHP >= 7.3 and cUrl >= 7.52.0 are required.');
|
||||||
$format = $format ?: '%s';
|
|
||||||
$result = sprintf($format, $this->formattedUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
// Always set a proxy url, even an empty value, because it tells curl
|
||||||
|
// to ignore proxy environment variables
|
||||||
|
$options = [CURLOPT_PROXY => (string) $this->url];
|
||||||
|
|
||||||
|
// If using a proxy, tell curl to ignore no_proxy environment variables
|
||||||
|
if ($this->url !== null) {
|
||||||
|
$options[CURLOPT_NOPROXY] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set any authorization
|
||||||
|
if ($this->auth !== null) {
|
||||||
|
$options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC;
|
||||||
|
$options[CURLOPT_PROXYUSERPWD] = $this->auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isSecure()) {
|
||||||
|
if (isset($sslOptions['cafile'])) {
|
||||||
|
$options[CURLOPT_PROXY_CAINFO] = $sslOptions['cafile'];
|
||||||
|
}
|
||||||
|
if (isset($sslOptions['capath'])) {
|
||||||
|
$options[CURLOPT_PROXY_CAPATH] = $sslOptions['capath'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the proxy url
|
* Returns proxy info associated with this request
|
||||||
*
|
*
|
||||||
* @return string Proxy url or empty
|
* An empty return value means that the user has not set a proxy.
|
||||||
|
* A non-empty value will either be the sanitized proxy url if a proxy is
|
||||||
|
* required, or a message indicating that a no_proxy value has disabled the
|
||||||
|
* proxy.
|
||||||
|
*
|
||||||
|
* @param ?string $format Output format specifier
|
||||||
*/
|
*/
|
||||||
public function getUrl(): string
|
public function getStatus(?string $format = null): string
|
||||||
{
|
{
|
||||||
return $this->url;
|
if ($this->status === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$format = $format ?? '%s';
|
||||||
|
if (strpos($format, '%s') !== false) {
|
||||||
|
return sprintf($format, $this->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException('String format specifier is missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this is a secure-proxy
|
* Returns true if the request url has been excluded by a no_proxy value
|
||||||
*
|
*
|
||||||
* @return bool False if not secure or there is no proxy
|
* A false value can also mean that the user has not set a proxy.
|
||||||
|
*/
|
||||||
|
public function isExcludedByNoProxy(): bool
|
||||||
|
{
|
||||||
|
return $this->status !== null && $this->url === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this is a secure (HTTPS) proxy
|
||||||
|
*
|
||||||
|
* A false value means that this is either an HTTP proxy, or that a proxy
|
||||||
|
* is not required for this request, or that the user has not set a proxy.
|
||||||
*/
|
*/
|
||||||
public function isSecure(): bool
|
public function isSecure(): bool
|
||||||
{
|
{
|
||||||
return $this->isSecure;
|
return 0 === strpos((string) $this->url, 'https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if an HTTPS proxy can be used.
|
||||||
|
*
|
||||||
|
* This depends on PHP7.3+ for CURL_VERSION_HTTPS_PROXY
|
||||||
|
* and curl including the feature (from version 7.52.0)
|
||||||
|
*/
|
||||||
|
public function supportsSecureProxy(): bool
|
||||||
|
{
|
||||||
|
if (false === ($version = curl_version()) || !defined('CURL_VERSION_HTTPS_PROXY')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$features = $version['features'];
|
||||||
|
|
||||||
|
return (bool) ($features & CURL_VERSION_HTTPS_PROXY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,6 @@ class RemoteFilesystem
|
||||||
private $redirects;
|
private $redirects;
|
||||||
/** @var int */
|
/** @var int */
|
||||||
private $maxRedirects = 20;
|
private $maxRedirects = 20;
|
||||||
/** @var ProxyManager */
|
|
||||||
private $proxyManager;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
|
@ -91,7 +89,6 @@ 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 = $authHelper ?? new AuthHelper($io, $config);
|
$this->authHelper = $authHelper ?? new AuthHelper($io, $config);
|
||||||
$this->proxyManager = ProxyManager::getInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -276,8 +273,8 @@ class RemoteFilesystem
|
||||||
|
|
||||||
$ctx = StreamContextFactory::getContext($fileUrl, $options, ['notification' => [$this, 'callbackGet']]);
|
$ctx = StreamContextFactory::getContext($fileUrl, $options, ['notification' => [$this, 'callbackGet']]);
|
||||||
|
|
||||||
$proxy = $this->proxyManager->getProxyForRequest($fileUrl);
|
$proxy = ProxyManager::getInstance()->getProxyForRequest($fileUrl);
|
||||||
$usingProxy = $proxy->getFormattedUrl(' using proxy (%s)');
|
$usingProxy = $proxy->getStatus(' 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, $proxy, $usingProxy);
|
unset($origFileUrl, $proxy, $usingProxy);
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,8 @@ final class StreamContextFactory
|
||||||
// Add stream proxy options if there is a proxy
|
// Add stream proxy options if there is a proxy
|
||||||
if (!$forCurl) {
|
if (!$forCurl) {
|
||||||
$proxy = ProxyManager::getInstance()->getProxyForRequest($url);
|
$proxy = ProxyManager::getInstance()->getProxyForRequest($url);
|
||||||
if ($proxyOptions = $proxy->getContextOptions()) {
|
$proxyOptions = $proxy->getContextOptions();
|
||||||
|
if ($proxyOptions !== null) {
|
||||||
$isHttpsRequest = 0 === strpos($url, 'https://');
|
$isHttpsRequest = 0 === strpos($url, 'https://');
|
||||||
|
|
||||||
if ($proxy->isSecure()) {
|
if ($proxy->isSecure()) {
|
||||||
|
|
|
@ -1,201 +0,0 @@
|
||||||
<?php declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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(): void
|
|
||||||
{
|
|
||||||
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(): void
|
|
||||||
{
|
|
||||||
parent::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(string $url): void
|
|
||||||
{
|
|
||||||
$_SERVER['http_proxy'] = $url;
|
|
||||||
|
|
||||||
self::expectException('RuntimeException');
|
|
||||||
ProxyHelper::getProxyData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataMalformed(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'no-host' => ['localhost'],
|
|
||||||
'no-port' => ['scheme://localhost'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataFormatting
|
|
||||||
*/
|
|
||||||
public function testUrlFormatting(string $url, string $expected): void
|
|
||||||
{
|
|
||||||
$_SERVER['http_proxy'] = $url;
|
|
||||||
|
|
||||||
[$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData();
|
|
||||||
$this->assertSame($expected, $httpProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataFormatting(): array
|
|
||||||
{
|
|
||||||
// url, expected
|
|
||||||
return [
|
|
||||||
'lowercases-scheme' => ['HTTP://proxy.com:8888', 'http://proxy.com:8888'],
|
|
||||||
'adds-http-port' => ['http://proxy.com', 'http://proxy.com:80'],
|
|
||||||
'adds-https-port' => ['https://proxy.com', 'https://proxy.com:443'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataCaseOverrides
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $server
|
|
||||||
*/
|
|
||||||
public function testLowercaseOverridesUppercase(array $server, string $expected, int $index): void
|
|
||||||
{
|
|
||||||
$_SERVER = array_merge($_SERVER, $server);
|
|
||||||
|
|
||||||
$list = ProxyHelper::getProxyData();
|
|
||||||
$this->assertSame($expected, $list[$index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataCaseOverrides(): array
|
|
||||||
{
|
|
||||||
// server, expected, list index
|
|
||||||
return [
|
|
||||||
[['HTTP_PROXY' => 'http://upper.com', 'http_proxy' => 'http://lower.com'], 'http://lower.com:80', 0],
|
|
||||||
[['HTTPS_PROXY' => 'http://upper.com', 'https_proxy' => 'http://lower.com'], 'http://lower.com:80', 1],
|
|
||||||
[['NO_PROXY' => 'upper.com', 'no_proxy' => 'lower.com'], 'lower.com', 2],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataCGIOverrides
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $server
|
|
||||||
*/
|
|
||||||
public function testCGIUpperCaseOverridesHttp(array $server, string $expected, int $index): void
|
|
||||||
{
|
|
||||||
$_SERVER = array_merge($_SERVER, $server);
|
|
||||||
|
|
||||||
$list = ProxyHelper::getProxyData();
|
|
||||||
$this->assertSame($expected, $list[$index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataCGIOverrides(): array
|
|
||||||
{
|
|
||||||
// server, expected, list index
|
|
||||||
return [
|
|
||||||
[['http_proxy' => 'http://http.com', 'CGI_HTTP_PROXY' => 'http://cgi.com'], 'http://cgi.com:80', 0],
|
|
||||||
[['http_proxy' => 'http://http.com', 'cgi_http_proxy' => 'http://cgi.com'], 'http://http.com:80', 0],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNoHttpsProxyUsesHttpProxy(): void
|
|
||||||
{
|
|
||||||
$_SERVER['http_proxy'] = 'http://http.com';
|
|
||||||
|
|
||||||
[$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData();
|
|
||||||
$this->assertSame('http://http.com:80', $httpsProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testNoHttpProxyDoesNotUseHttpsProxy(): void
|
|
||||||
{
|
|
||||||
$_SERVER['https_proxy'] = 'http://https.com';
|
|
||||||
|
|
||||||
[$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData();
|
|
||||||
$this->assertSame(null, $httpProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataContextOptions
|
|
||||||
*
|
|
||||||
* @param array<string, string> $expected
|
|
||||||
*
|
|
||||||
* @phpstan-param array{http: array{proxy: string, header?: string}} $expected
|
|
||||||
*/
|
|
||||||
public function testGetContextOptions(string $url, array $expected): void
|
|
||||||
{
|
|
||||||
$this->assertEquals($expected, ProxyHelper::getContextOptions($url));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataContextOptions(): array
|
|
||||||
{
|
|
||||||
// url, expected
|
|
||||||
return [
|
|
||||||
['http://proxy.com', ['http' => [
|
|
||||||
'proxy' => 'tcp://proxy.com:80',
|
|
||||||
]]],
|
|
||||||
['https://proxy.com', ['http' => [
|
|
||||||
'proxy' => 'ssl://proxy.com:443',
|
|
||||||
]]],
|
|
||||||
['http://user:p%40ss@proxy.com', ['http' => [
|
|
||||||
'proxy' => 'tcp://proxy.com:80',
|
|
||||||
'header' => 'Proxy-Authorization: Basic dXNlcjpwQHNz',
|
|
||||||
]]],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataRequestFullUri
|
|
||||||
*
|
|
||||||
* @param mixed[] $expected
|
|
||||||
*/
|
|
||||||
public function testSetRequestFullUri(string $requestUrl, array $expected): void
|
|
||||||
{
|
|
||||||
$options = [];
|
|
||||||
ProxyHelper::setRequestFullUri($requestUrl, $options);
|
|
||||||
|
|
||||||
$this->assertEquals($expected, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataRequestFullUri(): array
|
|
||||||
{
|
|
||||||
$options = ['http' => ['request_fulluri' => true]];
|
|
||||||
|
|
||||||
// $requestUrl, expected
|
|
||||||
return [
|
|
||||||
'http' => ['http://repo.org', $options],
|
|
||||||
'https' => ['https://repo.org', []],
|
|
||||||
'no-scheme' => ['repo.org', []],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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\ProxyItem;
|
||||||
|
use Composer\Test\TestCase;
|
||||||
|
|
||||||
|
class ProxyItemTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider dataMalformed
|
||||||
|
*/
|
||||||
|
public function testThrowsOnMalformedUrl(string $url): void
|
||||||
|
{
|
||||||
|
self::expectException('RuntimeException');
|
||||||
|
$proxyItem = new ProxyItem($url, 'http_proxy');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<string>>
|
||||||
|
*/
|
||||||
|
public static function dataMalformed(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ws-r' => ["http://user\rname@localhost:80"],
|
||||||
|
'ws-n' => ["http://user\nname@localhost:80"],
|
||||||
|
'ws-t' => ["http://user\tname@localhost:80"],
|
||||||
|
'no-host' => ['localhost'],
|
||||||
|
'no-port' => ['scheme://localhost'],
|
||||||
|
'port-0' => ['http://localhost:0'],
|
||||||
|
'port-big' => ['http://localhost:65536'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataFormatting
|
||||||
|
*/
|
||||||
|
public function testUrlFormatting(string $url, string $expected): void
|
||||||
|
{
|
||||||
|
$proxyItem = new ProxyItem($url, 'http_proxy');
|
||||||
|
$proxy = $proxyItem->toRequestProxy('http');
|
||||||
|
|
||||||
|
self::assertSame($expected, $proxy->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array<string>>
|
||||||
|
*/
|
||||||
|
public static function dataFormatting(): array
|
||||||
|
{
|
||||||
|
// url, expected
|
||||||
|
return [
|
||||||
|
'none' => ['http://proxy.com:8888', 'http://proxy.com:8888'],
|
||||||
|
'lowercases-scheme' => ['HTTP://proxy.com:8888', 'http://proxy.com:8888'],
|
||||||
|
'adds-http-scheme' => ['proxy.com:80', 'http://proxy.com:80'],
|
||||||
|
'adds-http-port' => ['http://proxy.com', 'http://proxy.com:80'],
|
||||||
|
'adds-https-port' => ['https://proxy.com', 'https://proxy.com:443'],
|
||||||
|
'removes-user' => ['http://user@proxy.com:6180', 'http://***@proxy.com:6180'],
|
||||||
|
'removes-user-pass' => ['http://user:p%40ss@proxy.com:6180', 'http://***:***@proxy.com:6180'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,8 +15,16 @@ namespace Composer\Test\Util\Http;
|
||||||
use Composer\Util\Http\ProxyManager;
|
use Composer\Util\Http\ProxyManager;
|
||||||
use Composer\Test\TestCase;
|
use Composer\Test\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @phpstan-import-type contextOptions from \Composer\Util\Http\RequestProxy
|
||||||
|
*/
|
||||||
class ProxyManagerTest extends TestCase
|
class ProxyManagerTest extends TestCase
|
||||||
{
|
{
|
||||||
|
// isTransitional can be removed after the transition period
|
||||||
|
|
||||||
|
/** @var bool */
|
||||||
|
private $isTransitional = true;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
unset(
|
unset(
|
||||||
|
@ -26,7 +34,8 @@ class ProxyManagerTest extends TestCase
|
||||||
$_SERVER['https_proxy'],
|
$_SERVER['https_proxy'],
|
||||||
$_SERVER['NO_PROXY'],
|
$_SERVER['NO_PROXY'],
|
||||||
$_SERVER['no_proxy'],
|
$_SERVER['no_proxy'],
|
||||||
$_SERVER['CGI_HTTP_PROXY']
|
$_SERVER['CGI_HTTP_PROXY'],
|
||||||
|
$_SERVER['cgi_http_proxy']
|
||||||
);
|
);
|
||||||
ProxyManager::reset();
|
ProxyManager::reset();
|
||||||
}
|
}
|
||||||
|
@ -41,7 +50,8 @@ class ProxyManagerTest extends TestCase
|
||||||
$_SERVER['https_proxy'],
|
$_SERVER['https_proxy'],
|
||||||
$_SERVER['NO_PROXY'],
|
$_SERVER['NO_PROXY'],
|
||||||
$_SERVER['no_proxy'],
|
$_SERVER['no_proxy'],
|
||||||
$_SERVER['CGI_HTTP_PROXY']
|
$_SERVER['CGI_HTTP_PROXY'],
|
||||||
|
$_SERVER['cgi_http_proxy']
|
||||||
);
|
);
|
||||||
ProxyManager::reset();
|
ProxyManager::reset();
|
||||||
}
|
}
|
||||||
|
@ -49,54 +59,137 @@ class ProxyManagerTest extends TestCase
|
||||||
public function testInstantiation(): void
|
public function testInstantiation(): void
|
||||||
{
|
{
|
||||||
$originalInstance = ProxyManager::getInstance();
|
$originalInstance = ProxyManager::getInstance();
|
||||||
$this->assertInstanceOf('Composer\Util\Http\ProxyManager', $originalInstance);
|
|
||||||
|
|
||||||
$sameInstance = ProxyManager::getInstance();
|
$sameInstance = ProxyManager::getInstance();
|
||||||
$this->assertTrue($originalInstance === $sameInstance);
|
self::assertTrue($originalInstance === $sameInstance);
|
||||||
|
|
||||||
ProxyManager::reset();
|
ProxyManager::reset();
|
||||||
$newInstance = ProxyManager::getInstance();
|
$newInstance = ProxyManager::getInstance();
|
||||||
$this->assertFalse($sameInstance === $newInstance);
|
self::assertFalse($sameInstance === $newInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGetProxyForRequestThrowsOnBadProxyUrl(): void
|
public function testGetProxyForRequestThrowsOnBadProxyUrl(): void
|
||||||
{
|
{
|
||||||
$_SERVER['http_proxy'] = 'localhost';
|
$_SERVER['http_proxy'] = 'localhost';
|
||||||
$proxyManager = ProxyManager::getInstance();
|
$proxyManager = ProxyManager::getInstance();
|
||||||
|
|
||||||
self::expectException('Composer\Downloader\TransportException');
|
self::expectException('Composer\Downloader\TransportException');
|
||||||
$proxyManager->getProxyForRequest('http://example.com');
|
$proxyManager->getProxyForRequest('http://example.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider dataRequest
|
* @dataProvider dataCaseOverrides
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $server
|
* @param array<string, string> $server
|
||||||
* @param mixed[] $expectedOptions
|
* @param non-empty-string $url
|
||||||
* @param non-empty-string $url
|
|
||||||
*/
|
*/
|
||||||
public function testGetProxyForRequest(array $server, string $url, string $expectedUrl, array $expectedOptions, bool $expectedSecure, string $expectedMessage): void
|
public function testLowercaseOverridesUppercase(array $server, string $url, string $expectedUrl): void
|
||||||
{
|
{
|
||||||
$_SERVER = array_merge($_SERVER, $server);
|
$_SERVER = array_merge($_SERVER, $server);
|
||||||
$proxyManager = ProxyManager::getInstance();
|
$proxyManager = ProxyManager::getInstance();
|
||||||
|
|
||||||
$proxy = $proxyManager->getProxyForRequest($url);
|
$proxy = $proxyManager->getProxyForRequest($url);
|
||||||
$this->assertInstanceOf('Composer\Util\Http\RequestProxy', $proxy);
|
self::assertSame($expectedUrl, $proxy->getStatus());
|
||||||
|
|
||||||
$this->assertSame($expectedUrl, $proxy->getUrl());
|
|
||||||
$this->assertSame($expectedOptions, $proxy->getContextOptions());
|
|
||||||
$this->assertSame($expectedSecure, $proxy->isSecure());
|
|
||||||
|
|
||||||
$message = $proxy->getFormattedUrl();
|
|
||||||
|
|
||||||
if ($expectedMessage) {
|
|
||||||
$condition = stripos($message, $expectedMessage) !== false;
|
|
||||||
} else {
|
|
||||||
$condition = $expectedMessage === $message;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertTrue($condition, 'lastProxy check');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{0: array<string, string>, 1: string, 2: string}>
|
||||||
|
*/
|
||||||
|
public static function dataCaseOverrides(): array
|
||||||
|
{
|
||||||
|
// server, url, expectedUrl
|
||||||
|
return [
|
||||||
|
[['HTTP_PROXY' => 'http://upper.com', 'http_proxy' => 'http://lower.com'], 'http://repo.org', 'http://lower.com:80'],
|
||||||
|
[['CGI_HTTP_PROXY' => 'http://upper.com', 'cgi_http_proxy' => 'http://lower.com'], 'http://repo.org', 'http://lower.com:80'],
|
||||||
|
[['HTTPS_PROXY' => 'http://upper.com', 'https_proxy' => 'http://lower.com'], 'https://repo.org', 'http://lower.com:80'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataCGIProxy
|
||||||
|
*
|
||||||
|
* @param array<string, string> $server
|
||||||
|
*/
|
||||||
|
public function testCGIProxyIsOnlyUsedWhenNoHttpProxy(array $server, string $expectedUrl): void
|
||||||
|
{
|
||||||
|
$_SERVER = array_merge($_SERVER, $server);
|
||||||
|
$proxyManager = ProxyManager::getInstance();
|
||||||
|
|
||||||
|
$proxy = $proxyManager->getProxyForRequest('http://repo.org');
|
||||||
|
self::assertSame($expectedUrl, $proxy->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{0: array<string, string>, 1: string}>
|
||||||
|
*/
|
||||||
|
public static function dataCGIProxy(): array
|
||||||
|
{
|
||||||
|
// server, expectedUrl
|
||||||
|
return [
|
||||||
|
[['CGI_HTTP_PROXY' => 'http://cgi.com:80'], 'http://cgi.com:80'],
|
||||||
|
[['http_proxy' => 'http://http.com:80', 'CGI_HTTP_PROXY' => 'http://cgi.com:80'], 'http://http.com:80'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoHttpProxyDoesNotUseHttpsProxy(): void
|
||||||
|
{
|
||||||
|
$_SERVER['https_proxy'] = 'https://proxy.com:443';
|
||||||
|
$proxyManager = ProxyManager::getInstance();
|
||||||
|
|
||||||
|
$proxy = $proxyManager->getProxyForRequest('http://repo.org');
|
||||||
|
self::assertSame('', $proxy->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNoHttpsProxyDoesNotUseHttpProxy(): void
|
||||||
|
{
|
||||||
|
$_SERVER['http_proxy'] = 'http://proxy.com:80';
|
||||||
|
|
||||||
|
// This can be removed after the transition period.
|
||||||
|
// An empty https_proxy value prevents using any http_proxy
|
||||||
|
if ($this->isTransitional) {
|
||||||
|
$_SERVER['https_proxy'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$proxyManager = ProxyManager::getInstance();
|
||||||
|
$proxy = $proxyManager->getProxyForRequest('https://repo.org');
|
||||||
|
self::assertSame('', $proxy->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test can be removed after the transition period
|
||||||
|
*/
|
||||||
|
public function testTransitional(): void
|
||||||
|
{
|
||||||
|
$_SERVER['http_proxy'] = 'http://proxy.com:80';
|
||||||
|
$proxyManager = ProxyManager::getInstance();
|
||||||
|
|
||||||
|
$proxy = $proxyManager->getProxyForRequest('https://repo.org');
|
||||||
|
self::assertSame('http://proxy.com:80', $proxy->getStatus());
|
||||||
|
self::assertTrue($proxyManager->needsTransitionWarning());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataRequest
|
||||||
|
*
|
||||||
|
* @param array<string, string> $server
|
||||||
|
* @param non-empty-string $url
|
||||||
|
* @param ?contextOptions $options
|
||||||
|
*/
|
||||||
|
public function testGetProxyForRequest(array $server, string $url, ?array $options, string $status, bool $excluded): void
|
||||||
|
{
|
||||||
|
$_SERVER = array_merge($_SERVER, $server);
|
||||||
|
$proxyManager = ProxyManager::getInstance();
|
||||||
|
|
||||||
|
$proxy = $proxyManager->getProxyForRequest($url);
|
||||||
|
self::assertSame($options, $proxy->getContextOptions());
|
||||||
|
self::assertSame($status, $proxy->getStatus());
|
||||||
|
self::assertSame($excluded, $proxy->isExcludedByNoProxy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests context options. curl options are tested in RequestProxyTest.php
|
||||||
|
*
|
||||||
|
* @return list<array{0: array<string, string>, 1: string, 2: ?contextOptions, 3: string, 4: bool}>
|
||||||
|
*/
|
||||||
public static function dataRequest(): array
|
public static function dataRequest(): array
|
||||||
{
|
{
|
||||||
$server = [
|
$server = [
|
||||||
|
@ -105,68 +198,26 @@ class ProxyManagerTest extends TestCase
|
||||||
'no_proxy' => 'other.repo.org',
|
'no_proxy' => 'other.repo.org',
|
||||||
];
|
];
|
||||||
|
|
||||||
// server, url, expectedUrl, expectedOptions, expectedSecure, expectedMessage
|
// server, url, options, status, excluded
|
||||||
return [
|
return [
|
||||||
[[], 'http://repo.org', '', [], false, ''],
|
[[], 'http://repo.org', null, '', false],
|
||||||
[$server, 'http://repo.org', 'http://user:p%40ss@proxy.com:80',
|
[$server, 'http://repo.org',
|
||||||
['http' => [
|
['http' => [
|
||||||
'proxy' => 'tcp://proxy.com:80',
|
'proxy' => 'tcp://proxy.com:80',
|
||||||
'header' => 'Proxy-Authorization: Basic dXNlcjpwQHNz',
|
'header' => 'Proxy-Authorization: Basic dXNlcjpwQHNz',
|
||||||
'request_fulluri' => true,
|
'request_fulluri' => true,
|
||||||
]],
|
]],
|
||||||
|
'http://***:***@proxy.com:80',
|
||||||
false,
|
false,
|
||||||
'http://user:***@proxy.com:80',
|
|
||||||
],
|
],
|
||||||
[
|
[$server, 'https://repo.org',
|
||||||
$server, 'https://repo.org', 'https://proxy.com:443',
|
|
||||||
['http' => [
|
['http' => [
|
||||||
'proxy' => 'ssl://proxy.com:443',
|
'proxy' => 'ssl://proxy.com:443',
|
||||||
]],
|
]],
|
||||||
true,
|
|
||||||
'https://proxy.com:443',
|
'https://proxy.com:443',
|
||||||
|
false,
|
||||||
],
|
],
|
||||||
[$server, 'https://other.repo.org', '', [], false, 'no_proxy'],
|
[$server, 'https://other.repo.org', null, 'excluded by no_proxy', true],
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider dataStatus
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $server
|
|
||||||
*/
|
|
||||||
public function testGetStatus(array $server, bool $expectedStatus, ?string $expectedMessage): void
|
|
||||||
{
|
|
||||||
$_SERVER = array_merge($_SERVER, $server);
|
|
||||||
$proxyManager = ProxyManager::getInstance();
|
|
||||||
$status = $proxyManager->isProxying();
|
|
||||||
$message = $proxyManager->getFormattedProxy();
|
|
||||||
|
|
||||||
$this->assertSame($expectedStatus, $status);
|
|
||||||
|
|
||||||
if ($expectedMessage !== null) {
|
|
||||||
$condition = stripos($message, $expectedMessage) !== false;
|
|
||||||
} else {
|
|
||||||
$condition = $expectedMessage === $message;
|
|
||||||
}
|
|
||||||
$this->assertTrue($condition, 'message check');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function dataStatus(): array
|
|
||||||
{
|
|
||||||
// server, expectedStatus, expectedMessage
|
|
||||||
return [
|
|
||||||
[[], false, null],
|
|
||||||
[['http_proxy' => 'localhost'], false, 'malformed'],
|
|
||||||
[
|
|
||||||
['http_proxy' => 'http://user:p%40ss@proxy.com:80'],
|
|
||||||
true,
|
|
||||||
'http=http://user:***@proxy.com:80',
|
|
||||||
],
|
|
||||||
[
|
|
||||||
['http_proxy' => 'proxy.com:80', 'https_proxy' => 'proxy.com:80'],
|
|
||||||
true,
|
|
||||||
'http=proxy.com:80, https=proxy.com:80',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,45 +17,174 @@ use Composer\Test\TestCase;
|
||||||
|
|
||||||
class RequestProxyTest extends TestCase
|
class RequestProxyTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
public function testFactoryNone(): void
|
||||||
* @dataProvider dataSecure
|
|
||||||
*/
|
|
||||||
public function testIsSecure(string $url, bool $expectedSecure): void
|
|
||||||
{
|
{
|
||||||
$proxy = new RequestProxy($url, [], '');
|
$proxy = RequestProxy::none();
|
||||||
|
|
||||||
$this->assertSame($expectedSecure, $proxy->isSecure());
|
$options = extension_loaded('curl') ? [CURLOPT_PROXY => ''] : [];
|
||||||
|
self::assertSame($options, $proxy->getCurlOptions([]));
|
||||||
|
self::assertNull($proxy->getContextOptions());
|
||||||
|
self::assertSame('', $proxy->getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFactoryNoProxy(): void
|
||||||
|
{
|
||||||
|
$proxy = RequestProxy::noProxy();
|
||||||
|
|
||||||
|
$options = extension_loaded('curl') ? [CURLOPT_PROXY => ''] : [];
|
||||||
|
self::assertSame($options, $proxy->getCurlOptions([]));
|
||||||
|
self::assertNull($proxy->getContextOptions());
|
||||||
|
self::assertSame('excluded by no_proxy', $proxy->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider dataSecure
|
||||||
|
*
|
||||||
|
* @param ?non-empty-string $url
|
||||||
|
*/
|
||||||
|
public function testIsSecure(?string $url, bool $expected): void
|
||||||
|
{
|
||||||
|
$proxy = new RequestProxy($url, null, null, null);
|
||||||
|
self::assertSame($expected, $proxy->isSecure());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{0: ?non-empty-string, 1: bool}>
|
||||||
|
*/
|
||||||
public static function dataSecure(): array
|
public static function dataSecure(): array
|
||||||
{
|
{
|
||||||
// url, secure
|
// url, expected
|
||||||
return [
|
return [
|
||||||
'basic' => ['http://proxy.com:80', false],
|
'basic' => ['http://proxy.com:80', false],
|
||||||
'secure' => ['https://proxy.com:443', true],
|
'secure' => ['https://proxy.com:443', true],
|
||||||
'none' => ['', false],
|
'none' => [null, false],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function testGetStatusThrowsOnBadFormatSpecifier(): void
|
||||||
* @dataProvider dataProxyUrl
|
|
||||||
*/
|
|
||||||
public function testGetFormattedUrlFormat(string $url, string $format, string $expected): void
|
|
||||||
{
|
{
|
||||||
$proxy = new RequestProxy($url, [], $url);
|
$proxy = new RequestProxy('http://proxy.com:80', null, null, 'http://proxy.com:80');
|
||||||
|
self::expectException('InvalidArgumentException');
|
||||||
$message = $proxy->getFormattedUrl($format);
|
$proxy->getStatus('using proxy');
|
||||||
$this->assertSame($expected, $message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function dataProxyUrl(): array
|
/**
|
||||||
|
* @dataProvider dataStatus
|
||||||
|
*
|
||||||
|
* @param ?non-empty-string $url
|
||||||
|
*/
|
||||||
|
public function testGetStatus(?string $url, ?string $format, string $expected): void
|
||||||
|
{
|
||||||
|
$proxy = new RequestProxy($url, null, null, $url);
|
||||||
|
|
||||||
|
if ($format === null) {
|
||||||
|
// try with and without optional param
|
||||||
|
self::assertSame($expected, $proxy->getStatus());
|
||||||
|
self::assertSame($expected, $proxy->getStatus($format));
|
||||||
|
} else {
|
||||||
|
self::assertSame($expected, $proxy->getStatus($format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{0: ?non-empty-string, 1: ?string, 2: string}>
|
||||||
|
*/
|
||||||
|
public static function dataStatus(): array
|
||||||
{
|
{
|
||||||
$format = 'proxy (%s)';
|
$format = 'proxy (%s)';
|
||||||
|
|
||||||
// url, format, expected
|
// url, format, expected
|
||||||
return [
|
return [
|
||||||
['', $format, ''],
|
'no-proxy' => [null, $format, ''],
|
||||||
['http://proxy.com:80', $format, 'proxy (http://proxy.com:80)'],
|
'null-format' => ['http://proxy.com:80', null, 'http://proxy.com:80'],
|
||||||
|
'with-format' => ['http://proxy.com:80', $format, 'proxy (http://proxy.com:80)'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test avoids HTTPS proxies so that it can be run on PHP < 7.3
|
||||||
|
*
|
||||||
|
* @requires extension curl
|
||||||
|
* @dataProvider dataCurlOptions
|
||||||
|
*
|
||||||
|
* @param ?non-empty-string $url
|
||||||
|
* @param ?non-empty-string $auth
|
||||||
|
* @param array<int, string|int> $expected
|
||||||
|
*/
|
||||||
|
public function testGetCurlOptions(?string $url, ?string $auth, array $expected): void
|
||||||
|
{
|
||||||
|
$proxy = new RequestProxy($url, $auth, null, null);
|
||||||
|
self::assertSame($expected, $proxy->getCurlOptions([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{0: ?string, 1: ?string, 2: array<int, string|int>}>
|
||||||
|
*/
|
||||||
|
public static function dataCurlOptions(): array
|
||||||
|
{
|
||||||
|
// url, auth, expected
|
||||||
|
return [
|
||||||
|
[null, null, [CURLOPT_PROXY => '']],
|
||||||
|
['http://proxy.com:80', null,
|
||||||
|
[
|
||||||
|
CURLOPT_PROXY => 'http://proxy.com:80',
|
||||||
|
CURLOPT_NOPROXY => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['http://proxy.com:80', 'user:p%40ss',
|
||||||
|
[
|
||||||
|
CURLOPT_PROXY => 'http://proxy.com:80',
|
||||||
|
CURLOPT_NOPROXY => '',
|
||||||
|
CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
|
||||||
|
CURLOPT_PROXYUSERPWD => 'user:p%40ss',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP >= 7.3.0
|
||||||
|
* @requires extension curl >= 7.52.0
|
||||||
|
* @dataProvider dataCurlSSLOptions
|
||||||
|
*
|
||||||
|
* @param non-empty-string $url
|
||||||
|
* @param ?non-empty-string $auth
|
||||||
|
* @param array<string, string> $sslOptions
|
||||||
|
* @param array<int, string|int> $expected
|
||||||
|
*/
|
||||||
|
public function testGetCurlOptionsWithSSL(string $url, ?string $auth, array $sslOptions, array $expected): void
|
||||||
|
{
|
||||||
|
$proxy = new RequestProxy($url, $auth, null, null);
|
||||||
|
self::assertSame($expected, $proxy->getCurlOptions($sslOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{0: string, 1: ?string, 2: array<string, string>, 3: array<int, string|int>}>
|
||||||
|
*/
|
||||||
|
public static function dataCurlSSLOptions(): array
|
||||||
|
{
|
||||||
|
// for PHPStan on PHP < 7.3
|
||||||
|
$caInfo = 10246; // CURLOPT_PROXY_CAINFO
|
||||||
|
$caPath = 10247; // CURLOPT_PROXY_CAPATH
|
||||||
|
|
||||||
|
// url, auth, sslOptions, expected
|
||||||
|
return [
|
||||||
|
['https://proxy.com:443', null, ['cafile' => '/certs/bundle.pem'],
|
||||||
|
[
|
||||||
|
CURLOPT_PROXY => 'https://proxy.com:443',
|
||||||
|
CURLOPT_NOPROXY => '',
|
||||||
|
$caInfo => '/certs/bundle.pem',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['https://proxy.com:443', 'user:p%40ss', ['capath' => '/certs'],
|
||||||
|
[
|
||||||
|
CURLOPT_PROXY => 'https://proxy.com:443',
|
||||||
|
CURLOPT_NOPROXY => '',
|
||||||
|
CURLOPT_PROXYAUTH => CURLAUTH_BASIC,
|
||||||
|
CURLOPT_PROXYUSERPWD => 'user:p%40ss',
|
||||||
|
$caPath => '/certs',
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue