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
php:
- 5.3.3
- 5.3
- 5.4
- 5.5
@ -29,7 +28,6 @@ matrix:
before_script:
- rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
- flags=""
- if [ `phpenv version-name` == "5.3.3" ]; then flags="--ignore-platform-reqs"; fi
- composer install $flags
- bin/composer install $flags
- 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
```
> **Note:** If the above fails due to readfile, use the `http` url or enable
> php_openssl.dll in php.ini
> **Note:** If the above fails due to readfile, enable 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`:

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
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
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
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
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>\"}.",
"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": {
"type": "object",
"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');
}
$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
// passed in a file to use
@ -146,14 +147,14 @@ EOT
file_put_contents($configFile, "{\n}\n");
}
$this->configFile = new JsonFile($configFile);
$this->configFile = new JsonFile($configFile, null, $io);
$this->configSource = new JsonConfigSource($this->configFile);
$authConfigFile = $input->getOption('global')
? ($this->config->get('home') . '/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);
// initialize the global file if it's not there
@ -326,6 +327,11 @@ EOT
'optimize-autoloader' => array($booleanValidator, $booleanNormalizer),
'classmap-authoritative' => 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),
);
$multiConfigValues = array(

View File

@ -239,7 +239,7 @@ EOT
if (null === $repositoryUrl) {
$sourceRepo = new CompositeRepository(Factory::createDefaultRepositories($io, $config));
} 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();
if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) {
$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\Factory;
use Composer\Config;
use Composer\Downloader\TransportException;
use Composer\Plugin\CommandEvent;
use Composer\Plugin\PluginEvents;
@ -73,7 +74,7 @@ EOT
$config = Factory::createConfig();
}
$this->rfs = new RemoteFilesystem($io, $config);
$this->rfs = Factory::createRemoteFilesystem($io, $config);
$this->process = new ProcessExecutor($io);
$io->write('Checking platform settings: ', false);
@ -83,10 +84,10 @@ EOT
$this->outputResult($this->checkGit());
$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);
$this->outputResult($this->checkHttp('https'));
$this->outputResult($this->checkHttp('https', $config));
$opts = stream_context_get_options(StreamContextFactory::getContext('http://example.org'));
if (!empty($opts['http']['proxy'])) {
@ -172,12 +173,32 @@ EOT
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 {
$this->rfs->getContents('packagist.org', $proto . '://packagist.org/packages.json', false);
} catch (\Exception $e) {
return $e;
} catch (TransportException $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;
@ -332,7 +353,13 @@ EOT
if ($result instanceof \Exception) {
$io->write('['.get_class($result).'] '.$result->getMessage());
} 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)
{
$baseUrl = (extension_loaded('openssl') ? 'https' : 'http') . '://' . self::HOMEPAGE;
$config = Factory::createConfig();
if ($config->get('disable-tls') === true) {
$baseUrl = 'http://' . self::HOMEPAGE;
} else {
$baseUrl = 'https://' . self::HOMEPAGE;
}
$io = $this->getIO();
$remoteFilesystem = new RemoteFilesystem($io, $config);
$remoteFilesystem = Factory::createRemoteFilesystem($io, $config);
$cacheDir = $config->get('cache-dir');
$rollbackDir = $config->get('home');
$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/ClassLoader.php'));
$this->addFile($phar, new \SplFileInfo(__DIR__ . '/../../res/cacert.pem'), false);
$this->addComposerBin($phar);
// Stubs

View File

@ -44,6 +44,8 @@ class Config
'classmap-authoritative' => false,
'prepend-autoloader' => true,
'github-domains' => array('github.com'),
'disable-tls' => false,
'cafile' => null,
'github-expose-hostname' => true,
'gitlab-domains' => array('gitlab.com'),
'store-auths' => 'prompt',
@ -174,6 +176,7 @@ class Config
case 'cache-files-dir':
case 'cache-repo-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
$env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_'));
@ -263,6 +266,9 @@ class Config
return $this->config[$key];
case 'disable-tls':
return $this->config[$key] !== 'false' && (bool) $this->config[$key];
default:
if (!isset($this->config[$key])) {
return null;

View File

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

View File

@ -17,6 +17,7 @@ use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
/**
@ -28,10 +29,10 @@ class GzipDownloader extends ArchiveDownloader
{
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);
parent::__construct($io, $config, $eventDispatcher, $cache);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
}
protected function extract($file, $path)

View File

@ -16,6 +16,7 @@ use Composer\Config;
use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
use RarArchive;
@ -30,10 +31,10 @@ class RarDownloader extends ArchiveDownloader
{
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);
parent::__construct($io, $config, $eventDispatcher, $cache);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
}
protected function extract($file, $path)

View File

@ -37,7 +37,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa
$this->io = $io;
$this->config = $config;
$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\Package\PackageInterface;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
/**
@ -29,11 +30,11 @@ class XzDownloader extends ArchiveDownloader
{
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);
parent::__construct($io, $config, $eventDispatcher, $cache);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
}
protected function extract($file, $path)

View File

@ -16,6 +16,7 @@ use Composer\Config;
use Composer\Cache;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
use ZipArchive;
@ -26,10 +27,10 @@ class ZipDownloader extends ArchiveDownloader
{
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);
parent::__construct($io, $config, $eventDispatcher, $cache);
parent::__construct($io, $config, $eventDispatcher, $cache, $rfs);
}
protected function extract($file, $path)

View File

@ -19,12 +19,14 @@ use Composer\Package\Archiver;
use Composer\Package\Version\VersionGuesser;
use Composer\Repository\RepositoryManager;
use Composer\Repository\WritableRepositoryInterface;
use Composer\Util\Filesystem;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Autoload\AutoloadGenerator;
use Composer\Semver\VersionParser;
use Composer\Downloader\TransportException;
use Seld\JsonLint\JsonParser;
/**
@ -161,7 +163,7 @@ class Factory
throw new \InvalidArgumentException('This function requires either an IOInterface or a RepositoryManager');
}
$factory = new static;
$rm = $factory->createRepositoryManager($io, $config);
$rm = $factory->createRepositoryManager($io, $config, null, self::createRemoteFilesystem($io, $config));
}
foreach ($config->getRepositories() as $index => $repo) {
@ -207,7 +209,8 @@ class Factory
if (is_string($localConfig)) {
$composerFile = $localConfig;
$file = new JsonFile($localConfig, new RemoteFilesystem($io));
$file = new JsonFile($localConfig, null, $io);
if (!$file->exists()) {
if ($localConfig === './composer.json' || $localConfig === 'composer.json') {
@ -260,16 +263,18 @@ class Factory
$io->loadConfiguration($config);
}
$rfs = self::createRemoteFilesystem($io, $config);
// initialize event dispatcher
$dispatcher = new EventDispatcher($composer, $io);
$composer->setEventDispatcher($dispatcher);
// initialize repository manager
$rm = $this->createRepositoryManager($io, $config, $dispatcher);
$rm = $this->createRepositoryManager($io, $config, $dispatcher, $rfs);
$composer->setRepositoryManager($rm);
// load local repository
$this->addLocalRepository($rm, $vendorDir);
$this->addLocalRepository($io, $rm, $vendorDir);
// force-set the version of the global package if not defined as
// guessing it adds no value and only takes time
@ -290,7 +295,7 @@ class Factory
if ($fullLoad) {
// initialize download manager
$dm = $this->createDownloadManager($io, $config, $dispatcher);
$dm = $this->createDownloadManager($io, $config, $dispatcher, $rfs);
$composer->setDownloadManager($dm);
// initialize autoload generator
@ -320,7 +325,8 @@ class Factory
$lockFile = "json" === pathinfo($composerFile, PATHINFO_EXTENSION)
? substr($composerFile, 0, -4).'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);
}
@ -333,9 +339,9 @@ class Factory
* @param EventDispatcher $eventDispatcher
* @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('vcs', 'Composer\Repository\VcsRepository');
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');
@ -355,9 +361,9 @@ class Factory
* @param Repository\RepositoryManager $rm
* @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
* @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;
if ($config->get('cache-files-ttl') > 0) {
@ -409,18 +415,21 @@ class Factory
break;
}
$dm->setDownloader('git', new Downloader\GitDownloader($io, $config));
$dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config));
$executor = new ProcessExecutor($io);
$fs = new Filesystem($executor);
$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('zip', new Downloader\ZipDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache));
$dm->setDownloader('path', new Downloader\PathDownloader($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, $executor, $rfs));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $eventDispatcher, $cache, $executor, $rfs));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $eventDispatcher, $cache, $rfs));
$dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $eventDispatcher, $cache, $rfs));
return $dm;
}
@ -503,4 +512,48 @@ class Factory
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\ParsingException;
use Composer\Util\RemoteFilesystem;
use Composer\IO\IOInterface;
use Composer\Downloader\TransportException;
/**
@ -35,6 +36,7 @@ class JsonFile
private $path;
private $rfs;
private $io;
/**
* Initializes json file reader/parser.
@ -43,7 +45,7 @@ class JsonFile
* @param RemoteFilesystem $rfs required for loading http/https json files
* @throws \InvalidArgumentException
*/
public function __construct($path, RemoteFilesystem $rfs = null)
public function __construct($path, RemoteFilesystem $rfs = null, IOInterface $io = null)
{
$this->path = $path;
@ -51,6 +53,7 @@ class JsonFile
throw new \InvalidArgumentException('http urls require a RemoteFilesystem instance to be passed');
}
$this->rfs = $rfs;
$this->io = $io;
}
/**
@ -83,6 +86,9 @@ class JsonFile
if ($this->rfs) {
$json = $this->rfs->getContents($this->path, $this->path, false);
} else {
if ($this->io && $this->io->isDebug()) {
$this->io->writeError('Reading ' . $this->path);
}
$json = file_get_contents($this->path);
}
} catch (TransportException $e) {

View File

@ -20,6 +20,7 @@ use Composer\DependencyResolver\Pool;
use Composer\Json\JsonFile;
use Composer\Cache;
use Composer\Config;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Util\RemoteFilesystem;
use Composer\Plugin\PluginEvents;
@ -58,7 +59,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
private $degradedMode = false;
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'])) {
// assume http as the default protocol
@ -89,7 +90,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito
$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->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->repoConfig = $repoConfig;
}

View File

@ -22,6 +22,7 @@ use Composer\Package\Link;
use Composer\Semver\Constraint\Constraint;
use Composer\Util\RemoteFilesystem;
use Composer\Config;
use Composer\Factory;
/**
* Builds list of package from PEAR channel.
@ -58,7 +59,7 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn
$this->url = rtrim($repoConfig['url'], '/');
$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->versionParser = new VersionParser();
$this->repoConfig = $repoConfig;

View File

@ -16,6 +16,7 @@ use Composer\IO\IOInterface;
use Composer\Config;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
use Composer\Util\RemoteFilesystem;
/**
* Repositories manager.
@ -32,12 +33,14 @@ class RepositoryManager
private $io;
private $config;
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->config = $config;
$this->eventDispatcher = $eventDispatcher;
$this->rfs = $rfs;
}
/**
@ -102,7 +105,7 @@ class RepositoryManager
$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\Config;
use Composer\Factory;
use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor;
use Composer\Util\RemoteFilesystem;
@ -62,7 +63,7 @@ abstract class VcsDriver implements VcsDriverInterface
$this->io = $io;
$this->config = $config;
$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\Json\JsonFile;
use Composer\Spdx\SpdxLicenses;
use Composer\Factory;
/**
* Validates a composer configuration.
@ -52,7 +53,7 @@ class ConfigValidator
// validate json schema
$laxValid = false;
try {
$json = new JsonFile($file, new RemoteFilesystem($this->io));
$json = new JsonFile($file, null, $this->io);
$manifest = $json->read();
$json->validateSchema(JsonFile::LAX_SCHEMA);

View File

@ -39,7 +39,7 @@ class GitHub
$this->io = $io;
$this->config = $config;
$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\Config;
use Composer\Factory;
use Composer\Downloader\TransportException;
use Composer\Json\JsonFile;
@ -40,7 +41,7 @@ class GitLab
$this->io = $io;
$this->config = $config;
$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 $progress;
private $lastProgress;
private $options;
private $options = array();
private $disableTls = false;
private $retryAuthFailure;
private $lastHeaders;
private $storeAuth;
@ -45,12 +46,31 @@ class RemoteFilesystem
* @param IOInterface $io The IO instance
* @param Config $config The config
* @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;
// 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->options = $options;
}
/**
@ -94,6 +114,11 @@ class RemoteFilesystem
return $this->options;
}
public function isTlsDisabled()
{
return $this->disableTls === true;
}
/**
* Returns the headers of the last request
*
@ -118,7 +143,7 @@ class RemoteFilesystem
*
* @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)) {
$originUrl = 'github.com';
@ -145,7 +170,7 @@ class RemoteFilesystem
unset($additionalOptions['retry-auth-failure']);
}
$options = $this->getOptionsForUrl($originUrl, $additionalOptions);
$options = $this->getOptionsForUrl($originUrl, $additionalOptions, $expectedCommonName);
if ($this->io->isDebug()) {
$this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl);
@ -294,7 +319,7 @@ class RemoteFilesystem
if ($this->retry) {
$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->storeAuth($this->originUrl, $this->storeAuth);
@ -304,7 +329,7 @@ class RemoteFilesystem
}
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])) {
$e->setHeaders($http_response_header);
}
@ -449,13 +474,27 @@ class RemoteFilesystem
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();
if (extension_loaded('zlib')) {
$headers[] = 'Accept-Encoding: gzip';
}
$options = array_replace_recursive($this->options, $additionalOptions);
$options = array_replace_recursive($this->options, $tlsOptions, $additionalOptions);
if (!$this->degradedMode) {
// degraded mode disables HTTP/1.1 which causes issues with some bad
// proxies/software due to the use of chunked encoding
@ -486,4 +525,200 @@ class RemoteFilesystem
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'));
}
/**
* @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\Util\Filesystem;
use Composer\Util\RemoteFilesystem;
class XzDownloaderTest extends \PHPUnit_Framework_TestCase
{
@ -63,7 +64,7 @@ class XzDownloaderTest extends \PHPUnit_Framework_TestCase
->method('get')
->with('vendor-dir')
->will($this->returnValue($this->testDir));
$downloader = new XzDownloader($io, $config);
$downloader = new XzDownloader($io, $config, null, null, null, new RemoteFilesystem($io));
try {
$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');
$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')
->with('vendor-dir')
->will($this->returnValue($this->testDir));

View File

@ -33,7 +33,7 @@ class FactoryMock extends Factory
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);
}
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);
$ref = new \ReflectionMethod($fs, 'getOptionsForUrl');
$prop = new \ReflectionProperty($fs, 'fileUrl');
$ref->setAccessible(true);
$prop->setAccessible(true);
$prop->setValue($fs, $fileUrl);
return $ref->invokeArgs($fs, $args);
}