From c9c6849df06624bb22df204f89715149ed5713b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draic=20Brady?= Date: Tue, 25 Feb 2014 22:50:24 +0000 Subject: [PATCH] Add Common Name (CN) matching checks and TLS connection retry (by default). For example, the communicated host will be github.com, but the CN is *.github.com. Also not matching api.github.com. The logic detects an initial TLS CN-mismatch error, and parses the correct CN from the error, then checks if the CN and URL have same host before retrying. --- src/Composer/Util/RemoteFilesystem.php | 58 +++++++++++++++++++------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index b5902087f..ecdd251b4 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -34,6 +34,7 @@ class RemoteFilesystem private $lastProgress; private $options; private $disableTls = false; + private $retryTls = true; /** * Constructor. @@ -119,7 +120,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 = '') { $this->bytesMax = 0; $this->originUrl = $originUrl; @@ -133,7 +134,7 @@ class RemoteFilesystem $this->io->setAuthentication($originUrl, urldecode($match[1]), urldecode($match[2])); } - $options = $this->getOptionsForUrl($originUrl, $additionalOptions); + $options = $this->getOptionsForUrl($originUrl, $additionalOptions, $expectedCommonName); if ($this->io->isDebug()) { $this->io->write((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl); @@ -224,14 +225,25 @@ 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]; + return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress, $expectedCommonName); + } + } + if ($this->retry) { $this->retry = false; - return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); + return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress, $expectedCommonName); } 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); } @@ -325,8 +337,33 @@ class RemoteFilesystem throw new TransportException('RETRY'); } - protected function getOptionsForUrl($originUrl, $additionalOptions, $disableTls = false) + protected function getOptionsForUrl($originUrl, $additionalOptions, $validCommonName = '') { + + // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN + if ($this->disableTls === false) { + if (!preg_match("|^https?://|", $originUrl)) { + $host = $originUrl; + } else { + $host = parse_url($originUrl, 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. + */ + if (strlen($validCommonName) > 0) { + 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; + } + $headers = array( sprintf( 'User-Agent: Composer/%s (%s; %s; PHP %s.%s.%s)', @@ -343,17 +380,6 @@ class RemoteFilesystem $headers[] = 'Accept-Encoding: gzip'; } - // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN - if ($this->disableTls === false) { - if (!preg_match("|^https?://|", $originUrl)) { - $host = $originUrl; - } else { - $host = parse_url($originUrl, PHP_URL_HOST); - } - $this->options['ssl']['CN_match'] = $host; - $this->options['ssl']['SNI_server_name'] = $host; - } - $options = array_replace_recursive($this->options, $additionalOptions); if ($this->io->hasAuthentication($originUrl)) {