1
0
Fork 0

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.
pull/2745/head
Pádraic Brady 2014-02-25 22:50:24 +00:00
parent 30c6aa3183
commit c9c6849df0
1 changed files with 42 additions and 16 deletions

View File

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