Browse Source

Code clarity and updates from the getcomposer.org installer

Jordi Boggiano 9 years ago
parent
commit
fc4d94f160
2 changed files with 93 additions and 56 deletions
  1. 4 3
      src/Composer/Command/ConfigCommand.php
  2. 89 53
      src/Composer/Util/RemoteFilesystem.php

+ 4 - 3
src/Composer/Command/ConfigCommand.php

@@ -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

+ 89 - 53
src/Composer/Util/RemoteFilesystem.php

@@ -44,23 +44,25 @@ class RemoteFilesystem
     /**
      * Constructor.
      *
-     * @param IOInterface $io      The IO instance
-     * @param Config      $config  The config
-     * @param array       $options The options
+     * @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, $options = array(), $disableTls = false)
+    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
-         */
+        // 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'])
-            || !\openssl_x509_parse(file_get_contents($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 {
@@ -316,15 +318,17 @@ class RemoteFilesystem
         }
 
         // Check if the failure was due to a Common Name mismatch with remote SSL cert and retry once (excl normal retry)
-        if (false === $result) {
-            if ($this->retryTls === true
-            && preg_match("|did not match expected CN|i", $errorMessage)
-            && preg_match("|Peer certificate CN=`(.*)' did not match|i", $errorMessage, $matches)) {
-                $this->retryTls = false;
-                $expectedCommonName = $matches[1];
-                $this->io->write("    <warning>Retrying download from ".$originUrl." with SSL Cert Common Name (CN): ".$expectedCommonName."</warning>");
-                return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress, $expectedCommonName);
-            }
+        if (
+            false === $result
+            && $this->retryTls === true
+            && preg_match('{did not match expected CN}i', $errorMessage)
+            && preg_match("{Peer certificate CN=`(.*)' did not match}i", $errorMessage, $matches)
+        ) {
+            $this->retryTls = false;
+            $expectedCommonName = $matches[1];
+            $this->io->write("    <warning>Retrying download from ".$originUrl." with SSL Cert Common Name (CN): ".$expectedCommonName."</warning>");
+
+            return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress, $expectedCommonName);
         }
 
         if ($this->retry) {
@@ -485,33 +489,30 @@ class RemoteFilesystem
 
     protected function getOptionsForUrl($originUrl, $additionalOptions, $validCommonName = '')
     {
+        $tlsOptions = array();
+
         // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN
         if ($this->disableTls === false) {
-            if (!preg_match("|^https?://|", $this->fileUrl)) {
+            if (!preg_match('{^https?://}', $this->fileUrl)) {
                 $host = $originUrl;
             } else {
                 $host = parse_url($this->fileUrl, PHP_URL_HOST);
             }
-            /**
-             * This is sheer painful, but hopefully it'll be a footnote once SAN support
-             * reaches PHP 5.4 and 5.5...
-             * Side-effect: We're betting on the CN being either a wildcard or www, e.g. *.github.com or www.example.com.
-             * TODO: Consider something more explicitly user based.
-             */
+
+            // This is sheer painful, but hopefully it'll be a footnote once SAN support
+            // reaches PHP 5.4 and 5.5...
+            // Side-effect: We're betting on the CN being either a wildcard or www, e.g. *.github.com or www.example.com.
             if (strlen($validCommonName) > 0) {
-                if (!preg_match("|".$host."$|i", $validCommonName)
-                || (count(explode('.', $validCommonName)) - count(explode('.', $host))) > 1) {
+                if (
+                    !preg_match('{'.$host.'$}i', $validCommonName)
+                    || (count(explode('.', $validCommonName)) - count(explode('.', $host))) > 1
+                ) {
                     throw new TransportException('Unable to read or match the Common Name (CN) from the remote SSL certificate.');
                 }
                 $host = $validCommonName;
             }
-            $this->options['ssl']['CN_match'] = $host;
-            $this->options['ssl']['SNI_server_name'] = $host;
-        }
-        if (defined('HHVM_VERSION')) {
-            $phpVersion = 'HHVM ' . HHVM_VERSION;
-        } else {
-            $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION;
+            $tlsOptions['ssl']['CN_match'] = $host;
+            $tlsOptions['ssl']['SNI_server_name'] = $host;
         }
 
         $headers = array();
@@ -520,7 +521,7 @@ class RemoteFilesystem
             $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
@@ -613,19 +614,27 @@ class RemoteFilesystem
          * The user may go download one if this occurs.
          */
         if (!isset($this->options['ssl']['cafile'])) {
-            $result = $this->getSystemCaRootBundlePath();
+            $result = self::getSystemCaRootBundlePath();
             if ($result) {
-                if (preg_match("|^phar://|", $result)) {
-                    $tmp = rtrim(sys_get_temp_dir(), '\\/');
-                    $target = $tmp . DIRECTORY_SEPARATOR . 'composer-cacert.pem';
-                    $cacert = file_get_contents($result);
-                    $write = file_put_contents($target, $cacert, LOCK_EX);
-                    if (!$write) {
-                        throw new TransportException('Unable to write bundled cacert.pem to: '.$target);
-                    }
-                    $options['ssl']['cafile'] = $target;
+                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 {
-                    $options['ssl']['cafile'] = $result;
+                    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.');
@@ -674,15 +683,17 @@ class RemoteFilesystem
     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     */
-    protected static function getSystemCaRootBundlePath()
+    private static function getSystemCaRootBundlePath()
     {
-        if (isset($found)) {
+        static $found = null;
+        if ($found !== null) {
             return $found;
         }
+
         // 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) && \openssl_x509_parse(file_get_contents($envCertFile))) {
+        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 $envCertFile;
         }
@@ -696,25 +707,50 @@ class RemoteFilesystem
             '/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
         );
 
-        static $found = false;
         $configured = ini_get('openssl.cafile');
-        if ($configured && strlen($configured) > 0 && is_readable($caBundle) && \openssl_x509_parse(file_get_contents($caBundle))) {
+        if ($configured && strlen($configured) > 0 && is_readable($caBundle) && self::validateCaFile(file_get_contents($caBundle))) {
             $found = true;
             $caBundle = $configured;
         } else {
             foreach ($caBundlePaths as $caBundle) {
-                if (is_readable($caBundle) && \openssl_x509_parse(file_get_contents($caBundle))) {
+                if (@is_readable($caBundle) && self::validateCaFile(file_get_contents($caBundle))) {
                     $found = true;
                     break;
                 }
             }
+            if (!$found) {
+                foreach ($caBundlePaths as $caBundle) {
+                    $caBundle = dirname($caBundle);
+                    if (is_dir($caBundle) && glob($caBundle.'/*')) {
+                        $found = true;
+                        break;
+                    }
+                }
+            }
         }
         if ($found) {
             $found = $caBundle;
         }
         return $found;
     }
+
+    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);
+    }
 }