2012-02-14 10:25:00 +00:00
< ? php
/*
* This file is part of Composer .
*
* ( c ) Nils Adermann < naderman @ naderman . de >
* Jordi Boggiano < j . boggiano @ seld . be >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Composer\Util ;
2014-04-11 12:27:14 +00:00
use Composer\Config ;
2016-02-11 13:17:45 +00:00
use Composer\IO\IOInterface ;
2016-02-11 13:29:37 +00:00
use Composer\Downloader\TransportException ;
2016-04-11 15:33:29 +00:00
use Composer\CaBundle\CaBundle ;
use Psr\Log\LoggerInterface ;
2012-02-14 10:25:00 +00:00
/**
* @ author François Pluchino < francois . pluchino @ opendisplay . com >
2013-04-26 21:23:35 +00:00
* @ author Jordi Boggiano < j . boggiano @ seld . be >
2013-08-15 15:14:48 +00:00
* @ author Nils Adermann < naderman @ naderman . de >
2012-02-14 10:25:00 +00:00
*/
class RemoteFilesystem
{
2012-02-15 12:11:29 +00:00
private $io ;
2014-04-11 12:27:14 +00:00
private $config ;
2016-02-27 22:39:57 +00:00
private $scheme ;
2012-02-14 10:25:00 +00:00
private $bytesMax ;
2012-02-17 10:50:36 +00:00
private $originUrl ;
private $fileUrl ;
2012-02-14 10:25:00 +00:00
private $fileName ;
2013-04-30 10:14:27 +00:00
private $retry ;
2012-03-05 10:28:23 +00:00
private $progress ;
2012-02-17 10:53:38 +00:00
private $lastProgress ;
2014-03-01 14:32:07 +00:00
private $options = array ();
2016-01-19 23:54:23 +00:00
private $peerCertificateMap = array ();
2014-02-24 19:15:54 +00:00
private $disableTls = false ;
2014-01-17 15:32:55 +00:00
private $retryAuthFailure ;
2014-04-24 14:41:42 +00:00
private $lastHeaders ;
2014-05-27 11:50:47 +00:00
private $storeAuth ;
2015-07-20 16:31:45 +00:00
private $degradedMode = false ;
2016-01-17 21:33:55 +00:00
private $redirects ;
2016-01-17 19:15:06 +00:00
private $maxRedirects = 20 ;
2012-02-14 10:25:00 +00:00
/**
* Constructor .
*
2016-02-11 13:29:37 +00:00
* @ param IOInterface $io The IO instance
* @ param Config $config The config
* @ param array $options The options
2016-01-10 17:37:11 +00:00
* @ param bool $disableTls
2012-02-14 10:25:00 +00:00
*/
2016-01-10 17:37:11 +00:00
public function __construct ( IOInterface $io , Config $config = null , array $options = array (), $disableTls = false )
2012-02-14 10:25:00 +00:00
{
$this -> io = $io ;
2014-02-23 12:36:27 +00:00
2016-01-10 17:37:11 +00:00
// Setup TLS options
// The cafile option can be set via config.json
2014-02-23 14:15:24 +00:00
if ( $disableTls === false ) {
2016-01-20 20:20:18 +00:00
$this -> options = $this -> getTlsDefaults ( $options );
2014-02-24 19:15:54 +00:00
} else {
$this -> disableTls = true ;
2014-02-23 12:36:27 +00:00
}
// handle the other externally set options normally.
$this -> options = array_replace_recursive ( $this -> options , $options );
2014-04-11 12:27:14 +00:00
$this -> config = $config ;
2012-02-14 10:25:00 +00:00
}
/**
* Copy the remote file in local .
*
2015-09-28 09:51:14 +00:00
* @ param string $originUrl The origin URL
* @ param string $fileUrl The file URL
* @ param string $fileName the local filename
* @ param bool $progress Display the progression
* @ param array $options Additional context options
2012-02-17 11:35:42 +00:00
*
2012-06-23 09:58:18 +00:00
* @ return bool true
2012-02-15 12:11:29 +00:00
*/
2014-02-24 19:15:54 +00:00
public function copy ( $originUrl , $fileUrl , $fileName , $progress = true , $options = array ())
2012-02-15 12:11:29 +00:00
{
2014-02-24 19:15:54 +00:00
return $this -> get ( $originUrl , $fileUrl , $options , $fileName , $progress );
2012-02-15 12:11:29 +00:00
}
/**
* Get the content .
*
2015-09-28 09:51:14 +00:00
* @ param string $originUrl The origin URL
* @ param string $fileUrl The file URL
* @ param bool $progress Display the progression
* @ param array $options Additional context options
2012-02-15 12:11:29 +00:00
*
2014-07-16 13:17:38 +00:00
* @ return bool | string The content
2012-02-15 12:11:29 +00:00
*/
2014-02-24 19:15:54 +00:00
public function getContents ( $originUrl , $fileUrl , $progress = true , $options = array ())
2012-02-15 12:11:29 +00:00
{
2014-02-24 19:15:54 +00:00
return $this -> get ( $originUrl , $fileUrl , $options , null , $progress );
2012-02-15 12:11:29 +00:00
}
2013-08-14 16:47:25 +00:00
/**
* Retrieve the options set in the constructor
*
* @ return array Options
*/
public function getOptions ()
{
return $this -> options ;
}
2016-01-18 12:28:03 +00:00
/**
* Merges new options
*
* @ return array $options
*/
public function setOptions ( array $options )
{
$this -> options = array_replace_recursive ( $this -> options , $options );
}
2014-03-01 14:32:07 +00:00
public function isTlsDisabled ()
{
return $this -> disableTls === true ;
}
2014-04-24 14:41:42 +00:00
/**
* Returns the headers of the last request
*
* @ return array
*/
public function getLastHeaders ()
{
return $this -> lastHeaders ;
}
2016-01-18 12:25:37 +00:00
/**
2016-02-11 13:29:37 +00:00
* @ param array $headers array of returned headers like from getLastHeaders ()
* @ param string $name header name ( case insensitive )
2016-01-18 12:25:37 +00:00
* @ return string | null
*/
public function findHeaderValue ( array $headers , $name )
{
$value = null ;
foreach ( $headers as $header ) {
2016-02-11 13:29:37 +00:00
if ( preg_match ( '{^' . $name . ':\s*(.+?)\s*$}i' , $header , $match )) {
2016-01-18 12:25:37 +00:00
$value = $match [ 1 ];
} elseif ( preg_match ( '{^HTTP/}i' , $header )) {
// In case of redirects, http_response_headers contains the headers of all responses
// so we reset the flag when a new response is being parsed as we are only interested in the last response
$value = null ;
}
}
return $value ;
}
/**
2016-02-11 13:29:37 +00:00
* @ param array $headers array of returned headers like from getLastHeaders ()
2016-01-18 12:25:37 +00:00
* @ return int | null
*/
public function findStatusCode ( array $headers )
{
$value = null ;
foreach ( $headers as $header ) {
if ( preg_match ( '{^HTTP/\S+ (\d+)}i' , $header , $match )) {
// In case of redirects, http_response_headers contains the headers of all responses
// so we can not return directly and need to keep iterating
2016-02-11 13:29:37 +00:00
$value = ( int ) $match [ 1 ];
2016-01-18 12:25:37 +00:00
}
}
return $value ;
}
2012-02-15 12:11:29 +00:00
/**
* Get file content or copy action .
*
2015-09-28 09:51:14 +00:00
* @ param string $originUrl The origin URL
* @ param string $fileUrl The file URL
* @ param array $additionalOptions context options
* @ param string $fileName the local filename
* @ param bool $progress Display the progression
2012-02-14 10:25:00 +00:00
*
2013-06-13 00:05:44 +00:00
* @ throws TransportException | \Exception
2013-06-13 11:28:24 +00:00
* @ throws TransportException When the file could not be downloaded
2016-02-11 13:29:37 +00:00
*
2013-06-13 00:05:44 +00:00
* @ return bool | string
2012-02-14 10:25:00 +00:00
*/
2016-01-17 13:41:00 +00:00
protected function get ( $originUrl , $fileUrl , $additionalOptions = array (), $fileName = null , $progress = true )
2012-02-14 10:25:00 +00:00
{
2014-05-07 16:38:58 +00:00
if ( strpos ( $originUrl , '.github.com' ) === ( strlen ( $originUrl ) - 11 )) {
$originUrl = 'github.com' ;
}
2015-11-14 15:00:14 +00:00
$this -> scheme = parse_url ( $fileUrl , PHP_URL_SCHEME );
2012-02-14 10:25:00 +00:00
$this -> bytesMax = 0 ;
2012-02-17 10:50:36 +00:00
$this -> originUrl = $originUrl ;
$this -> fileUrl = $fileUrl ;
2012-02-15 12:11:29 +00:00
$this -> fileName = $fileName ;
2012-03-05 10:28:23 +00:00
$this -> progress = $progress ;
2012-02-17 10:53:38 +00:00
$this -> lastProgress = null ;
2014-01-17 15:32:55 +00:00
$this -> retryAuthFailure = true ;
2014-04-24 14:41:42 +00:00
$this -> lastHeaders = array ();
2016-01-17 21:33:55 +00:00
$this -> redirects = 1 ; // The first request counts.
2012-02-17 10:50:36 +00:00
2013-06-17 13:41:48 +00:00
// capture username/password from URL if there is one
if ( preg_match ( '{^https?://(.+):(.+)@([^/]+)}i' , $fileUrl , $match )) {
$this -> io -> setAuthentication ( $originUrl , urldecode ( $match [ 1 ]), urldecode ( $match [ 2 ]));
}
2016-01-26 10:43:59 +00:00
$tempAdditionalOptions = $additionalOptions ;
if ( isset ( $tempAdditionalOptions [ 'retry-auth-failure' ])) {
2016-02-11 13:29:37 +00:00
$this -> retryAuthFailure = ( bool ) $tempAdditionalOptions [ 'retry-auth-failure' ];
2014-01-17 15:32:55 +00:00
2016-01-26 10:43:59 +00:00
unset ( $tempAdditionalOptions [ 'retry-auth-failure' ]);
2014-01-17 15:32:55 +00:00
}
2016-01-25 17:55:29 +00:00
$isRedirect = false ;
2016-01-26 10:43:59 +00:00
if ( isset ( $tempAdditionalOptions [ 'redirects' ])) {
$this -> redirects = $tempAdditionalOptions [ 'redirects' ];
2016-01-25 17:55:29 +00:00
$isRedirect = true ;
2016-01-17 21:33:55 +00:00
2016-01-26 10:43:59 +00:00
unset ( $tempAdditionalOptions [ 'redirects' ]);
2016-01-17 21:33:55 +00:00
}
2016-01-26 10:43:59 +00:00
$options = $this -> getOptionsForUrl ( $originUrl , $tempAdditionalOptions );
unset ( $tempAdditionalOptions );
2016-01-17 19:42:43 +00:00
$userlandFollow = isset ( $options [ 'http' ][ 'follow_location' ]) && ! $options [ 'http' ][ 'follow_location' ];
2013-06-17 13:41:48 +00:00
2016-03-02 18:28:44 +00:00
$origFileUrl = $fileUrl ;
2014-10-21 08:25:24 +00:00
2013-02-27 11:34:18 +00:00
if ( isset ( $options [ 'github-token' ])) {
2016-02-11 13:29:37 +00:00
$fileUrl .= ( false === strpos ( $fileUrl , '?' ) ? '?' : '&' ) . 'access_token=' . $options [ 'github-token' ];
2013-02-27 11:34:18 +00:00
unset ( $options [ 'github-token' ]);
}
2014-10-21 08:25:24 +00:00
2015-04-10 21:45:24 +00:00
if ( isset ( $options [ 'gitlab-token' ])) {
2016-02-11 13:29:37 +00:00
$fileUrl .= ( false === strpos ( $fileUrl , '?' ) ? '?' : '&' ) . 'access_token=' . $options [ 'gitlab-token' ];
2015-04-10 21:45:24 +00:00
unset ( $options [ 'gitlab-token' ]);
}
2014-02-26 14:51:06 +00:00
if ( isset ( $options [ 'http' ])) {
$options [ 'http' ][ 'ignore_errors' ] = true ;
}
2015-11-13 15:49:14 +00:00
2015-07-20 16:31:45 +00:00
if ( $this -> degradedMode && substr ( $fileUrl , 0 , 21 ) === 'http://packagist.org/' ) {
// access packagist using the resolved IPv4 instead of the hostname to force IPv4 protocol
$fileUrl = 'http://' . gethostbyname ( 'packagist.org' ) . substr ( $fileUrl , 20 );
}
2015-11-14 13:13:39 +00:00
2016-02-11 13:29:37 +00:00
$ctx = StreamContextFactory :: getContext ( $fileUrl , $options , array ( 'notification' => array ( $this , 'callbackGet' )));
2012-02-17 10:50:36 +00:00
2016-03-02 18:28:44 +00:00
$actualContextOptions = stream_context_get_options ( $ctx );
$usingProxy = ! empty ( $actualContextOptions [ 'http' ][ 'proxy' ]) ? ' using proxy ' . $actualContextOptions [ 'http' ][ 'proxy' ] : '' ;
$this -> io -> writeError (( substr ( $origFileUrl , 0 , 4 ) === 'http' ? 'Downloading ' : 'Reading ' ) . $origFileUrl . $usingProxy , true , IOInterface :: DEBUG );
unset ( $origFileUrl , $actualContextOptions );
2016-01-25 17:55:29 +00:00
if ( $this -> progress && ! $isRedirect ) {
2015-03-24 01:36:30 +00:00
$this -> io -> writeError ( " Downloading: <comment>Connecting...</comment> " , false );
2012-02-15 12:11:29 +00:00
}
2016-03-09 23:19:52 +00:00
// Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256
if (( substr ( $fileUrl , 0 , 23 ) !== 'http://packagist.org/p/' || ( false === strpos ( $fileUrl , '$' ) && false === strpos ( $fileUrl , '%24' ))) && $this -> config ) {
$this -> config -> prohibitUrlByConfig ( $fileUrl );
2016-02-11 13:15:03 +00:00
}
2012-10-18 08:30:32 +00:00
$errorMessage = '' ;
2012-10-18 15:09:23 +00:00
$errorCode = 0 ;
2013-04-30 20:39:08 +00:00
$result = false ;
2012-08-17 14:51:58 +00:00
set_error_handler ( function ( $code , $msg ) use ( & $errorMessage ) {
2012-10-18 08:30:32 +00:00
if ( $errorMessage ) {
$errorMessage .= " \n " ;
2012-08-17 14:51:58 +00:00
}
2012-10-18 08:30:32 +00:00
$errorMessage .= preg_replace ( '{^file_get_contents\(.*?\): }' , '' , $msg );
2012-08-17 14:51:58 +00:00
});
2012-10-18 14:02:24 +00:00
try {
$result = file_get_contents ( $fileUrl , false , $ctx );
2016-01-19 23:54:23 +00:00
2016-04-19 14:43:59 +00:00
if ( $this -> bytesMax && Platform :: strlen ( $result ) < $this -> bytesMax ) {
2016-03-31 23:36:05 +00:00
// alas, this is not possible via the stream callback because STREAM_NOTIFY_COMPLETED is documented, but not implemented anywhere in PHP
throw new TransportException ( 'Content-Length mismatch' );
}
2016-01-19 23:54:23 +00:00
if ( PHP_VERSION_ID < 50600 && ! empty ( $options [ 'ssl' ][ 'peer_fingerprint' ])) {
// Emulate fingerprint validation on PHP < 5.6
$params = stream_context_get_params ( $ctx );
$expectedPeerFingerprint = $options [ 'ssl' ][ 'peer_fingerprint' ];
2016-01-24 18:52:09 +00:00
$peerFingerprint = TlsHelper :: getCertificateFingerprint ( $params [ 'options' ][ 'ssl' ][ 'peer_certificate' ]);
2016-01-19 23:54:23 +00:00
// Constant time compare??!
if ( $expectedPeerFingerprint !== $peerFingerprint ) {
throw new TransportException ( 'Peer fingerprint did not match' );
}
}
2012-10-18 14:02:24 +00:00
} catch ( \Exception $e ) {
if ( $e instanceof TransportException && ! empty ( $http_response_header [ 0 ])) {
$e -> setHeaders ( $http_response_header );
2016-01-18 12:25:37 +00:00
$e -> setStatusCode ( $this -> findStatusCode ( $http_response_header ));
2012-10-18 14:02:24 +00:00
}
2014-02-26 14:51:06 +00:00
if ( $e instanceof TransportException && $result !== false ) {
$e -> setResponse ( $result );
}
$result = false ;
2012-10-18 14:02:24 +00:00
}
2012-10-18 08:30:32 +00:00
if ( $errorMessage && ! ini_get ( 'allow_url_fopen' )) {
2016-02-11 13:29:37 +00:00
$errorMessage = 'allow_url_fopen must be enabled in php.ini (' . $errorMessage . ')' ;
2012-10-18 08:30:32 +00:00
}
2012-08-17 14:51:58 +00:00
restore_error_handler ();
2013-04-30 10:14:27 +00:00
if ( isset ( $e ) && ! $this -> retry ) {
2015-07-20 16:44:03 +00:00
if ( ! $this -> degradedMode && false !== strpos ( $e -> getMessage (), 'Operation timed out' )) {
2015-07-20 16:31:45 +00:00
$this -> degradedMode = true ;
$this -> io -> writeError ( array (
2016-02-11 13:29:37 +00:00
'<error>' . $e -> getMessage () . '</error>' ,
2015-09-28 09:51:14 +00:00
'<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>' ,
2015-07-20 16:31:45 +00:00
));
2016-02-11 13:29:37 +00:00
return $this -> get ( $this -> originUrl , $this -> fileUrl , $additionalOptions , $this -> fileName , $this -> progress );
2015-07-20 16:31:45 +00:00
}
2012-10-18 14:02:24 +00:00
throw $e ;
}
2012-02-17 11:35:42 +00:00
2016-01-18 12:25:37 +00:00
$statusCode = null ;
if ( ! empty ( $http_response_header [ 0 ])) {
$statusCode = $this -> findStatusCode ( $http_response_header );
}
2016-01-25 17:55:29 +00:00
// handle 3xx redirects for php<5.6, 304 Not Modified is excluded
2016-02-10 15:24:49 +00:00
$hasFollowedRedirect = false ;
2016-01-25 17:55:29 +00:00
if ( $userlandFollow && $statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $this -> redirects < $this -> maxRedirects ) {
2016-02-10 15:24:49 +00:00
$hasFollowedRedirect = true ;
2016-01-25 17:55:29 +00:00
$result = $this -> handleRedirect ( $http_response_header , $additionalOptions , $result );
2016-01-17 19:15:06 +00:00
}
2014-02-26 14:51:06 +00:00
// fail 4xx and 5xx responses and capture the response
2016-01-18 12:25:37 +00:00
if ( $statusCode && $statusCode >= 400 && $statusCode <= 599 ) {
2014-02-26 14:51:06 +00:00
if ( ! $this -> retry ) {
2016-03-27 16:14:51 +00:00
if ( $this -> progress && ! $this -> retry && ! $isRedirect ) {
$this -> io -> overwriteError ( " Downloading: <error>Failed</error> " );
}
2016-02-11 13:29:37 +00:00
$e = new TransportException ( 'The "' . $this -> fileUrl . '" file could not be downloaded (' . $http_response_header [ 0 ] . ')' , $statusCode );
2014-02-26 14:51:06 +00:00
$e -> setHeaders ( $http_response_header );
$e -> setResponse ( $result );
2016-01-18 12:25:37 +00:00
$e -> setStatusCode ( $statusCode );
2014-02-26 14:51:06 +00:00
throw $e ;
}
$result = false ;
2012-03-09 22:44:10 +00:00
}
2016-01-25 17:55:29 +00:00
if ( $this -> progress && ! $this -> retry && ! $isRedirect ) {
2016-03-27 16:14:51 +00:00
$this -> io -> overwriteError ( " Downloading: " . ( $result === false ? '<error>Failed</error>' : '<comment>100%</comment>' ));
2015-07-20 16:31:45 +00:00
}
2012-03-18 20:05:10 +00:00
// decode gzip
2016-02-10 15:24:49 +00:00
if ( $result && extension_loaded ( 'zlib' ) && substr ( $fileUrl , 0 , 4 ) === 'http' && ! $hasFollowedRedirect ) {
2016-01-18 12:25:37 +00:00
$decode = 'gzip' === strtolower ( $this -> findHeaderValue ( $http_response_header , 'content-encoding' ));
2012-03-18 21:12:48 +00:00
if ( $decode ) {
2015-07-20 16:31:45 +00:00
try {
if ( PHP_VERSION_ID >= 50400 ) {
$result = zlib_decode ( $result );
} else {
// work around issue with gzuncompress & co that do not work with all gzip checksums
2016-02-11 13:29:37 +00:00
$result = file_get_contents ( 'compress.zlib://data:application/octet-stream;base64,' . base64_encode ( $result ));
2015-07-20 16:31:45 +00:00
}
2014-01-10 17:48:38 +00:00
2015-07-20 16:31:45 +00:00
if ( ! $result ) {
throw new TransportException ( 'Failed to decode zlib stream' );
}
} catch ( \Exception $e ) {
2015-07-20 16:44:03 +00:00
if ( $this -> degradedMode ) {
throw $e ;
}
2014-01-10 17:48:38 +00:00
2015-07-20 16:31:45 +00:00
$this -> degradedMode = true ;
$this -> io -> writeError ( array (
2016-02-11 13:29:37 +00:00
'<error>Failed to decode response: ' . $e -> getMessage () . '</error>' ,
2015-09-28 09:51:14 +00:00
'<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>' ,
2015-07-20 16:31:45 +00:00
));
2014-01-10 17:48:38 +00:00
2016-02-11 13:29:37 +00:00
return $this -> get ( $this -> originUrl , $this -> fileUrl , $additionalOptions , $this -> fileName , $this -> progress );
2014-01-10 17:48:38 +00:00
}
2012-03-18 20:05:10 +00:00
}
}
// handle copy command if download was successful
2016-01-25 17:55:29 +00:00
if ( false !== $result && null !== $fileName && ! $isRedirect ) {
2013-04-29 15:15:55 +00:00
if ( '' === $result ) {
2016-02-11 13:29:37 +00:00
throw new TransportException ( '"' . $this -> fileUrl . '" appears broken, and returned an empty 200 response' );
2013-04-29 15:15:55 +00:00
}
2012-10-18 08:30:32 +00:00
$errorMessage = '' ;
set_error_handler ( function ( $code , $msg ) use ( & $errorMessage ) {
if ( $errorMessage ) {
$errorMessage .= " \n " ;
}
$errorMessage .= preg_replace ( '{^file_put_contents\(.*?\): }' , '' , $msg );
});
2016-02-11 13:29:37 +00:00
$result = ( bool ) file_put_contents ( $fileName , $result );
2012-10-18 08:30:32 +00:00
restore_error_handler ();
2012-04-10 07:43:47 +00:00
if ( false === $result ) {
2016-02-11 13:29:37 +00:00
throw new TransportException ( 'The "' . $this -> fileUrl . '" file could not be written to ' . $fileName . ': ' . $errorMessage );
2012-04-10 07:43:47 +00:00
}
2012-03-18 20:05:10 +00:00
}
2016-01-25 19:17:56 +00:00
// Handle SSL cert match issues
2016-01-19 23:54:23 +00:00
if ( false === $result && false !== strpos ( $errorMessage , 'Peer certificate' ) && PHP_VERSION_ID < 50600 ) {
// Certificate name error, PHP doesn't support subjectAltName on PHP < 5.6
// The procedure to handle sAN for older PHP's is:
//
// 1. Open socket to remote server and fetch certificate (disabling peer
// validation because PHP errors without giving up the certificate.)
//
// 2. Verifying the domain in the URL against the names in the sAN field.
// If there is a match record the authority [host/port], certificate
// common name, and certificate fingerprint.
//
// 3. Retry the original request but changing the CN_match parameter to
// the common name extracted from the certificate in step 2.
//
// 4. To prevent any attempt at being hoodwinked by switching the
// certificate between steps 2 and 3 the fingerprint of the certificate
// presented in step 3 is compared against the one recorded in step 2.
2016-04-11 15:33:29 +00:00
if ( CaBundle :: isOpensslParseSafe ()) {
2016-01-24 18:52:09 +00:00
$certDetails = $this -> getCertificateCnAndFp ( $this -> fileUrl , $options );
2016-01-19 23:54:23 +00:00
2016-01-24 18:52:09 +00:00
if ( $certDetails ) {
$this -> peerCertificateMap [ $this -> getUrlAuthority ( $this -> fileUrl )] = $certDetails ;
2016-01-19 23:54:23 +00:00
2016-01-24 18:52:09 +00:00
$this -> retry = true ;
}
} else {
$this -> io -> writeError ( sprintf (
'<error>Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.</error>' ,
PHP_VERSION
));
2016-01-19 23:54:23 +00:00
}
}
2013-04-30 10:14:27 +00:00
if ( $this -> retry ) {
$this -> retry = false ;
2016-02-11 13:29:37 +00:00
$result = $this -> get ( $this -> originUrl , $this -> fileUrl , $additionalOptions , $this -> fileName , $this -> progress );
2014-05-27 11:50:47 +00:00
2016-01-26 10:43:59 +00:00
if ( $this -> storeAuth && $this -> config ) {
2016-01-25 19:17:56 +00:00
$authHelper = new AuthHelper ( $this -> io , $this -> config );
$authHelper -> storeAuth ( $this -> originUrl , $this -> storeAuth );
$this -> storeAuth = false ;
}
2014-05-27 11:50:47 +00:00
return $result ;
2012-02-17 10:50:36 +00:00
}
2013-04-30 10:14:27 +00:00
if ( false === $result ) {
2016-02-11 13:29:37 +00:00
$e = new TransportException ( 'The "' . $this -> fileUrl . '" file could not be downloaded: ' . $errorMessage , $errorCode );
2012-10-18 15:09:23 +00:00
if ( ! empty ( $http_response_header [ 0 ])) {
$e -> setHeaders ( $http_response_header );
}
2015-07-20 16:44:03 +00:00
if ( ! $this -> degradedMode && false !== strpos ( $e -> getMessage (), 'Operation timed out' )) {
2015-07-20 16:31:45 +00:00
$this -> degradedMode = true ;
$this -> io -> writeError ( array (
2016-02-11 13:29:37 +00:00
'<error>' . $e -> getMessage () . '</error>' ,
2015-09-28 09:51:14 +00:00
'<error>Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info</error>' ,
2015-07-20 16:31:45 +00:00
));
2016-02-11 13:29:37 +00:00
return $this -> get ( $this -> originUrl , $this -> fileUrl , $additionalOptions , $this -> fileName , $this -> progress );
2015-07-20 16:31:45 +00:00
}
2012-10-18 15:09:23 +00:00
throw $e ;
2012-02-16 22:41:26 +00:00
}
2013-04-30 10:14:27 +00:00
2014-04-24 14:41:42 +00:00
if ( ! empty ( $http_response_header [ 0 ])) {
$this -> lastHeaders = $http_response_header ;
}
2013-04-30 10:14:27 +00:00
return $result ;
2012-02-14 10:25:00 +00:00
}
/**
* Get notification action .
*
2016-02-11 13:29:37 +00:00
* @ param int $notificationCode The notification code
* @ param int $severity The severity level
* @ param string $message The message
* @ param int $messageCode The message code
* @ param int $bytesTransferred The loaded size
* @ param int $bytesMax The total size
2013-06-13 00:05:44 +00:00
* @ throws TransportException
2012-02-14 10:25:00 +00:00
*/
protected function callbackGet ( $notificationCode , $severity , $message , $messageCode , $bytesTransferred , $bytesMax )
{
switch ( $notificationCode ) {
case STREAM_NOTIFY_FAILURE :
2015-11-10 15:30:01 +00:00
if ( 400 === $messageCode ) {
// This might happen if your host is secured by ssl client certificate authentication
// but you do not send an appropriate certificate
2016-02-11 13:29:37 +00:00
throw new TransportException ( " The ' " . $this -> fileUrl . " ' URL could not be accessed: " . $message , $messageCode );
2015-11-10 15:30:01 +00:00
}
2016-02-11 13:29:37 +00:00
// intentional fallthrough to the next case as the notificationCode
// isn't always consistent and we should inspect the messageCode for 401s
2015-11-10 15:30:01 +00:00
2012-03-10 07:49:21 +00:00
case STREAM_NOTIFY_AUTH_REQUIRED :
if ( 401 === $messageCode ) {
2014-02-26 16:19:54 +00:00
// Bail if the caller is going to handle authentication failures itself.
if ( ! $this -> retryAuthFailure ) {
break ;
}
2014-04-11 12:27:14 +00:00
$this -> promptAuthAndRetry ( $messageCode );
2012-02-14 10:25:00 +00:00
}
2014-02-26 14:51:06 +00:00
break ;
2012-02-14 10:25:00 +00:00
2012-10-16 12:16:39 +00:00
case STREAM_NOTIFY_AUTH_RESULT :
if ( 403 === $messageCode ) {
2015-05-06 23:29:45 +00:00
// Bail if the caller is going to handle authentication failures itself.
if ( ! $this -> retryAuthFailure ) {
break ;
}
2014-04-11 12:27:14 +00:00
$this -> promptAuthAndRetry ( $messageCode , $message );
2012-10-16 12:16:39 +00:00
}
break ;
2012-02-14 10:25:00 +00:00
case STREAM_NOTIFY_FILE_SIZE_IS :
if ( $this -> bytesMax < $bytesMax ) {
$this -> bytesMax = $bytesMax ;
}
break ;
case STREAM_NOTIFY_PROGRESS :
2012-02-15 12:11:29 +00:00
if ( $this -> bytesMax > 0 && $this -> progress ) {
2015-06-14 08:13:18 +00:00
$progression = round ( $bytesTransferred / $this -> bytesMax * 100 );
2012-02-14 10:25:00 +00:00
2015-03-05 13:27:27 +00:00
if (( 0 === $progression % 5 ) && 100 !== $progression && $progression !== $this -> lastProgress ) {
2012-02-17 10:53:38 +00:00
$this -> lastProgress = $progression ;
2015-02-18 09:04:39 +00:00
$this -> io -> overwriteError ( " Downloading: <comment> $progression %</comment> " , false );
2012-02-14 10:25:00 +00:00
}
}
break ;
default :
break ;
}
}
2012-03-05 10:28:23 +00:00
2014-04-20 17:34:54 +00:00
protected function promptAuthAndRetry ( $httpStatus , $reason = null )
2013-12-31 14:31:03 +00:00
{
2014-04-11 12:27:14 +00:00
if ( $this -> config && in_array ( $this -> originUrl , $this -> config -> get ( 'github-domains' ), true )) {
2016-02-11 13:29:37 +00:00
$message = " \n " . 'Could not fetch ' . $this -> fileUrl . ', please create a GitHub OAuth token ' . ( $httpStatus === 404 ? 'to access private repos' : 'to go over the API rate limit' );
2015-01-19 16:28:27 +00:00
$gitHubUtil = new GitHub ( $this -> io , $this -> config , null );
2014-04-11 12:27:14 +00:00
if ( ! $gitHubUtil -> authorizeOAuth ( $this -> originUrl )
2016-02-11 13:29:37 +00:00
&& ( ! $this -> io -> isInteractive () || ! $gitHubUtil -> authorizeOAuthInteractively ( $this -> originUrl , $message ))
2014-04-11 12:27:14 +00:00
) {
2016-02-11 13:29:37 +00:00
throw new TransportException ( 'Could not authenticate against ' . $this -> originUrl , 401 );
2014-04-11 12:27:14 +00:00
}
2015-11-13 15:49:14 +00:00
} elseif ( $this -> config && in_array ( $this -> originUrl , $this -> config -> get ( 'gitlab-domains' ), true )) {
2016-02-11 13:29:37 +00:00
$message = " \n " . 'Could not fetch ' . $this -> fileUrl . ', enter your ' . $this -> originUrl . ' credentials ' . ( $httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit' );
2015-04-10 21:45:24 +00:00
$gitLabUtil = new GitLab ( $this -> io , $this -> config , null );
if ( ! $gitLabUtil -> authorizeOAuth ( $this -> originUrl )
2016-02-11 13:29:37 +00:00
&& ( ! $this -> io -> isInteractive () || ! $gitLabUtil -> authorizeOAuthInteractively ( $this -> scheme , $this -> originUrl , $message ))
2015-04-10 21:45:24 +00:00
) {
2016-02-11 13:29:37 +00:00
throw new TransportException ( 'Could not authenticate against ' . $this -> originUrl , 401 );
2015-02-20 22:12:48 +00:00
}
2014-04-11 12:27:14 +00:00
} else {
// 404s are only handled for github
if ( $httpStatus === 404 ) {
return ;
}
2014-04-11 13:01:20 +00:00
// fail if the console is not interactive
if ( ! $this -> io -> isInteractive ()) {
2014-04-11 12:27:14 +00:00
if ( $httpStatus === 401 ) {
$message = " The ' " . $this -> fileUrl . " ' URL required authentication. \n You must be using the interactive console to authenticate " ;
}
if ( $httpStatus === 403 ) {
$message = " The ' " . $this -> fileUrl . " ' URL could not be accessed: " . $reason ;
}
throw new TransportException ( $message , $httpStatus );
}
2014-04-11 13:01:20 +00:00
// fail if we already have auth
if ( $this -> io -> hasAuthentication ( $this -> originUrl )) {
2016-02-11 13:29:37 +00:00
throw new TransportException ( " Invalid credentials for ' " . $this -> fileUrl . " ', aborting. " , $httpStatus );
2014-04-11 13:01:20 +00:00
}
2014-04-11 12:27:14 +00:00
2016-02-11 13:29:37 +00:00
$this -> io -> overwriteError ( ' Authentication required (<info>' . parse_url ( $this -> fileUrl , PHP_URL_HOST ) . '</info>):' );
2014-04-11 12:27:14 +00:00
$username = $this -> io -> ask ( ' Username: ' );
$password = $this -> io -> askAndHideAnswer ( ' Password: ' );
$this -> io -> setAuthentication ( $this -> originUrl , $username , $password );
2014-05-27 11:50:47 +00:00
$this -> storeAuth = $this -> config -> get ( 'store-auths' );
2014-04-11 12:27:14 +00:00
}
2013-12-31 14:31:03 +00:00
$this -> retry = true ;
throw new TransportException ( 'RETRY' );
}
2016-01-16 16:26:14 +00:00
protected function getOptionsForUrl ( $originUrl , $additionalOptions )
2012-03-05 10:28:23 +00:00
{
2016-01-10 17:37:11 +00:00
$tlsOptions = array ();
2014-02-25 22:50:24 +00:00
// Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN
2016-01-24 19:10:11 +00:00
if ( $this -> disableTls === false && PHP_VERSION_ID < 50600 && ! stream_is_local ( $this -> fileUrl )) {
2016-01-24 19:09:35 +00:00
$host = parse_url ( $this -> fileUrl , PHP_URL_HOST );
2016-01-10 17:37:11 +00:00
2016-01-17 19:42:43 +00:00
if ( PHP_VERSION_ID >= 50304 ) {
// Must manually follow when setting CN_match because this causes all
// redirects to be validated against the same CN_match value.
$userlandFollow = true ;
} else {
// PHP < 5.3.4 does not support follow_location, for those people
// do some really nasty hard coded transformations. These will
// still breakdown if the site redirects to a domain we don't
// expect.
if ( $host === 'github.com' || $host === 'api.github.com' ) {
$host = '*.github.com' ;
}
2016-01-17 13:41:00 +00:00
}
2016-01-10 17:37:11 +00:00
$tlsOptions [ 'ssl' ][ 'CN_match' ] = $host ;
$tlsOptions [ 'ssl' ][ 'SNI_server_name' ] = $host ;
2014-02-25 22:50:24 +00:00
2016-01-24 18:52:09 +00:00
$urlAuthority = $this -> getUrlAuthority ( $this -> fileUrl );
if ( isset ( $this -> peerCertificateMap [ $urlAuthority ])) {
// Handle subjectAltName on lesser PHP's.
$certMap = $this -> peerCertificateMap [ $urlAuthority ];
2016-01-28 13:41:19 +00:00
$this -> io -> writeError ( sprintf (
'Using <info>%s</info> as CN for subjectAltName enabled host <info>%s</info>' ,
$certMap [ 'cn' ],
$urlAuthority
), true , IOInterface :: DEBUG );
2016-01-19 23:54:23 +00:00
2016-01-24 18:52:09 +00:00
$tlsOptions [ 'ssl' ][ 'CN_match' ] = $certMap [ 'cn' ];
$tlsOptions [ 'ssl' ][ 'peer_fingerprint' ] = $certMap [ 'fp' ];
}
2014-12-07 13:47:34 +00:00
}
2014-02-25 22:50:24 +00:00
2015-11-23 12:33:50 +00:00
$headers = array ();
2012-10-19 09:02:18 +00:00
2012-03-18 20:05:10 +00:00
if ( extension_loaded ( 'zlib' )) {
2012-10-19 09:02:18 +00:00
$headers [] = 'Accept-Encoding: gzip' ;
2012-03-18 20:05:10 +00:00
}
2016-01-10 17:37:11 +00:00
$options = array_replace_recursive ( $this -> options , $tlsOptions , $additionalOptions );
2015-07-20 16:31:45 +00:00
if ( ! $this -> degradedMode ) {
// degraded mode disables HTTP/1.1 which causes issues with some bad
// proxies/software due to the use of chunked encoding
$options [ 'http' ][ 'protocol_version' ] = 1.1 ;
$headers [] = 'Connection: close' ;
}
2013-02-27 12:23:59 +00:00
2016-01-17 19:42:43 +00:00
if ( isset ( $userlandFollow )) {
2016-01-17 19:15:06 +00:00
$options [ 'http' ][ 'follow_location' ] = 0 ;
}
2012-11-07 12:33:50 +00:00
if ( $this -> io -> hasAuthentication ( $originUrl )) {
$auth = $this -> io -> getAuthentication ( $originUrl );
2013-02-27 11:34:18 +00:00
if ( 'github.com' === $originUrl && 'x-oauth-basic' === $auth [ 'password' ]) {
$options [ 'github-token' ] = $auth [ 'username' ];
2015-11-13 15:49:14 +00:00
} elseif ( $this -> config && in_array ( $originUrl , $this -> config -> get ( 'gitlab-domains' ), true )) {
2015-11-21 21:51:43 +00:00
if ( $auth [ 'password' ] === 'oauth2' ) {
2016-02-11 13:29:37 +00:00
$headers [] = 'Authorization: Bearer ' . $auth [ 'username' ];
2015-04-10 21:45:24 +00:00
}
2013-02-27 11:34:18 +00:00
} else {
$authStr = base64_encode ( $auth [ 'username' ] . ':' . $auth [ 'password' ]);
2016-02-11 13:29:37 +00:00
$headers [] = 'Authorization: Basic ' . $authStr ;
2013-02-27 11:34:18 +00:00
}
2012-03-05 10:28:23 +00:00
}
2012-10-19 09:02:18 +00:00
if ( isset ( $options [ 'http' ][ 'header' ]) && ! is_array ( $options [ 'http' ][ 'header' ])) {
$options [ 'http' ][ 'header' ] = explode ( " \r \n " , trim ( $options [ 'http' ][ 'header' ], " \r \n " ));
}
foreach ( $headers as $header ) {
$options [ 'http' ][ 'header' ][] = $header ;
2012-10-18 14:02:24 +00:00
}
2012-10-03 09:56:31 +00:00
2014-02-23 12:36:27 +00:00
return $options ;
}
2016-01-25 17:55:29 +00:00
private function handleRedirect ( array $http_response_header , array $additionalOptions , $result )
{
if ( $locationHeader = $this -> findHeaderValue ( $http_response_header , 'location' )) {
if ( parse_url ( $locationHeader , PHP_URL_SCHEME )) {
// Absolute URL; e.g. https://example.com/composer
$targetUrl = $locationHeader ;
} elseif ( parse_url ( $locationHeader , PHP_URL_HOST )) {
// Scheme relative; e.g. //example.com/foo
2016-02-11 13:29:37 +00:00
$targetUrl = $this -> scheme . ':' . $locationHeader ;
2016-01-25 17:55:29 +00:00
} elseif ( '/' === $locationHeader [ 0 ]) {
// Absolute path; e.g. /foo
$urlHost = parse_url ( $this -> fileUrl , PHP_URL_HOST );
// Replace path using hostname as an anchor.
2016-02-11 13:29:37 +00:00
$targetUrl = preg_replace ( '{^(.+(?://|@)' . preg_quote ( $urlHost ) . '(?::\d+)?)(?:[/\?].*)?$}' , '\1' . $locationHeader , $this -> fileUrl );
2016-01-25 17:55:29 +00:00
} else {
// Relative path; e.g. foo
// This actually differs from PHP which seems to add duplicate slashes.
2016-02-11 13:29:37 +00:00
$targetUrl = preg_replace ( '{^(.+/)[^/?]*(?:\?.*)?$}' , '\1' . $locationHeader , $this -> fileUrl );
2016-01-25 17:55:29 +00:00
}
}
if ( ! empty ( $targetUrl )) {
$this -> redirects ++ ;
2016-02-11 13:29:37 +00:00
$this -> io -> writeError ( sprintf ( 'Following redirect (%u) %s' , $this -> redirects , $targetUrl ), true , IOInterface :: DEBUG );
2016-01-25 17:55:29 +00:00
$additionalOptions [ 'redirects' ] = $this -> redirects ;
return $this -> get ( $this -> originUrl , $targetUrl , $additionalOptions , $this -> fileName , $this -> progress );
}
if ( ! $this -> retry ) {
2016-02-11 13:29:37 +00:00
$e = new TransportException ( 'The "' . $this -> fileUrl . '" file could not be downloaded, got redirect without Location (' . $http_response_header [ 0 ] . ')' );
2016-01-25 17:55:29 +00:00
$e -> setHeaders ( $http_response_header );
$e -> setResponse ( $result );
throw $e ;
}
return false ;
}
2016-01-22 08:14:37 +00:00
/**
* @ param array $options
*
* @ return array
*/
2016-01-20 20:20:18 +00:00
private function getTlsDefaults ( array $options )
2014-02-23 12:36:27 +00:00
{
$ciphers = implode ( ':' , array (
'ECDHE-RSA-AES128-GCM-SHA256' ,
'ECDHE-ECDSA-AES128-GCM-SHA256' ,
'ECDHE-RSA-AES256-GCM-SHA384' ,
'ECDHE-ECDSA-AES256-GCM-SHA384' ,
'DHE-RSA-AES128-GCM-SHA256' ,
'DHE-DSS-AES128-GCM-SHA256' ,
'kEDH+AESGCM' ,
'ECDHE-RSA-AES128-SHA256' ,
'ECDHE-ECDSA-AES128-SHA256' ,
'ECDHE-RSA-AES128-SHA' ,
'ECDHE-ECDSA-AES128-SHA' ,
'ECDHE-RSA-AES256-SHA384' ,
'ECDHE-ECDSA-AES256-SHA384' ,
'ECDHE-RSA-AES256-SHA' ,
'ECDHE-ECDSA-AES256-SHA' ,
'DHE-RSA-AES128-SHA256' ,
'DHE-RSA-AES128-SHA' ,
'DHE-DSS-AES128-SHA256' ,
'DHE-RSA-AES256-SHA256' ,
'DHE-DSS-AES256-SHA' ,
'DHE-RSA-AES256-SHA' ,
'AES128-GCM-SHA256' ,
2016-01-25 19:17:56 +00:00
'AES256-GCM-SHA384' ,
2014-02-23 12:36:27 +00:00
'ECDHE-RSA-RC4-SHA' ,
'ECDHE-ECDSA-RC4-SHA' ,
'AES128' ,
'AES256' ,
'RC4-SHA' ,
'HIGH' ,
'!aNULL' ,
'!eNULL' ,
'!EXPORT' ,
'!DES' ,
'!3DES' ,
'!MD5' ,
2016-01-26 13:07:05 +00:00
'!PSK' ,
2014-02-23 12:36:27 +00:00
));
/**
* CN_match and SNI_server_name are only known once a URL is passed .
* They will be set in the getOptionsForUrl () method which receives a URL .
2016-02-11 13:29:37 +00:00
*
2014-02-23 12:36:27 +00:00
* cafile or capath can be overridden by passing in those options to constructor .
*/
2016-01-20 20:20:18 +00:00
$defaults = array (
2014-02-23 12:36:27 +00:00
'ssl' => array (
2016-02-11 13:29:37 +00:00
'ciphers' => $ciphers ,
'verify_peer' => true ,
'verify_depth' => 7 ,
'SNI_enabled' => true ,
2016-01-19 23:54:23 +00:00
'capture_peer_cert' => true ,
2016-01-26 13:07:05 +00:00
),
2014-02-23 12:36:27 +00:00
);
2016-01-10 16:10:22 +00:00
2016-01-20 20:35:06 +00:00
if ( isset ( $options [ 'ssl' ])) {
2016-01-22 13:27:08 +00:00
$defaults [ 'ssl' ] = array_replace_recursive ( $defaults [ 'ssl' ], $options [ 'ssl' ]);
2016-01-20 20:35:06 +00:00
}
2016-04-11 15:33:29 +00:00
$caBundleLogger = $this -> io instanceof LoggerInterface ? $this -> io : null ;
2014-02-23 12:36:27 +00:00
/**
2014-03-02 14:06:47 +00:00
* Attempt to find a local cafile or throw an exception if none pre - set
2014-02-23 12:36:27 +00:00
* The user may go download one if this occurs .
*/
2016-01-22 08:14:37 +00:00
if ( ! isset ( $defaults [ 'ssl' ][ 'cafile' ]) && ! isset ( $defaults [ 'ssl' ][ 'capath' ])) {
2016-04-11 15:33:29 +00:00
$result = CaBundle :: getSystemCaRootBundlePath ( $caBundleLogger );
2016-01-10 17:37:11 +00:00
2016-01-21 14:07:51 +00:00
if ( preg_match ( '{^phar://}' , $result )) {
2016-01-22 08:14:37 +00:00
$hash = hash_file ( 'sha256' , $result );
2016-01-21 15:02:44 +00:00
$targetPath = rtrim ( sys_get_temp_dir (), '\\/' ) . '/composer-cacert-' . $hash . '.pem' ;
2016-01-21 14:07:51 +00:00
2016-01-22 08:14:37 +00:00
if ( ! file_exists ( $targetPath ) || $hash !== hash_file ( 'sha256' , $targetPath )) {
2016-01-22 08:20:43 +00:00
$this -> streamCopy ( $result , $targetPath );
chmod ( $targetPath , 0666 );
2014-03-02 14:06:47 +00:00
}
2016-01-21 14:07:51 +00:00
$defaults [ 'ssl' ][ 'cafile' ] = $targetPath ;
} elseif ( is_dir ( $result )) {
$defaults [ 'ssl' ][ 'capath' ] = $result ;
2014-03-02 14:06:47 +00:00
} else {
2016-01-21 14:07:51 +00:00
$defaults [ 'ssl' ][ 'cafile' ] = $result ;
2014-03-02 14:06:47 +00:00
}
2014-02-23 12:36:27 +00:00
}
2016-04-11 15:33:29 +00:00
if ( isset ( $defaults [ 'ssl' ][ 'cafile' ]) && ( ! is_readable ( $defaults [ 'ssl' ][ 'cafile' ]) || ! CaBundle :: validateCaFile ( $defaults [ 'ssl' ][ 'cafile' ], $caBundleLogger ))) {
2016-01-20 20:24:13 +00:00
throw new TransportException ( 'The configured cafile was not valid or could not be read.' );
}
2016-01-20 20:27:26 +00:00
if ( isset ( $defaults [ 'ssl' ][ 'capath' ]) && ( ! is_dir ( $defaults [ 'ssl' ][ 'capath' ]) || ! is_readable ( $defaults [ 'ssl' ][ 'capath' ]))) {
2016-01-20 20:24:13 +00:00
throw new TransportException ( 'The configured capath was not valid or could not be read.' );
}
2014-02-23 12:36:27 +00:00
/**
* Disable TLS compression to prevent CRIME attacks where supported .
*/
2016-01-16 16:43:33 +00:00
if ( PHP_VERSION_ID >= 50413 ) {
2016-01-20 20:20:18 +00:00
$defaults [ 'ssl' ][ 'disable_compression' ] = true ;
2014-02-23 12:36:27 +00:00
}
2016-01-20 20:20:18 +00:00
return $defaults ;
2012-03-05 10:28:23 +00:00
}
2014-02-23 12:36:27 +00:00
2016-01-22 08:14:37 +00:00
/**
* Uses stream_copy_to_stream instead of copy to work around https :// bugs . php . net / bug . php ? id = 64634
*
* @ param string $source
* @ param string $target
*/
2016-01-22 08:20:43 +00:00
private function streamCopy ( $source , $target )
2016-01-22 08:14:37 +00:00
{
$source = fopen ( $source , 'r' );
$target = fopen ( $target , 'w+' );
stream_copy_to_stream ( $source , $target );
fclose ( $source );
fclose ( $target );
unset ( $source , $target );
}
2016-01-19 23:54:23 +00:00
2016-01-24 18:52:09 +00:00
/**
* Fetch certificate common name and fingerprint for validation of SAN .
*
* @ todo Remove when PHP 5.6 is minimum supported version .
*/
2016-01-19 23:54:23 +00:00
private function getCertificateCnAndFp ( $url , $options )
{
2016-01-24 18:52:09 +00:00
if ( PHP_VERSION_ID >= 50600 ) {
throw new \BadMethodCallException ( sprintf (
'%s must not be used on PHP >= 5.6' ,
__METHOD__
));
}
2016-02-11 13:29:37 +00:00
$context = StreamContextFactory :: getContext ( $url , $options , array ( 'options' => array (
'ssl' => array (
'capture_peer_cert' => true ,
'verify_peer' => false , // Yes this is fucking insane! But PHP is lame.
), ),
2016-01-19 23:54:23 +00:00
));
2016-01-24 18:52:09 +00:00
// Ideally this would just use stream_socket_client() to avoid sending a
// HTTP request but that does not capture the certificate.
2016-01-19 23:54:23 +00:00
if ( false === $handle = @ fopen ( $url , 'rb' , false , $context )) {
return ;
}
// Close non authenticated connection without reading any content.
fclose ( $handle );
2016-01-24 18:52:09 +00:00
$handle = null ;
2016-01-19 23:54:23 +00:00
$params = stream_context_get_params ( $context );
if ( ! empty ( $params [ 'options' ][ 'ssl' ][ 'peer_certificate' ])) {
$peerCertificate = $params [ 'options' ][ 'ssl' ][ 'peer_certificate' ];
2016-01-24 18:52:09 +00:00
if ( TlsHelper :: checkCertificateHost ( $peerCertificate , parse_url ( $url , PHP_URL_HOST ), $commonName )) {
2016-01-19 23:54:23 +00:00
return array (
'cn' => $commonName ,
2016-01-24 18:52:09 +00:00
'fp' => TlsHelper :: getCertificateFingerprint ( $peerCertificate ),
2016-01-19 23:54:23 +00:00
);
}
}
}
private function getUrlAuthority ( $url )
{
$defaultPorts = array (
2016-02-11 13:29:37 +00:00
'ftp' => 21 ,
'http' => 80 ,
'https' => 443 ,
2016-01-26 16:53:09 +00:00
'ssh2.sftp' => 22 ,
2016-02-11 13:29:37 +00:00
'ssh2.scp' => 22 ,
2016-01-19 23:54:23 +00:00
);
2016-01-24 18:52:09 +00:00
$scheme = parse_url ( $url , PHP_URL_SCHEME );
if ( ! isset ( $defaultPorts [ $scheme ])) {
throw new \InvalidArgumentException ( sprintf (
'Could not get default port for unknown scheme: %s' ,
$scheme
));
}
$defaultPort = $defaultPorts [ $scheme ];
$port = parse_url ( $url , PHP_URL_PORT ) ? : $defaultPort ;
2016-01-19 23:54:23 +00:00
2016-02-11 13:29:37 +00:00
return parse_url ( $url , PHP_URL_HOST ) . ':' . $port ;
2016-01-19 23:54:23 +00:00
}
2012-03-05 10:28:23 +00:00
}