diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php
index d373072170ac..377513068dcc 100644
--- a/src/Composer/Command/SelfUpdateCommand.php
+++ b/src/Composer/Command/SelfUpdateCommand.php
@@ -42,12 +42,12 @@ protected function configure()
protected function execute(InputInterface $input, OutputInterface $output)
{
$rfs = new RemoteFilesystem($this->getIO());
- $latest = trim($rfs->getContents('getcomposer.org', 'http://getcomposer.org/version', false));
+ $latest = trim($rfs->getContents('getcomposer.org', 'https://getcomposer.org/version', false));
if (Composer::VERSION !== $latest) {
$output->writeln(sprintf("Updating to version %s.", $latest));
- $remoteFilename = 'http://getcomposer.org/composer.phar';
+ $remoteFilename = 'https://getcomposer.org/composer.phar';
$localFilename = $_SERVER['argv'][0];
$tempFilename = basename($localFilename, '.phar').'-temp.phar';
diff --git a/src/Composer/Config.php b/src/Composer/Config.php
index 39e95fd0378f..324e36657216 100644
--- a/src/Composer/Config.php
+++ b/src/Composer/Config.php
@@ -22,13 +22,13 @@ class Config
'vendor-dir' => 'vendor',
'bin-dir' => '{$vendor-dir}/bin',
'notify-on-install' => true,
- 'github-protocols' => array('git', 'https', 'http'),
+ 'github-protocols' => array('https', 'git'),
);
public static $defaultRepositories = array(
'packagist' => array(
'type' => 'composer',
- 'url' => 'http://packagist.org',
+ 'url' => 'https://packagist.org',
)
);
diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php
index 51c98dec05c4..31229ee88b28 100644
--- a/src/Composer/Repository/ComposerRepository.php
+++ b/src/Composer/Repository/ComposerRepository.php
@@ -39,7 +39,7 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config)
{
if (!preg_match('{^\w+://}', $repoConfig['url'])) {
// assume http as the default protocol
- $repoConfig['url'] = 'http://'.$repoConfig['url'];
+ $repoConfig['url'] = 'https://'.$repoConfig['url'];
}
$repoConfig['url'] = rtrim($repoConfig['url'], '/');
if (function_exists('filter_var') && version_compare(PHP_VERSION, '5.3.3', '>=') && !filter_var($repoConfig['url'], FILTER_VALIDATE_URL)) {
diff --git a/src/Composer/Util/BadCryptoException.php b/src/Composer/Util/BadCryptoException.php
new file mode 100644
index 000000000000..72cf683b9cbb
--- /dev/null
+++ b/src/Composer/Util/BadCryptoException.php
@@ -0,0 +1,20 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Util;
+
+/**
+ * @author Evan Coury
+ */
+class BadCryptoException extends \Exception
+{
+}
diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php
index e82313033d17..d158e948a6ab 100644
--- a/src/Composer/Util/RemoteFilesystem.php
+++ b/src/Composer/Util/RemoteFilesystem.php
@@ -90,9 +90,15 @@ protected function get($originUrl, $fileUrl, $fileName = null, $progress = true)
$this->result = null;
$this->originUrl = $originUrl;
$this->fileUrl = $fileUrl;
+ $this->fileUrlParts = parse_url($fileUrl);
$this->fileName = $fileName;
$this->progress = $progress;
$this->lastProgress = null;
+ $this->ssl = ('https' === substr($fileUrl, 0, 5));
+
+ if (!extension_loaded('openssl') && $this->ssl) {
+ throw new \RuntimeException('You must enable the openssl extension in your php.ini to load information from '.$fileUrl);
+ }
$options = $this->getOptionsForUrl($originUrl);
$ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet')));
@@ -104,11 +110,22 @@ protected function get($originUrl, $fileUrl, $fileName = null, $progress = true)
$errorMessage = null;
set_error_handler(function ($code, $msg) use (&$errorMessage) {
$errorMessage = preg_replace('{^file_get_contents\(.+?\): }', '', $msg);
+ if ($this->ssl && strpos($errorMessage, 'SSL') !== false || strpos($errorMessage, 'enable crypto') !== false) {
+ throw new BadCryptoException($errorMessage, $code);
+ }
if (!ini_get('allow_url_fopen')) {
$errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')';
}
});
- $result = file_get_contents($fileUrl, false, $ctx);
+ try {
+ $result = file_get_contents($fileUrl, false, $ctx);
+ } catch (BadCryptoException $e) {
+ // SSL failed -- let's prompt the user about the certificate
+ $port = isset($this->fileUrlParts['port']) ? $this->fileUrlParts['port'] : 443;
+ $sslHelper = new SslHelper($this->io);
+ $sslHelper->verifySslCertificateFromServer($this->fileUrlParts['host'], $port, $fileUrl);
+ $result = file_get_contents($fileUrl, false, $ctx);
+ }
restore_error_handler();
// fix for 5.4.0 https://bugs.php.net/bug.php?id=61336
@@ -241,6 +258,18 @@ protected function getOptionsForUrl($originUrl)
$options['http']['header'] .= "Authorization: Basic $authStr\r\n";
}
+ if (!$this->ssl) return $options;
+
+ //if (getenv('COMPOSER_INSECURE_SSL_NOVERIFY') !== false) return $options;
+
+ $options['ssl'] = array(
+ 'verify_peer' => true,
+ 'allow_self_signed' => false,
+ 'cafile' => SslHelper::initCaBundleFile(),
+ // PHP improperly handles CN_match, so it's handled manually in Composer\Util\SslHelper
+ //'CN_match' => true,
+ );
+
return $options;
}
}
diff --git a/src/Composer/Util/SslHelper.php b/src/Composer/Util/SslHelper.php
new file mode 100644
index 000000000000..411bfd73a0a0
--- /dev/null
+++ b/src/Composer/Util/SslHelper.php
@@ -0,0 +1,147 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Util;
+
+use Composer\Factory;
+use Composer\IO\IOInterface;
+
+/**
+ * @author Evan Coury
+ */
+class SslHelper
+{
+ private $io;
+
+ public function __construct(IOInterface $io)
+ {
+ $this->io = $io;
+ $this->config = Factory::createConfig();
+ }
+
+ public function verifySslCertificateFromServer($hostname, $port = 443, $url = false)
+ {
+ $chain = $this->fetchCertificateChain($hostname, $port);
+ $cert = $chain[0];
+ $certInfo = openssl_x509_parse($cert);
+ $commonName = $certInfo['subject']['CN'];
+ $caCert = $chain[count($chain)-1];
+ $caCertInfo = openssl_x509_parse($caCert);
+
+ // Check if the certificate has expired
+ $expired = false;
+ if ($certInfo['validTo_time_t'] < time()) {
+ $expired = true;
+ if (!$this->io->askConfirmation("WARNING! The SSL certificate for {$hostname} has expired.\n\nAre you sure you wish to continue with the expired SSL certificate? [y/N] ", false)) {
+ throw new BadCryptoException('Encountered expired SSL certificate; exiting on user command.');
+ }
+ }
+ $expires = date('Y-m-d', $certInfo['validTo_time_t']) . (($expired) ? ' (WARNING: Certificate has expired!)' : '');
+
+ // Proper CN_Match handling -- PHP's is broken
+ $alternativeNames = explode(', ', str_replace('DNS:', '', $certInfo['extensions']['subjectAltName']));
+ $validDomain = true;
+ if ($hostname != $commonName && !in_array($hostname, $alternativeNames)) {
+ $validDomain = false;
+ if (!$this->io->askConfirmation("WARNING! The SSL certificate at {$hostname} is NOT VALID for the requested hostname. " .
+ "This could be an indication of a man-in-the-middle attack " .
+ "or a misconfigured server. It is strongly recommended that you DO NOT continue.\n\n" .
+ "Are you sure you want to continue with the invalid SSL certificate? [y/N] ", false)) {
+ throw new BadCryptoException('Encountered invalid SSL certificate; exiting on user command.');
+ }
+ }
+
+ // Get a readable Certificate Authority name
+ if (isset($caCertInfo['subject']['O'], $caCertInfo['subject']['CN'])) {
+ $caName = "{$caCertInfo['subject']['O']} » {$caCertInfo['subject']['CN']}";
+ } elseif (isset($caCertInfo['issuer']['O'], $caCertInfo['issuer']['CN'])) {
+ $caName = "{$caCertInfo['issuer']['O']} » {$caCertInfo['issuer']['CN']}";
+ } else {
+ $caName = $caCertInfo['name'];
+ }
+
+ // Get the certificate fingerprint
+ openssl_x509_export($cert, $certString);
+ $fingerprint = $this->getSha1Fingerprint($certString);
+
+ $this->io->write("\n #############################################################");
+ $this->io->write(' # WARNING: Composer is trying to connect to a secure server #');
+ $this->io->write(' # with an untrusted public key. Please review carefully. #');
+ $this->io->write(" #############################################################\n");
+
+ if ($url) $this->io->write(" Requested URL: {$url}");
+ $this->io->write(" Hostname: {$hostname}");
+ $this->io->write(' Common Name: ' . $commonName .
+ ((count($alternativeNames) > 0) ? ' (' . implode(', ', $alternativeNames) . ')' : '') . ''); // show alt names
+ $this->io->write(" Valid Until: {$expires}");
+ $this->io->write(" Fingerprint: {$fingerprint}");
+ $this->io->write(" Authority: {$caName}");
+
+ $this->io->write("\nTo verify the certificate fingerprint, go to https://{$hostname}/ in your web browser and view the certificate details.");
+
+ if ($this->io->askConfirmation("\nAre you sure you trust the Certificate Authority listed above and have verified the certificate fingerprint? [y/N] ", false)) {
+ openssl_x509_export($caCert, $caCertString);
+ file_put_contents(static::initCaBundleFile(), $caCertString, FILE_APPEND);
+ $this->io->write(" Added {$caName} as a trustworthy Certificate Authority.");
+ return true;
+ }
+
+ throw new BadCryptoException('Encountered untrusted SSL certificate; exiting on user command.');
+ }
+
+ protected function getSha1Fingerprint($certificate)
+ {
+ $certificate = str_replace('-----BEGIN CERTIFICATE-----', '', $certificate);
+ $certificate = str_replace('-----END CERTIFICATE-----', '', $certificate);
+ $certificate = base64_decode($certificate);
+ $fingerprint = strtoupper(sha1($certificate));
+ $fingerprint = str_split($fingerprint, 2);
+ return implode(':', $fingerprint);
+ }
+
+ protected function fetchCertificateChain($hostname, $port)
+ {
+ $context = stream_context_create(array('ssl' => array('capture_peer_cert_chain' => true)));
+ $socket = stream_socket_client("ssl://{$hostname}:{$port}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
+ $params = stream_context_get_params($socket);
+ return $params['options']['ssl']['peer_certificate_chain'];
+ }
+
+ public static function initCaBundleFile()
+ {
+ $config = Factory::createConfig();
+ $caBundleFile = $config->get('home') . '/trusted-ca-bundle.crt';
+
+ if (file_exists($caBundleFile)) {
+ return $caBundleFile;
+ }
+ touch($caBundleFile);
+
+ // Improve usability for some linux users by automatically
+ // importing the distro's CA bundle if it's found.
+ $caBundleLocations = array(
+ '/etc/pki/tls/certs/ca-bundle.crt',
+ '/usr/share/ssl/certs/ca-bubdle.crt',
+ );
+
+ foreach ($caBundleLocations as $caBundle) {
+ if (file_exists($caBundle) && is_readable($caBundle)) {
+ $trustedBundle = file_get_contents($caBundle);
+ break;
+ }
+ }
+
+ if (isset($trustedBundle)) file_put_contents($caBundleFile, $trustedBundle);
+
+ return $caBundleFile;
+ }
+}