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.
main
Pádraic Brady 10 years ago
parent 30c6aa3183
commit c9c6849df0

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

Loading…
Cancel
Save