1
0
Fork 0

Merge branch 'tls-config'

pull/4784/head
Jordi Boggiano 2016-01-16 16:55:19 +00:00
commit d7c61c50ad
34 changed files with 4496 additions and 74 deletions

View File

@ -12,7 +12,6 @@ addons:
- parallel - parallel
php: php:
- 5.3.3
- 5.3 - 5.3
- 5.4 - 5.4
- 5.5 - 5.5
@ -29,7 +28,6 @@ matrix:
before_script: before_script:
- rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
- flags="" - flags=""
- if [ `phpenv version-name` == "5.3.3" ]; then flags="--ignore-platform-reqs"; fi
- composer install $flags - composer install $flags
- bin/composer install $flags - bin/composer install $flags
- git config --global user.name travis-ci - git config --global user.name travis-ci

View File

@ -146,8 +146,9 @@ C:\Users\username>cd C:\bin
C:\bin>php -r "readfile('https://getcomposer.org/installer');" | php C:\bin>php -r "readfile('https://getcomposer.org/installer');" | php
``` ```
> **Note:** If the above fails due to readfile, use the `http` url or enable > **Note:** If the above fails due to readfile, enable php_openssl.dll in php.ini.
> php_openssl.dll in php.ini > You may use the `http` URL, however this will leave the request susceptible to a
> Man-In-The-Middle (MITM) attack.
Create a new `composer.bat` file alongside `composer.phar`: Create a new `composer.bat` file alongside `composer.phar`:

View File

@ -704,6 +704,11 @@ By default it points to $COMPOSER_HOME/cache on \*nix and OSX, and
This env var controls the time Composer waits for commands (such as git This env var controls the time Composer waits for commands (such as git
commands) to finish executing. The default value is 300 seconds (5 minutes). commands) to finish executing. The default value is 300 seconds (5 minutes).
### COMPOSER_CAFILE
By setting this environmental value, you can set a path to a certificate bundle
file to be used during SSL/TLS peer verification.
### COMPOSER_DISCARD_CHANGES ### COMPOSER_DISCARD_CHANGES
This env var controls the [`discard-changes`](06-config.md#discard-changes) config option. This env var controls the [`discard-changes`](06-config.md#discard-changes) config option.

View File

@ -40,6 +40,25 @@ of their API. [Read
more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens) on how to get more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens) on how to get
an OAuth token for GitHub. an OAuth token for GitHub.
## gitlab-oauth
A list of domain names and oauth keys. For example using `{"gitlab.com":
"oauthtoken"}` as the value of this option will use `oauthtoken` to access
private repositories on gitlab.
## disable-tls
Defaults to `false`. If set to true all HTTPS URLs will be tried with HTTP
instead and no network level encryption is performed. Enabling this is a
security risk and is NOT recommended. The better way is to enable the
php_openssl extension in php.ini.
## cafile
A way to set the path to the openssl CA file. In PHP 5.6+ you should rather
set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to
detect your system CA file automatically.
## http-basic ## http-basic
A list of domain names and username/passwords to authenticate against them. For A list of domain names and username/passwords to authenticate against them. For

3952
res/cacert.pem Normal file

File diff suppressed because it is too large Load Diff

View File

@ -141,6 +141,14 @@
"description": "A hash of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":\"<token>\"}.", "description": "A hash of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":\"<token>\"}.",
"additionalProperties": true "additionalProperties": true
}, },
"disable-tls": {
"type": "boolean",
"description": "Defaults to `false`. If set to true all HTTPS URLs will be tried with HTTP instead and no network level encryption is performed. Enabling this is a security risk and is NOT recommended. The better way is to enable the php_openssl extension in php.ini."
},
"cafile": {
"type": "string",
"description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically."
},
"http-basic": { "http-basic": {
"type": "object", "type": "object",
"description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.", "description": "A hash of domain name => {\"username\": \"...\", \"password\": \"...\"}.",

View File

@ -133,7 +133,8 @@ EOT
throw new \RuntimeException('--file and --global can not be combined'); throw new \RuntimeException('--file and --global can not be combined');
} }
$this->config = Factory::createConfig($this->getIO()); $io = $this->getIO();
$this->config = Factory::createConfig($io);
// Get the local composer.json, global config.json, or if the user // Get the local composer.json, global config.json, or if the user
// passed in a file to use // passed in a file to use
@ -146,14 +147,14 @@ EOT
file_put_contents($configFile, "{\n}\n"); file_put_contents($configFile, "{\n}\n");
} }
$this->configFile = new JsonFile($configFile); $this->configFile = new JsonFile($configFile, null, $io);
$this->configSource = new JsonConfigSource($this->configFile); $this->configSource = new JsonConfigSource($this->configFile);
$authConfigFile = $input->getOption('global') $authConfigFile = $input->getOption('global')
? ($this->config->get('home') . '/auth.json') ? ($this->config->get('home') . '/auth.json')
: dirname(realpath($configFile)) . '/auth.json'; : dirname(realpath($configFile)) . '/auth.json';
$this->authConfigFile = new JsonFile($authConfigFile); $this->authConfigFile = new JsonFile($authConfigFile, null, $io);
$this->authConfigSource = new JsonConfigSource($this->authConfigFile, true); $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true);
// initialize the global file if it's not there // initialize the global file if it's not there
@ -326,6 +327,11 @@ EOT
'optimize-autoloader' => array($booleanValidator, $booleanNormalizer), 'optimize-autoloader' => array($booleanValidator, $booleanNormalizer),
'classmap-authoritative' => array($booleanValidator, $booleanNormalizer), 'classmap-authoritative' => array($booleanValidator, $booleanNormalizer),
'prepend-autoloader' => array($booleanValidator, $booleanNormalizer), 'prepend-autoloader' => array($booleanValidator, $booleanNormalizer),
'disable-tls' => array($booleanValidator, $booleanNormalizer),
'cafile' => array(
function ($val) { return file_exists($val) && is_readable($val); },
function ($val) { return $val === 'null' ? null : $val; }
),
'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
); );
$multiConfigValues = array( $multiConfigValues = array(

View File

@ -239,7 +239,7 @@ EOT
if (null === $repositoryUrl) { if (null === $repositoryUrl) {
$sourceRepo = new CompositeRepository(Factory::createDefaultRepositories($io, $config)); $sourceRepo = new CompositeRepository(Factory::createDefaultRepositories($io, $config));
} elseif ("json" === pathinfo($repositoryUrl, PATHINFO_EXTENSION) && file_exists($repositoryUrl)) { } elseif ("json" === pathinfo($repositoryUrl, PATHINFO_EXTENSION) && file_exists($repositoryUrl)) {
$json = new JsonFile($repositoryUrl, new RemoteFilesystem($io, $config)); $json = new JsonFile($repositoryUrl, Factory::createRemoteFilesystem($io, $config));
$data = $json->read(); $data = $json->read();
if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) { if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) {
$sourceRepo = new ComposerRepository(array('url' => 'file://' . strtr(realpath($repositoryUrl), '\\', '/')), $io, $config); $sourceRepo = new ComposerRepository(array('url' => 'file://' . strtr(realpath($repositoryUrl), '\\', '/')), $io, $config);

View File

@ -14,6 +14,7 @@ namespace Composer\Command;
use Composer\Composer; use Composer\Composer;
use Composer\Factory; use Composer\Factory;
use Composer\Config;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\Plugin\CommandEvent; use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
@ -73,7 +74,7 @@ EOT
$config = Factory::createConfig(); $config = Factory::createConfig();
} }
$this->rfs = new RemoteFilesystem($io, $config); $this->rfs = Factory::createRemoteFilesystem($io, $config);
$this->process = new ProcessExecutor($io); $this->process = new ProcessExecutor($io);
$io->write('Checking platform settings: ', false); $io->write('Checking platform settings: ', false);
@ -83,10 +84,10 @@ EOT
$this->outputResult($this->checkGit()); $this->outputResult($this->checkGit());
$io->write('Checking http connectivity to packagist: ', false); $io->write('Checking http connectivity to packagist: ', false);
$this->outputResult($this->checkHttp('http')); $this->outputResult($this->checkHttp('http', $config));
$io->write('Checking https connectivity to packagist: ', false); $io->write('Checking https connectivity to packagist: ', false);
$this->outputResult($this->checkHttp('https')); $this->outputResult($this->checkHttp('https', $config));
$opts = stream_context_get_options(StreamContextFactory::getContext('http://example.org')); $opts = stream_context_get_options(StreamContextFactory::getContext('http://example.org'));
if (!empty($opts['http']['proxy'])) { if (!empty($opts['http']['proxy'])) {
@ -172,12 +173,32 @@ EOT
return true; return true;
} }
private function checkHttp($proto) private function checkHttp($proto, Config $config)
{ {
$disableTls = false;
$result = array();
if ($proto === 'https' && $config->get('disable-tls') === true) {
$disableTls = true;
$result[] = '<warning>Composer is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.</warning>';
}
if ($proto === 'https' && !extension_loaded('openssl') && !$disableTls) {
$result[] = '<error>Composer is configured to use SSL/TLS protection but the openssl extension is not available.</error>';
}
try { try {
$this->rfs->getContents('packagist.org', $proto . '://packagist.org/packages.json', false); $this->rfs->getContents('packagist.org', $proto . '://packagist.org/packages.json', false);
} catch (\Exception $e) { } catch (TransportException $e) {
return $e; if (false !== strpos($e->getMessage(), 'cafile')) {
$result[] = '<error>[' . get_class($e) . '] ' . $e->getMessage() . '</error>';
$result[] = '<error>Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.</error>';
$result[] = '<error>You can alternatively disable this error, at your own risk, by enabling the \'disable-tls\' option.</error>';
} else {
array_unshift($result, '[' . get_class($e) . '] ' . $e->getMessage());
}
}
if (count($result) > 0) {
return $result;
} }
return true; return true;
@ -332,7 +353,13 @@ EOT
if ($result instanceof \Exception) { if ($result instanceof \Exception) {
$io->write('['.get_class($result).'] '.$result->getMessage()); $io->write('['.get_class($result).'] '.$result->getMessage());
} elseif ($result) { } elseif ($result) {
$io->write(trim($result)); if (is_array($result)) {
foreach ($result as $message) {
$io->write($message);
}
} else {
$io->write($result);
}
} }
} }
} }

View File

@ -58,10 +58,17 @@ EOT
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$baseUrl = (extension_loaded('openssl') ? 'https' : 'http') . '://' . self::HOMEPAGE;
$config = Factory::createConfig(); $config = Factory::createConfig();
if ($config->get('disable-tls') === true) {
$baseUrl = 'http://' . self::HOMEPAGE;
} else {
$baseUrl = 'https://' . self::HOMEPAGE;
}
$io = $this->getIO(); $io = $this->getIO();
$remoteFilesystem = new RemoteFilesystem($io, $config); $remoteFilesystem = Factory::createRemoteFilesystem($io, $config);
$cacheDir = $config->get('cache-dir'); $cacheDir = $config->get('cache-dir');
$rollbackDir = $config->get('home'); $rollbackDir = $config->get('home');
$localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0];

View File

@ -137,6 +137,9 @@ class Compiler
$this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/include_paths.php')); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/include_paths.php'));
} }
$this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/ClassLoader.php')); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../vendor/composer/ClassLoader.php'));
$this->addFile($phar, new \SplFileInfo(__DIR__ . '/../../res/cacert.pem'), false);
$this->addComposerBin($phar); $this->addComposerBin($phar);
// Stubs // Stubs

View File

@ -44,6 +44,8 @@ class Config
'classmap-authoritative' => false, 'classmap-authoritative' => false,
'prepend-autoloader' => true, 'prepend-autoloader' => true,
'github-domains' => array('github.com'), 'github-domains' => array('github.com'),
'disable-tls' => false,
'cafile' => null,
'github-expose-hostname' => true, 'github-expose-hostname' => true,
'gitlab-domains' => array('gitlab.com'), 'gitlab-domains' => array('gitlab.com'),
'store-auths' => 'prompt', 'store-auths' => 'prompt',
@ -174,6 +176,7 @@ class Config
case 'cache-files-dir': case 'cache-files-dir':
case 'cache-repo-dir': case 'cache-repo-dir':
case 'cache-vcs-dir': case 'cache-vcs-dir':
case 'cafile':
// convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config
$env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_'));
@ -263,6 +266,9 @@ class Config
return $this->config[$key]; return $this->config[$key];
case 'disable-tls':
return $this->config[$key] !== 'false' && (bool) $this->config[$key];
default: default:
if (!isset($this->config[$key])) { if (!isset($this->config[$key])) {
return null; return null;

View File

@ -14,6 +14,7 @@ namespace Composer\Downloader;
use Composer\Config; use Composer\Config;
use Composer\Cache; use Composer\Cache;
use Composer\Factory;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
@ -54,7 +55,7 @@ class FileDownloader implements DownloaderInterface
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs ?: new RemoteFilesystem($io, $config); $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
$this->filesystem = $filesystem ?: new Filesystem(); $this->filesystem = $filesystem ?: new Filesystem();
$this->cache = $cache; $this->cache = $cache;

View File

@ -17,6 +17,7 @@ use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
/** /**
@ -28,10 +29,10 @@ class GzipDownloader extends ArchiveDownloader
{ {
protected $process; protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
{ {
$this->process = $process ?: new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache); parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
} }
protected function extract($file, $path) protected function extract($file, $path)

View File

@ -16,6 +16,7 @@ use Composer\Config;
use Composer\Cache; use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use RarArchive; use RarArchive;
@ -30,10 +31,10 @@ class RarDownloader extends ArchiveDownloader
{ {
protected $process; protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
{ {
$this->process = $process ?: new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache); parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
} }
protected function extract($file, $path) protected function extract($file, $path)

View File

@ -37,7 +37,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
$this->filesystem = $fs ?: new Filesystem; $this->filesystem = $fs ?: new Filesystem($this->process);
} }
/** /**

View File

@ -17,6 +17,7 @@ use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
/** /**
@ -29,11 +30,11 @@ class XzDownloader extends ArchiveDownloader
{ {
protected $process; protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
{ {
$this->process = $process ?: new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache); parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
} }
protected function extract($file, $path) protected function extract($file, $path)

View File

@ -16,6 +16,7 @@ use Composer\Config;
use Composer\Cache; use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use ZipArchive; use ZipArchive;
@ -26,10 +27,10 @@ class ZipDownloader extends ArchiveDownloader
{ {
protected $process; protected $process;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null, RemoteFilesystem $rfs = null)
{ {
$this->process = $process ?: new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
parent::__construct($io, $config, $eventDispatcher, $cache); parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
} }
protected function extract($file, $path) protected function extract($file, $path)

View File

@ -19,12 +19,14 @@ use Composer\Package\Archiver;
use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionGuesser;
use Composer\Repository\RepositoryManager; use Composer\Repository\RepositoryManager;
use Composer\Repository\WritableRepositoryInterface; use Composer\Repository\WritableRepositoryInterface;
use Composer\Util\Filesystem;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Autoload\AutoloadGenerator; use Composer\Autoload\AutoloadGenerator;
use Composer\Semver\VersionParser; use Composer\Semver\VersionParser;
use Composer\Downloader\TransportException;
use Seld\JsonLint\JsonParser; use Seld\JsonLint\JsonParser;
/** /**
@ -161,7 +163,7 @@ class Factory
throw new \InvalidArgumentException('This function requires either an IOInterface or a RepositoryManager'); throw new \InvalidArgumentException('This function requires either an IOInterface or a RepositoryManager');
} }
$factory = new static; $factory = new static;
$rm = $factory->createRepositoryManager($io, $config); $rm = $factory->createRepositoryManager($io, $config, null, self::createRemoteFilesystem($io, $config));
} }
foreach ($config->getRepositories() as $index => $repo) { foreach ($config->getRepositories() as $index => $repo) {
@ -207,7 +209,8 @@ class Factory
if (is_string($localConfig)) { if (is_string($localConfig)) {
$composerFile = $localConfig; $composerFile = $localConfig;
$file = new JsonFile($localConfig, new RemoteFilesystem($io));
$file = new JsonFile($localConfig, null, $io);
if (!$file->exists()) { if (!$file->exists()) {
if ($localConfig === './composer.json' || $localConfig === 'composer.json') { if ($localConfig === './composer.json' || $localConfig === 'composer.json') {
@ -260,16 +263,18 @@ class Factory
$io->loadConfiguration($config); $io->loadConfiguration($config);
} }
$rfs = self::createRemoteFilesystem($io, $config);
// initialize event dispatcher // initialize event dispatcher
$dispatcher = new EventDispatcher($composer, $io); $dispatcher = new EventDispatcher($composer, $io);
$composer->setEventDispatcher($dispatcher); $composer->setEventDispatcher($dispatcher);
// initialize repository manager // initialize repository manager
$rm = $this->createRepositoryManager($io, $config, $dispatcher); $rm = $this->createRepositoryManager($io, $config, $dispatcher, $rfs);
$composer->setRepositoryManager($rm); $composer->setRepositoryManager($rm);
// load local repository // load local repository
$this->addLocalRepository($rm, $vendorDir); $this->addLocalRepository($io, $rm, $vendorDir);
// force-set the version of the global package if not defined as // force-set the version of the global package if not defined as
// guessing it adds no value and only takes time // guessing it adds no value and only takes time
@ -290,7 +295,7 @@ class Factory
if ($fullLoad) { if ($fullLoad) {
// initialize download manager // initialize download manager
$dm = $this->createDownloadManager($io, $config, $dispatcher); $dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs);
$composer->setDownloadManager($dm); $composer->setDownloadManager($dm);
// initialize autoload generator // initialize autoload generator
@ -320,7 +325,8 @@ class Factory
$lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION) $lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION)
? substr($composerFile, 0, -4).'lock' ? substr($composerFile, 0, -4).'lock'
: $composerFile . '.lock'; : $composerFile . '.lock';
$locker = new Package\Locker($io, new JsonFile($lockFile, new RemoteFilesystem($io, $config)), $rm, $im, file_get_contents($composerFile));
$locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $rm, $im, file_get_contents($composerFile));
$composer->setLocker($locker); $composer->setLocker($locker);
} }
@ -333,9 +339,9 @@ class Factory
* @param EventDispatcher $eventDispatcher * @param EventDispatcher $eventDispatcher
* @return Repository\RepositoryManager * @return Repository\RepositoryManager
*/ */
protected function createRepositoryManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) protected function createRepositoryManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
{ {
$rm = new RepositoryManager($io, $config, $eventDispatcher); $rm = new RepositoryManager($io, $config, $eventDispatcher, $rfs);
$rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository');
$rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository');
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');
@ -355,9 +361,9 @@ class Factory
* @param Repository\RepositoryManager $rm * @param Repository\RepositoryManager $rm
* @param string $vendorDir * @param string $vendorDir
*/ */
protected function addLocalRepository(RepositoryManager $rm, $vendorDir) protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir)
{ {
$rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json'))); $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json', null, $io)));
} }
/** /**
@ -388,7 +394,7 @@ class Factory
* @param EventDispatcher $eventDispatcher * @param EventDispatcher $eventDispatcher
* @return Downloader\DownloadManager * @return Downloader\DownloadManager
*/ */
public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) public function createDownloadManager(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
{ {
$cache = null; $cache = null;
if ($config->get('cache-files-ttl') > 0) { if ($config->get('cache-files-ttl') > 0) {
@ -409,18 +415,21 @@ class Factory
break; break;
} }
$dm->setDownloader('git', new Downloader\GitDownloader($io, $config)); $executor = new ProcessExecutor($io);
$dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config)); $fs = new Filesystem($executor);
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config));
$dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $executor, $fs));
$dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $executor, $fs));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs));
$dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache)); $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs));
return $dm; return $dm;
} }
@ -503,4 +512,48 @@ class Factory
return $factory->createComposer($io, $config, $disablePlugins); return $factory->createComposer($io, $config, $disablePlugins);
} }
/**
* @param IOInterface $io IO instance
* @param Config $config Config instance
* @param array $options Array of options passed directly to RemoteFilesystem constructor
* @return RemoteFilesystem
*/
public static function createRemoteFilesystem(IOInterface $io, Config $config = null, $options = array())
{
static $warned = false;
$disableTls = false;
if ($config && $config->get('disable-tls') === true) {
if (!$warned) {
$io->write('<warning>You are running Composer with SSL/TLS protection disabled.</warning>');
}
$warned = true;
$disableTls = true;
} elseif (!extension_loaded('openssl')) {
throw new \RuntimeException('The openssl extension is required for SSL/TLS protection but is not available. '
. 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.');
}
$remoteFilesystemOptions = array();
if ($disableTls === false) {
if ($config && $config->get('cafile')) {
$remoteFilesystemOptions = array('ssl' => array('cafile' => $config->get('cafile')));
}
$remoteFilesystemOptions = array_merge_recursive($remoteFilesystemOptions, $options);
}
try {
$remoteFilesystem = new RemoteFilesystem($io, $config, $remoteFilesystemOptions, $disableTls);
} catch (TransportException $e) {
if (false !== strpos($e->getMessage(), 'cafile')) {
$io->write('<error>Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.</error>');
$io->write('<error>A valid CA certificate file is required for SSL/TLS protection.</error>');
if (PHP_VERSION_ID < 50600) {
$io->write('<error>It is recommended you upgrade to PHP 5.6+ which can detect your system CA file automatically.</error>');
}
$io->write('<error>You can disable this error, at your own risk, by setting the \'disable-tls\' option to true.</error>');
}
throw $e;
}
return $remoteFilesystem;
}
} }

View File

@ -16,6 +16,7 @@ use JsonSchema\Validator;
use Seld\JsonLint\JsonParser; use Seld\JsonLint\JsonParser;
use Seld\JsonLint\ParsingException; use Seld\JsonLint\ParsingException;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
/** /**
@ -35,6 +36,7 @@ class JsonFile
private $path; private $path;
private $rfs; private $rfs;
private $io;
/** /**
* Initializes json file reader/parser. * Initializes json file reader/parser.
@ -43,7 +45,7 @@ class JsonFile
* @param RemoteFilesystem $rfs required for loading http/https json files * @param RemoteFilesystem $rfs required for loading http/https json files
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function __construct($path, RemoteFilesystem $rfs = null) public function __construct($path, RemoteFilesystem $rfs = null, IOInterface $io = null)
{ {
$this->path = $path; $this->path = $path;
@ -51,6 +53,7 @@ class JsonFile
throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed'); throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed');
} }
$this->rfs = $rfs; $this->rfs = $rfs;
$this->io = $io;
} }
/** /**
@ -83,6 +86,9 @@ class JsonFile
if ($this->rfs) { if ($this->rfs) {
$json = $this->rfs->getContents($this->path, $this->path, false); $json = $this->rfs->getContents($this->path, $this->path, false);
} else { } else {
if ($this->io && $this->io->isDebug()) {
$this->io->writeError('Reading ' . $this->path);
}
$json = file_get_contents($this->path); $json = file_get_contents($this->path);
} }
} catch (TransportException $e) { } catch (TransportException $e) {

View File

@ -20,6 +20,7 @@ use Composer\DependencyResolver\Pool;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Cache; use Composer\Cache;
use Composer\Config; use Composer\Config;
use Composer\Factory;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
use Composer\Plugin\PluginEvents; use Composer\Plugin\PluginEvents;
@ -58,7 +59,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
private $degradedMode = false; private $degradedMode = false;
private $rootData; private $rootData;
public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) public function __construct(array $repoConfig, IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
{ {
if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) { if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) {
// assume http as the default protocol // assume http as the default protocol
@ -89,7 +90,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$this->io = $io; $this->io = $io;
$this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$'); $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.preg_replace('{[^a-z0-9.]}i', '-', $this->url), 'a-z0-9.$');
$this->loader = new ArrayLoader(); $this->loader = new ArrayLoader();
$this->rfs = new RemoteFilesystem($this->io, $this->config, $this->options); $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $this->config, $this->options);
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->repoConfig = $repoConfig; $this->repoConfig = $repoConfig;
} }

View File

@ -22,6 +22,7 @@ use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\Constraint;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
use Composer\Config; use Composer\Config;
use Composer\Factory;
/** /**
* Builds list of package from PEAR channel. * Builds list of package from PEAR channel.
@ -58,7 +59,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
$this->url = rtrim($repoConfig['url'], '/'); $this->url = rtrim($repoConfig['url'], '/');
$this->io = $io; $this->io = $io;
$this->rfs = $rfs ?: new RemoteFilesystem($this->io, $config); $this->rfs = $rfs ?: Factory::createRemoteFilesystem($this->io, $config);
$this->vendorAlias = isset($repoConfig['vendor-alias']) ? $repoConfig['vendor-alias'] : null; $this->vendorAlias = isset($repoConfig['vendor-alias']) ? $repoConfig['vendor-alias'] : null;
$this->versionParser = new VersionParser(); $this->versionParser = new VersionParser();
$this->repoConfig = $repoConfig; $this->repoConfig = $repoConfig;

View File

@ -16,6 +16,7 @@ use Composer\IO\IOInterface;
use Composer\Config; use Composer\Config;
use Composer\EventDispatcher\EventDispatcher; use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\RemoteFilesystem;
/** /**
* Repositories manager. * Repositories manager.
@ -32,12 +33,14 @@ class RepositoryManager
private $io; private $io;
private $config; private $config;
private $eventDispatcher; private $eventDispatcher;
private $rfs;
public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null) public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, RemoteFilesystem $rfs = null)
{ {
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs;
} }
/** /**
@ -102,7 +105,7 @@ class RepositoryManager
$class = $this->repositoryClasses[$type]; $class = $this->repositoryClasses[$type];
return new $class($config, $this->io, $this->config, $this->eventDispatcher); return new $class($config, $this->io, $this->config, $this->eventDispatcher, $this->rfs);
} }
/** /**

View File

@ -14,6 +14,7 @@ namespace Composer\Repository\Vcs;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\Config; use Composer\Config;
use Composer\Factory;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor; use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem; use Composer\Util\RemoteFilesystem;
@ -62,7 +63,7 @@ abstract class VcsDriver implements VcsDriverInterface
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor($io); $this->process = $process ?: new ProcessExecutor($io);
$this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io, $config); $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
} }
/** /**

View File

@ -19,6 +19,7 @@ use Composer\Json\JsonValidationException;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Spdx\SpdxLicenses; use Composer\Spdx\SpdxLicenses;
use Composer\Factory;
/** /**
* Validates a composer configuration. * Validates a composer configuration.
@ -52,7 +53,7 @@ class ConfigValidator
// validate json schema // validate json schema
$laxValid = false; $laxValid = false;
try { try {
$json = new JsonFile($file, new RemoteFilesystem($this->io)); $json = new JsonFile($file, null, $this->io);
$manifest = $json->read(); $manifest = $json->read();
$json->validateSchema(JsonFile::LAX_SCHEMA); $json->validateSchema(JsonFile::LAX_SCHEMA);

View File

@ -39,7 +39,7 @@ class GitHub
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor; $this->process = $process ?: new ProcessExecutor;
$this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io, $config); $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
} }
/** /**

View File

@ -14,6 +14,7 @@ namespace Composer\Util;
use Composer\IO\IOInterface; use Composer\IO\IOInterface;
use Composer\Config; use Composer\Config;
use Composer\Factory;
use Composer\Downloader\TransportException; use Composer\Downloader\TransportException;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
@ -40,7 +41,7 @@ class GitLab
$this->io = $io; $this->io = $io;
$this->config = $config; $this->config = $config;
$this->process = $process ?: new ProcessExecutor(); $this->process = $process ?: new ProcessExecutor();
$this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io, $config); $this->remoteFilesystem = $remoteFilesystem ?: Factory::createRemoteFilesystem($this->io, $config);
} }
/** /**

View File

@ -33,7 +33,8 @@ class RemoteFilesystem
private $retry; private $retry;
private $progress; private $progress;
private $lastProgress; private $lastProgress;
private $options; private $options = array();
private $disableTls = false;
private $retryAuthFailure; private $retryAuthFailure;
private $lastHeaders; private $lastHeaders;
private $storeAuth; private $storeAuth;
@ -45,12 +46,31 @@ class RemoteFilesystem
* @param IOInterface $io The IO instance * @param IOInterface $io The IO instance
* @param Config $config The config * @param Config $config The config
* @param array $options The options * @param array $options The options
* @param bool $disableTls
*/ */
public function __construct(IOInterface $io, Config $config = null, array $options = array()) public function __construct(IOInterface $io, Config $config = null, array $options = array(), $disableTls = false)
{ {
$this->io = $io; $this->io = $io;
// Setup TLS options
// The cafile option can be set via config.json
if ($disableTls === false) {
$this->options = $this->getTlsDefaults();
if (isset($options['ssl']['cafile'])
&& (
!is_readable($options['ssl']['cafile'])
|| !self::validateCaFile(file_get_contents($options['ssl']['cafile']))
)
) {
throw new TransportException('The configured cafile was not valid or could not be read.');
}
} else {
$this->disableTls = true;
}
// handle the other externally set options normally.
$this->options = array_replace_recursive($this->options, $options);
$this->config = $config; $this->config = $config;
$this->options = $options;
} }
/** /**
@ -94,6 +114,11 @@ class RemoteFilesystem
return $this->options; return $this->options;
} }
public function isTlsDisabled()
{
return $this->disableTls === true;
}
/** /**
* Returns the headers of the last request * Returns the headers of the last request
* *
@ -118,7 +143,7 @@ class RemoteFilesystem
* *
* @return bool|string * @return bool|string
*/ */
protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true) protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true, $expectedCommonName = '')
{ {
if (strpos($originUrl, '.github.com') === (strlen($originUrl) - 11)) { if (strpos($originUrl, '.github.com') === (strlen($originUrl) - 11)) {
$originUrl = 'github.com'; $originUrl = 'github.com';
@ -145,7 +170,7 @@ class RemoteFilesystem
unset($additionalOptions['retry-auth-failure']); unset($additionalOptions['retry-auth-failure']);
} }
$options = $this->getOptionsForUrl($originUrl, $additionalOptions); $options = $this->getOptionsForUrl($originUrl, $additionalOptions, $expectedCommonName);
if ($this->io->isDebug()) { if ($this->io->isDebug()) {
$this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl); $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl);
@ -294,7 +319,7 @@ class RemoteFilesystem
if ($this->retry) { if ($this->retry) {
$this->retry = false; $this->retry = false;
$result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress, $expectedCommonName);
$authHelper = new AuthHelper($this->io, $this->config); $authHelper = new AuthHelper($this->io, $this->config);
$authHelper->storeAuth($this->originUrl, $this->storeAuth); $authHelper->storeAuth($this->originUrl, $this->storeAuth);
@ -304,7 +329,7 @@ class RemoteFilesystem
} }
if (false === $result) { if (false === $result) {
$e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded: '.$errorMessage, $errorCode); $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded: '.$errorMessage.' using CN='.$expectedCommonName, $errorCode);
if (!empty($http_response_header[0])) { if (!empty($http_response_header[0])) {
$e->setHeaders($http_response_header); $e->setHeaders($http_response_header);
} }
@ -449,13 +474,27 @@ class RemoteFilesystem
protected function getOptionsForUrl($originUrl, $additionalOptions) protected function getOptionsForUrl($originUrl, $additionalOptions)
{ {
$tlsOptions = array();
// Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN
if ($this->disableTls === false && PHP_VERSION_ID < 50600) {
if (!preg_match('{^https?://}', $this->fileUrl)) {
$host = $originUrl;
} else {
$host = parse_url($this->fileUrl, PHP_URL_HOST);
}
$tlsOptions['ssl']['CN_match'] = $host;
$tlsOptions['ssl']['SNI_server_name'] = $host;
}
$headers = array(); $headers = array();
if (extension_loaded('zlib')) { if (extension_loaded('zlib')) {
$headers[] = 'Accept-Encoding: gzip'; $headers[] = 'Accept-Encoding: gzip';
} }
$options = array_replace_recursive($this->options, $additionalOptions); $options = array_replace_recursive($this->options, $tlsOptions, $additionalOptions);
if (!$this->degradedMode) { if (!$this->degradedMode) {
// degraded mode disables HTTP/1.1 which causes issues with some bad // degraded mode disables HTTP/1.1 which causes issues with some bad
// proxies/software due to the use of chunked encoding // proxies/software due to the use of chunked encoding
@ -486,4 +525,200 @@ class RemoteFilesystem
return $options; return $options;
} }
private function getTlsDefaults()
{
$ciphers = implode(':', array(
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'DHE-RSA-AES128-GCM-SHA256',
'DHE-DSS-AES128-GCM-SHA256',
'kEDH+AESGCM',
'ECDHE-RSA-AES128-SHA256',
'ECDHE-ECDSA-AES128-SHA256',
'ECDHE-RSA-AES128-SHA',
'ECDHE-ECDSA-AES128-SHA',
'ECDHE-RSA-AES256-SHA384',
'ECDHE-ECDSA-AES256-SHA384',
'ECDHE-RSA-AES256-SHA',
'ECDHE-ECDSA-AES256-SHA',
'DHE-RSA-AES128-SHA256',
'DHE-RSA-AES128-SHA',
'DHE-DSS-AES128-SHA256',
'DHE-RSA-AES256-SHA256',
'DHE-DSS-AES256-SHA',
'DHE-RSA-AES256-SHA',
'AES128-GCM-SHA256',
'AES256-GCM-SHA384',
'ECDHE-RSA-RC4-SHA',
'ECDHE-ECDSA-RC4-SHA',
'AES128',
'AES256',
'RC4-SHA',
'HIGH',
'!aNULL',
'!eNULL',
'!EXPORT',
'!DES',
'!3DES',
'!MD5',
'!PSK'
));
/**
* CN_match and SNI_server_name are only known once a URL is passed.
* They will be set in the getOptionsForUrl() method which receives a URL.
*
* cafile or capath can be overridden by passing in those options to constructor.
*/
$options = array(
'ssl' => array(
'ciphers' => $ciphers,
'verify_peer' => true,
'verify_depth' => 7,
'SNI_enabled' => true,
)
);
/**
* Attempt to find a local cafile or throw an exception if none pre-set
* The user may go download one if this occurs.
*/
if (!isset($this->options['ssl']['cafile'])) {
$result = self::getSystemCaRootBundlePath();
if ($result) {
if (preg_match('{^phar://}', $result)) {
$targetPath = rtrim(sys_get_temp_dir(), '\\/') . '/composer-cacert.pem';
// use stream_copy_to_stream instead of copy
// to work around https://bugs.php.net/bug.php?id=64634
$source = fopen($result, 'r');
$target = fopen($targetPath, 'w+');
stream_copy_to_stream($source, $target);
fclose($source);
fclose($target);
unset($source, $target);
$options['ssl']['cafile'] = $targetPath;
} else {
if (is_dir($result)) {
$options['ssl']['capath'] = $result;
} elseif ($result) {
$options['ssl']['cafile'] = $result;
}
}
} else {
throw new TransportException('A valid cafile could not be located automatically.');
}
}
/**
* Disable TLS compression to prevent CRIME attacks where supported.
*/
if (PHP_VERSION_ID >= 50413) {
$options['ssl']['disable_compression'] = true;
}
return $options;
}
/**
* This method was adapted from Sslurp.
* https://github.com/EvanDotPro/Sslurp
*
* (c) Evan Coury <me@evancoury.com>
*
* For the full copyright and license information, please see below:
*
* Copyright (c) 2013, Evan Coury
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
private static function getSystemCaRootBundlePath()
{
static $caPath = null;
if ($caPath !== null) {
return $caPath;
}
// If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that.
// This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
$envCertFile = getenv('SSL_CERT_FILE');
if ($envCertFile && is_readable($envCertFile) && self::validateCaFile(file_get_contents($envCertFile))) {
// Possibly throw exception instead of ignoring SSL_CERT_FILE if it's invalid?
return $caPath = $envCertFile;
}
$caBundlePaths = array(
'/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package)
'/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package)
'/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package)
'/usr/local/share/certs/ca-root-nss.crt', // FreeBSD (ca_root_nss_package)
'/usr/ssl/certs/ca-bundle.crt', // Cygwin
'/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package
'/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option)
'/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
'/etc/ssl/cert.pem', // OpenBSD
'/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x
__DIR__.'/../../../res/cacert.pem', // Bundled with Composer
);
$configured = ini_get('openssl.cafile');
if ($configured && strlen($configured) > 0 && is_readable($caBundle) && self::validateCaFile(file_get_contents($caBundle))) {
return $caPath = $configured;
}
foreach ($caBundlePaths as $caBundle) {
if (@is_readable($caBundle) && self::validateCaFile(file_get_contents($caBundle))) {
return $caPath = $caBundle;
}
}
foreach ($caBundlePaths as $caBundle) {
$caBundle = dirname($caBundle);
if (is_dir($caBundle) && glob($caBundle.'/*')) {
return $caPath = $caBundle;
}
}
return $caPath = false;
}
private static function validateCaFile($contents)
{
// assume the CA is valid if php is vulnerable to
// https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html
if (
PHP_VERSION_ID <= 50327
|| (PHP_VERSION_ID >= 50400 && PHP_VERSION_ID < 50422)
|| (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50506)
) {
return !empty($contents);
}
return (bool) openssl_x509_parse($contents);
}
} }

View File

@ -170,4 +170,20 @@ class ConfigTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array('https'), $config->get('github-protocols')); $this->assertEquals(array('https'), $config->get('github-protocols'));
} }
/**
* @group TLS
*/
public function testDisableTlsCanBeOverridden()
{
$config = new Config;
$config->merge(
array('config' => array('disable-tls' => 'false'))
);
$this->assertFalse($config->get('disable-tls'));
$config->merge(
array('config' => array('disable-tls' => 'true'))
);
$this->assertTrue($config->get('disable-tls'));
}
} }

View File

@ -0,0 +1,28 @@
<?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;
use Composer\Config;
class DefaultConfigTest extends \PHPUnit_Framework_TestCase
{
/**
* @group TLS
*/
public function testDefaultValuesAreAsExpected()
{
$config = new Config;
$this->assertFalse($config->get('disable-tls'));
}
}

View File

@ -14,6 +14,7 @@ namespace Composer\Test\Downloader;
use Composer\Downloader\XzDownloader; use Composer\Downloader\XzDownloader;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\RemoteFilesystem;
class XzDownloaderTest extends \PHPUnit_Framework_TestCase class XzDownloaderTest extends \PHPUnit_Framework_TestCase
{ {
@ -63,7 +64,7 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase
->method('get') ->method('get')
->with('vendor-dir') ->with('vendor-dir')
->will($this->returnValue($this->testDir)); ->will($this->returnValue($this->testDir));
$downloader = new XzDownloader($io, $config); $downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io));
try { try {
$downloader->download($packageMock, sys_get_temp_dir().'/composer-xz-test'); $downloader->download($packageMock, sys_get_temp_dir().'/composer-xz-test');

View File

@ -55,7 +55,15 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
$io = $this->getMock('Composer\IO\IOInterface'); $io = $this->getMock('Composer\IO\IOInterface');
$config = $this->getMock('Composer\Config'); $config = $this->getMock('Composer\Config');
$config->expects($this->any()) $config->expects($this->at(0))
->method('get')
->with('disable-tls')
->will($this->returnValue(false));
$config->expects($this->at(1))
->method('get')
->with('cafile')
->will($this->returnValue(null));
$config->expects($this->at(2))
->method('get') ->method('get')
->with('vendor-dir') ->with('vendor-dir')
->will($this->returnValue($this->testDir)); ->will($this->returnValue($this->testDir));

View File

@ -33,7 +33,7 @@ class FactoryMock extends Factory
return $config; return $config;
} }
protected function addLocalRepository(RepositoryManager $rm, $vendorDir) protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir)
{ {
} }

View File

@ -165,11 +165,41 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase
unlink($file); unlink($file);
} }
protected function callGetOptionsForUrl($io, array $args = array(), array $options = array()) /**
* @group TLS
*/
public function testGetOptionsForUrlCreatesSecureTlsDefaults()
{
$io = $this->getMock('Composer\IO\IOInterface');
$res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl'=>array('cafile'=>'/some/path/file.crt'))), array(), 'http://www.example.org');
$this->assertTrue(isset($res['ssl']['ciphers']));
$this->assertRegExp("|!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK|", $res['ssl']['ciphers']);
$this->assertTrue($res['ssl']['verify_peer']);
$this->assertTrue($res['ssl']['SNI_enabled']);
$this->assertEquals(7, $res['ssl']['verify_depth']);
if (PHP_VERSION_ID < 50600) {
$this->assertEquals('www.example.org', $res['ssl']['CN_match']);
$this->assertEquals('www.example.org', $res['ssl']['SNI_server_name']);
}
$this->assertEquals('/some/path/file.crt', $res['ssl']['cafile']);
if (version_compare(PHP_VERSION, '5.4.13') >= 0) {
$this->assertTrue($res['ssl']['disable_compression']);
} else {
$this->assertFalse(isset($res['ssl']['disable_compression']));
}
}
protected function callGetOptionsForUrl($io, array $args = array(), array $options = array(), $fileUrl = '')
{ {
$fs = new RemoteFilesystem($io, null, $options); $fs = new RemoteFilesystem($io, null, $options);
$ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); $ref = new \ReflectionMethod($fs, 'getOptionsForUrl');
$prop = new \ReflectionProperty($fs, 'fileUrl');
$ref->setAccessible(true); $ref->setAccessible(true);
$prop->setAccessible(true);
$prop->setValue($fs, $fileUrl);
return $ref->invokeArgs($fs, $args); return $ref->invokeArgs($fs, $args);
} }